Skip to content

Commit

Permalink
Implement PropNameInterStageStore
Browse files Browse the repository at this point in the history
Summary:
This implements a store for serializing and deserializing prop names through a
Filer, allowing for saving and retrieving names otherwise inaccessible across
module boundaries.

Reviewed By: IanChilds

Differential Revision: D6313805

fbshipit-source-id: de998db666305cc04861ca46f0851600ad8d39e1
  • Loading branch information
passy authored and facebook-github-bot committed Nov 15, 2017
1 parent eee1b21 commit 25a72a8
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho.specmodels.processor;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.facebook.litho.annotations.ResType;
import com.facebook.litho.specmodels.internal.ImmutableList;
import com.facebook.litho.specmodels.model.PropModel;
import com.facebook.litho.testing.assertj.LithoAssertions;
import com.facebook.litho.testing.specmodels.MockMethodParamModel;
import com.facebook.litho.testing.specmodels.MockSpecModel;
import com.squareup.javapoet.ClassName;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Optional;
import javax.annotation.processing.Filer;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import javax.tools.StandardLocation;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/** Tests {@link PropNameInterStageStore} */
public class PropNameInterStageStoreTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
Filer mFiler;

@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}

@Test
public void testLoad() throws IOException {
final PropNameInterStageStore store = new PropNameInterStageStore(mFiler);

final FileObject fileObject = makeFileObjectForString("arg0\narg1\n");
when(mFiler.getResource(any(JavaFileManager.Location.class), anyString(), anyString()))
.thenReturn(fileObject);

final Optional<ImmutableList<String>> strings =
store.loadNames(new MockName("com.example.MyComponentSpec"));
LithoAssertions.assertThat(strings.isPresent()).isTrue();
LithoAssertions.assertThat(strings.get()).containsExactly("arg0", "arg1");

verify(mFiler)
.getResource(
StandardLocation.CLASS_PATH, "", "META-INF/litho/com.example.MyComponentSpec.props");
}

@Test
public void testSave() throws IOException {
final PropNameInterStageStore store = new PropNameInterStageStore(mFiler);

final MockSpecModel specModel =
MockSpecModel.newBuilder()
.props(ImmutableList.of(makePropModel("param0"), makePropModel("param1")))
.specTypeName(ClassName.get(MyTestSpec.class))
.build();
store.saveNames(specModel);

verify(mFiler)
.createResource(
StandardLocation.CLASS_OUTPUT,
"",
"META-INF/litho/com.facebook.litho.specmodels.processor.PropNameInterStageStoreTest.MyTestSpec.props");

// Not checking the actually written values here because Java IO is a horrible mess.
}

public static class MyTestSpec {}

static FileObject makeFileObjectForString(String value) throws IOException {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(value.getBytes());
final FileObject file = mock(FileObject.class);
when(file.openInputStream()).thenReturn(inputStream);
return file;
}

static PropModel makePropModel(String name) {
return new PropModel(
MockMethodParamModel.newBuilder().name(name).build(), false, ResType.BOOL, "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,110 @@
package com.facebook.litho.specmodels.processor;

import com.facebook.litho.specmodels.internal.ImmutableList;
import com.facebook.litho.specmodels.model.PropModel;
import com.facebook.litho.specmodels.model.SpecModel;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.annotation.processing.Filer;
import javax.lang.model.element.Name;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import javax.tools.StandardLocation;

/** This will serve as store for parameter names. TODO(T21953762) */
/**
* This store retains prop names across multi-module annotation processor runs. This is needed as
* prop names are derived from method parameters which aren't persisted in the Java 7 bytecode and
* thus cannot be inferred if compilation occurs across modules.
*
* <p>The props names are serialized and stored as resources within the output JAR, where they can
* be read from again at a later point in time.
*/
public class PropNameInterStageStore {
private final Filer mFiler;

private static final String BASE_PATH = "META-INF/litho/";
private static final String FILE_EXT = ".props";

public PropNameInterStageStore(Filer filer) {
this.mFiler = filer;
}

/**
* @return List of names in order of definition. List may be empty if there are no custom props
* defined. Value may not be present if loading for the given spec model failed, i.e. we don't
* have inter-stage resources on the class path to facilitate the lookup.
*/
public Optional<ImmutableList<String>> loadNames(Name qualifiedName) {
return Optional.empty();
final Optional<FileObject> resource =
getResource(mFiler, StandardLocation.CLASS_PATH, "", BASE_PATH + qualifiedName + FILE_EXT);

return resource.map(
r -> {
final List<String> props = new ArrayList<>();
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(r.openInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
props.add(line);
}
} catch (final IOException err) {
// This can only happen due to buggy build systems.
throw new RuntimeException(err);
}

return ImmutableList.copyOf(props);
});
}

/** Saves the prop names of the given spec model at a well-known path within the resources. */
public void saveNames(SpecModel specModel) throws IOException {}
public void saveNames(SpecModel specModel) throws IOException {
// This is quite important, because we must not open resources without writing to them
// due to a bug in the Buck caching layer.
if (specModel.getProps().isEmpty()) {
return;
}

final FileObject outputFile =
mFiler.createResource(
StandardLocation.CLASS_OUTPUT, "", BASE_PATH + specModel.getSpecTypeName() + FILE_EXT);

try (Writer writer =
new BufferedWriter(new OutputStreamWriter(outputFile.openOutputStream()))) {
for (final PropModel propModel : specModel.getProps()) {
writer.write(propModel.getName() + "\n");
}
}
}

/**
* Helper method for obtaining resources from a {@link Filer}, taking care of some javac
* peculiarities.
*/
private static Optional<FileObject> getResource(
final Filer filer,
final JavaFileManager.Location location,
final String packageName,
final String filePath) {
try {
final FileObject resource = filer.getResource(location, packageName, filePath);
resource.openInputStream().close();
return Optional.of(resource);
} catch (final Exception e) {
// ClientCodeException can be thrown by a bug in the javac ClientCodeWrapper
if (!(e instanceof FileNotFoundException
|| e.getClass().getName().equals("com.sun.tools.javac.util.ClientCodeException"))) {
throw new RuntimeException(
String.format("Error opening resource %s/%s", packageName, filePath), e.getCause());
}
return Optional.empty();
}
}
}

0 comments on commit 25a72a8

Please sign in to comment.