GetFields ()
GetNotNull (field),
GetValue (field),
new XAttribute ("visibility", visibility),
- new XAttribute ("volatile", (field.AccessFlags & FieldAccessFlags.Volatile) != 0));
+ new XAttribute ("volatile", (field.AccessFlags & FieldAccessFlags.Volatile) != 0),
+ GetFieldJavadoc (field.Name));
}
}
+ XElement GetFieldJavadoc (string fieldName)
+ {
+ if (javadocsSource == null)
+ return null;
+ return javadocsSource
+ .Elements ("field")
+ .Where (f => fieldName == (string) f.Attribute ("name"))
+ .Elements ("javadoc")
+ .FirstOrDefault ();
+ }
+
string GetGenericType (FieldInfo field)
{
var signature = field.GetSignature ();
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs b/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs
index 2c2191d72..8b5527587 100644
--- a/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/ParameterFixupTests.cs
@@ -30,6 +30,21 @@ public void XmlDeclaration_FixedUpFromDocumentation()
}
}
+ [Test]
+ public void XmlDeclaration_FixedUpFromApiXmlJavadocs ()
+ {
+ string tempFile = null;
+
+ try {
+ tempFile = LoadToTempFile ("ParameterFixupApiXmlJavadocs.xml");
+
+ AssertXmlDeclaration ("Collection.class", "ParameterFixupFromJavadocs.xml", tempFile);
+ } finally {
+ if (File.Exists (tempFile))
+ File.Delete (tempFile);
+ }
+ }
+
[Test]
public void XmlDeclaration_FixedUpFromApiXmlDocumentation ()
{
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupApiXmlJavadocs.xml b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupApiXmlJavadocs.xml
new file mode 100644
index 000000000..eb42329d8
--- /dev/null
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupApiXmlJavadocs.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupFromJavadocs.xml b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupFromJavadocs.xml
new file mode 100644
index 000000000..63f7cf639
--- /dev/null
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/Resources/ParameterFixupFromJavadocs.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java b/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java
index 34f2d3e28..baad63fb1 100644
--- a/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java
+++ b/tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/JavaType.java
@@ -3,37 +3,76 @@
import java.util.ArrayList;
import java.util.List;
+/**
+ * JNI sig: Lcom/xamarin/JavaEnum;
+ */
enum JavaEnum {
+ /** FIRST; JNI sig: Lcom/xamarin/JavaEnum; */
FIRST,
+
+ /** SECOND; JNI sig: Lcom/xamarin/JavaEnum; */
+
SECOND;
+ /**
+ * summary
+ *
+ * Paragraphs of text?
+ *
+ * @return some value
+ */
public int switchValue() {
return 0;
}
}
+/**
+ * JNI sig: Lcom/xamarin/JavaType;
+ *
+ * @param
+ */
+
public class JavaType
implements Cloneable, Comparable>,
IJavaInterface, List>
{
+ /** JNI sig: STATIC_FINAL_OBJECT.L/java/lang/Object; */
+
@Deprecated
public static final Object STATIC_FINAL_OBJECT = new Object ();
+ /** JNI sig: STATIC_FINAL_INT32.I */
public static final int STATIC_FINAL_INT32 = 42;
+ /** JNI sig: STATIC_FINAL_INT32_MIN.I */
public static final int STATIC_FINAL_INT32_MIN = Integer.MIN_VALUE;
+ /** JNI sig: STATIC_FINAL_INT32_MAX.I */
public static final int STATIC_FINAL_INT32_MAX = Integer.MAX_VALUE;
+ /** JNI sig: STATIC_FINAL_CHAR_MIN.C */
public static final char STATIC_FINAL_CHAR_MIN = Character.MIN_VALUE;
+ /** JNI sig: STATIC_FINAL_CHAR_MAX.C */
public static final char STATIC_FINAL_CHAR_MAX = Character.MAX_VALUE;
+ /** JNI sig: STATIC_FINAL_INT64_MIN.J */
public static final long STATIC_FINAL_INT64_MIN = Long.MIN_VALUE;
+ /** JNI sig: STATIC_FINAL_INT64_MAX.J */
public static final long STATIC_FINAL_INT64_MAX = Long.MAX_VALUE;
+ /** JNI sig: STATIC_FINAL_SINGLE_MIN.F */
public static final float STATIC_FINAL_SINGLE_MIN = Float.MIN_VALUE;
+ /** JNI sig: STATIC_FINAL_SINGLE_MAX.F */
public static final float STATIC_FINAL_SINGLE_MAX = Float.MAX_VALUE;
+ /** JNI sig: STATIC_FINAL_DOUBLE_MIN.D */
public static final double STATIC_FINAL_DOUBLE_MIN = Double.MIN_VALUE;
+ /** JNI sig: STATIC_FINAL_DOUBLE_MAX.D */
public static final double STATIC_FINAL_DOUBLE_MAX = Double.MAX_VALUE;
+ /** JNI sig: STATIC_FINAL_STRING.Ljava/lang/String; */
public static final String STATIC_FINAL_STRING = "Hello, \\\"embedded\u0000Nulls\" and \uD83D\uDCA9!";
+ /** JNI sig: STATIC_FINAL_BOOL_FALSE.Z */
public static final boolean STATIC_FINAL_BOOL_FALSE = false;
+ /** JNI sig: STATIC_FINAL_BOOL_TRUE.Z */
public static final boolean STATIC_FINAL_BOOL_TRUE = true;
+ /** JNI sig: POSITIVE_INFINITY.D */
public static final double POSITIVE_INFINITY = 1.0 / 0.0;
+ /** JNI sig: NEGATIVE_INFINITY.D */
public static final double NEGATIVE_INFINITY = -1.0 / 0.0;
+ /** JNI sig: NaN.D */
public static final double NaN = 0.0d / 0.0;
@@ -51,60 +90,86 @@ public class JavaType
// N: Non-static inner class
// C: Class
// I: Interface
+
+ /** JNI sig: Lcom/xamarin/JavaType$PSC; */
+
public static abstract class PSC {
}
+ /** JNI sig: Lcom/xamarin/JavaType$RNC; */
protected abstract class RNC {
+ /** JNI sig: ()V */
protected RNC () {
}
+ /** JNI sig: (Ljava/lang/Object;Ljava/lang/Object;)V */
protected RNC (E value1, E2 value2) {
}
+ /** JNI sig: (Ljava/lang/Object;)Ljava/lang/Object; */
+
public abstract E2 fromE (E value);
+ /** JNI sig: Lcom/xamarin/JavaType$RNC$RPNC; */
public abstract class RPNC {
+ /** JNI sig: ()V */
public RPNC () {
}
+ /** JNI sig: (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V */
public RPNC (E value1, E2 value2, E3 value3) {
}
+ /** JNI sig: fromE2.(Ljava/lang/Object;)Ljava/lang/Object; */
public abstract E3 fromE2 (E2 value);
}
}
+ /** JNI sig: Lcom/xamarin/JavaType$ASC; */
+
@Deprecated
/* package */ static class ASC {
}
+ /** JNI sig: ()V */
public JavaType () {
}
+ /** JNI sig: (Ljava/lang/String;)V */
public JavaType (String value) {
}
+ /** JNI sig: INSTANCE_FINAL_OBJECT.Ljava/lang/Object; */
@Deprecated
public final Object INSTANCE_FINAL_OBJECT = new Object ();
+
+ /** JNI sig: INSTANCE_FINAL_E.Ljava/lang/Object; */
public final E INSTANCE_FINAL_E = null;
+ /** JNI sig: packageInstanceEArray.[Ljava/lang/Object; */
/* package */ E[] packageInstanceEArray;
+
+ /** JNI sig: protectedInstanceEList.Ljava/util/List; */
protected List protectedInstanceEList;
private List[] privateInstanceArrayOfListOfIntArrayArray;
+ /** JNI sig: compareTo.(Lcom/xamarin/JavaType;)I */
public int compareTo (JavaType value) {
return 0;
}
+ /** JNI sig: func.(Ljava/lang/StringBuilder;)Ljava/util/List; */
public List func (StringBuilder value) {
return null;
}
+ /** JNI sig: run.()V */
public void run () {
}
+ /** JNI sig: action.(Ljava/lang/Object;)V */
@Deprecated
public void action (Object value) {
Object local = new Object ();
@@ -118,10 +183,12 @@ public void run() {
r.run();
}
+ /** JNI sig: func.([Ljava/lang/String;)Ljava/lang/Integer; */
public java.lang.Integer func (String[] values) {
return values.length;
}
+ /** JNI sig: staticActionWithGenerics.(Ljava/lang/Object;Ljava/lang/Number;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V */
public static , TThrowable extends Throwable>
void staticActionWithGenerics (
T value1,
@@ -132,19 +199,23 @@ void staticActionWithGenerics (
throws IllegalArgumentException, NumberFormatException, TThrowable {
}
+ /** JNI sig: instanceActionWithGenerics.(Ljava/lang/Object;java/lang/Object;)V */
public
void instanceActionWithGenerics (
T value1,
E value2) {
}
+ /** JNI sig: sum.(I[I)I */
public static int sum (int first, int... remaining) {
return -1;
}
+ /** JNI sig: finalize.()V */
protected void finalize () {
}
+ /** JNI sig: finalize.(I)I */
public static int finalize (int value) {
return value;
}
diff --git a/tools/java-source-utils/.classpath b/tools/java-source-utils/.classpath
new file mode 100644
index 000000000..30230c1c4
--- /dev/null
+++ b/tools/java-source-utils/.classpath
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/java-source-utils/.gitignore b/tools/java-source-utils/.gitignore
new file mode 100644
index 000000000..5e4e31eb4
--- /dev/null
+++ b/tools/java-source-utils/.gitignore
@@ -0,0 +1,10 @@
+# Ignore Gradle project-specific cache directory
+.gradle
+
+# ???
+.settings
+
+# Ignore Gradle build output directory
+build
+
+
diff --git a/tools/java-source-utils/.project b/tools/java-source-utils/.project
new file mode 100644
index 000000000..8337a1c56
--- /dev/null
+++ b/tools/java-source-utils/.project
@@ -0,0 +1,23 @@
+
+
+ java-source-utils
+ Project java-source-utils created by Buildship.
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
diff --git a/tools/java-source-utils/CGManifest.json b/tools/java-source-utils/CGManifest.json
new file mode 100644
index 000000000..eeac90a8f
--- /dev/null
+++ b/tools/java-source-utils/CGManifest.json
@@ -0,0 +1,26 @@
+{
+ "Registrations": [
+ {
+ "Component": {
+ "Type": "maven",
+ "Maven": {
+ "GroupId": "com.github.javaparser",
+ "ArtifactId": "javaparser-core",
+ "Version": "3.16.1"
+ }
+ },
+ "DevelopmentDependency":false
+ },
+ {
+ "Component": {
+ "Type": "maven",
+ "Maven": {
+ "GroupId": "com.github.javaparser",
+ "ArtifactId": "javaparser-symbol-solver-core",
+ "Version": "3.16.1"
+ }
+ },
+ "DevelopmentDependency":false
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tools/java-source-utils/Directory.Build.targets b/tools/java-source-utils/Directory.Build.targets
new file mode 100644
index 000000000..2ef10a27e
--- /dev/null
+++ b/tools/java-source-utils/Directory.Build.targets
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ <_XATBJavaSourceFile Include="JavaType.java" />
+ <_XATBJavaSource Include="@(_XATBJavaSourceFile->'$(MSBuildThisFileDirectory)../../tests/Xamarin.Android.Tools.Bytecode-Tests/java/com/xamarin/%(Identity)')" />
+ <_XATBJavaDest Include="@(_XATBJavaSourceFile->'$(MSBuildThisFileDirectory)src/test/resources/com/xamarin/%(Identity)')" />
+
+
+
+
+ <_Dirs Include="@(_XATBJavaDest->'%(RelativeDir)')" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_JavaSource Include="src/test/resources/com/microsoft/android/Outer.java" />
+ <_JavaSource Include="src/test/resources/com/xamarin/JavaType.java" />
+
+
+
+
+
+
+
+
+
diff --git a/tools/java-source-utils/README.md b/tools/java-source-utils/README.md
new file mode 100644
index 000000000..9a845f808
--- /dev/null
+++ b/tools/java-source-utils/README.md
@@ -0,0 +1,8 @@
+# java-source-utils
+
+`java-source-utils` is a Java program which uses [JavaParser][0] to process
+Java source code in order to extract method parameter names and Javadoc
+documentation, as the typical alternative is to instead process Javadoc *HTML*
+to obtain this information, and Javadoc HTML is less "stable" than Java source.
+
+[0]: https://github.com/javaparser/javaparser
diff --git a/tools/java-source-utils/build.gradle b/tools/java-source-utils/build.gradle
new file mode 100644
index 000000000..516e39b6b
--- /dev/null
+++ b/tools/java-source-utils/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * This generated file contains a sample Java project to get you started.
+ * For more details take a look at the Java Quickstart chapter in the Gradle
+ * User Manual available at https://docs.gradle.org/6.3/userguide/tutorial_java_projects.html
+ */
+
+plugins {
+ // Apply the java plugin to add support for Java
+ id 'java'
+
+ // Apply the application plugin to add support for building a CLI application.
+ id 'application'
+}
+
+java {
+ ext.javaSourceVer = project.hasProperty('javaSourceVer') ? JavaVersion.toVersion(project.getProperty('javaSourceVer')) : JavaVersion.VERSION_1_8
+ ext.javaTargetVer = project.hasProperty('javaTargetVer') ? JavaVersion.toVersion(project.getProperty('javaTargetVer')) : JavaVersion.VERSION_1_8
+
+ sourceCompatibility = ext.javaSourceVer
+ targetCompatibility = ext.javaTargetVer
+}
+
+repositories {
+ // Use jcenter for resolving dependencies.
+ // You can declare any Maven/Ivy/file repository here.
+ jcenter()
+}
+
+dependencies {
+ // This dependency is used by the application.
+ implementation 'com.github.javaparser:javaparser-core:3.16.1'
+ implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.16.1'
+
+ // Use JUnit test framework
+ testImplementation 'junit:junit:4.12'
+}
+
+application {
+ // Define the main class for the application.
+ mainClassName = 'com.microsoft.android.App'
+}
+
+jar {
+ duplicatesStrategy = 'exclude'
+ manifest {
+ attributes 'Main-Class': 'com.microsoft.android.App'
+ }
+ from {
+ configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+ } {
+ exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
+ }
+ archiveName 'java-source-utils.jar'
+}
diff --git a/tools/java-source-utils/java-source-utils.csproj b/tools/java-source-utils/java-source-utils.csproj
new file mode 100644
index 000000000..dfd448b0e
--- /dev/null
+++ b/tools/java-source-utils/java-source-utils.csproj
@@ -0,0 +1,33 @@
+
+
+
+
+ net472;netcoreapp3.1
+ java-source-utils.jar
+ .jar
+ jar
+ false
+ none
+
+
+
+ $(UtilityOutputFullPath)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/java-source-utils/settings.gradle b/tools/java-source-utils/settings.gradle
new file mode 100644
index 000000000..8a5eebc59
--- /dev/null
+++ b/tools/java-source-utils/settings.gradle
@@ -0,0 +1,10 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * The settings file is used to specify which projects to include in your build.
+ *
+ * Detailed information about configuring a multi-project build in Gradle can be found
+ * in the user manual at https://docs.gradle.org/6.3/userguide/multi_project_builds.html
+ */
+
+rootProject.name = 'java-source-utils'
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/App.java b/tools/java-source-utils/src/main/java/com/microsoft/android/App.java
new file mode 100644
index 000000000..b67d58c02
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/App.java
@@ -0,0 +1,74 @@
+/*
+ * This Java source file was generated by the Gradle 'init' task.
+ */
+package com.microsoft.android;
+
+import java.io.IOException;
+
+import com.github.javaparser.*;
+import com.github.javaparser.ParserConfiguration;
+
+import com.microsoft.android.ast.*;
+import com.microsoft.android.util.Parameter;
+
+public class App {
+ public static final String APP_NAME = "java-source-utils";
+
+ public static void main (final String[] args) throws Throwable {
+ JavaSourceUtilsOptions options;
+ try {
+ options = JavaSourceUtilsOptions.parse(args);
+ if (options == null) {
+ System.out.println(APP_NAME + " " + JavaSourceUtilsOptions.HELP_STRING);
+ return;
+ }
+ } catch (Throwable t) {
+ System.err.println(APP_NAME + ": error: " + t.getMessage());
+ if (JavaSourceUtilsOptions.verboseOutput) {
+ t.printStackTrace(System.err);
+ }
+ System.err.println("Usage: " + APP_NAME + " " + JavaSourceUtilsOptions.HELP_STRING);
+ System.exit(1);
+ return;
+ }
+
+ try {
+ final JavaParser parser = createParser(options);
+ final JniPackagesInfoFactory packagesFactory = new JniPackagesInfoFactory(parser);
+ final JniPackagesInfo packages = packagesFactory.parse(options.inputFiles);
+
+ if ((options.outputParamsTxt = Parameter.normalize(options.outputParamsTxt, "")).length() > 0) {
+ generateParamsTxt(options.outputParamsTxt, packages);
+ }
+ generateXml(options.outputJavadocXml, packages);
+ options.close();
+ }
+ catch (Throwable t) {
+ options.close();
+ System.err.println(APP_NAME + ": internal error: " + t.getMessage());
+ if (JavaSourceUtilsOptions.verboseOutput) {
+ t.printStackTrace(System.err);
+ }
+ System.exit(2);
+ return;
+ }
+ }
+
+ static JavaParser createParser(JavaSourceUtilsOptions options) throws IOException {
+ final ParserConfiguration config = options.createConfiguration();
+ final JavaParser parser = new JavaParser(config);
+ return parser;
+ }
+
+ static void generateParamsTxt(String filename, JniPackagesInfo packages) throws Throwable {
+ try (final ParameterNameGenerator paramsTxtGen = new ParameterNameGenerator(filename)) {
+ paramsTxtGen.writePackages(packages);
+ }
+ }
+
+ static void generateXml(String filename, JniPackagesInfo packages) throws Throwable {
+ try (final JavadocXmlGenerator javadocXmlGen = new JavadocXmlGenerator(filename)) {
+ javadocXmlGen.writePackages(packages);
+ }
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/JavaSourceUtilsOptions.java b/tools/java-source-utils/src/main/java/com/microsoft/android/JavaSourceUtilsOptions.java
new file mode 100644
index 000000000..78ec4f613
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/JavaSourceUtilsOptions.java
@@ -0,0 +1,257 @@
+package com.microsoft.android;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import com.github.javaparser.*;
+import com.github.javaparser.JavaParser;
+import com.github.javaparser.ParserConfiguration;
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.*;
+import com.github.javaparser.ast.type.*;
+import com.github.javaparser.ast.body.*;
+import com.github.javaparser.ast.body.BodyDeclaration;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.ast.expr.Expression;
+import com.github.javaparser.ast.body.Parameter;
+import com.github.javaparser.ast.nodeTypes.*;
+import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc;
+import com.github.javaparser.ast.nodeTypes.NodeWithParameters;
+import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName;
+import com.github.javaparser.resolution.SymbolResolver;
+import com.github.javaparser.resolution.types.ResolvedType;
+import com.github.javaparser.symbolsolver.*;
+import com.github.javaparser.symbolsolver.model.resolution.TypeSolver;
+import com.github.javaparser.symbolsolver.resolution.typesolvers.*;
+
+
+public class JavaSourceUtilsOptions implements AutoCloseable {
+ public static final String HELP_STRING = "[-v] [<-a|--aar> AAR]* [<-j|--jar> JAR]* [<-s|--source> DIRS]*\n" +
+ "\t[--bootclasspath CLASSPATH]\n" +
+ "\t[<-P|--output-params> OUT.params.txt] [<-D|--output-javadoc> OUT.xml] FILES";
+
+ public static boolean verboseOutput;
+
+ public final List aarFiles = new ArrayList();
+ public final List jarFiles = new ArrayList();
+
+ public final Collection inputFiles = new ArrayList();
+
+ public boolean haveBootClassPath;
+ public String outputParamsTxt;
+ public String outputJavadocXml;
+
+ private final Collection sourceDirectoryFiles = new ArrayList();
+ private File extractedTempDir;
+
+
+ public void close() {
+ if (extractedTempDir != null) {
+ try {
+ Files.walk(extractedTempDir.toPath())
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .forEach(File::delete);
+ extractedTempDir.delete();
+ }
+ catch (Throwable t) {
+ System.err.println(App.APP_NAME + ": error deleting temp directory `" + extractedTempDir.getAbsolutePath() + "`: " + t.getMessage());
+ if (verboseOutput) {
+ t.printStackTrace(System.err);
+ }
+ }
+ }
+ extractedTempDir = null;
+ }
+
+ public ParserConfiguration createConfiguration() throws IOException {
+ final ParserConfiguration config = new ParserConfiguration()
+ // Associate Javadoc comments with AST members
+ .setAttributeComments(true)
+
+ // If there are blank lines between Javadoc blocks & declarations,
+ // *ignore* those blank lines and associate the Javadoc w/ the decls
+ .setDoNotAssignCommentsPrecedingEmptyLines(false)
+
+ // Associate Javadoc comments w/ the declaration, *not* with
+ // any annotations on the declaration
+ .setIgnoreAnnotationsWhenAttributingComments(true)
+ ;
+ final TypeSolver typeSolver = createTypeSolver(config);
+ config.setSymbolResolver(new JavaSymbolSolver(typeSolver));
+ return config;
+ }
+
+ private final TypeSolver createTypeSolver(ParserConfiguration config) throws IOException {
+ final CombinedTypeSolver typeSolver = new CombinedTypeSolver();
+ for (File file : aarFiles) {
+ typeSolver.add(new AarTypeSolver(file));
+ }
+ for (File file : jarFiles) {
+ typeSolver.add(new JarTypeSolver(file));
+ }
+ if (!haveBootClassPath) {
+ typeSolver.add(new ReflectionTypeSolver());
+ }
+ for (File srcDir : sourceDirectoryFiles) {
+ typeSolver.add(new JavaParserTypeSolver(srcDir, config));
+ }
+ return typeSolver;
+ }
+
+ public static JavaSourceUtilsOptions parse(final String[] args) throws IOException {
+ final JavaSourceUtilsOptions options = new JavaSourceUtilsOptions();
+
+ for (int i = 0; i < args.length; ++i) {
+ final String arg = args[i];
+ switch (arg) {
+ case "-bootclasspath": {
+ final String bootClassPath = getOptionValue(args, ++i, arg);
+ final ArrayList files = new ArrayList();
+ for (final String cp : bootClassPath.split(File.pathSeparator)) {
+ final File file = new File(cp);
+ if (!file.exists()) {
+ System.err.println(App.APP_NAME + ": warning: invalid file path for option `-bootclasspath`: " + cp);
+ continue;
+ }
+ files.add(file);
+ }
+ for (int j = files.size(); j > 0; --j) {
+ options.jarFiles.add(0, files.get(j-1));
+ }
+ options.haveBootClassPath = true;
+ break;
+ }
+ case "-a":
+ case "--aar": {
+ final File file = getOptionFile(args, ++i, arg);
+ if (file == null) {
+ break;
+ }
+ options.aarFiles.add(file);
+ break;
+ }
+ case "-j":
+ case "--jar": {
+ final File file = getOptionFile(args, ++i, arg);
+ if (file == null) {
+ break;
+ }
+ options.jarFiles.add(file);
+ break;
+ }
+ case "-s":
+ case "--source": {
+ final File dir = getOptionFile(args, ++i, arg);
+ if (dir == null) {
+ break;
+ }
+ options.sourceDirectoryFiles.add(dir);
+ break;
+ }
+ case "-D":
+ case "--output-javadoc": {
+ options.outputJavadocXml = getOptionValue(args, ++i, arg);
+ break;
+ }
+ case "-P":
+ case "--output-params": {
+ options.outputParamsTxt = getOptionValue(args, ++i, arg);
+ break;
+ }
+ case "-v": {
+ verboseOutput = true;
+ break;
+ }
+ case "-h":
+ case "--help": {
+ return null;
+ }
+ default: {
+ final File file = getOptionFile(args, i, "FILES");
+ if (file == null)
+ break;
+
+ if (file.isDirectory()) {
+ options.sourceDirectoryFiles.add(file);
+ Files.walk(file.toPath())
+ .filter(f -> Files.isRegularFile(f) && f.getFileName().toString().endsWith(".java"))
+ .map(Path::toFile)
+ .forEach(f -> options.inputFiles.add(f));
+ break;
+ }
+ if (file.getName().endsWith(".java")) {
+ options.inputFiles.add(file);
+ break;
+ }
+ if (!file.getName().endsWith(".jar") && !file.getName().endsWith(".zip")) {
+ System.err.println(App.APP_NAME + ": warning: ignoring input file `" + file.getAbsolutePath() +"`.");
+ break;
+ }
+ if (options.extractedTempDir == null) {
+ options.extractedTempDir = Files.createTempDirectory("ji-jst").toFile();
+ }
+ File toDir = new File(options.extractedTempDir, file.getName());
+ options.sourceDirectoryFiles.add(toDir);
+ extractTo(file, toDir, options.inputFiles);
+ break;
+ }
+ }
+ }
+ return options;
+ }
+
+ private static void extractTo(final File zipFilePath, final File toDir, final Collection inputFiles) throws IOException {
+ try (final ZipFile zipFile = new ZipFile(zipFilePath)) {
+ Enumeration extends ZipEntry> e = zipFile.entries();
+ while (e.hasMoreElements()) {
+ final ZipEntry entry = e.nextElement();
+ if (entry.isDirectory())
+ continue;
+ if (!entry.getName().endsWith(".java"))
+ continue;
+ final File target = new File(toDir, entry.getName());
+ if (verboseOutput) {
+ System.out.println ("# creating file: " + target.getAbsolutePath());
+ }
+ target.getParentFile().mkdirs();
+ final InputStream zipContents = zipFile.getInputStream(entry);
+ Files.copy(zipContents, target.toPath());
+ zipContents.close();
+ inputFiles.add(target);
+ }
+ }
+ }
+
+ static String getOptionValue(final String[] args, final int index, final String option) {
+ if (index >= args.length)
+ throw new IllegalArgumentException(
+ "Expected required value for option `" + option + "` at index " + index + ".");
+ return args[index];
+ }
+
+ static File getOptionFile(final String[] args, final int index, final String option) {
+ if (index >= args.length)
+ throw new IllegalArgumentException(
+ "Expected required value for option `" + option + "` at index " + index + ".");
+ final String fileName = args[index];
+ final File file = new File(fileName);
+ if (!file.exists()) {
+ System.err.println(App.APP_NAME + ": warning: invalid file path for option `" + option + "`: " + fileName);
+ return null;
+ }
+ return file;
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/JavadocXmlGenerator.java b/tools/java-source-utils/src/main/java/com/microsoft/android/JavadocXmlGenerator.java
new file mode 100644
index 000000000..41ad934d0
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/JavadocXmlGenerator.java
@@ -0,0 +1,174 @@
+package com.microsoft.android;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import com.microsoft.android.ast.*;
+import com.microsoft.android.util.Parameter;
+
+public final class JavadocXmlGenerator implements AutoCloseable {
+
+ final PrintStream output;
+
+ public JavadocXmlGenerator(final String output) throws FileNotFoundException, UnsupportedEncodingException {
+ if (output == null)
+ this.output = System.out;
+ else {
+ final File file = new File(output);
+ final File parent = file.getParentFile();
+ if (parent != null) {
+ parent.mkdirs();
+ }
+ this.output = new PrintStream(file, "UTF-8");
+ }
+ }
+
+ public JavadocXmlGenerator(final PrintStream output) {
+ Parameter.requireNotNull("output", output);
+
+ this.output = output;
+ }
+
+ public void close() {
+ if (output != System.out) {
+ output.flush();
+ output.close();
+ }
+ }
+
+ public final void writePackages(final JniPackagesInfo packages) throws ParserConfigurationException, TransformerException {
+ Parameter.requireNotNull("packages", packages);
+
+ final Document document = DocumentBuilderFactory.newInstance ()
+ .newDocumentBuilder()
+ .newDocument();
+ final Element api = document.createElement("api");
+ api.setAttribute("api-source", "java-source-utils");
+ document.appendChild(api);
+
+ for (JniPackageInfo packageInfo : packages.getSortedPackages()) {
+ writePackage(document, api, packageInfo);
+ }
+
+ Transformer transformer = TransformerFactory.newInstance()
+ .newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
+ transformer.transform(new DOMSource(document), new StreamResult(output));
+ }
+
+ private static final void writePackage(final Document document, final Element api, final JniPackageInfo packageInfo) {
+ final Element packageXml = document.createElement("package");
+ packageXml.setAttribute("name", packageInfo.getPackageName());
+ packageXml.setAttribute("jni-name", packageInfo.getPackageName().replace(".", "/"));
+ api.appendChild(packageXml);
+
+ for (JniTypeInfo typeInfo : packageInfo.getSortedTypes()) {
+ writeType(document, packageXml, typeInfo);
+ }
+ }
+
+ private static final void writeType(final Document document, final Element packageXml, final JniTypeInfo typeInfo) {
+ final Element typeXml = document.createElement(typeInfo.getTypeKind());
+ typeXml.setAttribute("name", typeInfo.getRawName());
+ typeXml.setAttribute("jni-signature", getTypeJniName(typeInfo));
+ packageXml.appendChild(typeXml);
+
+ writeJavadoc(document, typeXml, typeInfo.getJavadocComment());
+
+ for (JniMemberInfo memberInfo : typeInfo.getSortedMembers()) {
+ writeMember(document, typeXml, memberInfo);
+ }
+ }
+
+ private static String getTypeJniName(JniTypeInfo typeInfo) {
+ final String packageName = typeInfo.getDeclaringPackage().getPackageName();
+ final StringBuilder name = new StringBuilder();
+
+ name.append("L");
+ if (packageName.length() > 0) {
+ name.append(packageName.replace(".", "/"));
+ name.append("/");
+ }
+ name.append(typeInfo.getRawName().replace(".", "$"));
+ name.append(";");
+
+ return name.toString();
+ }
+
+ private static final void writeJavadoc(final Document document, final Element parent, String javadoc) {
+ javadoc = Parameter.normalize(javadoc, "");
+
+ if (javadoc.length() == 0) {
+ return;
+ }
+
+ final Element javadocXml = document.createElement("javadoc");
+ parent.appendChild(javadocXml);
+
+ javadocXml.appendChild(document.createCDATASection(javadoc));
+ }
+
+ private static void writeMember(final Document document, final Element typeXml, final JniMemberInfo memberInfo) {
+ JniMethodBaseInfo paramsInfo = null;
+ int paramsCount = 0;
+ if (memberInfo.isConstructor() || memberInfo.isMethod()) {
+ paramsInfo = (JniMethodBaseInfo) memberInfo;
+ paramsCount = paramsInfo.getParameters().size();
+ }
+ final String javadoc = Parameter.normalize(memberInfo.getJavadocComment(), "");
+ if (paramsCount == 0 && javadoc.length() == 0) {
+ return;
+ }
+
+ final Element memberXml = document.createElement(getMemberXmlElement(memberInfo));
+ if (!memberInfo.isConstructor()) {
+ memberXml.setAttribute("name", memberInfo.getName());
+ }
+ memberXml.setAttribute("jni-signature", memberInfo.getJniSignature());
+ typeXml.appendChild(memberXml);
+
+ if (memberInfo.isMethod()) {
+ final JniMethodInfo methodInfo = (JniMethodInfo) memberInfo;
+ memberXml.setAttribute("return", methodInfo.getJavaReturnType());
+ memberXml.setAttribute("jni-return", methodInfo.getJniReturnType());
+ }
+
+ if (paramsInfo != null) {
+ for (JniParameterInfo paramInfo : paramsInfo.getParameters()) {
+ final Element parameter = document.createElement("parameter");
+ parameter.setAttribute("name", paramInfo.name);
+ parameter.setAttribute("type", paramInfo.javaType);
+ parameter.setAttribute("jni-type", paramInfo.jniType);
+
+ memberXml.appendChild(parameter);
+ }
+ }
+
+ writeJavadoc(document, memberXml, memberInfo.getJavadocComment());
+ }
+
+ private static String getMemberXmlElement(JniMemberInfo member) {
+ if (member.isConstructor())
+ return "constructor";
+ if (member.isMethod())
+ return "method";
+ if (member.isField())
+ return "field";
+ throw new Error("Don't know XML element for: " + member.toString());
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java b/tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java
new file mode 100644
index 000000000..b63c3162b
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java
@@ -0,0 +1,414 @@
+package com.microsoft.android;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import com.github.javaparser.*;
+import com.github.javaparser.JavaParser;
+import com.github.javaparser.ParserConfiguration;
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.*;
+import com.github.javaparser.ast.comments.*;
+import com.github.javaparser.ast.type.*;
+import com.github.javaparser.ast.body.*;
+import com.github.javaparser.ast.body.BodyDeclaration;
+import com.github.javaparser.ast.body.Parameter;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.ast.comments.JavadocComment;
+import com.github.javaparser.ast.expr.Expression;
+import com.github.javaparser.ast.nodeTypes.*;
+import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc;
+import com.github.javaparser.ast.nodeTypes.NodeWithParameters;
+import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName;
+import com.github.javaparser.resolution.SymbolResolver;
+import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
+import com.github.javaparser.resolution.types.ResolvedReferenceType;
+import com.github.javaparser.resolution.types.ResolvedType;
+import com.github.javaparser.symbolsolver.*;
+import com.github.javaparser.symbolsolver.model.resolution.TypeSolver;
+import com.github.javaparser.symbolsolver.resolution.typesolvers.*;
+
+import com.github.javaparser.ParseResult;
+import com.github.javaparser.ast.CompilationUnit;
+
+import com.microsoft.android.ast.*;
+
+import javassist.compiler.ast.FieldDecl;
+
+import static com.microsoft.android.util.Parameter.*;
+
+public final class JniPackagesInfoFactory {
+
+ final JavaParser parser;
+
+ public JniPackagesInfoFactory(final JavaParser parser) {
+ requireNotNull("parser", parser);
+
+ this.parser = parser;
+ }
+
+ public JniPackagesInfo parse(final Collection files) throws Throwable {
+ requireNotNull("files", files);
+
+ final JniPackagesInfo packages = new JniPackagesInfo();
+
+ for (final File file : files) {
+ final ParseResult result = parser.parse(file);
+ final Optional unit = result.getResult();
+ if (!unit.isPresent()) {
+ logParseErrors(file, result.getProblems());
+ continue;
+ }
+ parse(packages, unit.get());
+ }
+
+ return packages;
+ }
+
+ private static void logParseErrors(final File file, final List problems) {
+ System.err.println(App.APP_NAME + ": could not parse file `" + file.getName() + "`:");
+ for (final Problem p : problems) {
+ System.err.print("\t");
+ Optional location = p.getLocation();
+ if (location.isPresent()) {
+ System.err.print(location.get());
+ System.err.print(": ");
+ }
+ System.err.println(p.getVerboseMessage());
+ if (JavaSourceUtilsOptions.verboseOutput && p.getCause().isPresent()) {
+ p.getCause().get().printStackTrace(System.err);
+ }
+ }
+ }
+
+ /** parse method */
+ private void parse(final JniPackagesInfo packages, final CompilationUnit unit) throws Throwable {
+ final String packageName = unit.getPackageDeclaration().isPresent()
+ ? unit.getPackageDeclaration().get().getNameAsString()
+ : "";
+ final JniPackageInfo packageInfo = packages.getPackage(packageName);
+
+ for (final TypeDeclaration> type : unit.getTypes()) {
+ if (JavaSourceUtilsOptions.verboseOutput && type.getFullyQualifiedName().isPresent()) {
+ System.out.println("Processing: " + type.getFullyQualifiedName().get());
+ }
+ if (type.isAnnotationDeclaration()) {
+ final AnnotationDeclaration annoDecl = type.asAnnotationDeclaration();
+ final JniTypeInfo annoInfo = createAnnotationInfo(packageInfo, annoDecl, null);
+ parseType(packageInfo, annoInfo, annoDecl);
+ continue;
+ }
+ if (type.isClassOrInterfaceDeclaration()) {
+ final ClassOrInterfaceDeclaration typeDecl = type.asClassOrInterfaceDeclaration();
+ final JniTypeInfo typeInfo = createTypeInfo(packageInfo, typeDecl, null);
+ parseType(packageInfo, typeInfo, typeDecl);
+ continue;
+ }
+ if (type.isEnumDeclaration()) {
+ final EnumDeclaration enumDecl = type.asEnumDeclaration();
+ final JniTypeInfo nestedEnum = createEnumInfo(packageInfo, enumDecl, null);
+ parseType(packageInfo, nestedEnum, enumDecl);
+ continue;
+ }
+ System.out.println("# TODO: unknown type decl " + type.getClass().getName());
+ System.out.println(type.toString());
+ }
+ }
+
+ static JniTypeInfo createAnnotationInfo(final JniPackageInfo packageInfo, final AnnotationDeclaration annotationDecl, JniTypeInfo declInfo) {
+ final String declName = declInfo == null ? "" : declInfo.getRawName() + ".";
+ final JniTypeInfo annotationInfo = new JniInterfaceInfo(packageInfo, declName + annotationDecl.getNameAsString());
+ packageInfo.add(annotationInfo);
+ fillJavadoc(annotationInfo, annotationDecl);
+ if (declInfo != null) {
+ for (String typeParameter : declInfo.getTypeParameters()) {
+ annotationInfo.addTypeParameter(typeParameter, declInfo.getTypeParameterJniType(typeParameter));
+ }
+ }
+ return annotationInfo;
+ }
+
+ static JniTypeInfo createEnumInfo(final JniPackageInfo packageInfo, final EnumDeclaration enumDecl, JniTypeInfo declInfo) {
+ final String declName = declInfo == null ? "" : declInfo.getRawName() + ".";
+ final JniTypeInfo enumInfo = new JniClassInfo(packageInfo, declName + enumDecl.getNameAsString());
+ packageInfo.add(enumInfo);
+ fillJavadoc(enumInfo, enumDecl);
+ if (declInfo != null) {
+ for (String typeParameter : declInfo.getTypeParameters()) {
+ enumInfo.addTypeParameter(typeParameter, declInfo.getTypeParameterJniType(typeParameter));
+ }
+ }
+ return enumInfo;
+ }
+
+ static JniTypeInfo createTypeInfo(final JniPackageInfo packageInfo, final ClassOrInterfaceDeclaration typeDecl, JniTypeInfo declInfo) {
+ final String declName = declInfo == null ? "" : declInfo.getRawName() + ".";
+ final JniTypeInfo typeInfo = typeDecl.isInterface()
+ ? new JniInterfaceInfo(packageInfo, declName + typeDecl.getNameAsString())
+ : new JniClassInfo(packageInfo, declName + typeDecl.getNameAsString());
+ packageInfo.add(typeInfo);
+ fillJavadoc(typeInfo, typeDecl);
+ if (declInfo != null) {
+ for (String typeParameter : declInfo.getTypeParameters()) {
+ typeInfo.addTypeParameter(typeParameter, declInfo.getTypeParameterJniType(typeParameter));
+ }
+ }
+ for (TypeParameter typeParameter : typeDecl.getTypeParameters()) {
+ typeInfo.addTypeParameter(
+ typeParameter.getNameAsString(),
+ getJniType(typeInfo, null, getTypeParameterBound(typeParameter)));
+ }
+ return typeInfo;
+ }
+
+ static ClassOrInterfaceType getTypeParameterBound(TypeParameter typeParameter) {
+ for (ClassOrInterfaceType boundType : typeParameter.getTypeBound()) {
+ return boundType;
+ }
+ return null;
+ }
+
+ private final void parseType(final JniPackageInfo packageInfo, final JniTypeInfo typeInfo, TypeDeclaration> typeDecl) {
+ for (final BodyDeclaration> body : typeDecl.getMembers()) {
+ if (body.isAnnotationDeclaration()) {
+ final AnnotationDeclaration annoDecl = body.asAnnotationDeclaration();
+ final JniTypeInfo annoInfo = createAnnotationInfo(packageInfo, annoDecl, typeInfo);
+ parseType(packageInfo, annoInfo, annoDecl);
+ continue;
+ }
+ if (body.isClassOrInterfaceDeclaration()) {
+ final ClassOrInterfaceDeclaration nestedDecl = body.asClassOrInterfaceDeclaration();
+ final JniTypeInfo nestedType = createTypeInfo(packageInfo, nestedDecl, typeInfo);
+ parseType(packageInfo, nestedType, nestedDecl);
+ continue;
+ }
+ if (body.isEnumDeclaration()) {
+ final EnumDeclaration enumDecl = body.asEnumDeclaration();
+ final JniTypeInfo nestedEnum = createEnumInfo(packageInfo, enumDecl, typeInfo);
+ parseType(packageInfo, nestedEnum, enumDecl);
+ continue;
+ }
+ if (body.isAnnotationMemberDeclaration()) {
+ parseAnnotationMemberDecl(typeInfo, body.asAnnotationMemberDeclaration());
+ continue;
+ }
+ if (body.isConstructorDeclaration()) {
+ parseConstructorDecl(typeInfo, body.asConstructorDeclaration());
+ continue;
+ }
+ if (body.isFieldDeclaration()) {
+ parseFieldDecl(typeInfo, body.asFieldDeclaration());
+ continue;
+ }
+ if (body.isMethodDeclaration()) {
+ parseMethodDecl(typeInfo, body.asMethodDeclaration());
+ continue;
+ }
+ if (body.isInitializerDeclaration()) {
+ // e.g. `static { CREATOR = null; }
+ continue;
+ }
+ System.out.println("# TODO: unknown body member " + body.getClass().getName());
+ System.out.println(body.toString());
+ }
+ }
+
+ private final void parseAnnotationMemberDecl(final JniTypeInfo typeInfo, final AnnotationMemberDeclaration memberDecl) {
+ final JniMethodInfo methodInfo = new JniMethodInfo(typeInfo, memberDecl.getNameAsString());
+ typeInfo.add(methodInfo);
+
+ methodInfo.setReturnType(
+ getJavaType(typeInfo, methodInfo, memberDecl.getType()),
+ getJniType(typeInfo, methodInfo, memberDecl.getType()));
+
+ fillJavadoc(methodInfo, memberDecl);
+ }
+
+ private final void parseFieldDecl(final JniTypeInfo typeInfo, final FieldDeclaration fieldDecl) {
+ for (VariableDeclarator f : fieldDecl.getVariables()) {
+ final JniFieldInfo fieldInfo = new JniFieldInfo(typeInfo, f.getNameAsString());
+ fieldInfo.setJniType(getJniType(typeInfo, null, f.getType()));
+
+ typeInfo.add(fieldInfo);
+
+ fillJavadoc(fieldInfo, fieldDecl);
+ }
+ }
+
+ private final void parseConstructorDecl(final JniTypeInfo typeInfo, final ConstructorDeclaration ctorDecl) {
+ final JniConstructorInfo ctorInfo = new JniConstructorInfo(typeInfo);
+ typeInfo.add(ctorInfo);
+
+ fillMethodBase(ctorInfo, ctorDecl);
+ fillJavadoc(ctorInfo, ctorDecl);
+ }
+
+ private final void parseMethodDecl(final JniTypeInfo typeInfo, final MethodDeclaration methodDecl) {
+ final JniMethodInfo methodInfo = new JniMethodInfo(typeInfo, methodDecl.getNameAsString());
+ typeInfo.add(methodInfo);
+
+ for (TypeParameter typeParameter : methodDecl.getTypeParameters()) {
+ methodInfo.addTypeParameter(
+ typeParameter.getNameAsString(),
+ getJniType(typeInfo, methodInfo, getTypeParameterBound(typeParameter)));
+ }
+ methodInfo.setReturnType(
+ getJavaType(typeInfo, methodInfo, methodDecl.getType()),
+ getJniType(typeInfo, methodInfo, methodDecl.getType()));
+
+ fillMethodBase(methodInfo, methodDecl);
+ fillJavadoc(methodInfo, methodDecl);
+ }
+
+ private static final void fillJavadoc(final HasJavadocComment member, NodeWithJavadoc> nodeWithJavadoc) {
+ JavadocComment javadoc = null;
+ if (nodeWithJavadoc.getJavadocComment().isPresent()) {
+ javadoc = nodeWithJavadoc.getJavadocComment().get();
+ } else {
+ Node node = (Node) nodeWithJavadoc;
+ if (!node.getParentNode().isPresent())
+ return;
+
+ /*
+ * Sometimes `JavaParser` won't associate a Javadoc comment block with
+ * the AST node we expect it to. In such circumstances the Javadoc
+ * comment will become an "orphan" comment, unassociated with anything.
+ *
+ * If `nodeWithJavadoc` has no Javadoc comment, use the *first*
+ * orphan Javadoc comment in the *parent* scope, then *remove* that
+ * comment so that it doesn't "stick around" for the next member we
+ * attempt to grab Javadoc comments for.
+ */
+ Node parent = node.getParentNode().get();
+ for (Comment c : parent.getOrphanComments()) {
+ if (c.isJavadocComment()) {
+ javadoc = c.asJavadocComment();
+ c.remove();
+ break;
+ }
+ }
+ }
+ if (javadoc != null) {
+ member.setJavadocComment(javadoc.parse().toText());
+ }
+ }
+
+ private final void fillMethodBase(final JniMethodBaseInfo methodBaseInfo, final CallableDeclaration> callableDecl) {
+ JniMethodInfo methodInfo = null;
+ if (methodBaseInfo instanceof JniMethodInfo) {
+ methodInfo = (JniMethodInfo) methodBaseInfo;
+ }
+ NodeWithParameters> params = callableDecl;
+ for (final Parameter p : params.getParameters()) {
+ String name = p.getNameAsString();
+ String javaType = getJavaType(methodBaseInfo.getDeclaringType(), methodInfo, p.getType());
+ String jniType = getJniType(methodBaseInfo.getDeclaringType(), methodInfo, p.getType());
+ methodBaseInfo.addParameter(new JniParameterInfo(name, javaType, jniType));
+ }
+ }
+
+ static String getJavaType(JniTypeInfo typeInfo, JniMethodInfo methodInfo, Type type) {
+ String typeName = type.asString();
+ if (methodInfo != null && methodInfo.getTypeParameters().contains(typeName))
+ return typeName;
+ if (typeInfo.getTypeParameters().contains(typeName))
+ return typeName;
+ try {
+ final ResolvedType rt = type.resolve();
+ return rt.describe();
+ } catch (final Throwable thr) {
+ return ".*" + type.asString();
+ }
+ }
+
+ static String getJniType(JniTypeInfo typeInfo, JniMethodInfo methodInfo, Type type) {
+ if (type == null) {
+ return "Ljava/lang/Object;";
+ }
+
+ if (type.isArrayType()) {
+ return getJniType(typeInfo, methodInfo, type.asArrayType());
+ }
+ if (type.isPrimitiveType()) {
+ return getPrimitiveJniType(type.asString());
+ }
+
+ if (methodInfo != null && methodInfo.getTypeParameters().contains(type.asString())) {
+ return methodInfo.getTypeParameterJniType(type.asString());
+ }
+ if (typeInfo.getTypeParameters().contains(type.asString())) {
+ return typeInfo.getTypeParameterJniType(type.asString());
+ }
+
+ try {
+ return getJniType(type.resolve());
+ }
+ catch (final Exception thr) {
+ }
+ return ".*" + type.asString();
+ }
+
+ static String getJniType(JniTypeInfo typeInfo, JniMethodInfo methodInfo, ArrayType type) {
+ final int level = type.getArrayLevel();
+ final StringBuilder depth = new StringBuilder();
+ for (int i = 0; i < level; ++i)
+ depth.append("[");
+ return depth.toString() + getJniType(typeInfo, methodInfo, type.getElementType());
+ }
+
+ static String getPrimitiveJniType(String javaType) {
+ switch (javaType) {
+ case "boolean": return "Z";
+ case "byte": return "B";
+ case "char": return "C";
+ case "double": return "D";
+ case "float": return "F";
+ case "int": return "I";
+ case "long": return "J";
+ case "short": return "S";
+ case "void": return "V";
+ }
+ throw new Error("Don't know JNI type for `" + javaType + "`!");
+ }
+
+ static String getJniType(ResolvedType type) {
+ if (type.isPrimitive()) {
+ return getPrimitiveJniType(type.asPrimitive().describe());
+ }
+ if (type.isReferenceType()) {
+ return getJniType(type.asReferenceType());
+ }
+ if (type.isVoid()) {
+ return "V";
+ }
+ return "-" + type.getClass().getName() + "-";
+ }
+
+ static String getJniType(ResolvedReferenceType type) {
+ final Optional typeDeclOpt = type.getTypeDeclaration();
+ if (!typeDeclOpt.isPresent())
+ throw new Error("Can't get `ResolvedReferenceTypeDeclaration` for type `" + type.toString() + "`!");
+
+ final ResolvedReferenceTypeDeclaration typeDecl = typeDeclOpt.get();
+ if (!type.hasName())
+ throw new Error("Type `" + type.toString() + "` has no name!");
+
+ StringBuilder name = new StringBuilder();
+ name.append("L");
+ name.append(typeDecl.getPackageName());
+ int len = name.length();
+ for (int i = 0; i < len; ++i) {
+ if (name.charAt (i) == '.') {
+ name.setCharAt(i, '/');
+ }
+ }
+ if (len > 1) {
+ name.append("/");
+ }
+ name.append(typeDecl.getName().replace(".", "$"));
+ name.append(";");
+ return name.toString();
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ParameterNameGenerator.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ParameterNameGenerator.java
new file mode 100644
index 000000000..7dbc56f82
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ParameterNameGenerator.java
@@ -0,0 +1,103 @@
+package com.microsoft.android;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.microsoft.android.ast.*;
+import com.microsoft.android.util.Parameter;
+
+public class ParameterNameGenerator implements AutoCloseable {
+
+ final PrintStream output;
+
+ public ParameterNameGenerator(final String output) throws FileNotFoundException, UnsupportedEncodingException {
+ if (output == null)
+ this.output = System.out;
+ else {
+ final File file = new File(output);
+ final File parent = file.getParentFile();
+ if (parent != null) {
+ parent.mkdirs();
+ }
+ this.output = new PrintStream(file, "UTF-8");
+ }
+ }
+
+ public ParameterNameGenerator(final PrintStream output) {
+ Parameter.requireNotNull("output", output);
+
+ this.output = output;
+ }
+
+ public void close() {
+ if (output != System.out) {
+ output.flush();
+ output.close();
+ }
+ }
+
+ public final void writePackages(final JniPackagesInfo packages) {
+ Parameter.requireNotNull("packages", packages);
+
+ boolean first = true;
+ for (JniPackageInfo packageInfo : packages.getSortedPackages()) {
+ if (!first)
+ output.println();
+ first = false;
+ writePackage(packageInfo);
+ }
+ }
+
+ private final void writePackage(final JniPackageInfo packageInfo) {
+ if (packageInfo.getPackageName().length() > 0) {
+ output.println("package " + packageInfo.getPackageName());
+ }
+ output.println(";---------------------------------------");
+
+ for (JniTypeInfo type : packageInfo.getSortedTypes()) {
+ writeType(type);
+ }
+ }
+
+ private final void writeType(JniTypeInfo type) {
+ output.println(" " + type.getTypeKind() + " " + type.getName());
+ final List sortedMethods = type.getSortedMembers()
+ .stream()
+ .filter(member -> member.isMethod() || member.isConstructor())
+ .map(member -> (JniMethodBaseInfo) member)
+ .filter(method -> method.getParameters().size() > 0)
+ .collect(Collectors.toList());
+
+ for (JniMethodBaseInfo method : sortedMethods) {
+ output.print(" ");
+ if (method.isMethod()) {
+ JniMethodInfo m = (JniMethodInfo) method;
+ Collection typeParameters = m.getTypeParameters();
+ if (typeParameters.size() > 0) {
+ output.print("<");
+ output.print(String.join(", ", typeParameters));
+ output.print("> ");
+ }
+ }
+ output.print(method.getName());
+ output.print("(");
+ boolean first = true;
+ for (JniParameterInfo parameter : method.getParameters()) {
+ if (!first) {
+ output.print(", ");
+ }
+ first = false;
+ output.print(parameter.javaType);
+ output.print(" ");
+ output.print(parameter.name);
+ }
+ output.print(")");
+ output.println();
+ }
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/HasJavadocComment.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/HasJavadocComment.java
new file mode 100644
index 000000000..6253eaf28
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/HasJavadocComment.java
@@ -0,0 +1,6 @@
+package com.microsoft.android.ast;
+
+public interface HasJavadocComment {
+ String getJavadocComment();
+ void setJavadocComment(String javaDocComment);
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniClassInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniClassInfo.java
new file mode 100644
index 000000000..6eec50deb
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniClassInfo.java
@@ -0,0 +1,12 @@
+package com.microsoft.android.ast;
+
+public class JniClassInfo extends JniTypeInfo {
+ public JniClassInfo(JniPackageInfo declaringPackage, String name) {
+ super(declaringPackage, name);
+ }
+
+ @Override
+ public String getTypeKind() {
+ return "class";
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniConstructorInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniConstructorInfo.java
new file mode 100644
index 000000000..adb099436
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniConstructorInfo.java
@@ -0,0 +1,17 @@
+package com.microsoft.android.ast;
+
+public final class JniConstructorInfo extends JniMethodBaseInfo {
+ public JniConstructorInfo(JniTypeInfo declaringType) {
+ super(declaringType, "#ctor");
+ }
+
+ @Override
+ public boolean isConstructor() {
+ return true;
+ }
+
+ @Override
+ public String getJniSignature() {
+ return super.getJniSignature() + "V";
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniFieldInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniFieldInfo.java
new file mode 100644
index 000000000..cfb87966e
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniFieldInfo.java
@@ -0,0 +1,26 @@
+package com.microsoft.android.ast;
+
+import com.microsoft.android.util.Parameter;
+
+public final class JniFieldInfo extends JniMemberInfo {
+
+ private String jniType;
+
+ public JniFieldInfo(JniTypeInfo declaringType, String name) {
+ super(declaringType, name);
+ }
+
+ @Override
+ public String getJniSignature() {
+ return jniType;
+ }
+
+ @Override
+ public boolean isField() {
+ return true;
+ }
+
+ public void setJniType(String jniType) {
+ this.jniType = Parameter.requireNotEmpty("jniType", jniType);
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniInterfaceInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniInterfaceInfo.java
new file mode 100644
index 000000000..55d891765
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniInterfaceInfo.java
@@ -0,0 +1,12 @@
+package com.microsoft.android.ast;
+
+public class JniInterfaceInfo extends JniTypeInfo {
+ public JniInterfaceInfo(JniPackageInfo declaringPackage, String name) {
+ super(declaringPackage, name);
+ }
+
+ @Override
+ public String getTypeKind() {
+ return "interface";
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMemberInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMemberInfo.java
new file mode 100644
index 000000000..7ee6005fb
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMemberInfo.java
@@ -0,0 +1,49 @@
+package com.microsoft.android.ast;
+
+import com.microsoft.android.util.Parameter;
+
+public abstract class JniMemberInfo implements HasJavadocComment {
+ private final String name;
+ private final JniTypeInfo declaringType;
+
+ String javadocComment = "";
+
+ JniMemberInfo(final JniTypeInfo declaringType, String name) {
+ Parameter.requireNotNull("declaringType", declaringType);
+
+ name = Parameter.requireNotEmpty("name", name);
+
+ this.declaringType = declaringType;
+ this.name = name;
+ }
+
+ public final JniTypeInfo getDeclaringType() {
+ return declaringType;
+ }
+
+ public abstract String getJniSignature();
+
+ public final String getName() {
+ return name;
+ }
+
+ public boolean isField() {
+ return false;
+ }
+
+ public boolean isMethod() {
+ return false;
+ }
+
+ public boolean isConstructor() {
+ return false;
+ }
+
+ public final String getJavadocComment() {
+ return javadocComment;
+ }
+
+ public final void setJavadocComment(String javaDocComment) {
+ this.javadocComment = Parameter.normalize(javaDocComment, "");
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodBaseInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodBaseInfo.java
new file mode 100644
index 000000000..4824c03c5
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodBaseInfo.java
@@ -0,0 +1,36 @@
+package com.microsoft.android.ast;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import com.microsoft.android.util.Parameter;
+
+public abstract class JniMethodBaseInfo extends JniMemberInfo {
+
+ private final Collection parameters = new ArrayList ();
+
+ JniMethodBaseInfo(final JniTypeInfo declaringType, final String name) {
+ super(declaringType, name);
+ }
+
+ public final void addParameter(final JniParameterInfo parameter) {
+ Parameter.requireNotNull("parameter", parameter);
+
+ parameters.add(parameter);
+ }
+
+ public Collection getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public String getJniSignature() {
+ final StringBuilder sig = new StringBuilder();
+ sig.append("(");
+ for (JniParameterInfo p : parameters) {
+ sig.append(p.jniType);
+ }
+ sig.append(")");
+ return sig.toString();
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodInfo.java
new file mode 100644
index 000000000..b10074307
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniMethodInfo.java
@@ -0,0 +1,64 @@
+package com.microsoft.android.ast;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+
+import com.microsoft.android.util.Parameter;
+
+public final class JniMethodInfo extends JniMethodBaseInfo {
+
+ private String javaReturnType;
+ private String jniReturnType;
+
+ private final Collection typeParameters = new ArrayList();
+ private final Map jniTypes = new HashMap();
+
+ public JniMethodInfo(JniTypeInfo declaringType, String name) {
+ super(declaringType, name);
+ }
+
+ @Override
+ public boolean isMethod() {
+ return true;
+ }
+
+ public final void setReturnType(String javaType, String jniType) {
+ this.javaReturnType = Parameter.normalize(javaType, "void");
+ this.jniReturnType = Parameter.normalize(jniType, "V");
+ }
+
+ public final void addTypeParameter(String typeParameter, String jniType) {
+ typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter);
+ jniType = Parameter.requireNotEmpty("jniType", jniType);
+
+ if (typeParameters.contains(typeParameter))
+ throw new IllegalArgumentException("Already added Type Parameter `" +typeParameter + "`");
+ typeParameters.add(typeParameter);
+ jniTypes.put(typeParameter, jniType);
+ }
+
+ public final Collection getTypeParameters() {
+ return typeParameters;
+ }
+
+ public final String getTypeParameterJniType(String typeParameter) {
+ typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter);
+ return jniTypes.get(typeParameter);
+ }
+
+ @Override
+ public String getJniSignature() {
+ return super.getJniSignature() + jniReturnType;
+ }
+
+ public final String getJavaReturnType() {
+ return javaReturnType;
+ }
+
+ public final String getJniReturnType() {
+ return jniReturnType;
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackageInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackageInfo.java
new file mode 100644
index 000000000..563426bf4
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackageInfo.java
@@ -0,0 +1,47 @@
+package com.microsoft.android.ast;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.microsoft.android.util.Parameter;
+
+public final class JniPackageInfo {
+ private final String packageName;
+
+ private final Map types = new HashMap();
+
+ public JniPackageInfo(String packageName) {
+ packageName = Parameter.normalize(packageName, "");
+
+ this.packageName = packageName;
+ }
+
+ public final String getPackageName() {
+ return this.packageName;
+ }
+
+ public final JniTypeInfo getType(String typeName) {
+ return types.getOrDefault(typeName, null);
+ }
+
+ public final void add(JniTypeInfo type) {
+ if (types.containsKey(type.getName()))
+ throw new IllegalArgumentException("type");
+ types.put(type.getName(), type);
+ }
+
+ public final Collection getTypes() {
+ return types.values();
+ }
+
+ public final Collection getSortedTypes() {
+ final List sortedTypes = types.values()
+ .stream()
+ .sorted((t1, t2) -> t1.getRawName().compareTo(t2.getRawName()))
+ .collect(Collectors.toList());
+ return sortedTypes;
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackagesInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackagesInfo.java
new file mode 100644
index 000000000..f7e84dcb0
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniPackagesInfo.java
@@ -0,0 +1,37 @@
+package com.microsoft.android.ast;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.microsoft.android.util.Parameter;
+
+public final class JniPackagesInfo {
+
+ private final Map packages = new HashMap();
+
+ public JniPackageInfo getPackage(String packageName) {
+ packageName = Parameter.normalize(packageName, "");
+
+ if (!packages.containsKey(packageName)) {
+ JniPackageInfo newPackage = new JniPackageInfo(packageName);
+ packages.put(packageName, newPackage);
+ return newPackage;
+ }
+ return packages.get(packageName);
+ }
+
+ public final Collection getPackages() {
+ return packages.values();
+ }
+
+ public final Collection getSortedPackages() {
+ final List sortedPackages = packages.values()
+ .stream()
+ .sorted((p1, p2) -> p1.getPackageName().compareTo(p2.getPackageName()))
+ .collect(Collectors.toList());
+ return sortedPackages;
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniParameterInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniParameterInfo.java
new file mode 100644
index 000000000..dc6c177c4
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniParameterInfo.java
@@ -0,0 +1,21 @@
+package com.microsoft.android.ast;
+
+public final class JniParameterInfo {
+ public final String name, jniType, javaType;
+
+ public JniParameterInfo(String name, String javaType, String jniType) {
+ if (name == null ||
+ (name = name.trim()).length() == 0)
+ throw new IllegalArgumentException("name");
+ if (javaType == null ||
+ (javaType = javaType.trim()).length() == 0)
+ throw new IllegalArgumentException("javaType");
+ if (jniType == null ||
+ (jniType = jniType.trim()).length() == 0)
+ throw new IllegalArgumentException("jniType");
+
+ this.name = name;
+ this.javaType = javaType;
+ this.jniType = jniType;
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniTypeInfo.java b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniTypeInfo.java
new file mode 100644
index 000000000..cca84a953
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/ast/JniTypeInfo.java
@@ -0,0 +1,93 @@
+package com.microsoft.android.ast;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.microsoft.android.util.Parameter;
+
+public abstract class JniTypeInfo implements HasJavadocComment {
+ private final String name;
+ private final JniPackageInfo declaringPackage;
+
+ private final Collection members = new ArrayList();
+ private final Collection typeParameters = new ArrayList();
+ private final Map jniTypes = new HashMap();
+
+ String javaDocComment = "";
+
+ JniTypeInfo(final JniPackageInfo declaringPackage, String name) {
+ Parameter.requireNotNull("declaringPackage", declaringPackage);
+
+ name = Parameter.requireNotEmpty("name", name);
+
+ this.declaringPackage = declaringPackage;
+ this.name = name;
+ }
+
+ public abstract String getTypeKind();
+
+ public final JniPackageInfo getDeclaringPackage() {
+ return declaringPackage;
+ }
+
+ public final String getName() {
+ if (typeParameters.isEmpty())
+ return name;
+ return name + "<" + String.join(",", typeParameters) + ">";
+ }
+
+ public final String getRawName() {
+ return name;
+ }
+
+ public final void addTypeParameter(String typeParameter, String jniType) {
+ typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter);
+ jniType = Parameter.requireNotEmpty("jniType", jniType);
+
+ if (typeParameters.contains(typeParameter)) {
+ jniTypes.replace(typeParameter, jniType);
+ return;
+ }
+ typeParameters.add(typeParameter);
+ jniTypes.put(typeParameter, jniType);
+ }
+
+ public final Collection getTypeParameters() {
+ return typeParameters;
+ }
+
+ public final String getTypeParameterJniType(String typeParameter) {
+ typeParameter = Parameter.requireNotEmpty("typeParameter", typeParameter);
+ return jniTypes.get(typeParameter);
+ }
+
+ public final void add(JniMemberInfo member) {
+ Parameter.requireNotNull("member", member);
+
+ members.add(member);
+ }
+
+ public final Collection getMembers() {
+ return members;
+ }
+
+ public final String getJavadocComment() {
+ return javaDocComment;
+ }
+
+ public final void setJavadocComment(String javaDocComment) {
+ this.javaDocComment = Parameter.normalize(javaDocComment, "");
+ }
+
+ public final Collection getSortedMembers() {
+ final List sortedMembers = members
+ .stream()
+ .sorted((m1, m2) -> (m1.getName() + "." + m1.getJniSignature()).compareTo((m2.getName() + "." + m2.getJniSignature())))
+ .collect(Collectors.toList());
+ return sortedMembers;
+ }
+}
diff --git a/tools/java-source-utils/src/main/java/com/microsoft/android/util/Parameter.java b/tools/java-source-utils/src/main/java/com/microsoft/android/util/Parameter.java
new file mode 100644
index 000000000..6b0ec1b39
--- /dev/null
+++ b/tools/java-source-utils/src/main/java/com/microsoft/android/util/Parameter.java
@@ -0,0 +1,27 @@
+package com.microsoft.android.util;
+
+public final class Parameter {
+ private Parameter() {
+ }
+
+ public static String requireNotEmpty(String parameterName, String value) {
+ if (value == null ||
+ (value = value.trim()).length() == 0)
+ throw new IllegalArgumentException(parameterName);
+ return value;
+ }
+
+ public static T requireNotNull(String parameterName, T value) {
+ if (value == null)
+ throw new IllegalArgumentException(parameterName);
+ return value;
+ }
+
+
+ public static String normalize(String value, String defaultValue) {
+ if (value == null ||
+ (value = value.trim()).length() == 0)
+ value = defaultValue;
+ return value;
+ }
+}
diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JavaSourceUtilsOptionsTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JavaSourceUtilsOptionsTest.java
new file mode 100644
index 000000000..8484cbe74
--- /dev/null
+++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JavaSourceUtilsOptionsTest.java
@@ -0,0 +1,20 @@
+/*
+ * This Java source file was generated by the Gradle 'init' task.
+ */
+package com.microsoft.android;
+
+import java.io.IOException;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class JavaSourceUtilsOptionsTest {
+
+ @Test public void testParse_HelpOptionReturnsNull() throws IOException {
+ JavaSourceUtilsOptions options;
+ options = JavaSourceUtilsOptions.parse(new String[]{"--help"});
+ assertNull(options);
+ options = JavaSourceUtilsOptions.parse(new String[]{"-h"});
+ assertNull(options);
+ }
+}
diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java
new file mode 100644
index 000000000..e16c5d26c
--- /dev/null
+++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java
@@ -0,0 +1,125 @@
+package com.microsoft.android;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import com.github.javaparser.JavaParser;
+import com.microsoft.android.ast.*;
+
+public final class JavadocXmlGeneratorTest {
+ @Test(expected = FileNotFoundException.class)
+ public void init_invalidFileThrows() throws FileNotFoundException, UnsupportedEncodingException {
+ try (JavadocXmlGenerator g = new JavadocXmlGenerator("/this/file/does/not/exist")) {
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWritePackages_nullPackages() throws ParserConfigurationException, TransformerException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes));
+
+ generator.writePackages(null);
+ }
+
+ @Test
+ public void testWritePackages_noPackages() throws ParserConfigurationException, TransformerException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes));
+
+ JniPackagesInfo packages = new JniPackagesInfo();
+ generator.writePackages(packages);
+
+ final String expected =
+ "\n" +
+ "\n";
+ assertEquals("no packages", expected, bytes.toString());
+ }
+
+
+ @Test
+ public void testWritePackages_demo() throws ParserConfigurationException, TransformerException {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ final JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes));
+ final JniPackagesInfo packages = JniPackagesInfoTest.createDemoInfo();
+
+ final String expected =
+ "\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " .(ILjava/lang/String;)V]]>\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n";
+
+ generator.writePackages(packages);
+ assertEquals("global package + example packages", expected, bytes.toString());
+ }
+
+ @Test
+ public void testWritePackages_Outer_java() throws Throwable {
+ testWritePackages("Outer.java", "Outer.xml");
+ }
+
+ @Test
+ public void testWritePackages_JavaType_java() throws Throwable {
+ testWritePackages("../../../com/xamarin/JavaType.java", "JavaType.xml");
+ }
+
+ private static void testWritePackages(final String resourceJava, final String resourceXml) throws Throwable {
+ final JavaParser parser = JniPackagesInfoFactoryTest.createParser();
+ final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser);
+ final File demoSource = new File(JniPackagesInfoFactoryTest.class.getResource(resourceJava).toURI());
+ final JniPackagesInfo packagesInfo = factory.parse(Arrays.asList(new File[]{demoSource}));
+
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ final JavadocXmlGenerator generator = new JavadocXmlGenerator(new PrintStream(bytes));
+
+ final String expected = JniPackagesInfoTest.getResourceContents(resourceXml);
+
+ generator.writePackages(packagesInfo);
+ assertEquals(resourceJava + " Javadoc XML", expected, bytes.toString());
+ }
+}
diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoFactoryTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoFactoryTest.java
new file mode 100644
index 000000000..730f14baa
--- /dev/null
+++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoFactoryTest.java
@@ -0,0 +1,59 @@
+package com.microsoft.android;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.io.File;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import com.github.javaparser.JavaParser;
+import com.github.javaparser.ParserConfiguration;
+import com.github.javaparser.symbolsolver.*;
+import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
+import com.github.javaparser.symbolsolver.resolution.typesolvers.*;
+
+
+import com.microsoft.android.ast.*;
+
+public class JniPackagesInfoFactoryTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInit_nullParser() {
+ new JniPackagesInfoFactory(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testParse_nullFiles() throws Throwable {
+ final JavaParser parser = new JavaParser();
+ final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser);
+ factory.parse(null);
+ }
+
+ @Test
+ public void testParse_demo() throws Throwable {
+ final JavaParser parser = createParser();
+ final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser);
+ final File demoSource = new File(JniPackagesInfoFactoryTest.class.getResource("Outer.java").toURI());
+ final JniPackagesInfo packagesInfo = factory.parse(Arrays.asList(new File[]{demoSource}));
+
+ assertEquals("Only one package processed", 1, packagesInfo.getPackages().size());
+ final JniPackageInfo p = packagesInfo.getPackage("example");
+ assertNotNull("Should have found `example` package", p);
+ assertEquals("Outer & Outer.Inner & Outer.Inner.NestedInner & Outer.MyAnnotation types found", 4, p.getTypes().size());
+
+ JniTypeInfo info = p.getType("Outer");
+ assertNotNull(info);
+ assertEquals("Outer", info.getName());
+ }
+
+ static JavaParser createParser() {
+ final CombinedTypeSolver typeSolver = new CombinedTypeSolver();
+ typeSolver.add(new ReflectionTypeSolver());
+ final ParserConfiguration config = new ParserConfiguration();
+ config.setSymbolResolver(new JavaSymbolSolver(typeSolver));
+ return new JavaParser(config);
+ }
+}
diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoTest.java
new file mode 100644
index 000000000..845bd091c
--- /dev/null
+++ b/tools/java-source-utils/src/test/java/com/microsoft/android/JniPackagesInfoTest.java
@@ -0,0 +1,82 @@
+package com.microsoft.android;
+
+import java.io.*;
+import java.net.URISyntaxException;
+
+import com.microsoft.android.ast.*;
+
+public class JniPackagesInfoTest {
+
+ static JniPackagesInfo createDemoInfo() {
+ JniPackagesInfo packages = new JniPackagesInfo();
+ JniPackageInfo global = packages.getPackage(null);
+
+ JniTypeInfo type = new JniClassInfo(global, "A");
+ type.setJavadocComment("jni-sig=LA;");
+ global.add(type);
+
+ JniFieldInfo field = new JniFieldInfo(type, "field");
+ field.setJniType("I");
+ field.setJavadocComment("jni-sig=field.I");
+ type.add(field);
+
+ JniConstructorInfo init = new JniConstructorInfo(type);
+ init.addParameter(new JniParameterInfo("one", "int", "I"));
+ init.addParameter(new JniParameterInfo("two", "java.lang.String", "Ljava/lang/String;"));
+ init.setJavadocComment("jni-sig=.(ILjava/lang/String;)V");
+ type.add(init);
+
+ JniMethodInfo method = new JniMethodInfo(type, "m");
+ method.addTypeParameter("T", "Ljava/lang/Object;");
+ method.addParameter(new JniParameterInfo("value", "T", "Ljava/lang/Object;"));
+ method.addParameter(new JniParameterInfo("x", "long", "J"));
+ method.setReturnType("void", "V");
+ method.setJavadocComment("jni-sig=m.(Ljava/lang/Object;J)V");
+ type.add(method);
+
+ type = new JniInterfaceInfo(global, "I");
+ type.addTypeParameter("T", "Ljava/lang/Object;");
+ type.setJavadocComment("jni-sig=LI;");
+ global.add(type);
+ method = new JniMethodInfo(type, "m");
+ method.addParameter(new JniParameterInfo("x", "java.util.List", "Ljava/util/List;"));
+ method.setReturnType("T", "Ljava/lang/Object;");
+ method.setJavadocComment("jni-sig=m.(Ljava/util/List;)Ljava/lang/Object;");
+ type.add(method);
+
+ JniPackageInfo example = packages.getPackage("example");
+ type = new JniInterfaceInfo(example, "Exampleable");
+ type.setJavadocComment("jni-sig=Lexample/Exampleable;");
+ example.add(type);
+
+ method = new JniMethodInfo(type, "noParameters");
+ method.setReturnType("void", "V");
+ method.setJavadocComment("jni-sig=noParameters.()V");
+ type.add(method);
+
+ method = new JniMethodInfo(type, "example");
+ method.addParameter(new JniParameterInfo("e", "java.lang.String", "Ljava/lang/String;"));
+ method.setReturnType("void", "V");
+ method.setJavadocComment("jni-sig=example.(Ljava/lang/String;)V");
+ type.add(method);
+
+ packages.getPackage("before.example");
+
+ return packages;
+ }
+
+ static String getResourceContents(String resourceName) throws IOException, URISyntaxException {
+ final File resourceFile = new File(JniPackagesInfoTest.class.getResource(resourceName).toURI());
+ final StringBuilder contents = new StringBuilder();
+ final String lineEnding = System.getProperty("line.separator");
+
+ String line;
+ try (final BufferedReader reader = new BufferedReader(new FileReader (resourceFile))) {
+ while((line = reader.readLine()) != null) {
+ contents.append(line);
+ contents.append(lineEnding);
+ }
+ }
+ return contents.toString();
+ }
+}
diff --git a/tools/java-source-utils/src/test/java/com/microsoft/android/ParameterNameGeneratorTest.java b/tools/java-source-utils/src/test/java/com/microsoft/android/ParameterNameGeneratorTest.java
new file mode 100644
index 000000000..99b7ea4ca
--- /dev/null
+++ b/tools/java-source-utils/src/test/java/com/microsoft/android/ParameterNameGeneratorTest.java
@@ -0,0 +1,93 @@
+package com.microsoft.android;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import com.github.javaparser.JavaParser;
+import com.microsoft.android.ast.*;
+
+public class ParameterNameGeneratorTest {
+
+ @Test(expected = FileNotFoundException.class)
+ public void init_invalidFileThrows() throws FileNotFoundException, UnsupportedEncodingException {
+ new ParameterNameGenerator("/this/file/does/not/exist");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWritePackages_nullPackages() {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes));
+
+ generator.writePackages(null);
+ }
+
+ @Test
+ public void testWritePackages_noPackages() {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes));
+
+ JniPackagesInfo packages = new JniPackagesInfo();
+ generator.writePackages(packages);
+ assertEquals("no packages", "", bytes.toString());
+ }
+
+
+ @Test
+ public void testWritePackages_demo() {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes));
+ JniPackagesInfo packages = JniPackagesInfoTest.createDemoInfo();
+
+ final String expected =
+ ";---------------------------------------\n" +
+ " class A\n" +
+ " #ctor(int one, java.lang.String two)\n" +
+ " m(T value, long x)\n" +
+ " interface I\n" +
+ " m(java.util.List x)\n" +
+ "\n" +
+ "package before.example\n" +
+ ";---------------------------------------\n" +
+ "\n" +
+ "package example\n" +
+ ";---------------------------------------\n" +
+ " interface Exampleable\n" +
+ " example(java.lang.String e)\n" +
+ "";
+
+ generator.writePackages(packages);
+ assertEquals("global package + example packages", expected, bytes.toString());
+ }
+
+ @Test
+ public void testWritePackages_Outer_java() throws Throwable {
+ testWritePackages("Outer.java", "Outer.params.txt");
+ }
+
+ @Test
+ public void testWritePackages_JavaType_java() throws Throwable {
+ testWritePackages("../../../com/xamarin/JavaType.java", "JavaType.params.txt");
+ }
+
+ private static void testWritePackages(final String resourceJava, final String resourceParamsTxt) throws Throwable {
+ final JavaParser parser = JniPackagesInfoFactoryTest.createParser();
+ final JniPackagesInfoFactory factory = new JniPackagesInfoFactory(parser);
+ final File demoSource = new File(JniPackagesInfoFactoryTest.class.getResource(resourceJava).toURI());
+ final JniPackagesInfo packagesInfo = factory.parse(Arrays.asList(new File[]{demoSource}));
+
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ final ParameterNameGenerator generator = new ParameterNameGenerator(new PrintStream(bytes));
+
+ final String expected = JniPackagesInfoTest.getResourceContents(resourceParamsTxt);
+
+ generator.writePackages(packagesInfo);
+ assertEquals(resourceJava + " parameter names", expected, bytes.toString());
+ }
+}
diff --git a/tools/java-source-utils/src/test/resources/com/.gitignore b/tools/java-source-utils/src/test/resources/com/.gitignore
new file mode 100644
index 000000000..f815dd72b
--- /dev/null
+++ b/tools/java-source-utils/src/test/resources/com/.gitignore
@@ -0,0 +1 @@
+xamarin
diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/.gitignore b/tools/java-source-utils/src/test/resources/com/microsoft/android/.gitignore
new file mode 100644
index 000000000..6b468b62a
--- /dev/null
+++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/.gitignore
@@ -0,0 +1 @@
+*.class
diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.params.txt b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.params.txt
new file mode 100644
index 000000000..e49e266d0
--- /dev/null
+++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.params.txt
@@ -0,0 +1,21 @@
+package com.xamarin
+;---------------------------------------
+ class JavaEnum
+ class JavaType
+ #ctor(java.lang.String value)
+ action(java.lang.Object value)
+ compareTo(com.xamarin.JavaType value)
+ finalize(int value)
+ func(java.lang.StringBuilder value)
+ func(java.lang.String[] values)
+ instanceActionWithGenerics(T value1, E value2)
+ staticActionWithGenerics(T value1, TExtendsNumber value2, java.util.List> unboundedList, java.util.List extends java.lang.Number> extendsList, java.util.List super java.lang.Throwable> superList)
+ sum(int first, int remaining)
+ class JavaType.ASC
+ class JavaType.PSC
+ class JavaType.RNC
+ #ctor(E value1, E2 value2)
+ fromE(E value)
+ class JavaType.RNC.RPNC
+ #ctor(E value1, E2 value2, E3 value3)
+ fromE2(E2 value)
diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.xml b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.xml
new file mode 100644
index 000000000..ff472f1c1
--- /dev/null
+++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/JavaType.xml
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+ Paragraphs of text?
+
+@return some value]]>
+
+
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java
new file mode 100644
index 000000000..3087e5d28
--- /dev/null
+++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java
@@ -0,0 +1,121 @@
+package example;
+
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Yay, Javadoc!
+ *
+ * JNI sig: Lexample/Outer;
+ */
+
+ @Outer.MyAnnotation(keys={"a", "b", "c"})
+public class Outer {
+
+ /**
+ * (java.lang.Object value)
+ *
+ * JNI sig: (Ljava/lang/Object;)V
+ */
+
+ public Outer(T value) {
+ value.run();
+ }
+
+ /**
+ * isU(java.util.List super U> list)
+ *
+ * This is a paragraph. Yay?
+ *
+ * JNI sig: (Ljava/util/List;)Ljava/lang/Error;
+ *
+ * @param list just some random items
+ * @return some value
+ */
+
+ public U isU(List super U> list) {
+ return null;
+ }
+
+ /**
+ * Just an example annotation, for use later…
+ *
+ * JNI sig: Lexample/Outer$MyAnnotation;
+ */
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE})
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+ public static @interface MyAnnotation {
+
+ /**
+ * JNI sig: ()[Ljava/lang/String;
+ *
+ * @return some random keys
+ */
+
+ String[] keys() default {};
+ }
+
+ /**
+ * JNI sig: Lexample/Outer$Inner;
+ */
+ public static interface Inner {
+ /**
+ * m(U value)
+ *
+ * JNI sig: ([[Ljava/lang/Readable;)V
+ *
+ * @throws Throwable never, just because
+ */
+ public default void m(V[][] values) throws Throwable {
+ for (V[] vs : values) {
+ for (V v : vs) {
+ v.read(null);
+ }
+ }
+ }
+
+ /**
+ * JNI sig: J
+ */
+ public static final long COUNT = 42;
+
+ /**
+ * JNI sig: Lexample/Outer$Inner$NestedInner;
+ */
+ public static class NestedInner {
+
+ /**
+ * JNI sig: S
+ */
+ public static final short S = 64;
+
+ /**
+ * map(java.util.Map map)
+ *
+ * JNI sig: map(Ljava/util/Map;)V
+ * @param map
+ */
+ public void map(Map m) {
+ }
+ }
+ }
+
+ /**
+ * method(java.lang.CharSequence a, short[] b, T[] values)
+ *
+ * JNI sig: (Ljava/lang/CharSequence;[S[Ljava/lang/Appendable;)Ljava/lang/Appendable;
+ */
+ public T method(CharSequence a, short[] b, T[] values) {
+ return null;
+ }
+
+ /**
+ * main(java.lang.String[] args)
+ *
+ * JNI sig: ([Ljava/lang/String;)V
+ */
+ public static void main(String[] args) {
+ }
+}
diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.params.txt b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.params.txt
new file mode 100644
index 000000000..fc2a52c46
--- /dev/null
+++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.params.txt
@@ -0,0 +1,12 @@
+package example
+;---------------------------------------
+ class Outer
+ #ctor(T value)
+ isU(java.util.List super U> list)
+ main(java.lang.String[] args)
+ method(java.lang.CharSequence a, short[] b, T[] values)
+ interface Outer.Inner
+ m(V[][] values)
+ class Outer.Inner.NestedInner
+ map(java.util.Map m)
+ interface Outer.MyAnnotation
diff --git a/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.xml b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.xml
new file mode 100644
index 000000000..e073732d1
--- /dev/null
+++ b/tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+ (java.lang.Object value)
+
+JNI sig: (Ljava/lang/Object;)V]]>
+
+
+
+ list)
+
+This is a paragraph. Yay?
+
+JNI sig: (Ljava/util/List;)Ljava/lang/Error;
+
+@param list just some random items
+@return some value]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ map)
+
+JNI sig: map(Ljava/util/Map;)V
+
+@param map]]>
+
+
+
+
+
+
+
+
+
+