diff options
author | Mathew Inwood <mathewi@google.com> | 2018-07-23 15:51:06 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2018-07-23 15:51:06 +0000 |
commit | 7f0aa734a3f4b3d1f7842b8546543668275f8ddb (patch) | |
tree | e1422a4d3becc0736afc7c985c501e3e7a5691c5 | |
parent | 3e3a6e4760e3e8f3b7ff30aac4aa1a64c13d52dc (diff) | |
parent | 6395690ec99bd13214c0530cac54d33b1f8e601b (diff) |
Merge "Add new "class2greylist" tool."
10 files changed, 732 insertions, 0 deletions
diff --git a/tools/hiddenapi/class2greylist/Android.bp b/tools/hiddenapi/class2greylist/Android.bp new file mode 100644 index 000000000000..7b1233bb8535 --- /dev/null +++ b/tools/hiddenapi/class2greylist/Android.bp @@ -0,0 +1,33 @@ +// +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +java_library_host { + name: "class2greylistlib", + srcs: ["src/**/*.java"], + static_libs: [ + "commons-cli-1.2", + "apache-bcel", + ], +} + +java_binary_host { + name: "class2greylist", + manifest: "src/class2greylist.mf", + static_libs: [ + "class2greylistlib", + ], +} + diff --git a/tools/hiddenapi/class2greylist/src/class2greylist.mf b/tools/hiddenapi/class2greylist/src/class2greylist.mf new file mode 100644 index 000000000000..ea3a3d9153a3 --- /dev/null +++ b/tools/hiddenapi/class2greylist/src/class2greylist.mf @@ -0,0 +1 @@ +Main-Class: com.android.class2greylist.Class2Greylist diff --git a/tools/hiddenapi/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java new file mode 100644 index 000000000000..66857525aa0c --- /dev/null +++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.class2greylist; + +import org.apache.bcel.Const; +import org.apache.bcel.classfile.AnnotationEntry; +import org.apache.bcel.classfile.DescendingVisitor; +import org.apache.bcel.classfile.ElementValuePair; +import org.apache.bcel.classfile.EmptyVisitor; +import org.apache.bcel.classfile.Field; +import org.apache.bcel.classfile.FieldOrMethod; +import org.apache.bcel.classfile.JavaClass; +import org.apache.bcel.classfile.Method; + +import java.util.Locale; + +/** + * Visits a JavaClass instance and pulls out all members annotated with a + * specific annotation. The signatures of such members are passed to {@link + * Status#greylistEntry(String)}. Any errors result in a call to {@link + * Status#error(String)}. + * + * If the annotation has a property "expectedSignature" the generated signature + * will be verified against the one specified there. If it differs, an error + * will be generated. + */ +public class AnnotationVisitor extends EmptyVisitor { + + private static final String EXPECTED_SIGNATURE = "expectedSignature"; + + private final JavaClass mClass; + private final String mAnnotationType; + private final Status mStatus; + private final DescendingVisitor mDescendingVisitor; + + public AnnotationVisitor(JavaClass clazz, String annotation, Status d) { + mClass = clazz; + mAnnotationType = annotation; + mStatus = d; + mDescendingVisitor = new DescendingVisitor(clazz, this); + } + + public void visit() { + mStatus.debug("Visit class %s", mClass.getClassName()); + mDescendingVisitor.visit(); + } + + private static String getClassDescriptor(JavaClass clazz) { + // JavaClass.getName() returns the Java-style name (with . not /), so we must fetch + // the original class name from the constant pool. + return clazz.getConstantPool().getConstantString( + clazz.getClassNameIndex(), Const.CONSTANT_Class); + } + + @Override + public void visitMethod(Method method) { + visitMember(method, "L%s;->%s%s"); + } + + @Override + public void visitField(Field field) { + visitMember(field, "L%s;->%s:%s"); + } + + private void visitMember(FieldOrMethod member, String signatureFormatString) { + JavaClass definingClass = (JavaClass) mDescendingVisitor.predecessor(); + mStatus.debug("Visit member %s : %s", member.getName(), member.getSignature()); + for (AnnotationEntry a : member.getAnnotationEntries()) { + if (mAnnotationType.equals(a.getAnnotationType())) { + mStatus.debug("Method has annotation %s", mAnnotationType); + String signature = String.format(Locale.US, signatureFormatString, + getClassDescriptor(definingClass), member.getName(), member.getSignature()); + for (ElementValuePair property : a.getElementValuePairs()) { + switch (property.getNameString()) { + case EXPECTED_SIGNATURE: + String expected = property.getValue().stringifyValue(); + if (!signature.equals(expected)) { + error(definingClass, member, + "Expected signature does not match generated:\n" + + "Expected: %s\n" + + "Generated: %s", expected, signature); + } + break; + } + } + mStatus.greylistEntry(signature); + } + } + } + + private void error(JavaClass clazz, FieldOrMethod member, String message, Object... args) { + StringBuilder error = new StringBuilder(); + error.append(clazz.getSourceFileName()) + .append(": ") + .append(clazz.getClassName()) + .append(".") + .append(member.getName()) + .append(": ") + .append(String.format(Locale.US, message, args)); + + mStatus.error(error.toString()); + } + +} diff --git a/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Class2Greylist.java new file mode 100644 index 000000000000..bcccf4aa18a9 --- /dev/null +++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Class2Greylist.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.class2greylist; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.GnuParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.OptionBuilder; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.cli.PatternOptionBuilder; + +import java.io.IOException; + +/** + * Build time tool for extracting a list of members from jar files that have the @UsedByApps + * annotation, for building the greylist. + */ +public class Class2Greylist { + + private static final String ANNOTATION_TYPE = "Landroid/annotation/UsedByApps;"; + + public static void main(String[] args) { + Options options = new Options(); + options.addOption(OptionBuilder + .withLongOpt("debug") + .hasArgs(0) + .withDescription("Enable debug") + .create("d")); + options.addOption(OptionBuilder + .withLongOpt("help") + .hasArgs(0) + .withDescription("Show this help") + .create("h")); + + CommandLineParser parser = new GnuParser(); + CommandLine cmd; + + try { + cmd = parser.parse(options, args); + } catch (ParseException e) { + System.err.println(e.getMessage()); + help(options); + return; + } + if (cmd.hasOption('h')) { + help(options); + } + + String[] jarFiles = cmd.getArgs(); + if (jarFiles.length == 0) { + System.err.println("Error: no jar files specified."); + help(options); + } + + Status status = new Status(cmd.hasOption('d')); + + for (String jarFile : jarFiles) { + status.debug("Processing jar file %s", jarFile); + try { + JarReader reader = new JarReader(status, jarFile); + reader.stream().forEach(clazz -> new AnnotationVisitor( + clazz, ANNOTATION_TYPE, status).visit()); + reader.close(); + } catch (IOException e) { + status.error(e); + } + } + if (status.ok()) { + System.exit(0); + } else { + System.exit(1); + } + + } + + private static void help(Options options) { + new HelpFormatter().printHelp( + "class2greylist path/to/classes.jar [classes2.jar ...]", + "Extracts greylist entries from classes jar files given", + options, null, true); + System.exit(1); + } +} diff --git a/tools/hiddenapi/class2greylist/src/com/android/class2greylist/JarReader.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/JarReader.java new file mode 100644 index 000000000000..f3a9d0b92e6c --- /dev/null +++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/JarReader.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.class2greylist; + +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; + +import java.io.IOException; +import java.util.Objects; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Reads {@link JavaClass} members from a zip/jar file, providing a stream of them for processing. + * Any errors are reported via {@link Status#error(Throwable)}. + */ +public class JarReader { + + private final Status mStatus; + private final String mFileName; + private final ZipFile mZipFile; + + public JarReader(Status s, String filename) throws IOException { + mStatus = s; + mFileName = filename; + mZipFile = new ZipFile(mFileName); + } + + private JavaClass openZipEntry(ZipEntry e) { + try { + mStatus.debug("Reading %s from %s", e.getName(), mFileName); + return new ClassParser(mZipFile.getInputStream(e), e.getName()).parse(); + } catch (IOException ioe) { + mStatus.error(ioe); + return null; + } + } + + + public Stream<JavaClass> stream() { + return mZipFile.stream() + .filter(zipEntry -> zipEntry.getName().endsWith(".class")) + .map(zipEntry -> openZipEntry(zipEntry)) + .filter(Objects::nonNull); + } + + public void close() throws IOException { + mZipFile.close(); + } +} diff --git a/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Status.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Status.java new file mode 100644 index 000000000000..d7078986d9cd --- /dev/null +++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Status.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.class2greylist; + +import java.util.Locale; + +public class Status { + + // Highlight "Error:" in red. + private static final String ERROR = "\u001B[31mError: \u001B[0m"; + + private final boolean mDebug; + private boolean mHasErrors; + + public Status(boolean debug) { + mDebug = debug; + } + + public void debug(String msg, Object... args) { + if (mDebug) { + System.err.println(String.format(Locale.US, msg, args)); + } + } + + public void error(Throwable t) { + System.err.print(ERROR); + t.printStackTrace(System.err); + mHasErrors = true; + } + + public void error(String message) { + System.err.print(ERROR); + System.err.println(message); + mHasErrors = true; + } + + public void greylistEntry(String signature) { + System.out.println(signature); + } + + public boolean ok() { + return !mHasErrors; + } +} diff --git a/tools/hiddenapi/class2greylist/test/Android.mk b/tools/hiddenapi/class2greylist/test/Android.mk new file mode 100644 index 000000000000..23f4156f6d03 --- /dev/null +++ b/tools/hiddenapi/class2greylist/test/Android.mk @@ -0,0 +1,32 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_MODULE := class2greylisttest + +LOCAL_STATIC_JAVA_LIBRARIES := class2greylistlib truth-host-prebuilt mockito-host junit-host + +# tag this module as a cts test artifact +LOCAL_COMPATIBILITY_SUITE := general-tests + +include $(BUILD_HOST_JAVA_LIBRARY) + +# Build the test APKs using their own makefiles +include $(call all-makefiles-under,$(LOCAL_PATH))
\ No newline at end of file diff --git a/tools/hiddenapi/class2greylist/test/AndroidTest.xml b/tools/hiddenapi/class2greylist/test/AndroidTest.xml new file mode 100644 index 000000000000..66bb63446f71 --- /dev/null +++ b/tools/hiddenapi/class2greylist/test/AndroidTest.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="class2greylist tests"> + <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > + <option name="jar" value="class2greylisttest.jar" /> + <option name="runtime-hint" value="1m" /> + </test> +</configuration>
\ No newline at end of file diff --git a/tools/hiddenapi/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java b/tools/hiddenapi/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java new file mode 100644 index 000000000000..2d9721803cac --- /dev/null +++ b/tools/hiddenapi/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.javac; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.android.class2greylist.Status; +import com.android.class2greylist.AnnotationVisitor; + +import com.google.common.base.Joiner; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +import java.io.IOException; + +public class AnnotationVisitorTest { + + private static final String ANNOTATION = "Lannotation/Anno;"; + + private Javac mJavac; + @Mock + private Status mStatus; + + @Before + public void setup() throws IOException { + initMocks(this); + mJavac = new Javac(); + mJavac.addSource("annotation.Anno", Joiner.on('\n').join( + "package annotation;", + "import static java.lang.annotation.RetentionPolicy.CLASS;", + "import java.lang.annotation.Retention;", + "import java.lang.annotation.Target;", + "@Retention(CLASS)", + "public @interface Anno {", + " String expectedSignature() default \"\";", + "}")); + } + + private void assertNoErrors() { + verify(mStatus, never()).error(any(Throwable.class)); + verify(mStatus, never()).error(any(String.class)); + } + + @Test + public void testGreylistMethod() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno", + " public void method() {}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus) + .visit(); + + assertNoErrors(); + ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); + verify(mStatus, times(1)).greylistEntry(greylist.capture()); + assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V"); + } + + @Test + public void testGreylistConstructor() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno", + " public Class() {}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus) + .visit(); + + assertNoErrors(); + ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); + verify(mStatus, times(1)).greylistEntry(greylist.capture()); + assertThat(greylist.getValue()).isEqualTo("La/b/Class;-><init>()V"); + } + + @Test + public void testGreylistField() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno", + " public int i;", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus) + .visit(); + + assertNoErrors(); + ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); + verify(mStatus, times(1)).greylistEntry(greylist.capture()); + assertThat(greylist.getValue()).isEqualTo("La/b/Class;->i:I"); + } + + @Test + public void testGreylistMethodExpectedSignature() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno(expectedSignature=\"La/b/Class;->method()V\")", + " public void method() {}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus) + .visit(); + + assertNoErrors(); + ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); + verify(mStatus, times(1)).greylistEntry(greylist.capture()); + assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V"); + } + + @Test + public void testGreylistMethodExpectedSignatureWrong() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno(expectedSignature=\"La/b/Class;->nomethod()V\")", + " public void method() {}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus) + .visit(); + + verify(mStatus, times(1)).error(any(String.class)); + } + + @Test + public void testGreylistInnerClassMethod() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " public class Inner {", + " @Anno", + " public void method() {}", + " }", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), ANNOTATION, + mStatus).visit(); + + assertNoErrors(); + ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); + verify(mStatus, times(1)).greylistEntry(greylist.capture()); + assertThat(greylist.getValue()).isEqualTo("La/b/Class$Inner;->method()V"); + } + + @Test + public void testMethodNotGreylisted() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "public class Class {", + " public void method() {}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus) + .visit(); + + assertNoErrors(); + verify(mStatus, never()).greylistEntry(any(String.class)); + } + +} diff --git a/tools/hiddenapi/class2greylist/test/src/com/android/javac/Javac.java b/tools/hiddenapi/class2greylist/test/src/com/android/javac/Javac.java new file mode 100644 index 000000000000..202f4121fc60 --- /dev/null +++ b/tools/hiddenapi/class2greylist/test/src/com/android/javac/Javac.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.javac; + +import com.google.common.io.Files; + +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +/** + * Helper class for compiling snippets of Java source and providing access to the resulting class + * files. + */ +public class Javac { + + private final JavaCompiler mJavac; + private final StandardJavaFileManager mFileMan; + private final List<JavaFileObject> mCompilationUnits; + private final File mClassOutDir; + + public Javac() throws IOException { + mJavac = ToolProvider.getSystemJavaCompiler(); + mFileMan = mJavac.getStandardFileManager(null, Locale.US, null); + mClassOutDir = Files.createTempDir(); + mFileMan.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(mClassOutDir)); + mFileMan.setLocation(StandardLocation.CLASS_PATH, Arrays.asList(mClassOutDir)); + mCompilationUnits = new ArrayList<>(); + } + + private String classToFileName(String classname) { + return classname.replace('.', '/'); + } + + public Javac addSource(String classname, String contents) { + JavaFileObject java = new SimpleJavaFileObject(URI.create( + String.format("string:///%s.java", classToFileName(classname))), + JavaFileObject.Kind.SOURCE + ){ + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return contents; + } + }; + mCompilationUnits.add(java); + return this; + } + + public boolean compile() { + JavaCompiler.CompilationTask task = mJavac.getTask( + null, + mFileMan, + null, + null, + null, + mCompilationUnits); + return task.call(); + } + + public InputStream getClassFile(String classname) throws IOException { + Iterable<? extends JavaFileObject> objs = mFileMan.getJavaFileObjects( + new File(mClassOutDir, String.format("%s.class", classToFileName(classname)))); + if (!objs.iterator().hasNext()) { + return null; + } + return objs.iterator().next().openInputStream(); + } + + public JavaClass getCompiledClass(String classname) throws IOException { + return new ClassParser(getClassFile(classname), + String.format("%s.class", classToFileName(classname))).parse(); + } +} |