> result = new LinkedHashSet<>();
+ for (Object object : objects) {
+ if (object instanceof String) {
+ object = ClassUtils.resolveClassName((String) object, null);
+ }
+ result.add((Class>) object);
+ }
+ return result;
+ }
+
+}
\ No newline at end of file
diff --git a/samples/vanilla-orm/src/main/java/app/main/model/Foo.java b/samples/vanilla-orm/src/main/java/app/main/model/Foo.java
new file mode 100644
index 000000000..27d07f437
--- /dev/null
+++ b/samples/vanilla-orm/src/main/java/app/main/model/Foo.java
@@ -0,0 +1,30 @@
+package app.main.model;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+
+@Entity
+public class Foo {
+ @Id
+ @GeneratedValue(strategy=GenerationType.AUTO)
+ private long id;
+ private String value;
+ public Foo() {
+ }
+ public Foo(String value) {
+ this.value = value;
+ }
+ public String getValue() {
+ return this.value;
+ }
+ public void setValue(String value) {
+ this.value = value;
+ }
+ @Override
+ public String toString() {
+ return String.format(
+ "Foo[id=%d, value='%s']",
+ id, value);
+ }
+}
diff --git a/samples/vanilla-orm/src/main/resources/META-INF/spring.factories b/samples/vanilla-orm/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..13e8da539
--- /dev/null
+++ b/samples/vanilla-orm/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,4 @@
+org.springframework.context.ApplicationListener=\
+app.main.BeanCountingApplicationListener,\
+app.main.StartupApplicationListener,\
+app.main.ShutdownApplicationListener
diff --git a/samples/vanilla-orm/src/main/resources/application.properties b/samples/vanilla-orm/src/main/resources/application.properties
new file mode 100644
index 000000000..7813d9f0a
--- /dev/null
+++ b/samples/vanilla-orm/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+logging.level.slim=debug
+#spring.jpa.show-sql=true
+spring.data.jpa.repositories.bootstrap-mode=lazy
diff --git a/samples/vanilla-orm/src/main/resources/hibernate.properties b/samples/vanilla-orm/src/main/resources/hibernate.properties
new file mode 100644
index 000000000..35396da22
--- /dev/null
+++ b/samples/vanilla-orm/src/main/resources/hibernate.properties
@@ -0,0 +1 @@
+hibernate.bytecode.provider=none
\ No newline at end of file
diff --git a/samples/vanilla-orm/src/test/java/app/main/CaptureSystemOutput.java b/samples/vanilla-orm/src/test/java/app/main/CaptureSystemOutput.java
new file mode 100644
index 000000000..4c7f3440d
--- /dev/null
+++ b/samples/vanilla-orm/src/test/java/app/main/CaptureSystemOutput.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * 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 app.main;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.ExtensionContext.Store;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.platform.commons.support.ReflectionSupport;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+
+/**
+ * {@code @CaptureSystemOutput} is a JUnit JUpiter extension for capturing output to
+ * {@code System.out} and {@code System.err} with expectations supported via Hamcrest
+ * matchers.
+ *
+ * Example Usage
+ *
+ *
+ * {@literal @}Test
+ * {@literal @}CaptureSystemOutput
+ * void systemOut(OutputCapture outputCapture) {
+ * outputCapture.expect(containsString("System.out!"));
+ *
+ * System.out.println("Printed to System.out!");
+ * }
+ *
+ * {@literal @}Test
+ * {@literal @}CaptureSystemOutput
+ * void systemErr(OutputCapture outputCapture) {
+ * outputCapture.expect(containsString("System.err!"));
+ *
+ * System.err.println("Printed to System.err!");
+ * }
+ *
+ *
+ *
+ * Based on code from Spring Boot's OutputCapture
+ * rule for JUnit 4 by Phillip Webb and Andy Wilkinson.
+ *
+ * @author Sam Brannen
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ */
+@Target({ TYPE, METHOD })
+@Retention(RUNTIME)
+@ExtendWith(CaptureSystemOutput.Extension.class)
+public @interface CaptureSystemOutput {
+
+ class Extension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
+
+ @Override
+ public void beforeEach(ExtensionContext context) throws Exception {
+ getOutputCapture(context).captureOutput();
+ }
+
+ @Override
+ public void afterEach(ExtensionContext context) throws Exception {
+ OutputCapture outputCapture = getOutputCapture(context);
+ try {
+ if (!outputCapture.matchers.isEmpty()) {
+ String output = outputCapture.toString();
+ assertThat(output, allOf(outputCapture.matchers));
+ }
+ }
+ finally {
+ outputCapture.releaseOutput();
+ }
+ }
+
+ @Override
+ public boolean supportsParameter(ParameterContext parameterContext,
+ ExtensionContext extensionContext) {
+ boolean isTestMethodLevel = extensionContext.getTestMethod().isPresent();
+ boolean isOutputCapture = parameterContext.getParameter()
+ .getType() == OutputCapture.class;
+ return isTestMethodLevel && isOutputCapture;
+ }
+
+ @Override
+ public Object resolveParameter(ParameterContext parameterContext,
+ ExtensionContext extensionContext) {
+ return getOutputCapture(extensionContext);
+ }
+
+ private OutputCapture getOutputCapture(ExtensionContext context) {
+ return getOrComputeIfAbsent(getStore(context), OutputCapture.class);
+ }
+
+ private V getOrComputeIfAbsent(Store store, Class type) {
+ return store.getOrComputeIfAbsent(type, ReflectionSupport::newInstance, type);
+ }
+
+ private Store getStore(ExtensionContext context) {
+ return context.getStore(
+ Namespace.create(getClass(), context.getRequiredTestMethod()));
+ }
+
+ }
+
+ /**
+ * {@code OutputCapture} captures output to {@code System.out} and {@code System.err}.
+ *
+ *
+ * To obtain an instance of {@code OutputCapture}, declare a parameter of type
+ * {@code OutputCapture} in a JUnit Jupiter {@code @Test}, {@code @BeforeEach}, or
+ * {@code @AfterEach} method.
+ *
+ *
+ * {@linkplain #expect Expectations} are supported via Hamcrest matchers.
+ *
+ *
+ * To obtain all output to {@code System.out} and {@code System.err}, simply invoke
+ * {@link #toString()}.
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author Sam Brannen
+ */
+ static class OutputCapture {
+
+ private final List> matchers = new ArrayList<>();
+
+ private CaptureOutputStream captureOut;
+
+ private CaptureOutputStream captureErr;
+
+ private ByteArrayOutputStream copy;
+
+ void captureOutput() {
+ this.copy = new ByteArrayOutputStream();
+ this.captureOut = new CaptureOutputStream(System.out, this.copy);
+ this.captureErr = new CaptureOutputStream(System.err, this.copy);
+ System.setOut(new PrintStream(this.captureOut));
+ System.setErr(new PrintStream(this.captureErr));
+ }
+
+ void releaseOutput() {
+ System.setOut(this.captureOut.getOriginal());
+ System.setErr(this.captureErr.getOriginal());
+ this.copy = null;
+ }
+
+ private void flush() {
+ try {
+ this.captureOut.flush();
+ this.captureErr.flush();
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+
+ /**
+ * Verify that the captured output is matched by the supplied {@code matcher}.
+ *
+ *
+ * Verification is performed after the test method has executed.
+ *
+ * @param matcher the matcher
+ */
+ public void expect(Matcher super String> matcher) {
+ this.matchers.add(matcher);
+ }
+
+ /**
+ * Return all captured output to {@code System.out} and {@code System.err} as a
+ * single string.
+ */
+ @Override
+ public String toString() {
+ flush();
+ return this.copy.toString();
+ }
+
+ private static class CaptureOutputStream extends OutputStream {
+
+ private final PrintStream original;
+
+ private final OutputStream copy;
+
+ CaptureOutputStream(PrintStream original, OutputStream copy) {
+ this.original = original;
+ this.copy = copy;
+ }
+
+ PrintStream getOriginal() {
+ return this.original;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ this.copy.write(b);
+ this.original.write(b);
+ this.original.flush();
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ this.copy.write(b, off, len);
+ this.original.write(b, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.copy.flush();
+ this.original.flush();
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/samples/vanilla-orm/src/test/java/app/main/CsvResultsWriterFactory.java b/samples/vanilla-orm/src/test/java/app/main/CsvResultsWriterFactory.java
new file mode 100644
index 000000000..0fcd4626e
--- /dev/null
+++ b/samples/vanilla-orm/src/test/java/app/main/CsvResultsWriterFactory.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2016-2017 the original author or authors.
+ *
+ * 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 app.main;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import jmh.mbr.core.ResultsWriter;
+import jmh.mbr.core.ResultsWriterFactory;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.RunResult;
+import org.openjdk.jmh.runner.format.OutputFormat;
+import org.openjdk.jmh.util.FileUtils;
+import org.openjdk.jmh.util.ScoreFormatter;
+import org.openjdk.jmh.util.Statistics;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class CsvResultsWriterFactory implements ResultsWriterFactory {
+
+ @Override
+ public ResultsWriter forUri(String uri) {
+ if (!uri.startsWith("csv:")) {
+ return null;
+ }
+ return new ResultsWriter() {
+
+ @Override
+ public void write(OutputFormat output, Collection results) {
+ StringBuilder report = new StringBuilder();
+ try {
+ Map params = new LinkedHashMap<>();
+ int paramPlaces = 0;
+ for (RunResult result : results) {
+ for (String param : result.getParams().getParamsKeys()) {
+ int count = paramPlaces;
+ params.computeIfAbsent(param, key -> count);
+ paramPlaces++;
+ }
+ }
+ Map auxes = new LinkedHashMap<>();
+ int auxPlaces = 0;
+ for (RunResult result : results) {
+ @SuppressWarnings("rawtypes")
+ Map second = result.getAggregatedResult()
+ .getSecondaryResults();
+ if (second != null) {
+ for (String aux : second.keySet()) {
+ int count = auxPlaces;
+ auxes.computeIfAbsent(aux, key -> count);
+ auxPlaces++;
+ }
+ }
+ }
+ StringBuilder header = new StringBuilder();
+ header.append("class, method, ");
+ params.forEach((key, value) -> header.append(key).append(", "));
+ auxes.forEach((key, value) -> header.append(propertyName(key))
+ .append(", "));
+ header.append("median, mean, range");
+ report.append(header.toString()).append(System.lineSeparator());
+ for (RunResult result : results) {
+ StringBuilder builder = new StringBuilder();
+ String benchmark = result.getParams().getBenchmark();
+ String cls = benchmark.substring(0, benchmark.lastIndexOf("."));
+ String mthd = benchmark.substring(benchmark.lastIndexOf(".") + 1);
+ builder.append(cls).append(", ").append(mthd).append(", ");
+ for (int i = 0; i < params.values().size(); i++) {
+ boolean found = false;
+ for (String param : result.getParams().getParamsKeys()) {
+ if (params.get(param) == i) {
+ builder.append(result.getParams().getParam(param))
+ .append(", ");
+ found = true;
+ }
+ }
+ if (!found) {
+ builder.append(", ");
+ }
+ }
+ @SuppressWarnings("rawtypes")
+ Map second = result.getAggregatedResult()
+ .getSecondaryResults();
+ if (second != null) {
+ for (int i = 0; i < auxes.values().size(); i++) {
+ boolean found = false;
+ for (String param : second.keySet()) {
+ if (auxes.get(param) == i) {
+ builder.append(ScoreFormatter
+ .format(second.get(param).getStatistics()
+ .getPercentile(0.5)))
+ .append(", ");
+ found = true;
+ }
+ }
+ if (!found) {
+ builder.append(", ");
+ }
+ }
+ }
+ Statistics statistics = result.getPrimaryResult().getStatistics();
+ builder.append(
+ ScoreFormatter.format(statistics.getPercentile(0.5)));
+ builder.append(", ");
+ builder.append(ScoreFormatter.format(statistics.getMean()));
+ builder.append(", ");
+ double error = (statistics.getMax() - statistics.getMin()) / 2;
+ builder.append(ScoreFormatter.format(error));
+ report.append(builder.toString()).append(System.lineSeparator());
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ output.println(report.toString());
+ if (uri != null) {
+ File file = new File(uri.substring("csv:".length()));
+ file.getParentFile().mkdirs();
+ if (file.getParentFile().exists()) {
+ try {
+ FileUtils.writeLines(file,
+ Collections.singleton(report.toString()));
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ private String propertyName(String key) {
+ if (key.matches("get[A-Z].*")) {
+ key = changeFirstCharacterCase(key.substring(3), false);
+ }
+ return key;
+ }
+ };
+ }
+
+ private static String changeFirstCharacterCase(String str, boolean capitalize) {
+ if (str == null || str.length() == 0) {
+ return str;
+ }
+
+ char baseChar = str.charAt(0);
+ char updatedChar;
+ if (capitalize) {
+ updatedChar = Character.toUpperCase(baseChar);
+ }
+ else {
+ updatedChar = Character.toLowerCase(baseChar);
+ }
+ if (baseChar == updatedChar) {
+ return str;
+ }
+
+ char[] chars = str.toCharArray();
+ chars[0] = updatedChar;
+ return new String(chars, 0, chars.length);
+ }
+
+}
diff --git a/samples/vanilla-orm/src/test/java/app/main/ProcessLauncherState.java b/samples/vanilla-orm/src/test/java/app/main/ProcessLauncherState.java
new file mode 100644
index 000000000..85d7c75a2
--- /dev/null
+++ b/samples/vanilla-orm/src/test/java/app/main/ProcessLauncherState.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2016-2017 the original author or authors.
+ *
+ * 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 app.main;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.boot.loader.archive.Archive;
+import org.springframework.boot.loader.thin.ArchiveUtils;
+import org.springframework.boot.loader.thin.DependencyResolver;
+import org.springframework.boot.loader.thin.PathResolver;
+import org.springframework.util.ReflectionUtils;
+
+public class ProcessLauncherState {
+
+ private static final Logger log = LoggerFactory.getLogger(ProcessLauncherState.class);
+
+ public static final String CLASS_COUNT_MARKER = "Class count";
+
+ public static final String BEAN_COUNT_MARKER = "Bean count";
+
+ private Process started;
+
+ private List args = new ArrayList<>();
+
+ private List progs = new ArrayList<>();
+
+ private static List DEFAULT_JVM_ARGS = Arrays.asList("-Xmx128m", "-cp", "",
+ "-Djava.security.egd=file:/dev/./urandom", "-noverify",
+ "-Dspring.data.jpa.repositories.bootstrap-mode=lazy",
+ "-Dspring.cache.type=none", "-Dspring.main.lazy-initialization=true",
+ "-Dspring.jmx.enabled=false");
+
+ private File home;
+
+ private String mainClass;
+
+ private String name = "thin";
+
+ private String[] profiles = new String[0];
+
+ private BufferedReader buffer;
+
+ private CountDownLatch latch = new CountDownLatch(1);
+
+ private int classes;
+
+ private int beans;
+
+ private long memory;
+
+ private long heap;
+
+ private String classpath;
+
+ public int getClasses() {
+ return classes;
+ }
+
+ public int getBeans() {
+ return beans;
+ }
+
+ public double getMemory() {
+ return memory / (1024. * 1024);
+ }
+
+ public double getHeap() {
+ return heap / (1024. * 1024);
+ }
+
+ public ProcessLauncherState(String dir, String... args) {
+ this.args.addAll(DEFAULT_JVM_ARGS);
+ String vendor = System.getProperty("java.vendor", "").toLowerCase();
+ if (vendor.contains("ibm") || vendor.contains("j9")) {
+ this.args.addAll(Arrays.asList("-Xms32m", "-Xquickstart", "-Xshareclasses",
+ "-Xscmx128m"));
+ }
+ else {
+ this.args.addAll(Arrays.asList("-XX:TieredStopAtLevel=1"));
+ }
+ if (System.getProperty("bench.args") != null) {
+ this.args.addAll(Arrays.asList(System.getProperty("bench.args").split(" ")));
+ }
+ this.progs.addAll(Arrays.asList(args));
+ this.home = new File(dir);
+ }
+
+ public void setMainClass(String mainClass) {
+ this.mainClass = mainClass;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setProfiles(String... profiles) {
+ this.profiles = profiles;
+ }
+
+ public void addArgs(String... args) {
+ this.args.addAll(Arrays.asList(args));
+ }
+
+ protected String getClasspath() {
+ return getClasspath(true);
+ }
+
+ protected String getClasspath(boolean includeTargetClasses) {
+ if (this.classpath == null) {
+ PathResolver resolver = new PathResolver(DependencyResolver.instance());
+ Archive root = ArchiveUtils.getArchive(ProcessLauncherState.class);
+ List resolved = resolver.resolve(root, name, profiles);
+ StringBuilder builder = new StringBuilder();
+ if (includeTargetClasses) {
+ builder.append(new File("target/classes").getAbsolutePath());
+ }
+ else {
+ builder.append(new File("target/orm-0.0.1.BUILD-SNAPSHOT.jar")
+ .getAbsolutePath());
+ }
+ try {
+ for (Archive archive : resolved) {
+ if (archive.getUrl().equals(root.getUrl())) {
+ continue;
+ }
+ if (builder.length() > 0) {
+ builder.append(File.pathSeparator);
+ }
+ builder.append(file(archive.getUrl().toString()));
+ }
+ }
+ catch (MalformedURLException e) {
+ throw new IllegalStateException("Cannot find archive", e);
+ }
+ log.debug("Classpath: " + builder);
+ this.classpath = builder.toString();
+ }
+ return this.classpath;
+ }
+
+ private String file(String path) {
+ if (path.endsWith("!/")) {
+ path = path.substring(0, path.length() - 2);
+ }
+ if (path.startsWith("jar:")) {
+ path = path.substring("jar:".length());
+ }
+ if (path.startsWith("file:")) {
+ path = path.substring("file:".length());
+ }
+ return path;
+ }
+
+ public String getPid() {
+ String pid = null;
+ try {
+ if (started != null) {
+ Field field = ReflectionUtils.findField(started.getClass(), "pid");
+ ReflectionUtils.makeAccessible(field);
+ pid = "" + ReflectionUtils.getField(field, started);
+ }
+ }
+ catch (Exception e) {
+ }
+ return pid;
+ }
+
+ public void after() throws Exception {
+ drain();
+ if (started != null && started.isAlive()) {
+ latch.await(10, TimeUnit.SECONDS);
+ Map metrics = VirtualMachineMetrics.fetch(getPid());
+ this.memory = VirtualMachineMetrics.total(metrics);
+ this.heap = VirtualMachineMetrics.heap(metrics);
+ this.classes = metrics.get("Classes").intValue();
+ System.out.println(
+ "Stopped " + mainClass + ": " + started.destroyForcibly().waitFor());
+ }
+ }
+
+ private BufferedReader getBuffer() {
+ return this.buffer;
+ }
+
+ public void run() throws Exception {
+ List jvmArgs = new ArrayList<>(this.args);
+ customize(jvmArgs);
+ started = exec(jvmArgs.toArray(new String[0]), this.progs.toArray(new String[0]));
+ InputStream stream = started.getInputStream();
+ this.buffer = new BufferedReader(new InputStreamReader(stream));
+ monitor();
+ }
+
+ public void before() throws Exception {
+ int classpath = args.indexOf("-cp");
+ if (classpath >= 0 && args.get(classpath + 1).length() == 0) {
+ args.set(classpath + 1, getClasspath());
+ }
+ }
+
+ protected void customize(List args) {
+ }
+
+ protected Process exec(String[] jvmArgs, String... progArgs) {
+ List args = new ArrayList<>(Arrays.asList(jvmArgs));
+ args.add(0, System.getProperty("java.home") + "/bin/java");
+ if (mainClass.length() > 0) {
+ args.add(mainClass);
+ }
+ int classpath = args.indexOf("-cp");
+ if (classpath >= 0 && args.get(classpath + 1).length() == 0) {
+ args.set(classpath + 1, getClasspath());
+ }
+ args.addAll(Arrays.asList(progArgs));
+ ProcessBuilder builder = new ProcessBuilder(args);
+ builder.redirectErrorStream(true);
+ builder.directory(getHome());
+ if (!"false".equals(System.getProperty("debug", "false"))) {
+ System.out.println("Executing: " + builder.command());
+ }
+ Process started;
+ try {
+ started = builder.start();
+ return started;
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ throw new IllegalStateException("Cannot calculate classpath");
+ }
+ }
+
+ protected void monitor() throws Exception {
+ // use this method to wait for an app to start
+ output(getBuffer(), StartupApplicationListener.MARKER);
+ }
+
+ protected void finish() throws Exception {
+ // use this method to wait for an app to stop
+ output(getBuffer(), ShutdownApplicationListener.MARKER);
+ }
+
+ protected void drain() throws Exception {
+ System.out.println("Draining console buffer");
+ output(getBuffer(), null);
+ latch.countDown();
+ }
+
+ protected void output(BufferedReader br, String marker) throws Exception {
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+ if (!"false".equals(System.getProperty("debug", "false"))) {
+ System.err.println("Scanning for: " + marker);
+ }
+ while ((marker != null || br.ready()) && (line = br.readLine()) != null
+ && (marker == null || !line.contains(marker))) {
+ sb.append(line + System.getProperty("line.separator"));
+ if (!"false".equals(System.getProperty("debug", "false"))) {
+ System.out.println(line);
+ }
+ if (line.contains(CLASS_COUNT_MARKER)) {
+ classes = Integer
+ .valueOf(line.substring(line.lastIndexOf("=") + 1).trim());
+ }
+ if (line.contains(BEAN_COUNT_MARKER)) {
+ int count = Integer
+ .valueOf(line.substring(line.lastIndexOf("=") + 1).trim());
+ beans = count > beans ? count : beans;
+ }
+ line = null;
+ }
+ if (line != null) {
+ sb.append(line + System.getProperty("line.separator"));
+ }
+ if ("false".equals(System.getProperty("debug", "false"))) {
+ System.out.println(sb.toString());
+ }
+ }
+
+ public File getHome() {
+ return home;
+ }
+
+}
diff --git a/samples/vanilla-orm/src/test/java/app/main/ProcessLauncherStateTests.java b/samples/vanilla-orm/src/test/java/app/main/ProcessLauncherStateTests.java
new file mode 100644
index 000000000..b40484b98
--- /dev/null
+++ b/samples/vanilla-orm/src/test/java/app/main/ProcessLauncherStateTests.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016-2017 the original author or authors.
+ *
+ * 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 app.main;
+
+import java.net.URL;
+
+import app.main.CaptureSystemOutput.OutputCapture;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class ProcessLauncherStateTests {
+
+ @Test
+ @CaptureSystemOutput
+ public void vanilla(OutputCapture output) throws Exception {
+ // System.setProperty("bench.args", "-verbose:class");
+ ProcessLauncherState state = new ProcessLauncherState("target") {
+ @Override
+ public void run() throws Exception {
+ super.run();
+ try {
+ new URL("http://localhost:8080/owners").getContent();
+ }
+ catch (Exception e) {
+ // ignore
+ }
+ }
+ };
+ state.addArgs("-Dinitialize=true");
+ state.setMainClass(SampleApplication.class.getName());
+ // state.setProfiles("intg");
+ state.before();
+ state.run();
+ state.after();
+ assertThat(output.toString()).contains("Benchmark app started");
+ assertThat(state.getHeap()).isGreaterThan(0);
+ assertThat(state.getClasses()).isGreaterThan(7000);
+ }
+
+}
diff --git a/samples/vanilla-orm/src/test/java/app/main/SampleApplicationTests.java b/samples/vanilla-orm/src/test/java/app/main/SampleApplicationTests.java
new file mode 100644
index 000000000..f48c83bed
--- /dev/null
+++ b/samples/vanilla-orm/src/test/java/app/main/SampleApplicationTests.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * 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 app.main;
+
+import org.junit.Before;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.server.WebHandler;
+
+/**
+ * @author Dave Syer
+ *
+ */
+@SpringBootTest
+@AutoConfigureWebTestClient
+public class SampleApplicationTests {
+
+ @Autowired
+ private WebHandler webHandler;
+
+ @Autowired
+ private WebTestClient client;
+
+ @Before
+ public void init() {
+ client = WebTestClient.bindToWebHandler(webHandler).build();
+ }
+
+ @Test
+ public void test() {
+ client.get().uri("/").exchange().expectBody(String.class).isEqualTo("{\"value\":\"Hello\"}");
+ }
+
+}
diff --git a/samples/vanilla-orm/src/test/java/app/main/SimpleBenchmark.java b/samples/vanilla-orm/src/test/java/app/main/SimpleBenchmark.java
new file mode 100644
index 000000000..95a8d9a90
--- /dev/null
+++ b/samples/vanilla-orm/src/test/java/app/main/SimpleBenchmark.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2016-2017 the original author or authors.
+ *
+ * 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 app.main;
+
+import java.net.URL;
+
+import jmh.mbr.junit5.Microbenchmark;
+import org.openjdk.jmh.annotations.AuxCounters;
+import org.openjdk.jmh.annotations.AuxCounters.Type;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+@Measurement(iterations = 5, time = 1)
+@Warmup(iterations = 1, time = 1)
+@Fork(value = 2, warmups = 0)
+@BenchmarkMode(Mode.AverageTime)
+@Microbenchmark
+public class SimpleBenchmark {
+
+ @Benchmark
+ public void main(MainState state) throws Exception {
+ state.setMainClass(state.sample.getConfig().getName());
+ state.run();
+ if (state.profile.toString().startsWith("first")) {
+ try {
+ System.out.println("Loading /");
+ new URL("http://localhost:8080/").getContent();
+ }
+ catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ @State(Scope.Thread)
+ @AuxCounters(Type.EVENTS)
+ public static class MainState extends ProcessLauncherState {
+
+ public static enum Profile {
+
+ demo, first;
+
+ }
+
+ public static enum Sample {
+
+ auto;
+
+ private Class> config;
+
+ private Sample(Class> config) {
+ this.config = config;
+ }
+
+ private Sample() {
+ this.config = SampleApplication.class;
+ }
+
+ public Class> getConfig() {
+ return config;
+ }
+
+ }
+
+ @Param // ("auto")
+ private Sample sample;
+
+ @Param
+ private Profile profile;
+
+ public MainState() {
+ super("target");
+ }
+
+ @Override
+ public int getClasses() {
+ return super.getClasses();
+ }
+
+ @Override
+ public int getBeans() {
+ return super.getBeans();
+ }
+
+ @Override
+ public double getMemory() {
+ return super.getMemory();
+ }
+
+ @Override
+ public double getHeap() {
+ return super.getHeap();
+ }
+
+ @TearDown(Level.Invocation)
+ public void stop() throws Exception {
+ super.after();
+ }
+
+ @Setup(Level.Trial)
+ public void start() throws Exception {
+ if (profile != Profile.demo) {
+ setProfiles(profile.toString());
+ }
+ super.before();
+ }
+
+ }
+
+}
diff --git a/samples/vanilla-orm/src/test/java/app/main/VirtualMachineMetrics.java b/samples/vanilla-orm/src/test/java/app/main/VirtualMachineMetrics.java
new file mode 100644
index 000000000..14000f7fb
--- /dev/null
+++ b/samples/vanilla-orm/src/test/java/app/main/VirtualMachineMetrics.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2016-2017 the original author or authors.
+ *
+ * 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 app.main;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.openmbean.CompositeData;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+
+import com.sun.tools.attach.VirtualMachine;
+
+/**
+ * @author Dave Syer
+ *
+ */
+@SuppressWarnings("restriction")
+public class VirtualMachineMetrics {
+
+ static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";
+
+ public static Map fetch(String pid) {
+ if (pid == null) {
+ return Collections.emptyMap();
+ }
+ try {
+ VirtualMachine vm = VirtualMachine.attach(pid);
+ vm.startLocalManagementAgent();
+ String connectorAddress = vm.getAgentProperties()
+ .getProperty(CONNECTOR_ADDRESS);
+ JMXServiceURL url = new JMXServiceURL(connectorAddress);
+ JMXConnector connector = JMXConnectorFactory.connect(url);
+ MBeanServerConnection connection = connector.getMBeanServerConnection();
+ gc(connection);
+ Map metrics = new HashMap<>(
+ new BufferPools(connection).getMetrics());
+ metrics.putAll(new Threads(connection).getMetrics());
+ metrics.putAll(new Classes(connection).getMetrics());
+ vm.detach();
+ return metrics;
+ }
+ catch (Exception e) {
+ return Collections.emptyMap();
+ }
+ }
+
+ private static void gc(MBeanServerConnection mBeanServer) {
+ try {
+ final ObjectName on = new ObjectName("java.lang:type=Memory");
+ mBeanServer.getMBeanInfo(on);
+ mBeanServer.invoke(on, "gc", new Object[0], new String[0]);
+ }
+ catch (Exception ignored) {
+ System.err.println("Unable to gc");
+ }
+ }
+
+ public static long total(Map metrics) {
+ return BufferPools.total(metrics);
+ }
+
+ public static long heap(Map metrics) {
+ return BufferPools.heap(metrics);
+ }
+
+}
+
+class Threads {
+
+ private final MBeanServerConnection mBeanServer;
+
+ public Threads(MBeanServerConnection mBeanServer) {
+ this.mBeanServer = mBeanServer;
+ }
+
+ public Map getMetrics() {
+ final Map gauges = new HashMap<>();
+ final String name = "Threads";
+ try {
+ final ObjectName on = new ObjectName("java.lang:type=Threading");
+ mBeanServer.getMBeanInfo(on);
+ Integer value = (Integer) mBeanServer.getAttribute(on, "ThreadCount");
+ gauges.put(name(name), Long.valueOf(value) * 1024 * 1024);
+ }
+ catch (Exception ignored) {
+ System.err.println("Unable to load thread pool MBeans: " + name);
+ }
+ return Collections.unmodifiableMap(gauges);
+ }
+
+ private static String name(String name) {
+ return name.replace(" ", "-");
+ }
+
+}
+
+class Classes {
+
+ private final MBeanServerConnection mBeanServer;
+
+ public Classes(MBeanServerConnection mBeanServer) {
+ this.mBeanServer = mBeanServer;
+ }
+
+ public Map getMetrics() {
+ final Map gauges = new HashMap<>();
+ final String name = "Classes";
+ try {
+ final ObjectName on = new ObjectName("java.lang:type=ClassLoading");
+ mBeanServer.getMBeanInfo(on);
+ Integer value = (Integer) mBeanServer.getAttribute(on, "LoadedClassCount");
+ gauges.put(name(name), Long.valueOf(value));
+ }
+ catch (Exception ignored) {
+ System.err.println("Unable to load thread pool MBeans: " + name);
+ }
+ return Collections.unmodifiableMap(gauges);
+ }
+
+ private static String name(String name) {
+ return name.replace(" ", "-");
+ }
+
+}
+
+class BufferPools {
+
+ private static final String[] ATTRIBUTES = { "HeapMemoryUsage",
+ "NonHeapMemoryUsage" };
+
+ private final MBeanServerConnection mBeanServer;
+
+ public BufferPools(MBeanServerConnection mBeanServer) {
+ this.mBeanServer = mBeanServer;
+ }
+
+ public static long total(Map metrics) {
+ long total = 0;
+ System.err.println(metrics);
+ for (int i = 0; i < ATTRIBUTES.length; i++) {
+ final String name = name(ATTRIBUTES[i]);
+ total += metrics.containsKey(name) ? metrics.get(name) : 0;
+ }
+ total += metrics.getOrDefault("Threads", 0L);
+ return total;
+ }
+
+ public static long heap(Map metrics) {
+ long total = 0;
+ for (int i = 0; i < ATTRIBUTES.length; i++) {
+ final String name = name(ATTRIBUTES[i]);
+ if (name.startsWith("Heap")) {
+ total += metrics.containsKey(name) ? metrics.get(name) : 0;
+ }
+ }
+ return total;
+ }
+
+ public Map getMetrics() {
+ final Map gauges = new HashMap<>();
+ for (int i = 0; i < ATTRIBUTES.length; i++) {
+ try {
+ final ObjectName on = new ObjectName("java.lang:type=Memory");
+ final String name = ATTRIBUTES[i];
+ mBeanServer.getMBeanInfo(on);
+ CompositeData value = (CompositeData) mBeanServer.getAttribute(on, name);
+ gauges.put(name(name), (Long) value.get("used"));
+ }
+ catch (Exception ignored) {
+ System.err.println("Unable to load memory pool MBeans");
+ }
+ }
+ return Collections.unmodifiableMap(gauges);
+ }
+
+ private static String name(String name) {
+ return name.replace(" ", "-");
+ }
+
+}
diff --git a/samples/vanilla-orm/src/test/resources/META-INF/services/jmh.mbr.core.ResultsWriterFactory b/samples/vanilla-orm/src/test/resources/META-INF/services/jmh.mbr.core.ResultsWriterFactory
new file mode 100644
index 000000000..03fd95b1d
--- /dev/null
+++ b/samples/vanilla-orm/src/test/resources/META-INF/services/jmh.mbr.core.ResultsWriterFactory
@@ -0,0 +1 @@
+app.main.CsvResultsWriterFactory
\ No newline at end of file
diff --git a/samples/vanilla-orm/src/test/resources/logback.xml b/samples/vanilla-orm/src/test/resources/logback.xml
new file mode 100644
index 000000000..a6fe8de4a
--- /dev/null
+++ b/samples/vanilla-orm/src/test/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/vanilla-rabbit/compile.sh b/samples/vanilla-rabbit/compile.sh
new file mode 100755
index 000000000..97966f74f
--- /dev/null
+++ b/samples/vanilla-rabbit/compile.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+../../mvnw -DskipTests clean package
+
+export JAR="vanilla-rabbit-0.0.1.BUILD-SNAPSHOT.jar"
+rm rabbit
+printf "Unpacking $JAR"
+rm -rf unpack
+mkdir unpack
+cd unpack
+jar -xvf ../target/$JAR >/dev/null 2>&1
+cp -R META-INF BOOT-INF/classes
+
+cd BOOT-INF/classes
+export LIBPATH=`find ../../BOOT-INF/lib | tr '\n' ':'`
+export CP=.:$LIBPATH
+
+# This would run it here... (as an exploded jar)
+#java -classpath $CP app.main.SampleApplication
+
+# Our feature being on the classpath is what triggers it
+export CP=$CP:../../../../../target/spring-graal-feature-0.5.0.BUILD-SNAPSHOT.jar
+
+printf "\n\nCompile\n"
+native-image \
+ -Dio.netty.noUnsafe=true \
+ --no-server \
+ -H:Name=rabbit\
+ -H:+ReportExceptionStackTraces \
+ --no-fallback \
+ --allow-incomplete-classpath \
+ --report-unsupported-elements-at-runtime \
+ -DremoveUnusedAutoconfig=true \
+ -cp $CP app.main.SampleApplication
+
+ #--debug-attach \
+mv rabbit ../../..
+
+printf "\n\nCompiled app (demo)\n"
+cd ../../..
+time ./rabbit
+
diff --git a/samples/vanilla-rabbit/pom.xml b/samples/vanilla-rabbit/pom.xml
new file mode 100644
index 000000000..ffc806cd3
--- /dev/null
+++ b/samples/vanilla-rabbit/pom.xml
@@ -0,0 +1,125 @@
+
+
+ 4.0.0
+
+ org.springframework.experimental
+ vanilla-rabbit
+ 0.0.1.BUILD-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.0.M5
+
+
+
+
+
+ org.reactivestreams
+ reactive-streams
+ 1.0.3
+
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+ org.springframework.boot
+ spring-boot-starter-json
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.springframework.boot.experimental
+ spring-boot-thin-launcher
+ ${thin.version}
+ test
+
+
+
+
+ 1.8
+ app.main.SampleApplication
+ 1.21
+ 1.0.22.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
+
+
+ spring-libs-snapshot
+ https://repo.spring.io/libs-snapshot
+
+ true
+
+
+ true
+
+
+
+ spring-libs-milestone
+ https://repo.spring.io/libs-milestone
+
+ false
+
+
+ true
+
+
+
+
+
+
+ spring-libs-snapshot
+ https://repo.spring.io/libs-snapshot
+
+ true
+
+
+ true
+
+
+
+ spring-libs-milestone
+ https://repo.spring.io/libs-milestone
+
+ true
+
+
+ true
+
+
+
+
+
diff --git a/samples/vanilla-rabbit/src/main/java/app/main/Receiver.java b/samples/vanilla-rabbit/src/main/java/app/main/Receiver.java
new file mode 100644
index 000000000..1df066e74
--- /dev/null
+++ b/samples/vanilla-rabbit/src/main/java/app/main/Receiver.java
@@ -0,0 +1,28 @@
+package app.main;
+
+import app.main.model.Foo;
+
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+public class Receiver {
+
+ private RabbitTemplate template;
+
+ public Receiver(RabbitTemplate template) {
+ this.template = template;
+ }
+
+ /**
+ * Send a message with empty routing key, JSON content and
+ * content_type=application/json
in the properties.
+ */
+ @RabbitListener(queues = "#{queue.name}")
+ public void receive(Foo message) {
+ System.out.println("Received <" + message + ">");
+ template.convertAndSend(new Foo(message.getValue().toUpperCase()));
+ }
+
+}
\ No newline at end of file
diff --git a/samples/vanilla-rabbit/src/main/java/app/main/SampleApplication.java b/samples/vanilla-rabbit/src/main/java/app/main/SampleApplication.java
new file mode 100644
index 000000000..28b774f86
--- /dev/null
+++ b/samples/vanilla-rabbit/src/main/java/app/main/SampleApplication.java
@@ -0,0 +1,63 @@
+package app.main;
+
+import app.main.model.Foo;
+
+import org.springframework.amqp.core.AnonymousQueue;
+import org.springframework.amqp.core.Binding;
+import org.springframework.amqp.core.BindingBuilder;
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.amqp.core.Queue;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.support.converter.ClassMapper;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication(proxyBeanMethods = false)
+public class SampleApplication {
+
+ @Bean
+ Queue queue() {
+ return new AnonymousQueue();
+ }
+
+ @Bean
+ TopicExchange input() {
+ return new TopicExchange("input");
+ }
+
+ @Bean
+ TopicExchange output() {
+ return new TopicExchange("output");
+ }
+
+ @Bean
+ Binding binding(Queue queue, @Qualifier("input") TopicExchange exchange) {
+ return BindingBuilder.bind(queue).to(exchange).with("#");
+ }
+
+
+ @Bean
+ Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
+ Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
+ converter.setClassMapper(new ClassMapper() {
+
+ @Override
+ public Class> toClass(MessageProperties properties) {
+ return Foo.class;
+ }
+
+ @Override
+ public void fromClass(Class> clazz, MessageProperties properties) {
+ }
+ });
+ return converter;
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(SampleApplication.class, args);
+ }
+
+}
diff --git a/samples/vanilla-rabbit/src/main/java/app/main/model/Foo.java b/samples/vanilla-rabbit/src/main/java/app/main/model/Foo.java
new file mode 100644
index 000000000..cefb6171e
--- /dev/null
+++ b/samples/vanilla-rabbit/src/main/java/app/main/model/Foo.java
@@ -0,0 +1,23 @@
+package app.main.model;
+
+public class Foo {
+ private long id;
+ private String value;
+ public Foo() {
+ }
+ public Foo(String value) {
+ this.value = value;
+ }
+ public String getValue() {
+ return this.value;
+ }
+ public void setValue(String value) {
+ this.value = value;
+ }
+ @Override
+ public String toString() {
+ return String.format(
+ "Foo[id=%d, value='%s']",
+ id, value);
+ }
+}
diff --git a/samples/vanilla-rabbit/src/main/resources/META-INF/native-image/reflect-config.json b/samples/vanilla-rabbit/src/main/resources/META-INF/native-image/reflect-config.json
new file mode 100644
index 000000000..1ae607c17
--- /dev/null
+++ b/samples/vanilla-rabbit/src/main/resources/META-INF/native-image/reflect-config.json
@@ -0,0 +1,12 @@
+[
+ {
+ "name": "app.main.model.Foo",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "app.main.Receiver",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true
+ }
+]
\ No newline at end of file
diff --git a/samples/vanilla-rabbit/src/main/resources/application.properties b/samples/vanilla-rabbit/src/main/resources/application.properties
new file mode 100644
index 000000000..ca58d9007
--- /dev/null
+++ b/samples/vanilla-rabbit/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+spring.rabbitmq.template.exchange=output
+spring.rabbitmq.template.routingKey=
\ No newline at end of file
diff --git a/samples/vanilla-rabbit/src/test/java/app/main/ClientApplication.java b/samples/vanilla-rabbit/src/test/java/app/main/ClientApplication.java
new file mode 100644
index 000000000..b5c8d5464
--- /dev/null
+++ b/samples/vanilla-rabbit/src/test/java/app/main/ClientApplication.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019-2019 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 app.main;
+
+import com.rabbitmq.client.impl.ClientVersion;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class ClientApplication {
+
+ public static void main(String[] args) {
+ System.err.println(ClientVersion.VERSION);
+ }
+}
diff --git a/samples/vanilla-rabbit/src/test/java/app/main/SampleApplicationTests.java b/samples/vanilla-rabbit/src/test/java/app/main/SampleApplicationTests.java
new file mode 100644
index 000000000..3f9fe5272
--- /dev/null
+++ b/samples/vanilla-rabbit/src/test/java/app/main/SampleApplicationTests.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * 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 app.main;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author Dave Syer
+ *
+ */
+@SpringBootTest
+@AutoConfigureWebTestClient
+public class SampleApplicationTests {
+
+ @Test
+ public void test() {
+ }
+
+}
diff --git a/samples/vanilla-rabbit/src/test/resources/logback.xml b/samples/vanilla-rabbit/src/test/resources/logback.xml
new file mode 100644
index 000000000..a6fe8de4a
--- /dev/null
+++ b/samples/vanilla-rabbit/src/test/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/vanilla-thymeleaf/.mvn/wrapper/maven-wrapper.jar b/samples/vanilla-thymeleaf/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 000000000..5fd4d5023
Binary files /dev/null and b/samples/vanilla-thymeleaf/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/samples/vanilla-thymeleaf/.mvn/wrapper/maven-wrapper.properties b/samples/vanilla-thymeleaf/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 000000000..c954cec91
--- /dev/null
+++ b/samples/vanilla-thymeleaf/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip
diff --git a/samples/vanilla-thymeleaf/bin/.mvn/wrapper/maven-wrapper.jar b/samples/vanilla-thymeleaf/bin/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 000000000..5fd4d5023
Binary files /dev/null and b/samples/vanilla-thymeleaf/bin/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/samples/vanilla-thymeleaf/bin/.mvn/wrapper/maven-wrapper.properties b/samples/vanilla-thymeleaf/bin/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 000000000..c954cec91
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip
diff --git a/samples/vanilla-thymeleaf/bin/compile.sh b/samples/vanilla-thymeleaf/bin/compile.sh
new file mode 100755
index 000000000..05ccd9d67
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/compile.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+../../mvnw -DskipTests clean package
+
+export JAR="vanilla-thymeleaf-0.1.0.jar"
+rm thymeleaf
+printf "Unpacking $JAR"
+rm -rf unpack
+mkdir unpack
+cd unpack
+jar -xvf ../target/$JAR >/dev/null 2>&1
+cp -R META-INF BOOT-INF/classes
+
+cd BOOT-INF/classes
+export LIBPATH=`find ../../BOOT-INF/lib | tr '\n' ':'`
+export CP=.:$LIBPATH
+
+# This would run it here... (as an exploded jar)
+#java -classpath $CP hello.Application
+
+# Our feature being on the classpath is what triggers it
+export CP=$CP:../../../../../target/spring-boot-graal-feature-0.5.0.BUILD-SNAPSHOT.jar
+
+printf "\n\nCompile\n"
+native-image \
+ -Dio.netty.noUnsafe=true \
+ --no-server \
+ -H:Name=thymeleaf \
+ -H:+ReportExceptionStackTraces \
+ --no-fallback \
+ --allow-incomplete-classpath \
+ --report-unsupported-elements-at-runtime \
+ -H:+TraceClassInitialization=io.netty.bootstrap.AbstractBootstrap \
+ -DremoveUnusedAutoconfig=true \
+ -cp $CP hello.Application
+
+ #--debug-attach \
+mv thymeleaf ../../..
+
+printf "\n\nCompiled app (demo)\n"
+cd ../../..
+./thymeleaf
+
diff --git a/samples/vanilla-thymeleaf/bin/mvnw b/samples/vanilla-thymeleaf/bin/mvnw
new file mode 100755
index 000000000..a1ba1bf55
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/mvnw
@@ -0,0 +1,233 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ #
+ # Look for the Apple JDKs first to preserve the existing behaviour, and then look
+ # for the new JDKs provided by Oracle.
+ #
+ if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
+ #
+ # Apple JDKs
+ #
+ export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
+ fi
+
+ if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
+ #
+ # Apple JDKs
+ #
+ export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
+ fi
+
+ if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
+ #
+ # Oracle JDKs
+ #
+ export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
+ fi
+
+ if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
+ #
+ # Apple JDKs
+ #
+ export JAVA_HOME=`/usr/libexec/java_home`
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ local basedir=$(pwd)
+ local wdir=$(pwd)
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ wdir=$(cd "$wdir/.."; pwd)
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} "$@"
diff --git a/samples/vanilla-thymeleaf/bin/mvnw.cmd b/samples/vanilla-thymeleaf/bin/mvnw.cmd
new file mode 100755
index 000000000..2b934e89d
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/mvnw.cmd
@@ -0,0 +1,145 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+set MAVEN_CMD_LINE_ARGS=%*
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar""
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
\ No newline at end of file
diff --git a/samples/vanilla-thymeleaf/bin/pom.xml b/samples/vanilla-thymeleaf/bin/pom.xml
new file mode 100644
index 000000000..22d9f88bc
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/pom.xml
@@ -0,0 +1,116 @@
+
+
+ 4.0.0
+
+ com.example
+ vanilla-thymeleaf
+ 0.1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.0.BUILD-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ 1.8
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+ false
+
+
+
+ spring-release
+ Spring release
+ https://repo.spring.io/release
+
+ false
+
+
+
+ spring-snapshot
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestone
+ Spring Milestone
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+ false
+
+
+
+ spring-release
+ Spring release
+ https://repo.spring.io/release
+
+ false
+
+
+
+ spring-snapshot
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestone
+ Spring Milestone
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
+
diff --git a/samples/vanilla-thymeleaf/bin/src/main/java/hello/Application.class b/samples/vanilla-thymeleaf/bin/src/main/java/hello/Application.class
new file mode 100644
index 000000000..d794ab0b1
Binary files /dev/null and b/samples/vanilla-thymeleaf/bin/src/main/java/hello/Application.class differ
diff --git a/samples/vanilla-thymeleaf/bin/src/main/java/hello/Greeting.class b/samples/vanilla-thymeleaf/bin/src/main/java/hello/Greeting.class
new file mode 100644
index 000000000..6a282571c
Binary files /dev/null and b/samples/vanilla-thymeleaf/bin/src/main/java/hello/Greeting.class differ
diff --git a/samples/vanilla-thymeleaf/bin/src/main/java/hello/GreetingController.class b/samples/vanilla-thymeleaf/bin/src/main/java/hello/GreetingController.class
new file mode 100644
index 000000000..131d664ac
Binary files /dev/null and b/samples/vanilla-thymeleaf/bin/src/main/java/hello/GreetingController.class differ
diff --git a/samples/vanilla-thymeleaf/bin/src/main/resources/META-INF/native-image/reflect-config.json b/samples/vanilla-thymeleaf/bin/src/main/resources/META-INF/native-image/reflect-config.json
new file mode 100644
index 000000000..378693672
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/src/main/resources/META-INF/native-image/reflect-config.json
@@ -0,0 +1,7 @@
+[
+ {
+ "name": "hello.Greeting",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true
+ }
+]
\ No newline at end of file
diff --git a/samples/vanilla-thymeleaf/bin/src/main/resources/static/index.html b/samples/vanilla-thymeleaf/bin/src/main/resources/static/index.html
new file mode 100644
index 000000000..33e9b7e90
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/src/main/resources/static/index.html
@@ -0,0 +1,10 @@
+
+
+
+ Getting Started: Serving Web Content
+
+
+
+ Get your greeting here
+
+
diff --git a/samples/vanilla-thymeleaf/bin/src/main/resources/templates/greeting.html b/samples/vanilla-thymeleaf/bin/src/main/resources/templates/greeting.html
new file mode 100644
index 000000000..b4c497975
--- /dev/null
+++ b/samples/vanilla-thymeleaf/bin/src/main/resources/templates/greeting.html
@@ -0,0 +1,10 @@
+
+
+
+ Getting Started: Serving Web Content
+
+
+
+
+
+
diff --git a/samples/vanilla-thymeleaf/bin/src/test/java/hello/ApplicationTests.class b/samples/vanilla-thymeleaf/bin/src/test/java/hello/ApplicationTests.class
new file mode 100644
index 000000000..318271e18
Binary files /dev/null and b/samples/vanilla-thymeleaf/bin/src/test/java/hello/ApplicationTests.class differ
diff --git a/samples/vanilla-thymeleaf/compile.sh b/samples/vanilla-thymeleaf/compile.sh
new file mode 100755
index 000000000..519d3dcc0
--- /dev/null
+++ b/samples/vanilla-thymeleaf/compile.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+../../mvnw -DskipTests clean package
+
+export JAR="vanilla-thymeleaf-0.1.0.jar"
+rm thymeleaf
+printf "Unpacking $JAR"
+rm -rf unpack
+mkdir unpack
+cd unpack
+jar -xvf ../target/$JAR >/dev/null 2>&1
+cp -R META-INF BOOT-INF/classes
+
+cd BOOT-INF/classes
+export LIBPATH=`find ../../BOOT-INF/lib | tr '\n' ':'`
+export CP=.:$LIBPATH
+
+# This would run it here... (as an exploded jar)
+#java -classpath $CP hello.Application
+
+# Our feature being on the classpath is what triggers it
+export CP=$CP:../../../../../target/spring-graal-feature-0.5.0.BUILD-SNAPSHOT.jar
+
+printf "\n\nCompile\n"
+native-image \
+ -Dio.netty.noUnsafe=true \
+ --no-server \
+ -H:Name=thymeleaf \
+ -H:+ReportExceptionStackTraces \
+ --no-fallback \
+ --allow-incomplete-classpath \
+ --report-unsupported-elements-at-runtime \
+ -DremoveUnusedAutoconfig=true \
+ -cp $CP hello.Application
+
+
+ #--debug-attach \
+mv thymeleaf ../../..
+
+printf "\n\nCompiled app (demo)\n"
+cd ../../..
+./thymeleaf
+
diff --git a/samples/vanilla-thymeleaf/mvnw b/samples/vanilla-thymeleaf/mvnw
new file mode 100755
index 000000000..a1ba1bf55
--- /dev/null
+++ b/samples/vanilla-thymeleaf/mvnw
@@ -0,0 +1,233 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ #
+ # Look for the Apple JDKs first to preserve the existing behaviour, and then look
+ # for the new JDKs provided by Oracle.
+ #
+ if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
+ #
+ # Apple JDKs
+ #
+ export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
+ fi
+
+ if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
+ #
+ # Apple JDKs
+ #
+ export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
+ fi
+
+ if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
+ #
+ # Oracle JDKs
+ #
+ export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
+ fi
+
+ if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
+ #
+ # Apple JDKs
+ #
+ export JAVA_HOME=`/usr/libexec/java_home`
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ local basedir=$(pwd)
+ local wdir=$(pwd)
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ wdir=$(cd "$wdir/.."; pwd)
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} "$@"
diff --git a/samples/vanilla-thymeleaf/mvnw.cmd b/samples/vanilla-thymeleaf/mvnw.cmd
new file mode 100755
index 000000000..2b934e89d
--- /dev/null
+++ b/samples/vanilla-thymeleaf/mvnw.cmd
@@ -0,0 +1,145 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+set MAVEN_CMD_LINE_ARGS=%*
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar""
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
\ No newline at end of file
diff --git a/samples/vanilla-thymeleaf/pom.xml b/samples/vanilla-thymeleaf/pom.xml
new file mode 100644
index 000000000..aabed5eae
--- /dev/null
+++ b/samples/vanilla-thymeleaf/pom.xml
@@ -0,0 +1,138 @@
+
+
+ 4.0.0
+
+ com.example
+ vanilla-thymeleaf
+ 0.1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.0.M5
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+ org.hibernate.validator
+ hibernate-validator
+
+
+ io.netty
+ netty-transport-native-epoll
+
+
+ io.netty
+ netty-codec-http2
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ 1.8
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+ false
+
+
+
+ spring-release
+ Spring release
+ https://repo.spring.io/release
+
+ false
+
+
+
+ spring-snapshot
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestone
+ Spring Milestone
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+ false
+
+
+
+ spring-release
+ Spring release
+ https://repo.spring.io/release
+
+ false
+
+
+
+ spring-snapshot
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestone
+ Spring Milestone
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
+
diff --git a/samples/vanilla-thymeleaf/src/main/java/hello/Application.java b/samples/vanilla-thymeleaf/src/main/java/hello/Application.java
new file mode 100644
index 000000000..59428955b
--- /dev/null
+++ b/samples/vanilla-thymeleaf/src/main/java/hello/Application.java
@@ -0,0 +1,13 @@
+package hello;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication(proxyBeanMethods = false)
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/samples/vanilla-thymeleaf/src/main/java/hello/GreetingController.java b/samples/vanilla-thymeleaf/src/main/java/hello/GreetingController.java
new file mode 100644
index 000000000..bcf1ffc66
--- /dev/null
+++ b/samples/vanilla-thymeleaf/src/main/java/hello/GreetingController.java
@@ -0,0 +1,54 @@
+package hello;
+
+import java.util.UUID;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@Controller
+public class GreetingController {
+
+ @GetMapping("/greeting")
+ public String greeting(
+ @RequestParam(name = "name", required = false, defaultValue = "World") String name,
+ Model model) {
+ model.addAttribute("greeting", new Greeting(name));
+ return "greeting";
+ }
+
+}
+
+class Greeting {
+
+ private String id = UUID.randomUUID().toString();
+
+ private String msg;
+
+ @SuppressWarnings("unused")
+ private Greeting() {
+ }
+
+ public Greeting(String msg) {
+ this.msg = msg;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getMsg() {
+ return msg;
+ }
+
+ public void setMsg(String msg) {
+ this.msg = msg;
+ }
+
+ @Override
+ public String toString() {
+ return "Greeting [msg=" + msg + "]";
+ }
+
+}
\ No newline at end of file
diff --git a/samples/vanilla-thymeleaf/src/main/resources/META-INF/native-image/reflect-config.json b/samples/vanilla-thymeleaf/src/main/resources/META-INF/native-image/reflect-config.json
new file mode 100644
index 000000000..378693672
--- /dev/null
+++ b/samples/vanilla-thymeleaf/src/main/resources/META-INF/native-image/reflect-config.json
@@ -0,0 +1,7 @@
+[
+ {
+ "name": "hello.Greeting",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true
+ }
+]
\ No newline at end of file
diff --git a/samples/vanilla-thymeleaf/src/main/resources/static/index.html b/samples/vanilla-thymeleaf/src/main/resources/static/index.html
new file mode 100644
index 000000000..33e9b7e90
--- /dev/null
+++ b/samples/vanilla-thymeleaf/src/main/resources/static/index.html
@@ -0,0 +1,10 @@
+
+
+
+ Getting Started: Serving Web Content
+
+
+
+ Get your greeting here
+
+
diff --git a/samples/vanilla-thymeleaf/src/main/resources/templates/greeting.html b/samples/vanilla-thymeleaf/src/main/resources/templates/greeting.html
new file mode 100644
index 000000000..b4c497975
--- /dev/null
+++ b/samples/vanilla-thymeleaf/src/main/resources/templates/greeting.html
@@ -0,0 +1,10 @@
+
+
+
+ Getting Started: Serving Web Content
+
+
+
+
+
+
diff --git a/samples/vanilla-thymeleaf/src/test/java/hello/ApplicationTests.java b/samples/vanilla-thymeleaf/src/test/java/hello/ApplicationTests.java
new file mode 100644
index 000000000..f69292552
--- /dev/null
+++ b/samples/vanilla-thymeleaf/src/test/java/hello/ApplicationTests.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012-2018 the original author or authors.
+ *
+ * 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 hello;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+@RunWith(SpringRunner.class)
+@WebFluxTest(controllers = GreetingController.class)
+public class ApplicationTests {
+
+ @Autowired
+ private WebTestClient client;
+
+ @Test
+ public void homePage() throws Exception {
+ // N.B. jsoup can be useful for asserting HTML content
+ client.get().uri("/index.html").exchange().expectBody(String.class).consumeWith(
+ content -> content.getResponseBody().contains("Get your greeting"));
+ }
+
+ @Test
+ public void greeting() throws Exception {
+ client.get().uri("/greeting").exchange().expectBody(String.class).consumeWith(
+ content -> content.getResponseBody().contains("Hello, World!"));
+ }
+
+ @Test
+ public void greetingWithUser() throws Exception {
+ client.get().uri("/greeting?name={name}", "Greg").exchange()
+ .expectBody(String.class).consumeWith(
+ content -> content.getResponseBody().contains("Hello, Greg!"));
+ }
+
+}
diff --git a/samples/vanilla-tx/compile.sh b/samples/vanilla-tx/compile.sh
new file mode 100755
index 000000000..0a2253ae2
--- /dev/null
+++ b/samples/vanilla-tx/compile.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+../../mvnw -DskipTests clean package
+
+export JAR="vanilla-tx-0.0.1.BUILD-SNAPSHOT.jar"
+rm tx
+printf "Unpacking $JAR"
+rm -rf unpack
+mkdir unpack
+cd unpack
+jar -xvf ../target/$JAR >/dev/null 2>&1
+cp -R META-INF BOOT-INF/classes
+
+cd BOOT-INF/classes
+export LIBPATH=`find ../../BOOT-INF/lib | tr '\n' ':'`
+export CP=.:$LIBPATH
+
+# This would run it here... (as an exploded jar)
+#java -classpath $CP hello.Application
+
+# Our feature being on the classpath is what triggers it
+export CP=$CP:../../../../../target/spring-graal-feature-0.5.0.BUILD-SNAPSHOT.jar
+
+printf "\n\nCompile\n"
+native-image \
+ -Dio.netty.noUnsafe=true \
+ --no-server \
+ -H:Name=tx \
+ -H:+ReportExceptionStackTraces \
+ -H:+TraceClassInitialization \
+ --no-fallback \
+ --allow-incomplete-classpath \
+ -H:EnableURLProtocols=https \
+ --report-unsupported-elements-at-runtime \
+ -DremoveUnusedAutoconfig=true \
+ -cp $CP app.main.SampleApplication
+
+ #--debug-attach \
+mv tx ../../..
+
+printf "\n\nCompiled app \n"
+cd ../../..
+./tx
+
diff --git a/samples/vanilla-tx/pom.xml b/samples/vanilla-tx/pom.xml
new file mode 100644
index 000000000..62624d669
--- /dev/null
+++ b/samples/vanilla-tx/pom.xml
@@ -0,0 +1,133 @@
+
+
+ 4.0.0
+
+ org.springframework.experimental
+ vanilla-tx
+ 0.0.1.BUILD-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.0.M5
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ io.netty
+ netty-transport-native-epoll
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.hibernate.validator
+ hibernate-validator
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+ 1.8
+ app.main.SampleApplication
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
+
+
+ spring-libs-snapshot
+ https://repo.spring.io/libs-snapshot
+
+ true
+
+
+ true
+
+
+
+ spring-libs-milestone
+ https://repo.spring.io/libs-milestone
+
+ false
+
+
+ true
+
+
+
+
+
+
+ spring-libs-snapshot
+ https://repo.spring.io/libs-snapshot
+
+ true
+
+
+ true
+
+
+
+ spring-libs-milestone
+ https://repo.spring.io/libs-milestone
+
+ true
+
+
+ true
+
+
+
+
+
diff --git a/samples/vanilla-tx/src/main/java/app/main/Finder.java b/samples/vanilla-tx/src/main/java/app/main/Finder.java
new file mode 100644
index 000000000..d37fa2f8d
--- /dev/null
+++ b/samples/vanilla-tx/src/main/java/app/main/Finder.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019-2019 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 app.main;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public interface Finder {
+
+ T find(long id);
+
+}
diff --git a/samples/vanilla-tx/src/main/java/app/main/Runner.java b/samples/vanilla-tx/src/main/java/app/main/Runner.java
new file mode 100644
index 000000000..4c61577bd
--- /dev/null
+++ b/samples/vanilla-tx/src/main/java/app/main/Runner.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2019-2019 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 app.main;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import app.main.model.Foo;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.util.Assert;
+
+/**
+ * @author Dave Syer
+ */
+@Component
+public class Runner implements CommandLineRunner, Finder {
+
+ private static final String GET_FOO = "SELECT VALUE from FOOS where ID=?";
+
+ private static final String ADD_FOO = "INSERT into FOOS (ID, VALUE) values (?, ?)";
+
+ private final JdbcTemplate entities;
+
+ private final FooMapper mapper = new FooMapper();
+
+ public Runner(JdbcTemplate entities) {
+ this.entities = entities;
+ }
+
+ @Override
+ @Transactional
+ public void run(String... args) throws Exception {
+ Assert.isTrue(TransactionSynchronizationManager.isActualTransactionActive(), "Expected transaction");
+ try {
+ find(1L);
+ }
+ catch (EmptyResultDataAccessException e) {
+ entities.update(ADD_FOO, 1L, "Hello");
+ }
+ }
+
+ class FooMapper implements RowMapper {
+
+ @Override
+ public Foo mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Foo(rs.getString(1));
+ }
+
+ }
+
+ @Override
+ public Foo find(long id) {
+ return entities.queryForObject(GET_FOO, mapper, id);
+ }
+
+}
\ No newline at end of file
diff --git a/samples/vanilla-tx/src/main/java/app/main/SampleApplication.java b/samples/vanilla-tx/src/main/java/app/main/SampleApplication.java
new file mode 100644
index 000000000..af8b15e86
--- /dev/null
+++ b/samples/vanilla-tx/src/main/java/app/main/SampleApplication.java
@@ -0,0 +1,29 @@
+package app.main;
+
+import app.main.model.Foo;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.reactive.function.server.RouterFunction;
+
+import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
+import static org.springframework.web.reactive.function.server.RouterFunctions.route;
+import static org.springframework.web.reactive.function.server.ServerResponse.ok;
+
+@SpringBootApplication(proxyBeanMethods = false)
+public class SampleApplication {
+
+ @Bean
+ public RouterFunction> userEndpoints(Finder entities) {
+ return route(GET("/"), request -> ok()
+ .body(Mono.fromCallable(() -> entities.find(1L)).subscribeOn(Schedulers.elastic()), Foo.class));
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(SampleApplication.class, args);
+ }
+
+}
diff --git a/samples/vanilla-tx/src/main/java/app/main/model/Foo.java b/samples/vanilla-tx/src/main/java/app/main/model/Foo.java
new file mode 100644
index 000000000..2bea6ba42
--- /dev/null
+++ b/samples/vanilla-tx/src/main/java/app/main/model/Foo.java
@@ -0,0 +1,27 @@
+package app.main.model;
+
+public class Foo {
+
+ private String value;
+
+ public Foo() {
+ }
+
+ public Foo(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Foo[value='%s']", value);
+ }
+
+}
diff --git a/samples/vanilla-tx/src/main/resources/application.properties b/samples/vanilla-tx/src/main/resources/application.properties
new file mode 100644
index 000000000..f64911866
--- /dev/null
+++ b/samples/vanilla-tx/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+logging.level.slim=debug
+spring.aop.proxy-target-class=false
\ No newline at end of file
diff --git a/samples/vanilla-tx/src/main/resources/schema.sql b/samples/vanilla-tx/src/main/resources/schema.sql
new file mode 100644
index 000000000..88e3db1fb
--- /dev/null
+++ b/samples/vanilla-tx/src/main/resources/schema.sql
@@ -0,0 +1,4 @@
+CREATE TABLE FOOS (
+ id INTEGER IDENTITY PRIMARY KEY,
+ value VARCHAR(30)
+);
\ No newline at end of file
diff --git a/samples/vanilla-tx/src/test/java/app/main/SampleApplicationTests.java b/samples/vanilla-tx/src/test/java/app/main/SampleApplicationTests.java
new file mode 100644
index 000000000..01a4ab0fa
--- /dev/null
+++ b/samples/vanilla-tx/src/test/java/app/main/SampleApplicationTests.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * 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 app.main;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+/**
+ * @author Dave Syer
+ *
+ */
+@SpringBootTest
+@AutoConfigureWebTestClient
+public class SampleApplicationTests {
+
+ @Autowired
+ private WebTestClient client;
+
+ @Test
+ public void test() {
+ client.get().uri("/").exchange().expectBody(String.class).isEqualTo("{\"value\":\"Hello\"}");
+ }
+
+}
diff --git a/samples/vanilla-tx/src/test/resources/logback.xml b/samples/vanilla-tx/src/test/resources/logback.xml
new file mode 100644
index 000000000..a6fe8de4a
--- /dev/null
+++ b/samples/vanilla-tx/src/test/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/webflux-netty/.mvn/wrapper/MavenWrapperDownloader.java b/samples/webflux-netty/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 000000000..72308aa47
--- /dev/null
+++ b/samples/webflux-netty/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,114 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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
+
+ https://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.
+*/
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL =
+ "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: : " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/samples/webflux-netty/.mvn/wrapper/maven-wrapper.jar b/samples/webflux-netty/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 000000000..01e679973
Binary files /dev/null and b/samples/webflux-netty/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/samples/webflux-netty/.mvn/wrapper/maven-wrapper.properties b/samples/webflux-netty/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 000000000..cd0d451cc
--- /dev/null
+++ b/samples/webflux-netty/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip
diff --git a/samples/webflux-netty/compile.sh b/samples/webflux-netty/compile.sh
new file mode 100755
index 000000000..203691465
--- /dev/null
+++ b/samples/webflux-netty/compile.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+../../mvnw clean install
+
+export JAR="webflux-netty-0.0.1-SNAPSHOT.jar"
+rm webflux-netty
+printf "Unpacking $JAR"
+rm -rf unpack
+mkdir unpack
+cd unpack
+jar -xvf ../target/$JAR >/dev/null 2>&1
+cp -R META-INF BOOT-INF/classes
+
+cd BOOT-INF/classes
+export LIBPATH=`find ../../BOOT-INF/lib | tr '\n' ':'`
+export CP=.:$LIBPATH
+
+# This would run it here... (as an exploded jar)
+#java -classpath $CP com.example.demo.DemoApplication
+
+# Our feature being on the classpath is what triggers it
+export CP=$CP:../../../../../target/spring-graal-feature-0.5.0.BUILD-SNAPSHOT.jar
+
+printf "\n\nCompile\n"
+native-image \
+ -Dio.netty.noUnsafe=true \
+ --no-server \
+ -H:+TraceClassInitialization \
+ -H:Name=webflux-netty \
+ -H:+ReportExceptionStackTraces \
+ --no-fallback \
+ --allow-incomplete-classpath \
+ --report-unsupported-elements-at-runtime \
+ -cp $CP com.example.demo.DemoApplication
+
+# -DremoveUnusedAutoconfig=true \
+mv webflux-netty ../../..
+
+printf "\n\nCompiled app (webflux-netty)\n"
+cd ../../..
+time ./webflux-netty
+
diff --git a/samples/webflux-netty/mvnw b/samples/webflux-netty/mvnw
new file mode 100644
index 000000000..8b9da3b8b
--- /dev/null
+++ b/samples/webflux-netty/mvnw
@@ -0,0 +1,286 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# https://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ wget "$jarUrl" -O "$wrapperJarPath"
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ curl -o "$wrapperJarPath" "$jarUrl"
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/samples/webflux-netty/mvnw.cmd b/samples/webflux-netty/mvnw.cmd
new file mode 100644
index 000000000..fef5a8f7f
--- /dev/null
+++ b/samples/webflux-netty/mvnw.cmd
@@ -0,0 +1,161 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
+FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ echo Found %WRAPPER_JAR%
+) else (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
+ echo Finished downloading %WRAPPER_JAR%
+)
+@REM End of extension
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/samples/webflux-netty/pom.xml b/samples/webflux-netty/pom.xml
new file mode 100644
index 000000000..68f7e0ef3
--- /dev/null
+++ b/samples/webflux-netty/pom.xml
@@ -0,0 +1,133 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.0.M5
+
+
+ com.example
+ webflux-netty
+ 0.0.1-SNAPSHOT
+ demo
+ Demo project for Spring Boot
+
+
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ org.hibernate.validator
+ hibernate-validator
+
+
+ io.netty
+ netty-transport-native-epoll
+
+
+ io.netty
+ netty-codec-http2
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+
+
+ org.springframework
+ spring-context-indexer
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+ false
+
+
+
+ spring-release
+ Spring release
+ https://repo.spring.io/release
+
+ false
+
+
+
+ spring-snapshot
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestone
+ Spring Milestone
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+ false
+
+
+
+ spring-release
+ Spring release
+ https://repo.spring.io/release
+
+ false
+
+
+
+ spring-snapshot
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestone
+ Spring Milestone
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
diff --git a/samples/webflux-netty/src/main/java/com/example/demo/DemoApplication.java b/samples/webflux-netty/src/main/java/com/example/demo/DemoApplication.java
new file mode 100644
index 000000000..1db6e609b
--- /dev/null
+++ b/samples/webflux-netty/src/main/java/com/example/demo/DemoApplication.java
@@ -0,0 +1,28 @@
+package com.example.demo;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@SpringBootApplication(proxyBeanMethods = false)
+public class DemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+ @RestController
+ class Foo {
+
+ @GetMapping("/")
+ public String greet() {
+ return "hi!";
+ }
+ }
+
+}
diff --git a/samples/webflux-netty/src/main/java/com/example/demo/Foobar.java b/samples/webflux-netty/src/main/java/com/example/demo/Foobar.java
new file mode 100644
index 000000000..251fb8335
--- /dev/null
+++ b/samples/webflux-netty/src/main/java/com/example/demo/Foobar.java
@@ -0,0 +1,13 @@
+package com.example.demo;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class Foobar {
+
+ @GetMapping("/x")
+ public String greet2() {
+ return "hix!";
+ }
+}
\ No newline at end of file
diff --git a/samples/webflux-netty/src/main/resources/application.properties b/samples/webflux-netty/src/main/resources/application.properties
new file mode 100644
index 000000000..e69de29bb
diff --git a/samples/webflux-netty/src/main/resources/logging.properties b/samples/webflux-netty/src/main/resources/logging.properties
new file mode 100644
index 000000000..70a31b86a
--- /dev/null
+++ b/samples/webflux-netty/src/main/resources/logging.properties
@@ -0,0 +1,12 @@
+handlers =java.util.logging.ConsoleHandler
+.level = INFO
+
+java.util.logging.ConsoleHandler.formatter = org.springframework.boot.logging.java.SimpleFormatter
+java.util.logging.ConsoleHandler.level = ALL
+
+org.hibernate.validator.internal.util.Version.level = WARNING
+org.apache.coyote.http11.Http11NioProtocol.level = WARNING
+org.apache.tomcat.util.net.NioSelectorPool.level = WARNING
+org.apache.catalina.startup.DigesterFactory.level = SEVERE
+org.apache.catalina.util.LifecycleBase.level = SEVERE
+org.eclipse.jetty.util.component.AbstractLifeCycle.level = SEVERE
diff --git a/samples/webflux-netty/src/test/java/com/example/demo/DemoApplicationTests.java b/samples/webflux-netty/src/test/java/com/example/demo/DemoApplicationTests.java
new file mode 100644
index 000000000..b4c8f5df3
--- /dev/null
+++ b/samples/webflux-netty/src/test/java/com/example/demo/DemoApplicationTests.java
@@ -0,0 +1,16 @@
+//package com.example.demo;
+//
+//import org.junit.Test;
+//import org.junit.runner.RunWith;
+//import org.springframework.boot.test.context.SpringBootTest;
+//import org.springframework.test.context.junit4.SpringRunner;
+//
+//@RunWith(SpringRunner.class)
+//@SpringBootTest
+//public class DemoApplicationTests {
+//
+// @Test
+// public void contextLoads() {
+// }
+//
+//}
diff --git a/src/json-shade/README.adoc b/src/json-shade/README.adoc
new file mode 100644
index 000000000..654800694
--- /dev/null
+++ b/src/json-shade/README.adoc
@@ -0,0 +1,5 @@
+## Shaded JSON
+
+This source was originally taken from `com.vaadin.external.google:android-json` which
+provides a clean room re-implementation of the `org.json` APIs and does not include the
+"Do not use for evil" clause.
diff --git a/src/json-shade/java/org/springframework/graal/json/JSON.java b/src/json-shade/java/org/springframework/graal/json/JSON.java
new file mode 100644
index 000000000..034179dfc
--- /dev/null
+++ b/src/json-shade/java/org/springframework/graal/json/JSON.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2010 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 org.springframework.graal.json;
+
+class JSON {
+
+ static double checkDouble(double d) throws JSONException {
+ if (Double.isInfinite(d) || Double.isNaN(d)) {
+ throw new JSONException("Forbidden numeric value: " + d);
+ }
+ return d;
+ }
+
+ static Boolean toBoolean(Object value) {
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ }
+ if (value instanceof String) {
+ String stringValue = (String) value;
+ if ("true".equalsIgnoreCase(stringValue)) {
+ return true;
+ }
+ if ("false".equalsIgnoreCase(stringValue)) {
+ return false;
+ }
+ }
+ return null;
+ }
+
+ static Double toDouble(Object value) {
+ if (value instanceof Double) {
+ return (Double) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).doubleValue();
+ }
+ if (value instanceof String) {
+ try {
+ return Double.valueOf((String) value);
+ }
+ catch (NumberFormatException ignored) {
+ }
+ }
+ return null;
+ }
+
+ static Integer toInteger(Object value) {
+ if (value instanceof Integer) {
+ return (Integer) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof String) {
+ try {
+ return (int) Double.parseDouble((String) value);
+ }
+ catch (NumberFormatException ignored) {
+ }
+ }
+ return null;
+ }
+
+ static Long toLong(Object value) {
+ if (value instanceof Long) {
+ return (Long) value;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).longValue();
+ }
+ if (value instanceof String) {
+ try {
+ return (long) Double.parseDouble((String) value);
+ }
+ catch (NumberFormatException ignored) {
+ }
+ }
+ return null;
+ }
+
+ static String toString(Object value) {
+ if (value instanceof String) {
+ return (String) value;
+ }
+ if (value != null) {
+ return String.valueOf(value);
+ }
+ return null;
+ }
+
+ public static JSONException typeMismatch(Object indexOrName, Object actual,
+ String requiredType) throws JSONException {
+ if (actual == null) {
+ throw new JSONException("Value at " + indexOrName + " is null.");
+ }
+ throw new JSONException("Value " + actual + " at " + indexOrName + " of type "
+ + actual.getClass().getName() + " cannot be converted to "
+ + requiredType);
+ }
+
+ public static JSONException typeMismatch(Object actual, String requiredType)
+ throws JSONException {
+ if (actual == null) {
+ throw new JSONException("Value is null.");
+ }
+ throw new JSONException(
+ "Value " + actual + " of type " + actual.getClass().getName()
+ + " cannot be converted to " + requiredType);
+ }
+
+}
diff --git a/src/json-shade/java/org/springframework/graal/json/JSONArray.java b/src/json-shade/java/org/springframework/graal/json/JSONArray.java
new file mode 100644
index 000000000..300644e92
--- /dev/null
+++ b/src/json-shade/java/org/springframework/graal/json/JSONArray.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2010 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 org.springframework.graal.json;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+// Note: this class was written without inspecting the non-free org.json source code.
+
+/**
+ * A dense indexed sequence of values. Values may be any mix of {@link JSONObject
+ * JSONObjects}, other {@link JSONArray JSONArrays}, Strings, Booleans, Integers, Longs,
+ * Doubles, {@code null} or {@link JSONObject#NULL}. Values may not be
+ * {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities}, or of any type not
+ * listed here.
+ *
+ * {@code JSONArray} has the same type coercion behavior and optional/mandatory accessors
+ * as {@link JSONObject}. See that class' documentation for details.
+ *
+ * Warning: this class represents null in two incompatible ways: the
+ * standard Java {@code null} reference, and the sentinel value {@link JSONObject#NULL}.
+ * In particular, {@code get} fails if the requested index holds the null reference, but
+ * succeeds if it holds {@code JSONObject.NULL}.
+ *
+ * Instances of this class are not thread safe. Although this class is nonfinal, it was
+ * not designed for inheritance and should not be subclassed. In particular, self-use by
+ * overridable methods is not specified. See Effective Java Item 17, "Design and
+ * Document or inheritance or else prohibit it" for further information.
+ */
+public class JSONArray {
+
+ private final List values;
+
+ /**
+ * Creates a {@code JSONArray} with no values.
+ */
+ public JSONArray() {
+ this.values = new ArrayList<>();
+ }
+
+ /**
+ * Creates a new {@code JSONArray} by copying all values from the given collection.
+ * @param copyFrom a collection whose values are of supported types. Unsupported
+ * values are not permitted and will yield an array in an inconsistent state.
+ */
+ /* Accept a raw type for API compatibility */
+ @SuppressWarnings("rawtypes")
+ public JSONArray(Collection copyFrom) {
+ this();
+ if (copyFrom != null) {
+ for (Iterator it = copyFrom.iterator(); it.hasNext();) {
+ put(JSONObject.wrap(it.next()));
+ }
+ }
+ }
+
+ /**
+ * Creates a new {@code JSONArray} with values from the next array in the tokener.
+ * @param readFrom a tokener whose nextValue() method will yield a {@code JSONArray}.
+ * @throws JSONException if the parse fails or doesn't yield a {@code JSONArray}.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray(JSONTokener readFrom) throws JSONException {
+ /*
+ * Getting the parser to populate this could get tricky. Instead, just parse to
+ * temporary JSONArray and then steal the data from that.
+ */
+ Object object = readFrom.nextValue();
+ if (object instanceof JSONArray) {
+ this.values = ((JSONArray) object).values;
+ }
+ else {
+ throw JSON.typeMismatch(object, "JSONArray");
+ }
+ }
+
+ /**
+ * Creates a new {@code JSONArray} with values from the JSON string.
+ * @param json a JSON-encoded string containing an array.
+ * @throws JSONException if the parse fails or doesn't yield a {@code
+ * JSONArray}.
+ */
+ public JSONArray(String json) throws JSONException {
+ this(new JSONTokener(json));
+ }
+
+ /**
+ * Creates a new {@code JSONArray} with values from the given primitive array.
+ * @param array a primitive array
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray(Object array) throws JSONException {
+ if (!array.getClass().isArray()) {
+ throw new JSONException("Not a primitive array: " + array.getClass());
+ }
+ final int length = Array.getLength(array);
+ this.values = new ArrayList<>(length);
+ for (int i = 0; i < length; ++i) {
+ put(JSONObject.wrap(Array.get(array, i)));
+ }
+ }
+
+ /**
+ * Returns the number of values in this array.
+ * @return the length of this array
+ */
+ public int length() {
+ return this.values.size();
+ }
+
+ /**
+ * Appends {@code value} to the end of this array.
+ *
+ * @param value the value
+ * @return this array.
+ */
+ public JSONArray put(boolean value) {
+ this.values.add(value);
+ return this;
+ }
+
+ /**
+ * Appends {@code value} to the end of this array.
+ *
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * @return this array.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray put(double value) throws JSONException {
+ this.values.add(JSON.checkDouble(value));
+ return this;
+ }
+
+ /**
+ * Appends {@code value} to the end of this array.
+ * @param value the value
+ * @return this array.
+ */
+ public JSONArray put(int value) {
+ this.values.add(value);
+ return this;
+ }
+
+ /**
+ * Appends {@code value} to the end of this array.
+ * @param value the value
+ * @return this array.
+ */
+ public JSONArray put(long value) {
+ this.values.add(value);
+ return this;
+ }
+
+ /**
+ * Appends {@code value} to the end of this array.
+ *
+ * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer,
+ * Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be
+ * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}. Unsupported
+ * values are not permitted and will cause the array to be in an inconsistent state.
+ * @return this array.
+ */
+ public JSONArray put(Object value) {
+ this.values.add(value);
+ return this;
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}, null padding this array to the
+ * required length if necessary. If a value already exists at {@code
+ * index}, it will be replaced.
+ * @param index the index to set the value to
+ * @param value the value
+ * @return this array.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray put(int index, boolean value) throws JSONException {
+ return put(index, (Boolean) value);
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}, null padding this array to the
+ * required length if necessary. If a value already exists at {@code
+ * index}, it will be replaced.
+ * @param index the index to set the value to
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * @return this array.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray put(int index, double value) throws JSONException {
+ return put(index, (Double) value);
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}, null padding this array to the
+ * required length if necessary. If a value already exists at {@code
+ * index}, it will be replaced.
+ * @param index the index to set the value to
+ * @param value the value
+ * @return this array.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray put(int index, int value) throws JSONException {
+ return put(index, (Integer) value);
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}, null padding this array to the
+ * required length if necessary. If a value already exists at {@code
+ * index}, it will be replaced.
+ * @param index the index to set the value to
+ * @param value the value
+ * @return this array.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray put(int index, long value) throws JSONException {
+ return put(index, (Long) value);
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}, null padding this array to the
+ * required length if necessary. If a value already exists at {@code
+ * index}, it will be replaced.
+ * @param index the index to set the value to
+ * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer,
+ * Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be
+ * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
+ * @return this array.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONArray put(int index, Object value) throws JSONException {
+ if (value instanceof Number) {
+ // deviate from the original by checking all Numbers, not just floats &
+ // doubles
+ JSON.checkDouble(((Number) value).doubleValue());
+ }
+ while (this.values.size() <= index) {
+ this.values.add(null);
+ }
+ this.values.set(index, value);
+ return this;
+ }
+
+ /**
+ * Returns true if this array has no value at {@code index}, or if its value is the
+ * {@code null} reference or {@link JSONObject#NULL}.
+ * @param index the index to set the value to
+ * @return true if this array has no value at {@code index}
+ */
+ public boolean isNull(int index) {
+ Object value = opt(index);
+ return value == null || value == JSONObject.NULL;
+ }
+
+ /**
+ * Returns the value at {@code index}.
+ * @param index the index to get the value from
+ * @return the value at {@code index}.
+ * @throws JSONException if this array has no value at {@code index}, or if that value
+ * is the {@code null} reference. This method returns normally if the value is
+ * {@code JSONObject#NULL}.
+ */
+ public Object get(int index) throws JSONException {
+ try {
+ Object value = this.values.get(index);
+ if (value == null) {
+ throw new JSONException("Value at " + index + " is null.");
+ }
+ return value;
+ }
+ catch (IndexOutOfBoundsException e) {
+ throw new JSONException(
+ "Index " + index + " out of range [0.." + this.values.size() + ")");
+ }
+ }
+
+ /**
+ * Returns the value at {@code index}, or null if the array has no value at
+ * {@code index}.
+ * @param index the index to get the value from
+ * @return the value at {@code index} or {@code null}
+ */
+ public Object opt(int index) {
+ if (index < 0 || index >= this.values.size()) {
+ return null;
+ }
+ return this.values.get(index);
+ }
+
+ /**
+ * Removes and returns the value at {@code index}, or null if the array has no value
+ * at {@code index}.
+ * @param index the index of the value to remove
+ * @return the previous value at {@code index}
+ */
+ public Object remove(int index) {
+ if (index < 0 || index >= this.values.size()) {
+ return null;
+ }
+ return this.values.remove(index);
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a boolean or can be coerced
+ * to a boolean.
+ * @param index the index to get the value from
+ * @return the value at {@code index}
+ * @throws JSONException if the value at {@code index} doesn't exist or cannot be
+ * coerced to a boolean.
+ */
+ public boolean getBoolean(int index) throws JSONException {
+ Object object = get(index);
+ Boolean result = JSON.toBoolean(object);
+ if (result == null) {
+ throw JSON.typeMismatch(index, object, "boolean");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a boolean or can be coerced
+ * to a boolean. Returns false otherwise.
+ * @param index the index to get the value from
+ * @return the {@code value} or {@code false}
+ */
+ public boolean optBoolean(int index) {
+ return optBoolean(index, false);
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a boolean or can be coerced
+ * to a boolean. Returns {@code fallback} otherwise.
+ * @param index the index to get the value from
+ * @param fallback the fallback value
+ * @return the value at {@code index} of {@code fallback}
+ */
+ public boolean optBoolean(int index, boolean fallback) {
+ Object object = opt(index);
+ Boolean result = JSON.toBoolean(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a double or can be coerced
+ * to a double.
+ * @param index the index to get the value from
+ * @return the {@code value}
+ * @throws JSONException if the value at {@code index} doesn't exist or cannot be
+ * coerced to a double.
+ */
+ public double getDouble(int index) throws JSONException {
+ Object object = get(index);
+ Double result = JSON.toDouble(object);
+ if (result == null) {
+ throw JSON.typeMismatch(index, object, "double");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a double or can be coerced
+ * to a double. Returns {@code NaN} otherwise.
+ * @param index the index to get the value from
+ * @return the {@code value} or {@code NaN}
+ */
+ public double optDouble(int index) {
+ return optDouble(index, Double.NaN);
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a double or can be coerced
+ * to a double. Returns {@code fallback} otherwise.
+ * @param index the index to get the value from
+ * @param fallback the fallback value
+ * @return the value at {@code index} of {@code fallback}
+ */
+ public double optDouble(int index, double fallback) {
+ Object object = opt(index);
+ Double result = JSON.toDouble(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is an int or can be coerced to
+ * an int.
+ * @param index the index to get the value from
+ * @return the {@code value}
+ * @throws JSONException if the value at {@code index} doesn't exist or cannot be
+ * coerced to an int.
+ */
+ public int getInt(int index) throws JSONException {
+ Object object = get(index);
+ Integer result = JSON.toInteger(object);
+ if (result == null) {
+ throw JSON.typeMismatch(index, object, "int");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is an int or can be coerced to
+ * an int. Returns 0 otherwise.
+ * @param index the index to get the value from
+ * @return the {@code value} or {@code 0}
+ */
+ public int optInt(int index) {
+ return optInt(index, 0);
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is an int or can be coerced to
+ * an int. Returns {@code fallback} otherwise.
+ * @param index the index to get the value from
+ * @param fallback the fallback value
+ * @return the value at {@code index} of {@code fallback}
+ */
+ public int optInt(int index, int fallback) {
+ Object object = opt(index);
+ Integer result = JSON.toInteger(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a long or can be coerced to
+ * a long.
+ * @param index the index to get the value from
+ * @return the {@code value}
+ *
+ * @throws JSONException if the value at {@code index} doesn't exist or cannot be
+ * coerced to a long.
+ */
+ public long getLong(int index) throws JSONException {
+ Object object = get(index);
+ Long result = JSON.toLong(object);
+ if (result == null) {
+ throw JSON.typeMismatch(index, object, "long");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a long or can be coerced to
+ * a long. Returns 0 otherwise.
+ * @param index the index to get the value from
+ * @return the {@code value} or {@code 0}
+ */
+ public long optLong(int index) {
+ return optLong(index, 0L);
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a long or can be coerced to
+ * a long. Returns {@code fallback} otherwise.
+ * @param index the index to get the value from
+ * @param fallback the fallback value
+ * @return the value at {@code index} of {@code fallback}
+ */
+ public long optLong(int index, long fallback) {
+ Object object = opt(index);
+ Long result = JSON.toLong(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists, coercing it if necessary.
+ * @param index the index to get the value from
+ * @return the {@code value}
+ * @throws JSONException if no such value exists.
+ */
+ public String getString(int index) throws JSONException {
+ Object object = get(index);
+ String result = JSON.toString(object);
+ if (result == null) {
+ throw JSON.typeMismatch(index, object, "String");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists, coercing it if necessary. Returns
+ * the empty string if no such value exists.
+ * @param index the index to get the value from
+ * @return the {@code value} or an empty string
+ */
+ public String optString(int index) {
+ return optString(index, "");
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists, coercing it if necessary. Returns
+ * {@code fallback} if no such value exists.
+ * @param index the index to get the value from
+ * @param fallback the fallback value
+ * @return the value at {@code index} of {@code fallback}
+ */
+ public String optString(int index, String fallback) {
+ Object object = opt(index);
+ String result = JSON.toString(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a {@code
+ * JSONArray}.
+ * @param index the index to get the value from
+ * @return the array at {@code index}
+ * @throws JSONException if the value doesn't exist or is not a {@code
+ * JSONArray}.
+ */
+ public JSONArray getJSONArray(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof JSONArray) {
+ return (JSONArray) object;
+ }
+ else {
+ throw JSON.typeMismatch(index, object, "JSONArray");
+ }
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a {@code
+ * JSONArray}. Returns null otherwise.
+ * @param index the index to get the value from
+ * @return the array at {@code index} or {@code null}
+ */
+ public JSONArray optJSONArray(int index) {
+ Object object = opt(index);
+ return object instanceof JSONArray ? (JSONArray) object : null;
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a {@code
+ * JSONObject}.
+ * @param index the index to get the value from
+ * @return the object at {@code index}
+ * @throws JSONException if the value doesn't exist or is not a {@code
+ * JSONObject}.
+ */
+ public JSONObject getJSONObject(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof JSONObject) {
+ return (JSONObject) object;
+ }
+ else {
+ throw JSON.typeMismatch(index, object, "JSONObject");
+ }
+ }
+
+ /**
+ * Returns the value at {@code index} if it exists and is a {@code
+ * JSONObject}. Returns null otherwise.
+ * @param index the index to get the value from
+ * @return the object at {@code index} or {@code null}
+ */
+ public JSONObject optJSONObject(int index) {
+ Object object = opt(index);
+ return object instanceof JSONObject ? (JSONObject) object : null;
+ }
+
+ /**
+ * Returns a new object whose values are the values in this array, and whose names are
+ * the values in {@code names}. Names and values are paired up by index from 0 through
+ * to the shorter array's length. Names that are not strings will be coerced to
+ * strings. This method returns null if either array is empty.
+ * @param names the property names
+ * @return a json object
+ * @throws JSONException if processing of json failed
+ */
+ public JSONObject toJSONObject(JSONArray names) throws JSONException {
+ JSONObject result = new JSONObject();
+ int length = Math.min(names.length(), this.values.size());
+ if (length == 0) {
+ return null;
+ }
+ for (int i = 0; i < length; i++) {
+ String name = JSON.toString(names.opt(i));
+ result.put(name, opt(i));
+ }
+ return result;
+ }
+
+ /**
+ * Returns a new string by alternating this array's values with {@code
+ * separator}. This array's string values are quoted and have their special characters
+ * escaped. For example, the array containing the strings '12" pizza', 'taco' and
+ * 'soda' joined on '+' returns this: "12\" pizza"+"taco"+"soda"
+ * @param separator the separator to use
+ * @return the joined value
+ * @throws JSONException if processing of json failed
+ */
+ public String join(String separator) throws JSONException {
+ JSONStringer stringer = new JSONStringer();
+ stringer.open(JSONStringer.Scope.NULL, "");
+ for (int i = 0, size = this.values.size(); i < size; i++) {
+ if (i > 0) {
+ stringer.out.append(separator);
+ }
+ stringer.value(this.values.get(i));
+ }
+ stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, "");
+ return stringer.out.toString();
+ }
+
+ /**
+ * Encodes this array as a compact JSON string, such as: [94043,90210]
+ * @return a compact JSON string representation of this array
+ */
+ @Override
+ public String toString() {
+ try {
+ JSONStringer stringer = new JSONStringer();
+ writeTo(stringer);
+ return stringer.toString();
+ }
+ catch (JSONException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Encodes this array as a human readable JSON string for debugging, such as:
+ * [
+ * 94043,
+ * 90210
+ * ]
+ *
+ * @param indentSpaces the number of spaces to indent for each level of nesting.
+ * @return a human readable JSON string of this array
+ * @throws JSONException if processing of json failed
+ */
+ public String toString(int indentSpaces) throws JSONException {
+ JSONStringer stringer = new JSONStringer(indentSpaces);
+ writeTo(stringer);
+ return stringer.toString();
+ }
+
+ void writeTo(JSONStringer stringer) throws JSONException {
+ stringer.array();
+ for (Object value : this.values) {
+ stringer.value(value);
+ }
+ stringer.endArray();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof JSONArray && ((JSONArray) o).values.equals(this.values);
+ }
+
+ @Override
+ public int hashCode() {
+ // diverge from the original, which doesn't implement hashCode
+ return this.values.hashCode();
+ }
+
+}
diff --git a/src/json-shade/java/org/springframework/graal/json/JSONException.java b/src/json-shade/java/org/springframework/graal/json/JSONException.java
new file mode 100644
index 000000000..92dc2ad23
--- /dev/null
+++ b/src/json-shade/java/org/springframework/graal/json/JSONException.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 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 org.springframework.graal.json;
+
+// Note: this class was written without inspecting the non-free org.json source code.
+
+/**
+ * Thrown to indicate a problem with the JSON API. Such problems include:
+ *
+ * Attempts to parse or construct malformed documents
+ * Use of null as a name
+ * Use of numeric types not available to JSON, such as {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * Lookups using an out of range index or nonexistent name
+ * Type mismatches on lookups
+ *
+ *
+ * Although this is a checked exception, it is rarely recoverable. Most callers should
+ * simply wrap this exception in an unchecked exception and rethrow:
+ * public JSONArray toJSONObject() {
+ * try {
+ * JSONObject result = new JSONObject();
+ * ...
+ * } catch (JSONException e) {
+ * throw new RuntimeException(e);
+ * }
+ * }
+ */
+public class JSONException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public JSONException(String s) {
+ super(s);
+ }
+
+}
diff --git a/src/json-shade/java/org/springframework/graal/json/JSONObject.java b/src/json-shade/java/org/springframework/graal/json/JSONObject.java
new file mode 100644
index 000000000..b59d369b0
--- /dev/null
+++ b/src/json-shade/java/org/springframework/graal/json/JSONObject.java
@@ -0,0 +1,840 @@
+/*
+ * Copyright (C) 2010 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 org.springframework.graal.json;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+// Note: this class was written without inspecting the non-free org.json source code.
+
+/**
+ * A modifiable set of name/value mappings. Names are unique, non-null strings. Values may
+ * be any mix of {@link JSONObject JSONObjects}, {@link JSONArray JSONArrays}, Strings,
+ * Booleans, Integers, Longs, Doubles or {@link #NULL}. Values may not be {@code null},
+ * {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities}, or of any type not
+ * listed here.
+ *
+ * This class can coerce values to another type when requested.
+ *
+ *
+ * This class can look up both mandatory and optional values:
+ *
+ * Use getType ()
to retrieve a mandatory value. This fails with a
+ * {@code JSONException} if the requested name has no value or if the value cannot be
+ * coerced to the requested type.
+ * Use optType ()
to retrieve an optional value. This returns a
+ * system- or user-supplied default if the requested name has no value or if the value
+ * cannot be coerced to the requested type.
+ *
+ *
+ * Warning: this class represents null in two incompatible ways: the
+ * standard Java {@code null} reference, and the sentinel value {@link JSONObject#NULL}.
+ * In particular, calling {@code put(name, null)} removes the named entry from the object
+ * but {@code put(name, JSONObject.NULL)} stores an entry whose value is
+ * {@code JSONObject.NULL}.
+ *
+ * Instances of this class are not thread safe. Although this class is nonfinal, it was
+ * not designed for inheritance and should not be subclassed. In particular, self-use by
+ * overrideable methods is not specified. See Effective Java Item 17, "Design and
+ * Document or inheritance or else prohibit it" for further information.
+ */
+public class JSONObject {
+
+ private static final Double NEGATIVE_ZERO = -0d;
+
+ /**
+ * A sentinel value used to explicitly define a name with no value. Unlike
+ * {@code null}, names with this value:
+ *
+ * show up in the {@link #names} array
+ * show up in the {@link #keys} iterator
+ * return {@code true} for {@link #has(String)}
+ * do not throw on {@link #get(String)}
+ * are included in the encoded JSON string.
+ *
+ *
+ * This value violates the general contract of {@link Object#equals} by returning true
+ * when compared to {@code null}. Its {@link #toString} method returns "null".
+ */
+ public static final Object NULL = new Object() {
+
+ @Override
+ public boolean equals(Object o) {
+ return o == this || o == null; // API specifies this broken equals
+ // implementation
+ }
+
+ @Override
+ public String toString() {
+ return "null";
+ }
+
+ };
+
+ private final Map nameValuePairs;
+
+ /**
+ * Creates a {@code JSONObject} with no name/value mappings.
+ */
+ public JSONObject() {
+ this.nameValuePairs = new LinkedHashMap<>();
+ }
+
+ /**
+ * Creates a new {@code JSONObject} by copying all name/value mappings from the given
+ * map.
+ *
+ * @param copyFrom a map whose keys are of type {@link String} and whose values are of
+ * supported types.
+ * @throws NullPointerException if any of the map's keys are null.
+ */
+ /* (accept a raw type for API compatibility) */
+ @SuppressWarnings("rawtypes")
+ public JSONObject(Map copyFrom) {
+ this();
+ Map, ?> contentsTyped = copyFrom;
+ for (Map.Entry, ?> entry : contentsTyped.entrySet()) {
+ /*
+ * Deviate from the original by checking that keys are non-null and of the
+ * proper type. (We still defer validating the values).
+ */
+ String key = (String) entry.getKey();
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+ this.nameValuePairs.put(key, wrap(entry.getValue()));
+ }
+ }
+
+ /**
+ * Creates a new {@code JSONObject} with name/value mappings from the next object in
+ * the tokener.
+ * @param readFrom a tokener whose nextValue() method will yield a {@code JSONObject}.
+ * @throws JSONException if the parse fails or doesn't yield a {@code JSONObject}.
+ */
+ public JSONObject(JSONTokener readFrom) throws JSONException {
+ /*
+ * Getting the parser to populate this could get tricky. Instead, just parse to
+ * temporary JSONObject and then steal the data from that.
+ */
+ Object object = readFrom.nextValue();
+ if (object instanceof JSONObject) {
+ this.nameValuePairs = ((JSONObject) object).nameValuePairs;
+ }
+ else {
+ throw JSON.typeMismatch(object, "JSONObject");
+ }
+ }
+
+ /**
+ * Creates a new {@code JSONObject} with name/value mappings from the JSON string.
+ * @param json a JSON-encoded string containing an object.
+ * @throws JSONException if the parse fails or doesn't yield a {@code
+ * JSONObject}.
+ */
+ public JSONObject(String json) throws JSONException {
+ this(new JSONTokener(json));
+ }
+
+ /**
+ * Creates a new {@code JSONObject} by copying mappings for the listed names from the
+ * given object. Names that aren't present in {@code copyFrom} will be skipped.
+ * @param copyFrom the source
+ * @param names the property names
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject(JSONObject copyFrom, String[] names) throws JSONException {
+ this();
+ for (String name : names) {
+ Object value = copyFrom.opt(name);
+ if (value != null) {
+ this.nameValuePairs.put(name, value);
+ }
+ }
+ }
+
+ /**
+ * Returns the number of name/value mappings in this object.
+ * @return the number of name/value mappings in this object
+ */
+ public int length() {
+ return this.nameValuePairs.size();
+ }
+
+ /**
+ * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with
+ * the same name.
+ * @param name the name of the property
+ * @param value the value of the property
+ * @return this object.
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject put(String name, boolean value) throws JSONException {
+ this.nameValuePairs.put(checkName(name), value);
+ return this;
+ }
+
+ /**
+ * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with
+ * the same name.
+ * @param name the name of the property
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * @return this object.
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject put(String name, double value) throws JSONException {
+ this.nameValuePairs.put(checkName(name), JSON.checkDouble(value));
+ return this;
+ }
+
+ /**
+ * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with
+ * the same name.
+ * @param name the name of the property
+ * @param value the value of the property
+ * @return this object.
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject put(String name, int value) throws JSONException {
+ this.nameValuePairs.put(checkName(name), value);
+ return this;
+ }
+
+ /**
+ * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with
+ * the same name.
+ * @param name the name of the property
+ * @param value the value of the property
+ * @return this object.
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject put(String name, long value) throws JSONException {
+ this.nameValuePairs.put(checkName(name), value);
+ return this;
+ }
+
+ /**
+ * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with
+ * the same name. If the value is {@code null}, any existing mapping for {@code name}
+ * is removed.
+ * @param name the name of the property
+ * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer,
+ * Long, Double, {@link #NULL}, or {@code null}. May not be {@link Double#isNaN()
+ * NaNs} or {@link Double#isInfinite() infinities}.
+ * @return this object.
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject put(String name, Object value) throws JSONException {
+ if (value == null) {
+ this.nameValuePairs.remove(name);
+ return this;
+ }
+ if (value instanceof Number) {
+ // deviate from the original by checking all Numbers, not just floats &
+ // doubles
+ JSON.checkDouble(((Number) value).doubleValue());
+ }
+ this.nameValuePairs.put(checkName(name), value);
+ return this;
+ }
+
+ /**
+ * Equivalent to {@code put(name, value)} when both parameters are non-null; does
+ * nothing otherwise.
+ * @param name the name of the property
+ * @param value the value of the property
+ * @return this object.
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject putOpt(String name, Object value) throws JSONException {
+ if (name == null || value == null) {
+ return this;
+ }
+ return put(name, value);
+ }
+
+ /**
+ * Appends {@code value} to the array already mapped to {@code name}. If this object
+ * has no mapping for {@code name}, this inserts a new mapping. If the mapping exists
+ * but its value is not an array, the existing and new values are inserted in order
+ * into a new array which is itself mapped to {@code name}. In aggregate, this allows
+ * values to be added to a mapping one at a time.
+ * @param name the name of the property
+ * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer,
+ * Long, Double, {@link #NULL} or null. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * @return this object.
+ * @throws JSONException if an error occurs
+ */
+ public JSONObject accumulate(String name, Object value) throws JSONException {
+ Object current = this.nameValuePairs.get(checkName(name));
+ if (current == null) {
+ return put(name, value);
+ }
+
+ // check in accumulate, since array.put(Object) doesn't do any checking
+ if (value instanceof Number) {
+ JSON.checkDouble(((Number) value).doubleValue());
+ }
+
+ if (current instanceof JSONArray) {
+ JSONArray array = (JSONArray) current;
+ array.put(value);
+ }
+ else {
+ JSONArray array = new JSONArray();
+ array.put(current);
+ array.put(value);
+ this.nameValuePairs.put(name, array);
+ }
+ return this;
+ }
+
+ String checkName(String name) throws JSONException {
+ if (name == null) {
+ throw new JSONException("Names must be non-null");
+ }
+ return name;
+ }
+
+ /**
+ * Removes the named mapping if it exists; does nothing otherwise.
+ *
+ * @param name the name of the property
+ * @return the value previously mapped by {@code name}, or null if there was no such
+ * mapping.
+ */
+ public Object remove(String name) {
+ return this.nameValuePairs.remove(name);
+ }
+
+ /**
+ * Returns true if this object has no mapping for {@code name} or if it has a mapping
+ * whose value is {@link #NULL}.
+ * @param name the name of the property
+ * @return true if this object has no mapping for {@code name}
+ */
+ public boolean isNull(String name) {
+ Object value = this.nameValuePairs.get(name);
+ return value == null || value == NULL;
+ }
+
+ /**
+ * Returns true if this object has a mapping for {@code name}. The mapping may be
+ * {@link #NULL}.
+ * @param name the name of the property
+ * @return true if this object has a mapping for {@code name}
+ */
+ public boolean has(String name) {
+ return this.nameValuePairs.containsKey(name);
+ }
+
+ /**
+ * Returns the value mapped by {@code name}.
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if no such mapping exists.
+ */
+ public Object get(String name) throws JSONException {
+ Object result = this.nameValuePairs.get(name);
+ if (result == null) {
+ throw new JSONException("No value for " + name);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value mapped by {@code name}, or null if no such mapping exists.
+ * @param name the name of the property
+ * @return the value or {@code null}
+ */
+ public Object opt(String name) {
+ return this.nameValuePairs.get(name);
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a boolean or can be
+ * coerced to a boolean.
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if the mapping doesn't exist or cannot be coerced to a
+ * boolean.
+ */
+ public boolean getBoolean(String name) throws JSONException {
+ Object object = get(name);
+ Boolean result = JSON.toBoolean(object);
+ if (result == null) {
+ throw JSON.typeMismatch(name, object, "boolean");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a boolean or can be
+ * coerced to a boolean. Returns false otherwise.
+ * @param name the name of the property
+ * @return the value or {@code null}
+ */
+ public boolean optBoolean(String name) {
+ return optBoolean(name, false);
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a boolean or can be
+ * coerced to a boolean. Returns {@code fallback} otherwise.
+ * @param name the name of the property
+ * @param fallback a fallback value
+ * @return the value or {@code fallback}
+ */
+ public boolean optBoolean(String name, boolean fallback) {
+ Object object = opt(name);
+ Boolean result = JSON.toBoolean(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a double or can be
+ * coerced to a double.
+ *
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if the mapping doesn't exist or cannot be coerced to a
+ * double.
+ */
+ public double getDouble(String name) throws JSONException {
+ Object object = get(name);
+ Double result = JSON.toDouble(object);
+ if (result == null) {
+ throw JSON.typeMismatch(name, object, "double");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a double or can be
+ * coerced to a double. Returns {@code NaN} otherwise.
+ * @param name the name of the property
+ * @return the value or {@code NaN}
+ */
+ public double optDouble(String name) {
+ return optDouble(name, Double.NaN);
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a double or can be
+ * coerced to a double. Returns {@code fallback} otherwise.
+ * @param name the name of the property
+ * @param fallback a fallback value
+ * @return the value or {@code fallback}
+ */
+ public double optDouble(String name, double fallback) {
+ Object object = opt(name);
+ Double result = JSON.toDouble(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is an int or can be
+ * coerced to an int.
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if the mapping doesn't exist or cannot be coerced to an int.
+ */
+ public int getInt(String name) throws JSONException {
+ Object object = get(name);
+ Integer result = JSON.toInteger(object);
+ if (result == null) {
+ throw JSON.typeMismatch(name, object, "int");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is an int or can be
+ * coerced to an int. Returns 0 otherwise.
+ * @param name the name of the property
+ * @return the value of {@code 0}
+ */
+ public int optInt(String name) {
+ return optInt(name, 0);
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is an int or can be
+ * coerced to an int. Returns {@code fallback} otherwise.
+ * @param name the name of the property
+ * @param fallback a fallback value
+ * @return the value or {@code fallback}
+ */
+ public int optInt(String name, int fallback) {
+ Object object = opt(name);
+ Integer result = JSON.toInteger(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a long or can be
+ * coerced to a long. Note that JSON represents numbers as doubles, so this is
+ * lossy ; use strings to transfer numbers via JSON.
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if the mapping doesn't exist or cannot be coerced to a long.
+ */
+ public long getLong(String name) throws JSONException {
+ Object object = get(name);
+ Long result = JSON.toLong(object);
+ if (result == null) {
+ throw JSON.typeMismatch(name, object, "long");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a long or can be
+ * coerced to a long. Returns 0 otherwise. Note that JSON represents numbers as
+ * doubles, so this is lossy ; use strings to transfer numbers via
+ * JSON.
+ * @param name the name of the property
+ * @return the value or {@code 0L}
+ */
+ public long optLong(String name) {
+ return optLong(name, 0L);
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a long or can be
+ * coerced to a long. Returns {@code fallback} otherwise. Note that JSON represents
+ * numbers as doubles, so this is lossy ; use strings to transfer
+ * numbers via JSON.
+ * @param name the name of the property
+ * @param fallback a fallback value
+ * @return the value or {@code fallback}
+ */
+ public long optLong(String name, long fallback) {
+ Object object = opt(name);
+ Long result = JSON.toLong(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists, coercing it if necessary.
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if no such mapping exists.
+ */
+ public String getString(String name) throws JSONException {
+ Object object = get(name);
+ String result = JSON.toString(object);
+ if (result == null) {
+ throw JSON.typeMismatch(name, object, "String");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists, coercing it if necessary.
+ * Returns the empty string if no such mapping exists.
+ * @param name the name of the property
+ * @return the value or an empty string
+ */
+ public String optString(String name) {
+ return optString(name, "");
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists, coercing it if necessary.
+ * Returns {@code fallback} if no such mapping exists.
+ * @param name the name of the property
+ * @param fallback a fallback value
+ * @return the value or {@code fallback}
+ */
+ public String optString(String name, String fallback) {
+ Object object = opt(name);
+ String result = JSON.toString(object);
+ return result != null ? result : fallback;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a {@code
+ * JSONArray}.
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if the mapping doesn't exist or is not a {@code
+ * JSONArray}.
+ */
+ public JSONArray getJSONArray(String name) throws JSONException {
+ Object object = get(name);
+ if (object instanceof JSONArray) {
+ return (JSONArray) object;
+ }
+ else {
+ throw JSON.typeMismatch(name, object, "JSONArray");
+ }
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a {@code
+ * JSONArray}. Returns null otherwise.
+ * @param name the name of the property
+ * @return the value or {@code null}
+ */
+ public JSONArray optJSONArray(String name) {
+ Object object = opt(name);
+ return object instanceof JSONArray ? (JSONArray) object : null;
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a {@code
+ * JSONObject}.
+ * @param name the name of the property
+ * @return the value
+ * @throws JSONException if the mapping doesn't exist or is not a {@code
+ * JSONObject}.
+ */
+ public JSONObject getJSONObject(String name) throws JSONException {
+ Object object = get(name);
+ if (object instanceof JSONObject) {
+ return (JSONObject) object;
+ }
+ else {
+ throw JSON.typeMismatch(name, object, "JSONObject");
+ }
+ }
+
+ /**
+ * Returns the value mapped by {@code name} if it exists and is a {@code
+ * JSONObject}. Returns null otherwise.
+ * @param name the name of the property
+ * @return the value or {@code null}
+ */
+ public JSONObject optJSONObject(String name) {
+ Object object = opt(name);
+ return object instanceof JSONObject ? (JSONObject) object : null;
+ }
+
+ /**
+ * Returns an array with the values corresponding to {@code names}. The array contains
+ * null for names that aren't mapped. This method returns null if {@code names} is
+ * either null or empty.
+ * @param names the names of the properties
+ * @return the array
+ */
+ public JSONArray toJSONArray(JSONArray names) {
+ JSONArray result = new JSONArray();
+ if (names == null) {
+ return null;
+ }
+ int length = names.length();
+ if (length == 0) {
+ return null;
+ }
+ for (int i = 0; i < length; i++) {
+ String name = JSON.toString(names.opt(i));
+ result.put(opt(name));
+ }
+ return result;
+ }
+
+ /**
+ * Returns an iterator of the {@code String} names in this object. The returned
+ * iterator supports {@link Iterator#remove() remove}, which will remove the
+ * corresponding mapping from this object. If this object is modified after the
+ * iterator is returned, the iterator's behavior is undefined. The order of the keys
+ * is undefined.
+ * @return the keys
+ */
+ /* Return a raw type for API compatibility */
+ @SuppressWarnings("rawtypes")
+ public Iterator keys() {
+ return this.nameValuePairs.keySet().iterator();
+ }
+
+ /**
+ * Returns an array containing the string names in this object. This method returns
+ * null if this object contains no mappings.
+ * @return the array
+ */
+ public JSONArray names() {
+ return this.nameValuePairs.isEmpty() ? null
+ : new JSONArray(new ArrayList<>(this.nameValuePairs.keySet()));
+ }
+
+ /**
+ * Encodes this object as a compact JSON string, such as:
+ * {"query":"Pizza","locations":[94043,90210]}
+ * @return a string representation of the object.
+ */
+ @Override
+ public String toString() {
+ try {
+ JSONStringer stringer = new JSONStringer();
+ writeTo(stringer);
+ return stringer.toString();
+ }
+ catch (JSONException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Encodes this object as a human readable JSON string for debugging, such as:
+ * {
+ * "query": "Pizza",
+ * "locations": [
+ * 94043,
+ * 90210
+ * ]
+ * }
+ * @param indentSpaces the number of spaces to indent for each level of nesting.
+ * @return a string representation of the object.
+ * @throws JSONException if an error occurs
+ */
+ public String toString(int indentSpaces) throws JSONException {
+ JSONStringer stringer = new JSONStringer(indentSpaces);
+ writeTo(stringer);
+ return stringer.toString();
+ }
+
+ void writeTo(JSONStringer stringer) throws JSONException {
+ stringer.object();
+ for (Map.Entry entry : this.nameValuePairs.entrySet()) {
+ stringer.key(entry.getKey()).value(entry.getValue());
+ }
+ stringer.endObject();
+ }
+
+ /**
+ * Encodes the number as a JSON string.
+ * @param number a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * @return the encoded value
+ * @throws JSONException if an error occurs
+ */
+ public static String numberToString(Number number) throws JSONException {
+ if (number == null) {
+ throw new JSONException("Number must be non-null");
+ }
+
+ double doubleValue = number.doubleValue();
+ JSON.checkDouble(doubleValue);
+
+ // the original returns "-0" instead of "-0.0" for negative zero
+ if (number.equals(NEGATIVE_ZERO)) {
+ return "-0";
+ }
+
+ long longValue = number.longValue();
+ if (doubleValue == longValue) {
+ return Long.toString(longValue);
+ }
+
+ return number.toString();
+ }
+
+ /**
+ * Encodes {@code data} as a JSON string. This applies quotes and any necessary
+ * character escaping.
+ * @param data the string to encode. Null will be interpreted as an empty string.
+ * @return the quoted value
+ */
+ public static String quote(String data) {
+ if (data == null) {
+ return "\"\"";
+ }
+ try {
+ JSONStringer stringer = new JSONStringer();
+ stringer.open(JSONStringer.Scope.NULL, "");
+ stringer.value(data);
+ stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, "");
+ return stringer.toString();
+ }
+ catch (JSONException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Wraps the given object if necessary.
+ *
+ * If the object is null or , returns {@link #NULL}. If the object is a
+ * {@code JSONArray} or {@code JSONObject}, no wrapping is necessary. If the object is
+ * {@code NULL}, no wrapping is necessary. If the object is an array or
+ * {@code Collection}, returns an equivalent {@code JSONArray}. If the object is a
+ * {@code Map}, returns an equivalent {@code JSONObject}. If the object is a primitive
+ * wrapper type or {@code String}, returns the object. Otherwise if the object is from
+ * a {@code java} package, returns the result of {@code toString}. If wrapping fails,
+ * returns null.
+ * @param o the object to wrap
+ * @return the wrapped object
+ */
+ @SuppressWarnings("rawtypes")
+ public static Object wrap(Object o) {
+ if (o == null) {
+ return NULL;
+ }
+ if (o instanceof JSONArray || o instanceof JSONObject) {
+ return o;
+ }
+ if (o.equals(NULL)) {
+ return o;
+ }
+ try {
+ if (o instanceof Collection) {
+ return new JSONArray((Collection) o);
+ }
+ else if (o.getClass().isArray()) {
+ return new JSONArray(o);
+ }
+ if (o instanceof Map) {
+ return new JSONObject((Map) o);
+ }
+ if (o instanceof Boolean || o instanceof Byte || o instanceof Character
+ || o instanceof Double || o instanceof Float || o instanceof Integer
+ || o instanceof Long || o instanceof Short || o instanceof String) {
+ return o;
+ }
+ if (o.getClass().getPackage().getName().startsWith("java.")) {
+ return o.toString();
+ }
+ }
+ catch (Exception ignored) {
+ }
+ return null;
+ }
+
+}
diff --git a/src/json-shade/java/org/springframework/graal/json/JSONStringer.java b/src/json-shade/java/org/springframework/graal/json/JSONStringer.java
new file mode 100644
index 000000000..3f6d98156
--- /dev/null
+++ b/src/json-shade/java/org/springframework/graal/json/JSONStringer.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2010 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 org.springframework.graal.json;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+// Note: this class was written without inspecting the non-free org.json source code.
+
+/**
+ * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most application
+ * developers should use those methods directly and disregard this API. For example:
+ * JSONObject object = ...
+ * String json = object.toString();
+ *
+ * Stringers only encode well-formed JSON strings. In particular:
+ *
+ * The stringer must have exactly one top-level array or object.
+ * Lexical scopes must be balanced: every call to {@link #array} must have a matching
+ * call to {@link #endArray} and every call to {@link #object} must have a matching call
+ * to {@link #endObject}.
+ * Arrays may not contain keys (property names).
+ * Objects must alternate keys (property names) and values.
+ * Values are inserted with either literal {@link #value(Object) value} calls, or by
+ * nesting arrays or objects.
+ *
+ * Calls that would result in a malformed JSON string will fail with a
+ * {@link JSONException}.
+ *
+ * This class provides no facility for pretty-printing (ie. indenting) output. To encode
+ * indented output, use {@link JSONObject#toString(int)} or
+ * {@link JSONArray#toString(int)}.
+ *
+ * Some implementations of the API support at most 20 levels of nesting. Attempts to
+ * create more than 20 levels of nesting may fail with a {@link JSONException}.
+ *
+ * Each stringer may be used to encode a single top level value. Instances of this class
+ * are not thread safe. Although this class is nonfinal, it was not designed for
+ * inheritance and should not be subclassed. In particular, self-use by overrideable
+ * methods is not specified. See Effective Java Item 17, "Design and Document or
+ * inheritance or else prohibit it" for further information.
+ */
+public class JSONStringer {
+
+ /**
+ * The output data, containing at most one top-level array or object.
+ */
+ final StringBuilder out = new StringBuilder();
+
+ /**
+ * Lexical scoping elements within this stringer, necessary to insert the appropriate
+ * separator characters (ie. commas and colons) and to detect nesting errors.
+ */
+ enum Scope {
+
+ /**
+ * An array with no elements requires no separators or newlines before it is
+ * closed.
+ */
+ EMPTY_ARRAY,
+
+ /**
+ * An array with at least one value requires a comma and newline before the next
+ * element.
+ */
+ NONEMPTY_ARRAY,
+
+ /**
+ * An object with no keys or values requires no separators or newlines before it
+ * is closed.
+ */
+ EMPTY_OBJECT,
+
+ /**
+ * An object whose most recent element is a key. The next element must be a value.
+ */
+ DANGLING_KEY,
+
+ /**
+ * An object with at least one name/value pair requires a comma and newline before
+ * the next element.
+ */
+ NONEMPTY_OBJECT,
+
+ /**
+ * A special bracketless array needed by JSONStringer.join() and
+ * JSONObject.quote() only. Not used for JSON encoding.
+ */
+ NULL
+
+ }
+
+ /**
+ * Unlike the original implementation, this stack isn't limited to 20 levels of
+ * nesting.
+ */
+ private final List stack = new ArrayList<>();
+
+ /**
+ * A string containing a full set of spaces for a single level of indentation, or null
+ * for no pretty printing.
+ */
+ private final String indent;
+
+ public JSONStringer() {
+ this.indent = null;
+ }
+
+ JSONStringer(int indentSpaces) {
+ char[] indentChars = new char[indentSpaces];
+ Arrays.fill(indentChars, ' ');
+ this.indent = new String(indentChars);
+ }
+
+ /**
+ * Begins encoding a new array. Each call to this method must be paired with a call to
+ * {@link #endArray}.
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer array() throws JSONException {
+ return open(Scope.EMPTY_ARRAY, "[");
+ }
+
+ /**
+ * Ends encoding the current array.
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer endArray() throws JSONException {
+ return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]");
+ }
+
+ /**
+ * Begins encoding a new object. Each call to this method must be paired with a call
+ * to {@link #endObject}.
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer object() throws JSONException {
+ return open(Scope.EMPTY_OBJECT, "{");
+ }
+
+ /**
+ * Ends encoding the current object.
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer endObject() throws JSONException {
+ return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}");
+ }
+
+ /**
+ * Enters a new scope by appending any necessary whitespace and the given bracket.
+ * @param empty any necessary whitespace
+ * @param openBracket the open bracket
+ * @return this object
+ * @throws JSONException if processing of json failed
+ */
+ JSONStringer open(Scope empty, String openBracket) throws JSONException {
+ if (this.stack.isEmpty() && this.out.length() > 0) {
+ throw new JSONException("Nesting problem: multiple top-level roots");
+ }
+ beforeValue();
+ this.stack.add(empty);
+ this.out.append(openBracket);
+ return this;
+ }
+
+ /**
+ * Closes the current scope by appending any necessary whitespace and the given
+ * bracket.
+ * @param empty any necessary whitespace
+ * @param nonempty the current scope
+ * @param closeBracket the close bracket
+ * @return the JSON stringer
+ * @throws JSONException if processing of json failed
+ */
+ JSONStringer close(Scope empty, Scope nonempty, String closeBracket)
+ throws JSONException {
+ Scope context = peek();
+ if (context != nonempty && context != empty) {
+ throw new JSONException("Nesting problem");
+ }
+
+ this.stack.remove(this.stack.size() - 1);
+ if (context == nonempty) {
+ newline();
+ }
+ this.out.append(closeBracket);
+ return this;
+ }
+
+ /**
+ * Returns the value on the top of the stack.
+ * @return the scope
+ * @throws JSONException if processing of json failed
+ */
+ private Scope peek() throws JSONException {
+ if (this.stack.isEmpty()) {
+ throw new JSONException("Nesting problem");
+ }
+ return this.stack.get(this.stack.size() - 1);
+ }
+
+ /**
+ * Replace the value on the top of the stack with the given value.
+ * @param topOfStack the scope at the top of the stack
+ */
+ private void replaceTop(Scope topOfStack) {
+ this.stack.set(this.stack.size() - 1, topOfStack);
+ }
+
+ /**
+ * Encodes {@code value}.
+ * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer,
+ * Long, Double or null. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer value(Object value) throws JSONException {
+ if (this.stack.isEmpty()) {
+ throw new JSONException("Nesting problem");
+ }
+
+ if (value instanceof JSONArray) {
+ ((JSONArray) value).writeTo(this);
+ return this;
+
+ }
+ else if (value instanceof JSONObject) {
+ ((JSONObject) value).writeTo(this);
+ return this;
+ }
+
+ beforeValue();
+
+ if (value == null || value instanceof Boolean || value == JSONObject.NULL) {
+ this.out.append(value);
+
+ }
+ else if (value instanceof Number) {
+ this.out.append(JSONObject.numberToString((Number) value));
+
+ }
+ else {
+ string(value.toString());
+ }
+
+ return this;
+ }
+
+ /**
+ * Encodes {@code value} to this stringer.
+ * @param value the value to encode
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer value(boolean value) throws JSONException {
+ if (this.stack.isEmpty()) {
+ throw new JSONException("Nesting problem");
+ }
+ beforeValue();
+ this.out.append(value);
+ return this;
+ }
+
+ /**
+ * Encodes {@code value} to this stringer.
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities}.
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer value(double value) throws JSONException {
+ if (this.stack.isEmpty()) {
+ throw new JSONException("Nesting problem");
+ }
+ beforeValue();
+ this.out.append(JSONObject.numberToString(value));
+ return this;
+ }
+
+ /**
+ * Encodes {@code value} to this stringer.
+ * @param value the value to encode
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer value(long value) throws JSONException {
+ if (this.stack.isEmpty()) {
+ throw new JSONException("Nesting problem");
+ }
+ beforeValue();
+ this.out.append(value);
+ return this;
+ }
+
+ private void string(String value) {
+ this.out.append("\"");
+ for (int i = 0, length = value.length(); i < length; i++) {
+ char c = value.charAt(i);
+
+ /*
+ * From RFC 4627, "All Unicode characters may be placed within the quotation
+ * marks except for the characters that must be escaped: quotation mark,
+ * reverse solidus, and the control characters (U+0000 through U+001F)."
+ */
+ switch (c) {
+ case '"':
+ case '\\':
+ case '/':
+ this.out.append('\\').append(c);
+ break;
+
+ case '\t':
+ this.out.append("\\t");
+ break;
+
+ case '\b':
+ this.out.append("\\b");
+ break;
+
+ case '\n':
+ this.out.append("\\n");
+ break;
+
+ case '\r':
+ this.out.append("\\r");
+ break;
+
+ case '\f':
+ this.out.append("\\f");
+ break;
+
+ default:
+ if (c <= 0x1F) {
+ this.out.append(String.format("\\u%04x", (int) c));
+ }
+ else {
+ this.out.append(c);
+ }
+ break;
+ }
+
+ }
+ this.out.append("\"");
+ }
+
+ private void newline() {
+ if (this.indent == null) {
+ return;
+ }
+
+ this.out.append("\n");
+ for (int i = 0; i < this.stack.size(); i++) {
+ this.out.append(this.indent);
+ }
+ }
+
+ /**
+ * Encodes the key (property name) to this stringer.
+ * @param name the name of the forthcoming value. May not be null.
+ * @return this stringer.
+ * @throws JSONException if processing of json failed
+ */
+ public JSONStringer key(String name) throws JSONException {
+ if (name == null) {
+ throw new JSONException("Names must be non-null");
+ }
+ beforeKey();
+ string(name);
+ return this;
+ }
+
+ /**
+ * Inserts any necessary separators and whitespace before a name. Also adjusts the
+ * stack to expect the key's value.
+ * @throws JSONException if processing of json failed
+ */
+ private void beforeKey() throws JSONException {
+ Scope context = peek();
+ if (context == Scope.NONEMPTY_OBJECT) { // first in object
+ this.out.append(',');
+ }
+ else if (context != Scope.EMPTY_OBJECT) { // not in an object!
+ throw new JSONException("Nesting problem");
+ }
+ newline();
+ replaceTop(Scope.DANGLING_KEY);
+ }
+
+ /**
+ * Inserts any necessary separators and whitespace before a literal value, inline
+ * array, or inline object. Also adjusts the stack to expect either a closing bracket
+ * or another element.
+ * @throws JSONException if processing of json failed
+ */
+ private void beforeValue() throws JSONException {
+ if (this.stack.isEmpty()) {
+ return;
+ }
+
+ Scope context = peek();
+ if (context == Scope.EMPTY_ARRAY) { // first in array
+ replaceTop(Scope.NONEMPTY_ARRAY);
+ newline();
+ }
+ else if (context == Scope.NONEMPTY_ARRAY) { // another in array
+ this.out.append(',');
+ newline();
+ }
+ else if (context == Scope.DANGLING_KEY) { // value for key
+ this.out.append(this.indent == null ? ":" : ": ");
+ replaceTop(Scope.NONEMPTY_OBJECT);
+ }
+ else if (context != Scope.NULL) {
+ throw new JSONException("Nesting problem");
+ }
+ }
+
+ /**
+ * Returns the encoded JSON string.
+ *
+ * If invoked with unterminated arrays or unclosed objects, this method's return value
+ * is undefined.
+ *
+ * Warning: although it contradicts the general contract of
+ * {@link Object#toString}, this method returns null if the stringer contains no data.
+ * @return the encoded JSON string.
+ */
+ @Override
+ public String toString() {
+ return this.out.length() == 0 ? null : this.out.toString();
+ }
+
+}
diff --git a/src/json-shade/java/org/springframework/graal/json/JSONTokener.java b/src/json-shade/java/org/springframework/graal/json/JSONTokener.java
new file mode 100644
index 000000000..64f20f632
--- /dev/null
+++ b/src/json-shade/java/org/springframework/graal/json/JSONTokener.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2010 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 org.springframework.graal.json;
+
+// Note: this class was written without inspecting the non-free org.json source code.
+
+/**
+ * Parses a JSON (RFC 4627 ) encoded
+ * string into the corresponding object. Most clients of this class will use only need the
+ * {@link #JSONTokener(String) constructor} and {@link #nextValue} method. Example usage:
+ *
+ * String json = "{"
+ * + " \"query\": \"Pizza\", "
+ * + " \"locations\": [ 94043, 90210 ] "
+ * + "}";
+ *
+ * JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
+ * String query = object.getString("query");
+ * JSONArray locations = object.getJSONArray("locations");
+ *
+ * For best interoperability and performance use JSON that complies with RFC 4627, such as
+ * that generated by {@link JSONStringer}. For legacy reasons this parser is lenient, so a
+ * successful parse does not indicate that the input string was valid JSON. All of the
+ * following syntax errors will be ignored:
+ *
+ * End of line comments starting with {@code //} or {@code #} and ending with a
+ * newline character.
+ * C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such
+ * comments may not be nested.
+ * Strings that are unquoted or {@code 'single quoted'}.
+ * Hexadecimal integers prefixed with {@code 0x} or {@code 0X}.
+ * Octal integers prefixed with {@code 0}.
+ * Array elements separated by {@code ;}.
+ * Unnecessary array separators. These are interpreted as if null was the omitted
+ * value.
+ * Key-value pairs separated by {@code =} or {@code =>}.
+ * Key-value pairs separated by {@code ;}.
+ *
+ *
+ * Each tokener may be used to parse a single JSON string. Instances of this class are not
+ * thread safe. Although this class is nonfinal, it was not designed for inheritance and
+ * should not be subclassed. In particular, self-use by overrideable methods is not
+ * specified. See Effective Java Item 17, "Design and Document or inheritance or
+ * else prohibit it" for further information.
+ */
+public class JSONTokener {
+
+ /**
+ * The input JSON.
+ */
+ private final String in;
+
+ /**
+ * The index of the next character to be returned by {@link #next}. When the input is
+ * exhausted, this equals the input's length.
+ */
+ private int pos;
+
+ /**
+ * @param in JSON encoded string. Null is not permitted and will yield a tokener that
+ * throws {@code NullPointerExceptions} when methods are called.
+ */
+ public JSONTokener(String in) {
+ // consume an optional byte order mark (BOM) if it exists
+ if (in != null && in.startsWith("\ufeff")) {
+ in = in.substring(1);
+ }
+ this.in = in;
+ }
+
+ /**
+ * Returns the next value from the input.
+ * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long,
+ * Double or {@link JSONObject#NULL}.
+ * @throws JSONException if the input is malformed.
+ */
+ public Object nextValue() throws JSONException {
+ int c = nextCleanInternal();
+ switch (c) {
+ case -1:
+ throw syntaxError("End of input");
+
+ case '{':
+ return readObject();
+
+ case '[':
+ return readArray();
+
+ case '\'':
+ case '"':
+ return nextString((char) c);
+
+ default:
+ this.pos--;
+ return readLiteral();
+ }
+ }
+
+ private int nextCleanInternal() throws JSONException {
+ while (this.pos < this.in.length()) {
+ int c = this.in.charAt(this.pos++);
+ switch (c) {
+ case '\t':
+ case ' ':
+ case '\n':
+ case '\r':
+ continue;
+
+ case '/':
+ if (this.pos == this.in.length()) {
+ return c;
+ }
+
+ char peek = this.in.charAt(this.pos);
+ switch (peek) {
+ case '*':
+ // skip a /* c-style comment */
+ this.pos++;
+ int commentEnd = this.in.indexOf("*/", this.pos);
+ if (commentEnd == -1) {
+ throw syntaxError("Unterminated comment");
+ }
+ this.pos = commentEnd + 2;
+ continue;
+
+ case '/':
+ // skip a // end-of-line comment
+ this.pos++;
+ skipToEndOfLine();
+ continue;
+
+ default:
+ return c;
+ }
+
+ case '#':
+ /*
+ * Skip a # hash end-of-line comment. The JSON RFC doesn't specify this
+ * behavior, but it's required to parse existing documents. See
+ * http://b/2571423.
+ */
+ skipToEndOfLine();
+ continue;
+
+ default:
+ return c;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Advances the position until after the next newline character. If the line is
+ * terminated by "\r\n", the '\n' must be consumed as whitespace by the caller.
+ */
+ private void skipToEndOfLine() {
+ for (; this.pos < this.in.length(); this.pos++) {
+ char c = this.in.charAt(this.pos);
+ if (c == '\r' || c == '\n') {
+ this.pos++;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns the string up to but not including {@code quote}, unescaping any character
+ * escape sequences encountered along the way. The opening quote should have already
+ * been read. This consumes the closing quote, but does not include it in the returned
+ * string.
+ * @param quote either ' or ".
+ * @return the string up to but not including {@code quote}
+ * @throws NumberFormatException if any unicode escape sequences are malformed.
+ * @throws JSONException if processing of json failed
+ */
+ public String nextString(char quote) throws JSONException {
+ /*
+ * For strings that are free of escape sequences, we can just extract the result
+ * as a substring of the input. But if we encounter an escape sequence, we need to
+ * use a StringBuilder to compose the result.
+ */
+ StringBuilder builder = null;
+
+ /* the index of the first character not yet appended to the builder. */
+ int start = this.pos;
+
+ while (this.pos < this.in.length()) {
+ int c = this.in.charAt(this.pos++);
+ if (c == quote) {
+ if (builder == null) {
+ // a new string avoids leaking memory
+ return new String(this.in.substring(start, this.pos - 1));
+ }
+ else {
+ builder.append(this.in, start, this.pos - 1);
+ return builder.toString();
+ }
+ }
+
+ if (c == '\\') {
+ if (this.pos == this.in.length()) {
+ throw syntaxError("Unterminated escape sequence");
+ }
+ if (builder == null) {
+ builder = new StringBuilder();
+ }
+ builder.append(this.in, start, this.pos - 1);
+ builder.append(readEscapeCharacter());
+ start = this.pos;
+ }
+ }
+
+ throw syntaxError("Unterminated string");
+ }
+
+ /**
+ * Unescapes the character identified by the character or characters that immediately
+ * follow a backslash. The backslash '\' should have already been read. This supports
+ * both unicode escapes "u000A" and two-character escapes "\n".
+ * @return the unescaped char
+ * @throws NumberFormatException if any unicode escape sequences are malformed.
+ * @throws JSONException if processing of json failed
+ */
+ private char readEscapeCharacter() throws JSONException {
+ char escaped = this.in.charAt(this.pos++);
+ switch (escaped) {
+ case 'u':
+ if (this.pos + 4 > this.in.length()) {
+ throw syntaxError("Unterminated escape sequence");
+ }
+ String hex = this.in.substring(this.pos, this.pos + 4);
+ this.pos += 4;
+ return (char) Integer.parseInt(hex, 16);
+
+ case 't':
+ return '\t';
+
+ case 'b':
+ return '\b';
+
+ case 'n':
+ return '\n';
+
+ case 'r':
+ return '\r';
+
+ case 'f':
+ return '\f';
+
+ case '\'':
+ case '"':
+ case '\\':
+ default:
+ return escaped;
+ }
+ }
+
+ /**
+ * Reads a null, boolean, numeric or unquoted string literal value. Numeric values
+ * will be returned as an Integer, Long, or Double, in that order of preference.
+ * @return a literal value
+ * @throws JSONException if processing of json failed
+ */
+ private Object readLiteral() throws JSONException {
+ String literal = nextToInternal("{}[]/\\:,=;# \t\f");
+
+ if (literal.isEmpty()) {
+ throw syntaxError("Expected literal value");
+ }
+ else if ("null".equalsIgnoreCase(literal)) {
+ return JSONObject.NULL;
+ }
+ else if ("true".equalsIgnoreCase(literal)) {
+ return Boolean.TRUE;
+ }
+ else if ("false".equalsIgnoreCase(literal)) {
+ return Boolean.FALSE;
+ }
+
+ /* try to parse as an integral type... */
+ if (literal.indexOf('.') == -1) {
+ int base = 10;
+ String number = literal;
+ if (number.startsWith("0x") || number.startsWith("0X")) {
+ number = number.substring(2);
+ base = 16;
+ }
+ else if (number.startsWith("0") && number.length() > 1) {
+ number = number.substring(1);
+ base = 8;
+ }
+ try {
+ long longValue = Long.parseLong(number, base);
+ if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) {
+ return (int) longValue;
+ }
+ else {
+ return longValue;
+ }
+ }
+ catch (NumberFormatException e) {
+ /*
+ * This only happens for integral numbers greater than Long.MAX_VALUE,
+ * numbers in exponential form (5e-10) and unquoted strings. Fall through
+ * to try floating point.
+ */
+ }
+ }
+
+ /* ...next try to parse as a floating point... */
+ try {
+ return Double.valueOf(literal);
+ }
+ catch (NumberFormatException ignored) {
+ }
+
+ /* ... finally give up. We have an unquoted string */
+ return new String(literal); // a new string avoids leaking memory
+ }
+
+ /**
+ * Returns the string up to but not including any of the given characters or a newline
+ * character. This does not consume the excluded character.
+ * @return the string up to but not including any of the given characters or a newline
+ * character
+ */
+ private String nextToInternal(String excluded) {
+ int start = this.pos;
+ for (; this.pos < this.in.length(); this.pos++) {
+ char c = this.in.charAt(this.pos);
+ if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) {
+ return this.in.substring(start, this.pos);
+ }
+ }
+ return this.in.substring(start);
+ }
+
+ /**
+ * Reads a sequence of key/value pairs and the trailing closing brace '}' of an
+ * object. The opening brace '{' should have already been read.
+ * @return an object
+ * @throws JSONException if processing of json failed
+ */
+ private JSONObject readObject() throws JSONException {
+ JSONObject result = new JSONObject();
+
+ /* Peek to see if this is the empty object. */
+ int first = nextCleanInternal();
+ if (first == '}') {
+ return result;
+ }
+ else if (first != -1) {
+ this.pos--;
+ }
+
+ while (true) {
+ Object name = nextValue();
+ if (!(name instanceof String)) {
+ if (name == null) {
+ throw syntaxError("Names cannot be null");
+ }
+ else {
+ throw syntaxError("Names must be strings, but " + name
+ + " is of type " + name.getClass().getName());
+ }
+ }
+
+ /*
+ * Expect the name/value separator to be either a colon ':', an equals sign
+ * '=', or an arrow "=>". The last two are bogus but we include them because
+ * that's what the original implementation did.
+ */
+ int separator = nextCleanInternal();
+ if (separator != ':' && separator != '=') {
+ throw syntaxError("Expected ':' after " + name);
+ }
+ if (this.pos < this.in.length() && this.in.charAt(this.pos) == '>') {
+ this.pos++;
+ }
+
+ result.put((String) name, nextValue());
+
+ switch (nextCleanInternal()) {
+ case '}':
+ return result;
+ case ';':
+ case ',':
+ continue;
+ default:
+ throw syntaxError("Unterminated object");
+ }
+ }
+ }
+
+ /**
+ * Reads a sequence of values and the trailing closing brace ']' of an array. The
+ * opening brace '[' should have already been read. Note that "[]" yields an empty
+ * array, but "[,]" returns a two-element array equivalent to "[null,null]".
+ * @return an array
+ * @throws JSONException if processing of json failed
+ */
+ private JSONArray readArray() throws JSONException {
+ JSONArray result = new JSONArray();
+
+ /* to cover input that ends with ",]". */
+ boolean hasTrailingSeparator = false;
+
+ while (true) {
+ switch (nextCleanInternal()) {
+ case -1:
+ throw syntaxError("Unterminated array");
+ case ']':
+ if (hasTrailingSeparator) {
+ result.put(null);
+ }
+ return result;
+ case ',':
+ case ';':
+ /* A separator without a value first means "null". */
+ result.put(null);
+ hasTrailingSeparator = true;
+ continue;
+ default:
+ this.pos--;
+ }
+
+ result.put(nextValue());
+
+ switch (nextCleanInternal()) {
+ case ']':
+ return result;
+ case ',':
+ case ';':
+ hasTrailingSeparator = true;
+ continue;
+ default:
+ throw syntaxError("Unterminated array");
+ }
+ }
+ }
+
+ /**
+ * Returns an exception containing the given message plus the current position and the
+ * entire input string.
+ * @param message the message
+ * @return an exception
+ */
+ public JSONException syntaxError(String message) {
+ return new JSONException(message + this);
+ }
+
+ /**
+ * Returns the current position and the entire input string.
+ * @return the current position and the entire input string.
+ */
+ @Override
+ public String toString() {
+ // consistent with the original implementation
+ return " at character " + this.pos + " of " + this.in;
+ }
+
+ /*
+ * Legacy APIs.
+ *
+ * None of the methods below are on the critical path of parsing JSON documents. They
+ * exist only because they were exposed by the original implementation and may be used
+ * by some clients.
+ */
+
+ public boolean more() {
+ return this.pos < this.in.length();
+ }
+
+ public char next() {
+ return this.pos < this.in.length() ? this.in.charAt(this.pos++) : '\0';
+ }
+
+ public char next(char c) throws JSONException {
+ char result = next();
+ if (result != c) {
+ throw syntaxError("Expected " + c + " but was " + result);
+ }
+ return result;
+ }
+
+ public char nextClean() throws JSONException {
+ int nextCleanInt = nextCleanInternal();
+ return nextCleanInt == -1 ? '\0' : (char) nextCleanInt;
+ }
+
+ public String next(int length) throws JSONException {
+ if (this.pos + length > this.in.length()) {
+ throw syntaxError(length + " is out of bounds");
+ }
+ String result = this.in.substring(this.pos, this.pos + length);
+ this.pos += length;
+ return result;
+ }
+
+ public String nextTo(String excluded) {
+ if (excluded == null) {
+ throw new NullPointerException("excluded == null");
+ }
+ return nextToInternal(excluded).trim();
+ }
+
+ public String nextTo(char excluded) {
+ return nextToInternal(String.valueOf(excluded)).trim();
+ }
+
+ public void skipPast(String thru) {
+ int thruStart = this.in.indexOf(thru, this.pos);
+ this.pos = thruStart == -1 ? this.in.length() : (thruStart + thru.length());
+ }
+
+ public char skipTo(char to) {
+ int index = this.in.indexOf(to, this.pos);
+ if (index != -1) {
+ this.pos = index;
+ return to;
+ }
+ else {
+ return '\0';
+ }
+ }
+
+ public void back() {
+ if (--this.pos == -1) {
+ this.pos = 0;
+ }
+ }
+
+ public static int dehexchar(char hex) {
+ if (hex >= '0' && hex <= '9') {
+ return hex - '0';
+ }
+ else if (hex >= 'A' && hex <= 'F') {
+ return hex - 'A' + 10;
+ }
+ else if (hex >= 'a' && hex <= 'f') {
+ return hex - 'a' + 10;
+ }
+ else {
+ return -1;
+ }
+ }
+
+}
diff --git a/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java b/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java
new file mode 100644
index 000000000..be97bad53
--- /dev/null
+++ b/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.CleanerJava6",onlyWith= NettyIsAround.class)
+final class CleanerJava6Substitution {
+ private CleanerJava6Substitution() {
+ }
+
+ @Alias
+ @RecomputeFieldValue(
+ kind = RecomputeFieldValue.Kind.FieldOffset,
+ declClassName = "java.nio.DirectByteBuffer",
+ name = "cleaner")
+ private static long CLEANER_FIELD_OFFSET;
+}
diff --git a/src/main/java/io/netty/util/internal/svm/NettyIsAround.java b/src/main/java/io/netty/util/internal/svm/NettyIsAround.java
new file mode 100644
index 000000000..002aec085
--- /dev/null
+++ b/src/main/java/io/netty/util/internal/svm/NettyIsAround.java
@@ -0,0 +1,17 @@
+package io.netty.util.internal.svm;
+
+import java.util.function.BooleanSupplier;
+
+public class NettyIsAround implements BooleanSupplier {
+
+ @Override
+ public boolean getAsBoolean() {
+ try {
+ Class.forName("io.netty.util.internal.CleanerJava6");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+}
diff --git a/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java b/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java
new file mode 100644
index 000000000..d4156e2a8
--- /dev/null
+++ b/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.PlatformDependent0",onlyWith=NettyIsAround.class)
+final class PlatformDependent0Substitution {
+ private PlatformDependent0Substitution() {
+ }
+
+ @Alias
+ @RecomputeFieldValue(
+ kind = RecomputeFieldValue.Kind.FieldOffset,
+ declClassName = "java.nio.Buffer",
+ name = "address")
+ private static long ADDRESS_FIELD_OFFSET;
+}
diff --git a/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java b/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java
new file mode 100644
index 000000000..df49877db
--- /dev/null
+++ b/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.PlatformDependent",onlyWith=NettyIsAround.class)
+final class PlatformDependentSubstitution {
+ private PlatformDependentSubstitution() {
+ }
+
+ /**
+ * The class PlatformDependent caches the byte array base offset by reading the
+ * field from PlatformDependent0. The automatic recomputation of Substrate VM
+ * correctly recomputes the field in PlatformDependent0, but since the caching
+ * in PlatformDependent happens during image building, the non-recomputed value
+ * is cached.
+ */
+ @Alias
+ @RecomputeFieldValue(
+ kind = RecomputeFieldValue.Kind.ArrayBaseOffset,
+ declClass = byte[].class)
+ private static long BYTE_ARRAY_BASE_OFFSET;
+}
diff --git a/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java b/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java
new file mode 100644
index 000000000..f14bd3276
--- /dev/null
+++ b/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess", onlyWith=NettyIsAround.class)
+final class UnsafeRefArrayAccessSubstitution {
+ private UnsafeRefArrayAccessSubstitution() {
+ }
+
+ @Alias
+ @RecomputeFieldValue(
+ kind = RecomputeFieldValue.Kind.ArrayIndexShift,
+ declClass = Object[].class)
+ public static int REF_ELEMENT_SHIFT;
+}
diff --git a/src/main/java/io/netty/util/internal/svm/package-info.java b/src/main/java/io/netty/util/internal/svm/package-info.java
new file mode 100644
index 000000000..e9b82ec03
--- /dev/null
+++ b/src/main/java/io/netty/util/internal/svm/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2019 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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.
+ */
+
+/**
+ * SVM substitutions for classes that will cause trouble while compiling
+ * into native image.
+ */
+package io.netty.util.internal.svm;
diff --git a/src/main/java/org/springframework/dao/DataAccessException.java b/src/main/java/org/springframework/dao/DataAccessException.java
new file mode 100644
index 000000000..187ec4470
--- /dev/null
+++ b/src/main/java/org/springframework/dao/DataAccessException.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * 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 org.springframework.dao;
+
+import org.springframework.core.NestedRuntimeException;
+import org.springframework.lang.Nullable;
+
+/**
+ * Root of the hierarchy of data access exceptions discussed in
+ * Expert One-On-One J2EE Design and Development .
+ * Please see Chapter 9 of this book for detailed discussion of the
+ * motivation for this package.
+ *
+ *
This exception hierarchy aims to let user code find and handle the
+ * kind of error encountered without knowing the details of the particular
+ * data access API in use (e.g. JDBC). Thus it is possible to react to an
+ * optimistic locking failure without knowing that JDBC is being used.
+ *
+ *
As this class is a runtime exception, there is no need for user code
+ * to catch it or subclasses if any error is to be considered fatal
+ * (the usual case).
+ *
+ * @author Rod Johnson
+ */
+@SuppressWarnings("serial")
+public abstract class DataAccessException extends NestedRuntimeException {
+
+ /**
+ * Constructor for DataAccessException.
+ * @param msg the detail message
+ */
+ public DataAccessException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructor for DataAccessException.
+ * @param msg the detail message
+ * @param cause the root cause (usually from using a underlying
+ * data access API such as JDBC)
+ */
+ public DataAccessException(@Nullable String msg, @Nullable Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationDescriptor.java b/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationDescriptor.java
new file mode 100644
index 000000000..8e3f08164
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationDescriptor.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.buildtimeinit;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Andy Clement
+ */
+public class InitializationDescriptor {
+
+ private final List buildtimeClasses;
+
+ private final List buildtimePackages;
+
+ private final List runtimeClasses;
+
+ private final List runtimePackages;
+
+ public InitializationDescriptor() {
+ this.buildtimeClasses = new ArrayList<>();
+ this.buildtimePackages = new ArrayList<>();
+ this.runtimeClasses = new ArrayList<>();
+ this.runtimePackages = new ArrayList<>();
+ }
+
+ public InitializationDescriptor(InitializationDescriptor metadata) {
+ this.buildtimeClasses = new ArrayList<>(metadata.buildtimeClasses);
+ this.buildtimePackages = new ArrayList<>(metadata.buildtimePackages);
+ this.runtimeClasses = new ArrayList<>(metadata.runtimeClasses);
+ this.runtimePackages = new ArrayList<>(metadata.runtimePackages);
+ }
+
+ public List getBuildtimeClasses() {
+ return this.buildtimeClasses;
+ }
+
+ public List getBuildtimePackages() {
+ return this.buildtimePackages;
+ }
+
+ public List getRuntimeClasses() {
+ return this.runtimeClasses;
+ }
+
+ public List getRuntimePackages() {
+ return this.runtimePackages;
+ }
+
+ public void addBuildtimeClass(String clazz) {
+ this.buildtimeClasses.add(clazz);
+ }
+
+ public void addBuildtimePackage(String pkg) {
+ this.buildtimePackages.add(pkg);
+ }
+
+ public void addRuntimeClass(String clazz) {
+ this.runtimeClasses.add(clazz);
+ }
+
+ public void addRuntimePackage(String pkg) {
+ this.runtimePackages.add(pkg);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(String.format("InitializationDescriptor #%s buildtime-classes #%s buildtime-packages #%s runtime-classes #%s runtime-packages\n",
+ buildtimeClasses.size(), buildtimePackages.size(), runtimeClasses.size(), runtimePackages.size()));
+// result.append("buildtime classes:\n");
+// this.buildtimeClasses.forEach(cd -> {
+// result.append(String.format("%s\n",cd));
+// });
+// result.append("buildtime packages:\n");
+// this.buildtimePackages.forEach(cd -> {
+// result.append(String.format("%s\n",cd));
+// });
+// result.append("runtime classes:\n");
+// this.runtimeClasses.forEach(cd -> {
+// result.append(String.format("%s\n",cd));
+// });
+// result.append("runtime packages:\n");
+// this.runtimePackages.forEach(cd -> {
+// result.append(String.format("%s\n",cd));
+// });
+ return result.toString();
+ }
+
+ public boolean isEmpty() {
+ return buildtimeClasses.isEmpty() && runtimeClasses.isEmpty();
+ }
+
+ public static InitializationDescriptor of(String jsonString) {
+ try {
+ return InitializationJsonMarshaller.read(jsonString);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to read json:\n"+jsonString, e);
+ }
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationJsonConverter.java b/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationJsonConverter.java
new file mode 100644
index 000000000..14265d0ef
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationJsonConverter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.buildtimeinit;
+
+import org.springframework.graal.json.JSONArray;
+import org.springframework.graal.json.JSONObject;
+
+/**
+ * Converter to change resource descriptor objects into JSON objects
+ *
+ * @author Andy Clement
+ */
+class InitializationJsonConverter {
+
+ public JSONObject toJsonArray(InitializationDescriptor metadata) throws Exception {
+ JSONObject object = new JSONObject();
+ JSONArray jsonArray = new JSONArray();
+ for (String p : metadata.getBuildtimeClasses()) {
+ jsonArray.put(toClassJsonObject(p));
+ }
+ for (String p : metadata.getBuildtimePackages()) {
+ jsonArray.put(toPackageJsonObject(p));
+ }
+ object.put("buildTimeInitialization", jsonArray);
+ for (String p : metadata.getRuntimeClasses()) {
+ jsonArray.put(toClassJsonObject(p));
+ }
+ for (String p : metadata.getRuntimePackages()) {
+ jsonArray.put(toPackageJsonObject(p));
+ }
+ object.put("runtimeInitialization", jsonArray);
+ return object;
+ }
+
+ public JSONObject toPackageJsonObject(String pattern) throws Exception {
+ JSONObject object = new JSONObject();
+ object.put("package", pattern);
+ return object;
+ }
+
+ public JSONObject toClassJsonObject(String pattern) throws Exception {
+ JSONObject object = new JSONObject();
+ object.put("class", pattern);
+ return object;
+ }
+}
diff --git a/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationJsonMarshaller.java b/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationJsonMarshaller.java
new file mode 100644
index 000000000..a319ea7b2
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/buildtimeinit/InitializationJsonMarshaller.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.buildtimeinit;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.springframework.graal.json.JSONArray;
+import org.springframework.graal.json.JSONObject;
+
+/**
+ * Marshaller to write {@link InitializationDescriptor} as JSON.
+ *
+ * @author Andy Clement
+ */
+public class InitializationJsonMarshaller {
+
+ private static final int BUFFER_SIZE = 4098;
+
+ public void write(InitializationDescriptor metadata, OutputStream outputStream)
+ throws IOException {
+ try {
+ InitializationJsonConverter converter = new InitializationJsonConverter();
+ JSONObject jsonObject = converter.toJsonArray(metadata);
+ outputStream.write(jsonObject.toString(2).getBytes(StandardCharsets.UTF_8));
+ }
+ catch (Exception ex) {
+ if (ex instanceof IOException) {
+ throw (IOException) ex;
+ }
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ public static InitializationDescriptor read(String input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))) {
+ return read(bais);
+ }
+ }
+
+ public static InitializationDescriptor read(byte[] input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input)) {
+ return read(bais);
+ }
+ }
+
+ public static InitializationDescriptor read(InputStream inputStream) throws Exception {
+ InitializationDescriptor metadata = toDelayInitDescriptor(new JSONObject(toString(inputStream)));
+ return metadata;
+ }
+
+ private static InitializationDescriptor toDelayInitDescriptor(JSONObject object) throws Exception {
+ InitializationDescriptor rd = new InitializationDescriptor();
+ JSONArray array = object.getJSONArray("buildTimeInitialization");
+ for (int i=0;i listOfParameterTypes = null;
+// if (parameterTypes != null) {
+// listOfParameterTypes = new ArrayList<>();
+// for (int i=0;i proxyDescriptors;
+
+ public ProxiesDescriptor() {
+ this.proxyDescriptors = new ArrayList<>();
+ }
+
+ public ProxiesDescriptor(ProxiesDescriptor metadata) {
+ this.proxyDescriptors = new ArrayList<>(metadata.proxyDescriptors);
+ }
+
+ public List getProxyDescriptors() {
+ return this.proxyDescriptors;
+ }
+
+ public void add(ProxyDescriptor proxyDescriptor) {
+ this.proxyDescriptors.add(proxyDescriptor);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(String.format("ProxyDescriptors #%s\n",proxyDescriptors.size()));
+ this.proxyDescriptors.forEach(cd -> {
+ result.append(String.format("%s: \n",cd));
+ });
+ return result.toString();
+ }
+
+ public boolean isEmpty() {
+ return proxyDescriptors.isEmpty();
+ }
+
+ public static ProxiesDescriptor of(String jsonString) {
+ try {
+ return ProxiesDescriptorJsonMarshaller.read(jsonString);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to read json:\n"+jsonString, e);
+ }
+ }
+
+ public void consume(Consumer> consumer) {
+ proxyDescriptors.stream().forEach(pd -> consumer.accept(pd.getInterfaces()));
+ }
+
+// public boolean hasClassDescriptor(String string) {
+// for (ProxyDescriptor cd: classDescriptors) {
+// if (cd.getName().equals(string)) {
+// return true;
+// }
+// }
+// return false;
+// }
+//
+// public ProxyDescriptor getClassDescriptor(String type) {
+// for (ProxyDescriptor cd: classDescriptors) {
+// if (cd.getName().equals(type)) {
+// return cd;
+// }
+// }
+// return null;
+// }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/proxies/ProxiesDescriptorJsonConverter.java b/src/main/java/org/springframework/graal/domain/proxies/ProxiesDescriptorJsonConverter.java
new file mode 100644
index 000000000..f540d40fa
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/proxies/ProxiesDescriptorJsonConverter.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.proxies;
+
+import java.util.List;
+
+import org.springframework.graal.json.JSONArray;
+
+/**
+ * Converter to change reflection descriptor objects into JSON objects
+ *
+ * @author Andy Clement
+ */
+class ProxiesDescriptorJsonConverter {
+
+ public JSONArray toJsonArray(ProxiesDescriptor metadata) throws Exception {
+ JSONArray jsonArray = new JSONArray();
+ for (ProxyDescriptor cd : metadata.getProxyDescriptors()) {
+ jsonArray.put(toJsonArray(cd));
+ }
+ return jsonArray;
+ }
+
+ public JSONArray toJsonArray(ProxyDescriptor pd) throws Exception {
+ JSONArray jsonArray = new JSONArray();
+ List interfaces = pd.getInterfaces();
+ for (String intface: interfaces) {
+ jsonArray.put(intface);
+ }
+ return jsonArray;
+// JSONObject jsonObject = new JSONObject();
+// jsonObject.put("name", cd.getName());
+// Set flags = cd.getFlags();
+// if (flags != null) {
+// for (Flag flag: Flag.values()) {
+// if (flags.contains(flag)) {
+// putTrueFlag(jsonObject,flag.name());
+// }
+// }
+// }
+// List fds = cd.getFields();
+// if (fds != null) {
+// JSONArray fieldJsonArray = new JSONArray();
+// for (FieldDescriptor fd: fds) {
+// JSONObject fieldjo = new JSONObject();
+// fieldjo.put("name", fd.getName());
+// if (fd.isAllowWrite()) {
+// fieldjo.put("allowWrite", "true");
+// }
+// fieldJsonArray.put(fieldjo);
+// }
+// jsonObject.put("fields", fieldJsonArray);
+// }
+// List mds = cd.getMethods();
+// if (mds != null) {
+// JSONArray methodsJsonArray = new JSONArray();
+// for (MethodDescriptor md: mds) {
+// JSONObject methodJsonObject = new JSONObject();
+// methodJsonObject.put("name", md.getName());
+// List parameterTypes = md.getParameterTypes();
+// JSONArray parameterArray = new JSONArray();
+// if (parameterTypes != null) {
+// for (String pt: parameterTypes) {
+// parameterArray.put(pt);
+// }
+// }
+// methodJsonObject.put("parameterTypes",parameterArray);
+// methodsJsonArray.put(methodJsonObject);
+// }
+// jsonObject.put("methods", methodsJsonArray);
+// }
+// return jsonObject;
+ }
+
+// private void putTrueFlag(JSONObject jsonObject, String name) throws Exception {
+// jsonObject.put(name, true);
+// }
+}
diff --git a/src/main/java/org/springframework/graal/domain/proxies/ProxiesDescriptorJsonMarshaller.java b/src/main/java/org/springframework/graal/domain/proxies/ProxiesDescriptorJsonMarshaller.java
new file mode 100644
index 000000000..413762dd3
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/proxies/ProxiesDescriptorJsonMarshaller.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.proxies;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.graal.json.JSONArray;
+
+/**
+ * Marshaller to write {@link ProxiesDescriptor} as JSON.
+ *
+ * @author Andy Clement
+ */
+public class ProxiesDescriptorJsonMarshaller {
+
+ private static final int BUFFER_SIZE = 4098;
+
+ public void write(ProxiesDescriptor metadata, OutputStream outputStream)
+ throws IOException {
+ try {
+ ProxiesDescriptorJsonConverter converter = new ProxiesDescriptorJsonConverter();
+ JSONArray jsonArray = converter.toJsonArray(metadata);
+ outputStream.write(jsonArray.toString(2).getBytes(StandardCharsets.UTF_8));
+ }
+ catch (Exception ex) {
+ if (ex instanceof IOException) {
+ throw (IOException) ex;
+ }
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ public static ProxiesDescriptor read(String input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))) {
+ return read(bais);
+ }
+ }
+
+ public static ProxiesDescriptor read(byte[] input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input)) {
+ return read(bais);
+ }
+ }
+
+ public static ProxiesDescriptor read(InputStream inputStream) throws Exception {
+ ProxiesDescriptor metadata = toProxiesDescriptor(new JSONArray(toString(inputStream)));
+ return metadata;
+ }
+
+ private static ProxiesDescriptor toProxiesDescriptor(JSONArray array) throws Exception {
+ ProxiesDescriptor pds = new ProxiesDescriptor();
+ for (int i=0;i interfaces = new ArrayList<>();
+ for (int i=0;i listOfParameterTypes = null;
+// if (parameterTypes != null) {
+// listOfParameterTypes = new ArrayList<>();
+// for (int i=0;i {
+
+ private List interfaces; // e.g. java.io.Serializable
+
+ ProxyDescriptor() {
+ }
+
+ ProxyDescriptor(List interfaces) {
+ this.interfaces = interfaces;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ProxyDescriptor other = (ProxyDescriptor) o;
+ boolean result = true;
+ result = result && nullSafeEquals(this.interfaces, other.interfaces);
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = nullSafeHashCode(this.interfaces);
+ return result;
+ }
+
+ private boolean nullSafeEquals(Object o1, Object o2) {
+ if (o1 == o2) {
+ return true;
+ }
+ if (o1 == null || o2 == null) {
+ return false;
+ }
+ return o1.equals(o2);
+ }
+
+ private int nullSafeHashCode(Object o) {
+ return (o != null) ? o.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder string = new StringBuilder();
+ buildToStringProperty(string, "interfaces", this.interfaces);
+ return string.toString();
+ }
+
+ protected void buildToStringProperty(StringBuilder string, String property, Object value) {
+ if (value != null) {
+ string.append(" ").append(property).append(":").append(value);
+ }
+ }
+
+ @Override
+ public int compareTo(ProxyDescriptor o) {
+ List l = this.interfaces;
+ List r = o.interfaces;
+ if (l.size() != r.size()) {
+ return l.size() - r.size();
+ }
+ for (int i = 0; i < l.size(); i++) {
+ int cmpTo = l.get(i).compareTo(r.get(i));
+ if (cmpTo != 0) {
+ return cmpTo;
+ }
+ }
+ return 0; // equal!
+ }
+
+ public static ProxyDescriptor of(List interfaces) {
+ ProxyDescriptor pd = new ProxyDescriptor();
+ pd.setInterfaces(interfaces);
+ return pd;
+ }
+
+ public List getInterfaces() {
+ return this.interfaces;
+ }
+
+ public void setInterfaces(List interfaces) {
+ this.interfaces = new ArrayList<>();
+ this.interfaces.addAll(interfaces);
+ }
+
+ /**
+ * Used when new data is to be added to an already existing class descriptor
+ * (additional members, flag settings).
+ *
+ * @param cd the ClassDescriptor to merge into this one
+ */
+// public void merge(ProxyDescriptor cd) {
+// for (Flag flag : cd.getFlags()) {
+// this.setFlag(flag);
+// }
+// for (FieldDescriptor fd : cd.getFields()) {
+// FieldDescriptor existingSimilarOne = null;
+// for (FieldDescriptor existingFd: getFields()) {
+// if (existingFd.getName().equals(fd.getName())) {
+// existingSimilarOne = existingFd;
+// break;
+// }
+// }
+// if (existingSimilarOne != null) {
+// if (fd.isAllowWrite()) {
+// existingSimilarOne.setAllowWrite(true);
+// }
+// } else {
+// addFieldDescriptor(fd);
+// }
+// }
+// for (MethodDescriptor methodDescriptor : cd.getMethods()) {
+// if (!containsMethodDescriptor(methodDescriptor)) {
+// addMethodDescriptor(methodDescriptor);
+// }
+// }
+// }
+
+ public boolean containsInterface(String intface) {
+ return interfaces.contains(intface);
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/reflect/ClassDescriptor.java b/src/main/java/org/springframework/graal/domain/reflect/ClassDescriptor.java
new file mode 100644
index 000000000..2087d47da
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/reflect/ClassDescriptor.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.reflect;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Reflection information about a single class.
+ *
+ * @author Andy Clement
+ * @see ReflectionDescriptor
+ */
+public final class ClassDescriptor implements Comparable {
+
+ private String name; // e.g. java.lang.Class
+
+ private List fields;
+
+ private List methods; // includes constructors ""
+
+ private Set flags; // Inclusion in list indicates they are set
+
+ public enum Flag {
+ allPublicFields, //
+ allDeclaredFields, //
+ allDeclaredConstructors, //
+ allPublicConstructors, //
+ allDeclaredMethods, //
+ allPublicMethods, //
+ allDeclaredClasses, //
+ allPublicClasses;
+ }
+
+ ClassDescriptor() {
+ }
+
+ ClassDescriptor(String name, List fields, List methods, Set flags) {
+ this.name = name;
+ this.fields = fields;
+ this.methods = methods;
+ this.flags = flags;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ClassDescriptor other = (ClassDescriptor) o;
+ boolean result = true;
+ result = result && nullSafeEquals(this.name, other.name);
+ result = result && nullSafeEquals(this.flags, other.flags);
+ result = result && nullSafeEquals(this.fields, other.fields);
+ result = result && nullSafeEquals(this.methods, other.methods);
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = nullSafeHashCode(this.name);
+ result = 31 * result + nullSafeHashCode(this.flags);
+ result = 31 * result + nullSafeHashCode(this.fields);
+ result = 31 * result + nullSafeHashCode(this.methods);
+ return result;
+ }
+
+ private boolean nullSafeEquals(Object o1, Object o2) {
+ if (o1 == o2) {
+ return true;
+ }
+ if (o1 == null || o2 == null) {
+ return false;
+ }
+ return o1.equals(o2);
+ }
+
+ private int nullSafeHashCode(Object o) {
+ return (o != null) ? o.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder string = new StringBuilder(this.name);
+ buildToStringProperty(string, "setFlags", this.flags);
+ buildToStringProperty(string, "fields", this.fields);
+ buildToStringProperty(string, "methods", this.methods);
+ return string.toString();
+ }
+
+ protected void buildToStringProperty(StringBuilder string, String property, Object value) {
+ if (value != null) {
+ string.append(" ").append(property).append(":").append(value);
+ }
+ }
+
+ @Override
+ public int compareTo(ClassDescriptor o) {
+ return getName().compareTo(o.getName());
+ }
+
+ public Set getFlags() {
+ return this.flags;
+ }
+
+ public List getFields() {
+ return this.fields;
+ }
+
+ public List getMethods() {
+ return this.methods;
+ }
+
+ public static ClassDescriptor of(String name) {
+ ClassDescriptor cd = new ClassDescriptor();
+ cd.setName(name);
+ return cd;
+ }
+
+// public static ClassDescriptor newGroup(String name, String type, String sourceType,
+// String sourceMethod) {
+// return new ClassDescriptor(ItemType.GROUP, name, null, type, sourceType,
+// sourceMethod, null, null, null);
+// }
+//
+// public static ClassDescriptor newProperty(String prefix, String name, String type,
+// String sourceType, String sourceMethod, String description,
+// Object defaultValue, ItemDeprecation deprecation) {
+// return new ClassDescriptor(ItemType.PROPERTY, prefix, name, type, sourceType,
+// sourceMethod, description, defaultValue, deprecation);
+// }
+//
+// public static String newItemMetadataPrefix(String prefix, String suffix) {
+// return prefix.toLowerCase(Locale.ENGLISH)
+// + ReflectionDescriptor.toDashedCase(suffix);
+// }
+
+ public void setFlag(Flag f) {
+ if (flags == null) {
+ flags = new TreeSet<>();
+ }
+ flags.add(f);
+ }
+
+ public void addMethodDescriptor(MethodDescriptor methodDescriptor) {
+ if (methods == null) {
+ methods = new ArrayList<>();
+ }
+ methods.add(methodDescriptor);
+ }
+
+ public void addFieldDescriptor(FieldDescriptor fieldDescriptor) {
+ if (fields == null) {
+ fields = new ArrayList<>();
+ }
+ fields.add(fieldDescriptor);
+ }
+
+ /**
+ * Used when new data is to be added to an already existing class descriptor (additional members, flag settings).
+ *
+ * @param cd the ClassDescriptor to merge into this one
+ */
+ public void merge(ClassDescriptor cd) {
+ for (Flag flag : cd.getFlags()) {
+ this.setFlag(flag);
+ }
+ for (FieldDescriptor fd : cd.getFields()) {
+ FieldDescriptor existingSimilarOne = null;
+ for (FieldDescriptor existingFd: getFields()) {
+ if (existingFd.getName().equals(fd.getName())) {
+ existingSimilarOne = existingFd;
+ break;
+ }
+ }
+ if (existingSimilarOne != null) {
+ if (fd.isAllowWrite()) {
+ existingSimilarOne.setAllowWrite(true);
+ }
+ } else {
+ addFieldDescriptor(fd);
+ }
+ }
+ for (MethodDescriptor methodDescriptor : cd.getMethods()) {
+ if (!containsMethodDescriptor(methodDescriptor)) {
+ addMethodDescriptor(methodDescriptor);
+ }
+ }
+ }
+
+ private boolean containsMethodDescriptor(MethodDescriptor methodDescriptor) {
+ return methods.contains(methodDescriptor);
+ }
+
+ public MethodDescriptor getMethodDescriptor(String name,String...parameterTypes) {
+ MethodDescriptor searchmd = MethodDescriptor.of(name, parameterTypes);
+ for (MethodDescriptor md: methods) {
+ if (md.equals(searchmd)) {
+ return md;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/reflect/FieldDescriptor.java b/src/main/java/org/springframework/graal/domain/reflect/FieldDescriptor.java
new file mode 100644
index 000000000..5462777b1
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/reflect/FieldDescriptor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.reflect;
+
+/**
+ *
+ * @author Andy Clement
+ * @see ReflectionDescriptor
+ */
+public final class FieldDescriptor extends MemberDescriptor implements Comparable {
+
+ private boolean allowWrite = false;
+
+ private boolean allowUnsafeAccess = false;
+
+ FieldDescriptor(String name, boolean allowWrite, boolean allowUnsafeAccess) {
+ super(name);
+ this.allowWrite = allowWrite;
+ this.allowUnsafeAccess = allowUnsafeAccess;
+ }
+
+ public boolean isAllowWrite() {
+ return this.allowWrite;
+ }
+
+ public boolean isAllowUnsafeAccess() {
+ return this.allowUnsafeAccess;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ FieldDescriptor other = (FieldDescriptor) o;
+ boolean result = true;
+ result = result && nullSafeEquals(this.name, other.name);
+ result = result && nullSafeEquals(this.allowWrite, other.allowWrite);
+ result = result && nullSafeEquals(this.allowUnsafeAccess, other.allowUnsafeAccess);
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = nullSafeHashCode(this.name);
+ result = 31 * result + nullSafeHashCode(this.allowWrite);
+ result = 31 * result + nullSafeHashCode(this.allowUnsafeAccess);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder string = new StringBuilder(this.name);
+ buildToStringProperty(string, "name", this.name);
+ buildToStringProperty(string, "allowWrite", this.allowWrite);
+ buildToStringProperty(string, "allowUnsafeAccess", this.allowUnsafeAccess);
+ return string.toString();
+ }
+
+ @Override
+ public int compareTo(FieldDescriptor o) {
+ return getName().compareTo(o.getName());
+ }
+
+ public void setAllowWrite(boolean b) {
+ this.allowWrite = b;
+ }
+
+ public void setAllowUnsafeAccess(boolean b) {
+ this.allowUnsafeAccess = b;
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/reflect/JsonConverter.java b/src/main/java/org/springframework/graal/domain/reflect/JsonConverter.java
new file mode 100644
index 000000000..ada7b8602
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/reflect/JsonConverter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.reflect;
+
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.graal.json.JSONArray;
+import org.springframework.graal.json.JSONObject;
+import org.springframework.graal.domain.reflect.ClassDescriptor.Flag;
+
+/**
+ * Converter to change reflection descriptor objects into JSON objects
+ *
+ * @author Andy Clement
+ */
+class JsonConverter {
+
+ public JSONArray toJsonArray(ReflectionDescriptor metadata) throws Exception {
+ JSONArray jsonArray = new JSONArray();
+ for (ClassDescriptor cd : metadata.getClassDescriptors()) {
+ jsonArray.put(toJsonObject(cd));
+ }
+ return jsonArray;
+ }
+
+ public JSONObject toJsonObject(ClassDescriptor cd) throws Exception {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("name", cd.getName());
+ Set flags = cd.getFlags();
+ if (flags != null) {
+ for (Flag flag: Flag.values()) {
+ if (flags.contains(flag)) {
+ putTrueFlag(jsonObject,flag.name());
+ }
+ }
+ }
+ List fds = cd.getFields();
+ if (fds != null) {
+ JSONArray fieldJsonArray = new JSONArray();
+ for (FieldDescriptor fd: fds) {
+ JSONObject fieldjo = new JSONObject();
+ fieldjo.put("name", fd.getName());
+ if (fd.isAllowWrite()) {
+ fieldjo.put("allowWrite", "true");
+ }
+ fieldJsonArray.put(fieldjo);
+ }
+ jsonObject.put("fields", fieldJsonArray);
+ }
+ List mds = cd.getMethods();
+ if (mds != null) {
+ JSONArray methodsJsonArray = new JSONArray();
+ for (MethodDescriptor md: mds) {
+ JSONObject methodJsonObject = new JSONObject();
+ methodJsonObject.put("name", md.getName());
+ List parameterTypes = md.getParameterTypes();
+ JSONArray parameterArray = new JSONArray();
+ if (parameterTypes != null) {
+ for (String pt: parameterTypes) {
+ parameterArray.put(pt);
+ }
+ }
+ methodJsonObject.put("parameterTypes",parameterArray);
+ methodsJsonArray.put(methodJsonObject);
+ }
+ jsonObject.put("methods", methodsJsonArray);
+ }
+ return jsonObject;
+ }
+
+ private void putTrueFlag(JSONObject jsonObject, String name) throws Exception {
+ jsonObject.put(name, true);
+ }
+}
diff --git a/src/main/java/org/springframework/graal/domain/reflect/JsonMarshaller.java b/src/main/java/org/springframework/graal/domain/reflect/JsonMarshaller.java
new file mode 100644
index 000000000..6ba8b7d76
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/reflect/JsonMarshaller.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.reflect;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.graal.domain.reflect.ClassDescriptor.Flag;
+import org.springframework.graal.json.JSONArray;
+import org.springframework.graal.json.JSONObject;
+
+/**
+ * Marshaller to write {@link ReflectionDescriptor} as JSON.
+ *
+ * @author Andy Clement
+ */
+public class JsonMarshaller {
+
+ private static final int BUFFER_SIZE = 4098;
+
+ public void write(ReflectionDescriptor metadata, OutputStream outputStream)
+ throws IOException {
+ try {
+ JsonConverter converter = new JsonConverter();
+ JSONArray jsonArray = converter.toJsonArray(metadata);
+ outputStream.write(jsonArray.toString(2).getBytes(StandardCharsets.UTF_8));
+ }
+ catch (Exception ex) {
+ if (ex instanceof IOException) {
+ throw (IOException) ex;
+ }
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ public static ReflectionDescriptor read(String input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))) {
+ return read(bais);
+ }
+ }
+
+ public static ReflectionDescriptor read(byte[] input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input)) {
+ return read(bais);
+ }
+ }
+
+ public static ReflectionDescriptor read(InputStream inputStream) throws Exception {
+ ReflectionDescriptor metadata = toReflectionDescriptor(new JSONArray(toString(inputStream)));
+ return metadata;
+ }
+
+ private static ReflectionDescriptor toReflectionDescriptor(JSONArray array) throws Exception {
+ ReflectionDescriptor rd = new ReflectionDescriptor();
+ for (int i=0;i listOfParameterTypes = null;
+ if (parameterTypes != null) {
+ listOfParameterTypes = new ArrayList<>();
+ for (int i=0;i {
+
+ public final static String CONSTRUCTOR_NAME = "";
+
+ public final static List NO_PARAMS = Collections.emptyList();
+
+ private List parameterTypes; // e.g. char[], java.lang.String, java.lang.Object[]
+
+ MethodDescriptor() {
+ }
+
+ MethodDescriptor(String name, List parameterTypes) {
+ super(name);
+ this.parameterTypes = parameterTypes;
+ }
+
+ public List getParameterTypes() {
+ return this.parameterTypes;
+ }
+
+ public void setParameterTypes(List parameterTypes) {
+ this.parameterTypes = parameterTypes;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MethodDescriptor other = (MethodDescriptor) o;
+ boolean result = true;
+ result = result && nullSafeEquals(this.name, other.name);
+ result = result && nullSafeEquals(this.parameterTypes, other.parameterTypes);
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = nullSafeHashCode(this.name);
+ result = 31 * result + nullSafeHashCode(this.parameterTypes);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder string = new StringBuilder(this.name);
+ buildToStringProperty(string, "name", this.name);
+ buildToStringProperty(string, "parameterTypes", this.parameterTypes);
+ return string.toString();
+ }
+
+ @Override
+ public int compareTo(MethodDescriptor o) {
+ return getName().compareTo(o.getName());
+ }
+
+ public static MethodDescriptor of(String name, String... parameterTypes) {
+ MethodDescriptor md = new MethodDescriptor();
+ md.setName(name);
+ if (parameterTypes != null) {
+ md.setParameterTypes(Arrays.asList(parameterTypes));
+ } else {
+ md.setParameterTypes(NO_PARAMS);
+ }
+ return md;
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/reflect/ReflectionDescriptor.java b/src/main/java/org/springframework/graal/domain/reflect/ReflectionDescriptor.java
new file mode 100644
index 000000000..56d89c867
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/reflect/ReflectionDescriptor.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.reflect;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * https://github.com/oracle/graal/blob/master/substratevm/REFLECTION.md
+ *
+ * @author Andy Clement
+ */
+public class ReflectionDescriptor {
+
+ private final List classDescriptors;
+
+ public ReflectionDescriptor() {
+ this.classDescriptors = new ArrayList<>();
+ }
+
+ public ReflectionDescriptor(ReflectionDescriptor metadata) {
+ this.classDescriptors = new ArrayList<>(metadata.classDescriptors);
+ }
+
+ public void sort() {
+ classDescriptors.sort((a,b) -> a.getName().compareTo(b.getName()));
+ }
+
+ public List getClassDescriptors() {
+ return this.classDescriptors;
+ }
+
+ public void add(ClassDescriptor classDescriptor) {
+ this.classDescriptors.add(classDescriptor);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(String.format("ClassDescriptors #%s\n",classDescriptors.size()));
+ this.classDescriptors.forEach(cd -> {
+ result.append(String.format("%s: \n",cd));
+ });
+ return result.toString();
+ }
+
+ public boolean isEmpty() {
+ return classDescriptors.isEmpty();
+ }
+
+ public static ReflectionDescriptor of(String jsonString) {
+ try {
+ return JsonMarshaller.read(jsonString);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to read json:\n"+jsonString, e);
+ }
+ }
+
+ public boolean hasClassDescriptor(String string) {
+ for (ClassDescriptor cd: classDescriptors) {
+ if (cd.getName().equals(string)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public ClassDescriptor getClassDescriptor(String type) {
+ for (ClassDescriptor cd: classDescriptors) {
+ if (cd.getName().equals(type)) {
+ return cd;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/resources/ResourcesDescriptor.java b/src/main/java/org/springframework/graal/domain/resources/ResourcesDescriptor.java
new file mode 100644
index 000000000..0f8bcc95c
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/resources/ResourcesDescriptor.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.resources;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * https://github.com/oracle/graal/blob/master/substratevm/RESOURCES.md
+ *
+ * @author Andy Clement
+ */
+public class ResourcesDescriptor {
+
+ private final List patterns;
+
+ public ResourcesDescriptor() {
+ this.patterns = new ArrayList<>();
+ }
+
+ public ResourcesDescriptor(ResourcesDescriptor metadata) {
+ this.patterns = new ArrayList<>(metadata.patterns);
+ }
+
+ public List getPatterns() {
+ return this.patterns;
+ }
+
+ public void add(String pattern) {
+ this.patterns.add(pattern);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(String.format("ResourcesDescriptors #%s\n",patterns.size()));
+ this.patterns.forEach(cd -> {
+ result.append(String.format("%s: \n",cd));
+ });
+ return result.toString();
+ }
+
+ public boolean isEmpty() {
+ return patterns.isEmpty();
+ }
+
+ public static ResourcesDescriptor of(String jsonString) {
+ try {
+ return ResourcesJsonMarshaller.read(jsonString);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to read json:\n"+jsonString, e);
+ }
+ }
+
+// public boolean hasClassDescriptor(String string) {
+// for (ProxyDescriptor cd: classDescriptors) {
+// if (cd.getName().equals(string)) {
+// return true;
+// }
+// }
+// return false;
+// }
+//
+// public ProxyDescriptor getClassDescriptor(String type) {
+// for (ProxyDescriptor cd: classDescriptors) {
+// if (cd.getName().equals(type)) {
+// return cd;
+// }
+// }
+// return null;
+// }
+
+}
diff --git a/src/main/java/org/springframework/graal/domain/resources/ResourcesJsonConverter.java b/src/main/java/org/springframework/graal/domain/resources/ResourcesJsonConverter.java
new file mode 100644
index 000000000..11e152e44
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/resources/ResourcesJsonConverter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.resources;
+
+import org.springframework.graal.json.JSONArray;
+import org.springframework.graal.json.JSONObject;
+
+/**
+ * Converter to change resource descriptor objects into JSON objects
+ *
+ * @author Andy Clement
+ */
+class ResourcesJsonConverter {
+
+ public JSONObject toJsonArray(ResourcesDescriptor metadata) throws Exception {
+ JSONObject object = new JSONObject();
+ JSONArray jsonArray = new JSONArray();
+ for (String p : metadata.getPatterns()) {
+ jsonArray.put(toJsonObject(p));
+ }
+ object.put("resources", jsonArray);
+ return object;
+ }
+
+ public JSONObject toJsonObject(String pattern) throws Exception {
+ JSONObject object = new JSONObject();
+ object.put("pattern", pattern);
+ return object;
+// JSONObject jsonObject = new JSONObject();
+// jsonObject.put("name", cd.getName());
+// Set flags = cd.getFlags();
+// if (flags != null) {
+// for (Flag flag: Flag.values()) {
+// if (flags.contains(flag)) {
+// putTrueFlag(jsonObject,flag.name());
+// }
+// }
+// }
+// List fds = cd.getFields();
+// if (fds != null) {
+// JSONArray fieldJsonArray = new JSONArray();
+// for (FieldDescriptor fd: fds) {
+// JSONObject fieldjo = new JSONObject();
+// fieldjo.put("name", fd.getName());
+// if (fd.isAllowWrite()) {
+// fieldjo.put("allowWrite", "true");
+// }
+// fieldJsonArray.put(fieldjo);
+// }
+// jsonObject.put("fields", fieldJsonArray);
+// }
+// List mds = cd.getMethods();
+// if (mds != null) {
+// JSONArray methodsJsonArray = new JSONArray();
+// for (MethodDescriptor md: mds) {
+// JSONObject methodJsonObject = new JSONObject();
+// methodJsonObject.put("name", md.getName());
+// List parameterTypes = md.getParameterTypes();
+// JSONArray parameterArray = new JSONArray();
+// if (parameterTypes != null) {
+// for (String pt: parameterTypes) {
+// parameterArray.put(pt);
+// }
+// }
+// methodJsonObject.put("parameterTypes",parameterArray);
+// methodsJsonArray.put(methodJsonObject);
+// }
+// jsonObject.put("methods", methodsJsonArray);
+// }
+// return jsonObject;
+ }
+
+// private void putTrueFlag(JSONObject jsonObject, String name) throws Exception {
+// jsonObject.put(name, true);
+// }
+}
diff --git a/src/main/java/org/springframework/graal/domain/resources/ResourcesJsonMarshaller.java b/src/main/java/org/springframework/graal/domain/resources/ResourcesJsonMarshaller.java
new file mode 100644
index 000000000..b0f3e4117
--- /dev/null
+++ b/src/main/java/org/springframework/graal/domain/resources/ResourcesJsonMarshaller.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.domain.resources;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.springframework.graal.json.JSONArray;
+import org.springframework.graal.json.JSONObject;
+
+/**
+ * Marshaller to write {@link ResourcesDescriptor} as JSON.
+ *
+ * @author Andy Clement
+ */
+public class ResourcesJsonMarshaller {
+
+ private static final int BUFFER_SIZE = 4098;
+
+ public void write(ResourcesDescriptor metadata, OutputStream outputStream)
+ throws IOException {
+ try {
+ ResourcesJsonConverter converter = new ResourcesJsonConverter();
+ JSONObject jsonObject = converter.toJsonArray(metadata);
+ outputStream.write(jsonObject.toString(2).getBytes(StandardCharsets.UTF_8));
+ }
+ catch (Exception ex) {
+ if (ex instanceof IOException) {
+ throw (IOException) ex;
+ }
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ public static ResourcesDescriptor read(String input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))) {
+ return read(bais);
+ }
+ }
+
+ public static ResourcesDescriptor read(byte[] input) throws Exception {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(input)) {
+ return read(bais);
+ }
+ }
+
+ public static ResourcesDescriptor read(InputStream inputStream) throws Exception {
+ ResourcesDescriptor metadata = toResourcesDescriptor(new JSONObject(toString(inputStream)));
+ return metadata;
+ }
+
+ private static ResourcesDescriptor toResourcesDescriptor(JSONObject object) throws Exception {
+ ResourcesDescriptor rd = new ResourcesDescriptor();
+ JSONArray array = object.getJSONArray("resources");
+ for (int i=0;i listOfParameterTypes = null;
+// if (parameterTypes != null) {
+// listOfParameterTypes = new ArrayList<>();
+// for (int i=0;i> proxyRegisteringConsumer = interfaceNames -> {
+ System.out.println("- "+interfaceNames);
+ boolean isOK= true;
+ Class>[] interfaces = new Class>[interfaceNames.size()];
+ for (int i = 0; i < interfaceNames.size(); i++) {
+ String className = interfaceNames.get(i);
+ Class> clazz = imageClassLoader.findClassByName(className, false);
+ if (clazz == null) {
+ System.out.println("Skipping dynamic proxy registration due to missing type: "+className);
+ isOK=false;
+ break;
+ }
+ if (!clazz.isInterface()) {
+ throw new RuntimeException("The class \"" + className + "\" is not an interface.");
+ }
+ interfaces[i] = clazz;
+ }
+ if (isOK) {
+ /* The interfaces array can be empty. The java.lang.reflect.Proxy API allows it. */
+ dynamicProxySupport.addProxyClass(interfaces);
+ }
+ };
+ pd.consume(proxyRegisteringConsumer);
+ }
+}
diff --git a/src/main/java/org/springframework/graal/support/InitializationHandler.java b/src/main/java/org/springframework/graal/support/InitializationHandler.java
new file mode 100644
index 000000000..a8935b5fa
--- /dev/null
+++ b/src/main/java/org/springframework/graal/support/InitializationHandler.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.support;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess;
+import org.springframework.graal.domain.buildtimeinit.InitializationDescriptor;
+import org.springframework.graal.domain.buildtimeinit.InitializationJsonMarshaller;
+import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
+
+/**
+ *
+ * @author Andy Clement
+ */
+public class InitializationHandler {
+
+ public InitializationDescriptor compute() {
+ try {
+ InputStream s = this.getClass().getResourceAsStream("/initialization.json");
+ return InitializationJsonMarshaller.read(s);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public void register(BeforeAnalysisAccess access) {
+ InitializationDescriptor id = compute();
+ System.out.println("SBG: forcing explicit class initialization at build or runtime:");
+ System.out.println(id.toString());
+ List collect = id.getBuildtimeClasses().stream()
+ .map(access::findClassByName).filter(Objects::nonNull).collect(Collectors.toList());
+ RuntimeClassInitialization.initializeAtBuildTime(collect.toArray(new Class[] {}));
+ id.getRuntimeClasses().stream()
+ .map(access::findClassByName).filter(Objects::nonNull)
+ .forEach(RuntimeClassInitialization::initializeAtRunTime);
+ System.out.println("Registering these packages for buildtime initialization: \n"+id.getBuildtimePackages());
+ RuntimeClassInitialization.initializeAtBuildTime(id.getBuildtimePackages().toArray(new String[] {}));
+ System.out.println("Registering these packages for runtime initialization: \n"+id.getRuntimePackages());
+ RuntimeClassInitialization.initializeAtRunTime(id.getRuntimePackages().toArray(new String[] {}));
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/support/ReflectionHandler.java b/src/main/java/org/springframework/graal/support/ReflectionHandler.java
new file mode 100644
index 000000000..eab07fa15
--- /dev/null
+++ b/src/main/java/org/springframework/graal/support/ReflectionHandler.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.support;
+
+import java.io.InputStream;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Array;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.graalvm.nativeimage.ImageSingletons;
+import org.graalvm.nativeimage.hosted.Feature.DuringSetupAccess;
+import org.graalvm.nativeimage.impl.RuntimeReflectionSupport;
+import org.graalvm.util.GuardedAnnotationAccess;
+import org.springframework.graal.domain.reflect.ClassDescriptor;
+import org.springframework.graal.domain.reflect.ClassDescriptor.Flag;
+import org.springframework.graal.domain.reflect.FieldDescriptor;
+import org.springframework.graal.domain.reflect.JsonMarshaller;
+import org.springframework.graal.domain.reflect.MethodDescriptor;
+import org.springframework.graal.domain.reflect.ReflectionDescriptor;
+
+import com.oracle.svm.hosted.FeatureImpl.DuringSetupAccessImpl;
+import com.oracle.svm.hosted.ImageClassLoader;
+import com.oracle.svm.hosted.config.ReflectionRegistryAdapter;
+
+/**
+ * Loads up the constant data defined in resource file and registers reflective access being
+ * necessary with the image build. Also provides an method (addAccess(String typename, Flag... flags) }
+ * usable from elsewhere when needing to register reflective access to a type (e.g. used when resource
+ * processing).
+ *
+ * @author Andy Clement
+ */
+public class ReflectionHandler {
+
+ private final static String RESOURCE_FILE = "/reflect.json";
+
+ private ReflectionRegistryAdapter rra;
+
+ private ReflectionDescriptor constantReflectionDescriptor;
+
+ private ImageClassLoader cl;
+
+ public ReflectionDescriptor getConstantData() {
+ if (constantReflectionDescriptor == null) {
+ try {
+ InputStream s = this.getClass().getResourceAsStream(RESOURCE_FILE);
+ constantReflectionDescriptor = JsonMarshaller.read(s);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unexpectedly can't load "+RESOURCE_FILE, e);
+ }
+ }
+ return constantReflectionDescriptor;
+ }
+
+ public void register(DuringSetupAccess a) {
+ DuringSetupAccessImpl access = (DuringSetupAccessImpl) a;
+ RuntimeReflectionSupport rrs = ImageSingletons.lookup(RuntimeReflectionSupport.class);
+ cl = access.getImageClassLoader();
+ rra = new ReflectionRegistryAdapter(rrs, cl);
+ ReflectionDescriptor reflectionDescriptor = getConstantData();
+
+ System.out.println("SBG: reflection registering #"+reflectionDescriptor.getClassDescriptors().size()+" entries");
+ for (ClassDescriptor classDescriptor : reflectionDescriptor.getClassDescriptors()) {
+ Class> type = null;
+ String n2 = classDescriptor.getName();
+ if (n2.endsWith("[]")) {
+ System.out.println("ARRAY: "+n2.substring(0,n2.length()-2));
+ type = rra.resolveType(n2.substring(0,n2.length()-2));
+ System.out.println("Array base type resolved as "+type.getName());
+ Object o = Array.newInstance(type, 1);
+ type = o.getClass();
+ System.out.println("Class of array is "+type.getName());
+ } else {
+ type = rra.resolveType(classDescriptor.getName());
+ }
+ if (type == null) {
+ System.out.println("SBG: WARNING: "+RESOURCE_FILE+" included "+classDescriptor.getName()+" but it doesn't exist on the classpath, skipping...");
+ continue;
+ }
+ rra.registerType(type);
+ Set flags = classDescriptor.getFlags();
+ if (flags != null) {
+ for (Flag flag: flags) {
+ try {
+ switch (flag) {
+ case allDeclaredClasses:
+ rra.registerDeclaredClasses(type);
+ break;
+ case allDeclaredFields:
+ rra.registerDeclaredFields(type);
+ break;
+ case allPublicFields:
+ rra.registerPublicFields(type);
+ break;
+ case allDeclaredConstructors:
+ rra.registerDeclaredConstructors(type);
+ break;
+ case allPublicConstructors:
+ rra.registerPublicConstructors(type);
+ break;
+ case allDeclaredMethods:
+ rra.registerDeclaredMethods(type);
+ break;
+ case allPublicMethods:
+ rra.registerPublicMethods(type);
+ break;
+ case allPublicClasses:
+ rra.registerPublicClasses(type);
+ break;
+ }
+ } catch (NoClassDefFoundError ncdfe) {
+ System.out.println("SBG: ERROR: problem handling flag: "+flag+" for "+type.getName()+" because of missing "+ncdfe.getMessage());
+ }
+ }
+ }
+
+ // Process all specific methods defined in the input class descriptor (including constructors)
+ List methods = classDescriptor.getMethods();
+ if (methods != null) {
+ for (MethodDescriptor methodDescriptor : methods) {
+ String n = methodDescriptor.getName();
+ List parameterTypes = methodDescriptor.getParameterTypes();
+ if (parameterTypes == null) {
+ if (n.equals("")) {
+ rra.registerAllConstructors(type);
+ } else {
+ rra.registerAllMethodsWithName(type, n);
+ }
+ } else {
+ List> collect = parameterTypes.stream().map(pname -> rra.resolveType(pname))
+ .collect(Collectors.toList());
+ try {
+ if (n.equals("")) {
+ rra.registerConstructor(type, collect);
+ } else {
+ rra.registerMethod(type, n, collect);
+ }
+ } catch (NoSuchMethodException nsme) {
+ throw new IllegalStateException("Couldn't find: " + methodDescriptor.toString(), nsme);
+ }
+ }
+ }
+ }
+
+ // Process all specific fields defined in the input class descriptor
+ List fields = classDescriptor.getFields();
+ if (fields != null) {
+ for (FieldDescriptor fieldDescriptor : fields) {
+ try {
+ rra.registerField(type, fieldDescriptor.getName(), fieldDescriptor.isAllowWrite(),fieldDescriptor.isAllowUnsafeAccess());
+ } catch (NoSuchFieldException nsfe) {
+ throw new IllegalStateException("Couldn't find field: " + type.getName()+"."+fieldDescriptor.getName(), nsfe);
+// System.out.println("SBG: WARNING: skipping reflection registration of field "+type.getName()+"."+fieldDescriptor.getName()+": field not found");
+ }
+ }
+ }
+ }
+ registerLogback();
+ }
+
+ // TODO review - not strictly correct as they may ask with different flags (but right now they don't)
+ public static final Set added = new HashSet<>();
+
+ /**
+ * Record that reflective access to a type (and a selection of its members based on the flags) should
+ * be possible at runtime. This method will pre-emptively check all type references to ensure later
+ * native-image processing will not fail if, for example, it trips up over a type reference in a
+ * generic type that isn't on the image building classpath. NOTE: it is assumed that if elements are
+ * not accessible that the runtime doesn't need them (this is done under the spring model where
+ * conditional checks on auto configuration would cause no attempts to be made to types/members that
+ * aren't added here).
+ *
+ * @param typename the dotted type name for which to add reflective access
+ * @param flags any members that should be accessible via reflection
+ * @return the class, if the type was successfully registered for reflective access, otherwise null
+ */
+ public Class> addAccess(String typename, Flag...flags) {
+ if (!added.add(typename)) {
+ return null;
+ }
+ System.out.println("SBG: INFO: Registering reflective access to "+typename);
+ // This can return null if, for example, the supertype of the specified type is not
+ // on the classpath. In a simple app there may be a number of types coming in from
+ // spring-boot-autoconfigure but they extend types not on the classpath.
+ Class> type = rra.resolveType(typename);
+ if (type == null) {
+ System.out.println("SBG: ERROR: CANNOT RESOLVE "+typename+" ???");
+ return null;
+ }
+ if (constantReflectionDescriptor.hasClassDescriptor(typename)) {
+ System.out.println("SBG: WARNING: type "+typename+" being added dynamically whilst "+RESOURCE_FILE+
+ " already contains it - does it need to be in the file? ");
+ }
+ rra.registerType(type);
+ for (Flag flag: flags) {
+ try {
+ switch (flag) {
+ case allDeclaredClasses:
+ if (verify(type.getDeclaredClasses())) {
+ rra.registerDeclaredClasses(type);
+ }
+ break;
+ case allDeclaredFields:
+ if (verify(type.getDeclaredFields())) {
+ rra.registerDeclaredFields(type);
+ }
+ break;
+ case allPublicFields:
+ if (verify(type.getFields())) {
+ rra.registerPublicFields(type);
+ }
+ break;
+ case allDeclaredConstructors:
+ if (verify(type.getDeclaredConstructors())) {
+ rra.registerDeclaredConstructors(type);
+ }
+ break;
+ case allPublicConstructors:
+ if (verify(type.getConstructors())) {
+ rra.registerPublicConstructors(type);
+ }
+ break;
+ case allDeclaredMethods:
+ if (verify(type.getDeclaredMethods())) {
+ rra.registerDeclaredMethods(type);
+ }
+ break;
+ case allPublicMethods:
+ if (verify(type.getMethods())) {
+ rra.registerPublicMethods(type);
+ }
+ break;
+ case allPublicClasses:
+ if (verify(type.getClasses())) {
+ rra.registerPublicClasses(type);
+ }
+ break;
+ }
+ } catch (NoClassDefFoundError ncdfe) {
+ System.out.println("SBG: ERROR: problem handling flag: "+flag+" for "+type.getName()+" because of missing "+ncdfe.getMessage());
+ }
+ }
+ return type;
+ }
+
+
+ private boolean verify(Object[] things) {
+ for (Object o: things) {
+ try {
+ if (o instanceof Method) {
+ ((Method)o).getGenericReturnType();
+ }
+ if (o instanceof Field) {
+ ((Field)o).getGenericType();
+ }
+ if (o instanceof AccessibleObject) {
+ AccessibleObject accessibleObject = (AccessibleObject) o;
+ GuardedAnnotationAccess.getDeclaredAnnotations(accessibleObject);
+ }
+
+ if (o instanceof Parameter) {
+ Parameter parameter = (Parameter) o;
+ parameter.getType();
+ }
+ if (o instanceof Executable) {
+ Executable e = (Executable)o;
+ e.getGenericParameterTypes();
+ e.getGenericExceptionTypes();
+ e.getParameters();
+ }
+ } catch (Exception e) {
+ System.out.println("REFLECTION PROBLEM LATER due to reference from "+o+" to "+e.getMessage());
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ // TODO this is horrible, it should be packaged with logback
+ // from PatternLayout
+ private String logBackPatterns[] = new String[] { "ch.qos.logback.core.pattern.IdentityCompositeConverter", "ch.qos.logback.core.pattern.ReplacingCompositeConverter",
+ "DateConverter", "RelativeTimeConverter", "LevelConverter", "ThreadConverter", "LoggerConverter",
+ "MessageConverter", "ClassOfCallerConverter", "MethodOfCallerConverter", "LineOfCallerConverter",
+ "FileOfCallerConverter", "MDCConverter", "ThrowableProxyConverter", "RootCauseFirstThrowableProxyConverter",
+ "ExtendedThrowableProxyConverter", "NopThrowableInformationConverter", "ContextNameConverter",
+ "CallerDataConverter", "MarkerConverter", "PropertyConverter", "LineSeparatorConverter",
+ "color.BlackCompositeConverter", "color.RedCompositeConverter", "color.GreenCompositeConverter",
+ "color.YellowCompositeConverter", "color.BlueCompositeConverter", "color.MagentaCompositeConverter",
+ "color.CyanCompositeConverter", "color.WhiteCompositeConverter", "color.GrayCompositeConverter",
+ "color.BoldRedCompositeConverter", "color.BoldGreenCompositeConverter",
+ "color.BoldYellowCompositeConverter", "color.BoldBlueCompositeConverter",
+ "color.BoldMagentaCompositeConverter", "color.BoldCyanCompositeConverter",
+ "color.BoldWhiteCompositeConverter", "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter",
+ "LocalSequenceNumberConverter", "org.springframework.boot.logging.logback.ColorConverter",
+ "org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter",
+ "org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"};
+// what would a reflection hint look like here? Would it specify maven coords for logback as a requirement on the classpath?
+// does logback have a feature? or meta data files for graal?
+ private void registerLogback() {
+ try {
+ addAccess("ch.qos.logback.core.Appender", Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ } catch (NoClassDefFoundError e) {
+ System.out.println("Logback not found, skipping registration logback types");
+ return;
+ }
+ addAccess("org.springframework.boot.logging.logback.LogbackLoggingSystem", Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ for (String p: logBackPatterns) {
+ if (p.startsWith("org")) {
+ addAccess(p, Flag.allDeclaredConstructors,Flag.allDeclaredMethods);
+ } else if (p.startsWith("ch.")) {
+ addAccess(p, Flag.allDeclaredConstructors,Flag.allDeclaredMethods);
+ } else if (p.startsWith("color.")) {
+ addAccess("ch.qos.logback.core.pattern."+p,Flag.allDeclaredConstructors,Flag.allDeclaredMethods);
+ } else {
+ addAccess("ch.qos.logback.classic.pattern."+p,Flag.allDeclaredConstructors,Flag.allDeclaredMethods);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/support/ResourcesHandler.java b/src/main/java/org/springframework/graal/support/ResourcesHandler.java
new file mode 100644
index 000000000..397fedbe8
--- /dev/null
+++ b/src/main/java/org/springframework/graal/support/ResourcesHandler.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.support;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.graalvm.nativeimage.ImageSingletons;
+import org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess;
+import org.springframework.graal.domain.reflect.ClassDescriptor.Flag;
+import org.springframework.graal.domain.resources.ResourcesDescriptor;
+import org.springframework.graal.domain.resources.ResourcesJsonMarshaller;
+import org.springframework.graal.type.HintDescriptor;
+import org.springframework.graal.type.MissingTypeException;
+import org.springframework.graal.type.Type;
+import org.springframework.graal.type.TypeSystem;
+
+import com.oracle.svm.core.jdk.Resources;
+import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl;
+import com.oracle.svm.hosted.ImageClassLoader;
+import com.oracle.svm.core.configure.ResourcesRegistry;
+
+public class ResourcesHandler {
+
+ private TypeSystem ts;
+
+ private ImageClassLoader cl;
+
+ private ReflectionHandler reflectionHandler;
+
+ private static boolean REMOVE_UNNECESSARY_CONFIGURATIONS;
+
+ static {
+ REMOVE_UNNECESSARY_CONFIGURATIONS = Boolean.valueOf(System.getProperty("removeUnusedAutoconfig","false"));
+ System.out.println("Remove unused config = "+REMOVE_UNNECESSARY_CONFIGURATIONS);
+ }
+
+ public ResourcesHandler(ReflectionHandler reflectionHandler) {
+ this.reflectionHandler = reflectionHandler;
+ }
+
+ public ResourcesDescriptor compute() {
+ try {
+ InputStream s = this.getClass().getResourceAsStream("/resources.json");
+ ResourcesDescriptor read = ResourcesJsonMarshaller.read(s);
+ return read;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public void register(BeforeAnalysisAccess access) {
+ cl = ((BeforeAnalysisAccessImpl) access).getImageClassLoader();
+ ts = TypeSystem.get(cl.getClasspath());
+ ResourcesDescriptor rd = compute();
+ ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class);
+ // Patterns can be added to the registry, resources can be directly registered
+ // against Resources
+ // resourcesRegistry.addResources("*");
+ // Resources.registerResource(relativePath, inputstream);
+ System.out.println("SBG: adding resources - #" + rd.getPatterns().size()+" patterns");
+
+ for (String pattern : rd.getPatterns()) {
+ if (pattern.equals("META-INF/spring.factories")) {
+ continue; // leave to special handling...
+ }
+// if (pattern.contains("logging.properties")) {
+// URL resource = cl.getClassLoader().getResource(pattern);
+// System.out.println("Can I find "+pattern+"? "+resource);
+// }
+ resourcesRegistry.addResources(pattern);
+ }
+ processSpringFactories();
+ processSpringComponents();
+ }
+
+ public void processSpringComponents() {
+ Enumeration springComponents = fetchResources("META-INF/spring.components");
+ if (springComponents.hasMoreElements()) {
+ log("Processing META-INF/spring.components files...");
+ while (springComponents.hasMoreElements()) {
+ URL springFactory = springComponents.nextElement();
+ processSpringComponents(ts, springFactory);
+ }
+ } else {
+// System.out.println("No META-INF/spring.components found");
+ System.out.println("Found no META-INF/spring.components -> generating one...");
+ List> components = scanClasspathForIndexedStereotypes();
+ List> filteredComponents = filterComponents(components);
+ Properties p = new Properties();
+ for (Entry filteredComponent: filteredComponents) {
+ String k = filteredComponent.getKey();
+ System.out.println("- "+k);
+ p.put(k, filteredComponent.getValue());
+ reflectionHandler.addAccess(k,Flag.allDeclaredConstructors, Flag.allDeclaredMethods, Flag.allDeclaredClasses);
+ ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class);
+ resourcesRegistry.addResources(k.replace(".", "/")+".class");
+ processComponent(k, new HashSet<>());
+ }
+ System.out.println("Computed spring.components is ");
+ System.out.println("vvv");
+ p.list(System.out);
+ System.out.println("^^^");
+ System.out.println(">>> "+p.toString());
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ p.store(baos,"");
+ baos.close();
+ byte[] bs = baos.toByteArray();
+ ByteArrayInputStream bais = new ByteArrayInputStream(bs);
+ Resources.registerResource("META-INF/spring.components", bais);
+ System.out.println("BAOS: "+new String(bs));
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ private void processComponent(String typename, Set visited) {
+ if (!visited.add(typename)) {
+ return;
+ }
+ ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class);
+ Type componentType = ts.resolveDotted(typename);
+ System.out.println("> Component processing: "+typename);
+ List conditionalTypes = componentType.findConditionalOnClassValue();
+ if (conditionalTypes != null) {
+ for (String lDescriptor : conditionalTypes) {
+ Type t = ts.Lresolve(lDescriptor, true);
+ boolean exists = (t != null);
+ if (!exists) {
+ return;
+ } else {
+ try {
+ reflectionHandler.addAccess(lDescriptor.substring(1,lDescriptor.length()-1).replace("/", "."),Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ resourcesRegistry.addResources(lDescriptor.substring(1,lDescriptor.length()-1)+".class");
+ } catch (NoClassDefFoundError e) {
+ System.out.println("Conditional type "+fromLtoDotted(lDescriptor)+" not found for component "+componentType.getName());
+ }
+
+ }
+ }
+ }
+ try {
+ // String configNameDotted = configType.getName().replace("/",".");
+ System.out.println("Including auto-configuration "+typename);
+ reflectionHandler.addAccess(typename,Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ resourcesRegistry.addResources(typename.replace(".", "/")+".class");
+ } catch (NoClassDefFoundError e) {
+ // Example:
+ // PROBLEM? Can't register Type:org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration because cannot find javax/servlet/Filter
+ // java.lang.NoClassDefFoundError: javax/servlet/Filter
+ // ... at com.oracle.svm.hosted.config.ReflectionRegistryAdapter.registerDeclaredConstructors(ReflectionRegistryAdapter.java:97)
+ System.out.println("PROBLEM? Can't register "+typename+" because cannot find "+e.getMessage());
+ }
+
+ Map> imports = componentType.findImports();
+ if (imports != null) {
+ System.out.println("Imports found on "+typename+" are "+imports);
+ for (Map.Entry> importsEntry: imports.entrySet()) {
+ reflectionHandler.addAccess(importsEntry.getKey(),Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ for (String imported: importsEntry.getValue()) {
+ String importedName = fromLtoDotted(imported);
+ try {
+ Type t = ts.resolveDotted(importedName);
+ processComponent( t.getName().replace("/", "."), visited);
+ } catch (MissingTypeException mte) {
+ System.out.println("Cannot find imported "+importedName+" so skipping processing that");
+ }
+ }
+ }
+ }
+
+ // Without this code, error at:
+ // java.lang.ClassNotFoundException cannot be cast to java.lang.Class[]
+ // at org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector$ConfigurationPropertiesBeanRegistrar.lambda$collectClasses$1(EnableConfigurationPropertiesImportSelector.java:80)
+ List ecProperties = componentType.findEnableConfigurationPropertiesValue();
+ if (ecProperties != null) {
+ for (String ecPropertyDescriptor: ecProperties) {
+ String ecPropertyName = fromLtoDotted(ecPropertyDescriptor);
+ System.out.println("ECP "+ecPropertyName);
+ try {
+ reflectionHandler.addAccess(ecPropertyName,Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ resourcesRegistry.addResources(ecPropertyName.replace(".", "/")+".class");
+ } catch (NoClassDefFoundError e) {
+ System.out.println("Not found for registration: "+ecPropertyName);
+ }
+ }
+ }
+
+ // Find @Bean methods and add them
+// List methodsWithAtBean = configType.getMethodsWithAtBean();
+// if (methodsWithAtBean.size() != 0) {
+// System.out.println(configType+" here they are: "+
+// methodsWithAtBean.stream().map(m -> m.getName()+m.getDesc()).collect(Collectors.toList()));
+// for (Method m: methodsWithAtBean) {
+// String desc = m.getDesc();
+// String retType = desc.substring(desc.lastIndexOf(")")+1); //Lorg/springframework/boot/task/TaskExecutorBuilder;
+// System.out.println("@Bean return type "+retType);
+// reflectionHandler.addAccess(fromLtoDotted(retType), Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+// }
+// }
+
+ List nestedTypes = componentType.getNestedTypes();
+ for (Type t: nestedTypes) {
+ if (visited.add(t.getName())) {
+ processComponent(t.getName().replace("/", "."), visited);
+ }
+ }
+ }
+
+ private void processSpringComponents(TypeSystem ts, URL springComponentsFile) {
+ Properties p = new Properties();
+ loadSpringFactoryFile(springComponentsFile, p);
+ // Example:
+ // com.example.demo.Foobar=org.springframework.stereotype.Component
+ // com.example.demo.DemoApplication=org.springframework.stereotype.Component
+ Enumeration keys = p.keys();
+ ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class);
+ while (keys.hasMoreElements()) {
+ String k = (String)keys.nextElement();
+ System.out.println("Registering Spring Component: "+k);
+ reflectionHandler.addAccess(k,Flag.allDeclaredConstructors, Flag.allDeclaredMethods, Flag.allDeclaredClasses);
+ resourcesRegistry.addResources(k.replace(".", "/")+".class");
+ // Register nested types of the component
+ Type baseType = ts.resolveDotted(k);
+ for (Type t: baseType.getNestedTypes()) {
+ String n = t.getName().replace("/", ".");
+ reflectionHandler.addAccess(n,Flag.allDeclaredConstructors, Flag.allDeclaredMethods, Flag.allDeclaredClasses);
+ resourcesRegistry.addResources(t.getName()+".class");
+ }
+ registerHierarchy(baseType, new HashSet<>(), resourcesRegistry);
+ }
+ }
+
+ public void registerHierarchy(Type t, Set visited, ResourcesRegistry resourcesRegistry) {
+ if (t == null || t.getName().equals("java/lang/Object") || !visited.add(t)) {
+ return;
+ }
+ String desc = t.getName();
+ System.out.println("Hierarchy registration of "+t.getName());
+ reflectionHandler.addAccess(desc.replace("/", "."),Flag.allDeclaredConstructors, Flag.allDeclaredMethods, Flag.allDeclaredClasses);
+ resourcesRegistry.addResources(desc.replace("$", ".")+".class");
+ Type s = t.getSuperclass();
+ registerHierarchy(s, visited, resourcesRegistry);
+ Type[] is = t.getInterfaces();
+ for (Type i: is) {
+ registerHierarchy(i, visited, resourcesRegistry);
+ }
+ // TODO inners of those supertypes/interfaces?
+ }
+
+ /**
+ * Find all META-INF/spring.factories - for any configurations listed in each, check if those configurations use ConditionalOnClass.
+ * If the classes listed in ConditionalOnClass can't be found, discard the configuration from spring.factories. Register either
+ * the unchanged or modified spring.factories files with the system.
+ */
+ public void processSpringFactories() {
+ log("Processing META-INF/spring.factories files...");
+ Enumeration springFactories = fetchResources("META-INF/spring.factories");
+ while (springFactories.hasMoreElements()) {
+ URL springFactory = springFactories.nextElement();
+ processSpringFactory(ts, springFactory);
+ }
+ }
+
+
+ private List> filterComponents(List> as) {
+ List> filtered = new ArrayList<>();
+ List> subtypesToRemove = new ArrayList<>();
+ for (Entry a: as) {
+ String type = a.getKey();
+ subtypesToRemove.addAll(as.stream().filter(e -> e.getKey().startsWith(type+"$")).collect(Collectors.toList()));
+ }
+ filtered.addAll(as);
+ filtered.removeAll(subtypesToRemove);
+ return filtered;
+ }
+
+ private List> scanClasspathForIndexedStereotypes() {
+ return findDirectories(ts.getClasspath())
+ .flatMap(this::findClasses)
+ .map(this::typenameOfClass)
+ .map(this::isIndexedOrEntity)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+
+// app.main.SampleTransactionManagementConfiguration=org.springframework.stereotype.Component
+// app.main.model.Foo=javax.persistence.Entity
+// app.main.model.FooRepository=org.springframework.data.repository.Repository
+// app.main.SampleApplication=org.springframework.stereotype.Component
+ private Entry isIndexedOrEntity(String slashedClassname) {
+ Entry entry = ts.resolveSlashed(slashedClassname).isIndexedOrEntity();
+// if (entry != null) {
+// System.out.println("isIndexed for "+slashedClassname+" returned "+entry);
+// }
+ return entry;
+ }
+
+ private String typenameOfClass(File f) {
+ return Utils.scanClass(f).getClassname();
+ }
+
+ private Stream findClasses(File dir) {
+ ArrayList classfiles = new ArrayList<>();
+ walk(dir,classfiles);
+ return classfiles.stream();
+ }
+
+ private void walk(File dir, ArrayList classfiles) {
+ File[] fs = dir.listFiles();
+ for (File f: fs) {
+ if (f.isDirectory()) {
+ walk(f,classfiles);
+ } else if (f.getName().endsWith(".class")) {
+ classfiles.add(f);
+ }
+ }
+ }
+
+ private Stream findDirectories(List classpath) {
+ List directories = new ArrayList<>();
+ for (String classpathEntry: classpath) {
+ File f = new File(classpathEntry);
+ if (f.isDirectory()) {
+ directories.add(f);
+ }
+ }
+ return directories.stream();
+ }
+
+ private void processSpringFactory(TypeSystem ts, URL springFactory) {
+ List forRemoval = new ArrayList<>();
+ Properties p = new Properties();
+ loadSpringFactoryFile(springFactory, p);
+
+ Enumeration keyz = p.keys();
+ // Handle all keys except EnableAutoConfiguration
+ while (keyz.hasMoreElements()) {
+ String k = (String)keyz.nextElement();
+ if (!k.equals("org.springframework.boot.autoconfigure.EnableAutoConfiguration")) {
+ String classesList = p.getProperty(k);
+ for (String s: classesList.split(",")) {
+ try {
+ reflectionHandler.addAccess(s,Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ System.out.println("NEEDS ADDING TO RESOURCE LIST? "+s);
+ } catch (NoClassDefFoundError ncdfe) {
+ System.out.println("SBG: WARNING: Whilst processing "+k+" problem adding access for type: "+s+" because of missing "+ncdfe.getMessage());
+ }
+ }
+ }
+ }
+
+ String configsString = (String) p.get("org.springframework.boot.autoconfigure.EnableAutoConfiguration");
+ if (configsString != null) {
+ List configs = new ArrayList<>();
+ for (String s: configsString.split(",")) {
+ configs.add(s);
+ }
+ // TODO what about ConditionalOnResource?
+ System.out.println(
+ "Spring.factories processing: looking at #" + configs.size() + " configuration references");
+ for (Iterator iterator = configs.iterator(); iterator.hasNext();) {
+ String config = iterator.next();
+ boolean needToAddThem = true;
+ if (!verifyType(config)) {
+ System.out.println("Excluding auto-configuration " + config);
+ System.out.println("= COC failed so just adding class forname access (no methods/ctors)");
+ if (REMOVE_UNNECESSARY_CONFIGURATIONS) {
+ forRemoval.add(config);
+ needToAddThem = false;
+ }
+ }
+ if (needToAddThem) {
+ System.out.println("Resource Adding: "+config);
+ reflectionHandler.addAccess(config); // no flags as it isn't going to trigger
+ ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class);
+ resourcesRegistry.addResources(config.replace(".", "/").replace("$", ".")+".class");
+ }
+ }
+ configs.removeAll(forRemoval);
+ p.put("org.springframework.boot.autoconfigure.EnableAutoConfiguration", String.join(",", configs));
+ }
+ try {
+ if (forRemoval.size() == 0) {
+ Resources.registerResource("META-INF/spring.factories", springFactory.openStream());
+ } else {
+ System.out.println(" removed " + forRemoval.size() + " configurations");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ p.store(baos,"");
+ baos.close();
+ byte[] bs = baos.toByteArray();
+ ByteArrayInputStream bais = new ByteArrayInputStream(bs);
+ Resources.registerResource("META-INF/spring.factories", bais);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private void loadSpringFactoryFile(URL springFactory, Properties p) {
+ try (InputStream is = springFactory.openStream()) {
+ p.load(is);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to load spring.factories", e);
+ }
+ }
+
+ /**
+ * For the specified type (dotted name) determine which types must be reflectable at runtime. This means
+ * looking at annotations and following any type references within those.
+ */
+ private boolean verifyType(String name) {
+ return processType(name, new HashSet<>());
+ }
+
+ private boolean processType(String config, Set visited) {
+ return processType(ts.resolveDotted(config), visited, 0);
+ }
+
+ private boolean processType(Type configType, Set visited, int depth) {
+ System.out.println(spaces(depth)+"Processing type "+configType.getName());
+ ResourcesRegistry resourcesRegistry = ImageSingletons.lookup(ResourcesRegistry.class);
+
+ // This would fetch 'things we care about from a graal point of view'
+ // a list
+ // ConditionalOnClass (annotation instance)
+ // configType.getRelevantAnnotations();
+ // Then for each of those give me the relevant 'types i have to look around for'
+
+
+ Set missing = ts.resolveCompleteFindMissingTypes(configType);
+ if (!missing.isEmpty()) {
+ // No point continuing with this type, it cannot be resolved against current classpath
+ // The assumption is it will never need to be accessed anyway
+ System.out.println(spaces(depth)+"for "+configType.getName()+" missing types are "+missing);
+ return false;
+ }
+
+ Set missingAnnotationTypes = ts.resolveCompleteFindMissingAnnotationTypes(configType);
+ if (!missingAnnotationTypes.isEmpty()) {
+ // If only the annotations are missing, it is ok to reflect on the existence of the type, it is
+ // just not safe to reflect on the annotations on that type.
+ System.out.println(spaces(depth)+"for "+configType.getName()+" missing annotation types are "+missingAnnotationTypes);
+ }
+ boolean passesTests = true;
+ Set toMakeAccessible = new HashSet<>();
+ Map> hints = configType.getHints();
+ if (!hints.isEmpty()) {
+ int h=1;
+ for (Map.Entry> hint: hints.entrySet()) {
+ HintDescriptor hintDescriptor = hint.getKey();
+ List typeReferences = hint.getValue();
+ System.out.println(spaces(depth)+"checking @CompilationHint "+h+"/"+hints.size()+" "+hintDescriptor.getAnnotationChain());
+
+ String[] name = hintDescriptor.getName();
+ if (name != null) {
+ // The CollectionHint included a list of types to worry about in the annotation
+ // itself (e.g. as used on import selector to specify.
+ for (String n: name) {
+ resourcesRegistry.addResources(n.replace(".", "/")+".class");
+ reflectionHandler.addAccess(n,Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ }
+ }
+
+ if (h==1) { // only do this once all hints should have this in common, TODO polish this up...
+ // This handles the case for something like:
+ // ReactiveWebServerFactoryConfiguration$EmbeddedTomcat with ConditionalOnClass
+ // TODO is this too much repetition for certain types?
+ for (Type annotatedType : hintDescriptor.getAnnotationChain()) {
+ try {
+ System.out.println("Handling annotated thingy: "+annotatedType.getName());
+ String t = annotatedType.getDescriptor();
+ reflectionHandler.addAccess(t.substring(1,t.length()-1).replace("/", "."),Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ resourcesRegistry.addResources(t.substring(1,t.length()-1).replace("$", ".")+".class");
+ } catch (NoClassDefFoundError e) {
+ System.out.println(spaces(depth)+annotatedType.getName()+" not found for configuration "+configType.getName());
+ }
+
+ }
+ }
+
+ if (typeReferences != null) {
+ for (String typeReference: typeReferences) { // La/b/C;
+ Type t = ts.Lresolve(typeReference, true);
+ boolean exists = (t != null);
+ System.out.println(spaces(depth)+" does "+fromLtoDotted(typeReference)+" exist? "+exists);
+ if (exists) {
+ // TODO should this specify what aspects of reflection are required (methods/fields/ctors/annotations)
+ toMakeAccessible.add(typeReference);
+ if (hintDescriptor.isFollow()) {
+ processType(t, visited, depth+1);
+ }
+ } else if (hintDescriptor.isSkipIfTypesMissing()) {
+ passesTests = false;
+ }
+ }
+ }
+ h++;
+ }
+ }
+
+ if (passesTests || !REMOVE_UNNECESSARY_CONFIGURATIONS) {
+ for (String t: toMakeAccessible) {
+ try {
+ reflectionHandler.addAccess(t.substring(1,t.length()-1).replace("/", "."),Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ resourcesRegistry.addResources(t.substring(1,t.length()-1).replace("$", ".")+".class");
+ } catch (NoClassDefFoundError e) {
+ System.out.println(spaces(depth)+"Conditional type "+fromLtoDotted(t)+" not found for configuration "+configType.getName());
+ }
+ }
+ }
+
+ if (passesTests) {
+ try {
+ String configNameDotted = configType.getName().replace("/",".");
+ System.out.println(spaces(depth)+"including reflective/resource access to "+configNameDotted);
+ visited.add(configType.getName());
+ reflectionHandler.addAccess(configNameDotted,Flag.allDeclaredConstructors, Flag.allDeclaredMethods);
+ System.out.println("res: "+configType.getName().replace("$", ".")+".class");
+ resourcesRegistry.addResources(configType.getName().replace("$", ".")+".class");
+ // In some cases the superclass of the config needs to be accessible
+ // TODO need this guard? if (isConfiguration(configType)) {
+ registerHierarchy(configType, new HashSet<>(), resourcesRegistry);
+ } catch (NoClassDefFoundError e) {
+ // Example:
+ // PROBLEM? Can't register Type:org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration because cannot find javax/servlet/Filter
+ // java.lang.NoClassDefFoundError: javax/servlet/Filter
+ // ... at com.oracle.svm.hosted.config.ReflectionRegistryAdapter.registerDeclaredConstructors(ReflectionRegistryAdapter.java:97)
+ System.out.println("PROBLEM? Can't register "+configType.getName()+" because cannot find "+e.getMessage());
+ }
+ }
+
+ // HibernateJpaConfiguration has a supertype also covered with @Configuration - so more than just registering
+ // the hierarchy as accessible, it may contain more config to chase down
+ Type s = configType.getSuperclass();
+ while (s!= null) {
+ processType(s, visited, depth+1);
+ s = s.getSuperclass();
+ }
+
+ // If the outer type is failing a test, we don't need to recurse...
+ if (passesTests) {
+ List nestedTypes = configType.getNestedTypes();
+ for (Type t: nestedTypes) {
+ if (visited.add(t.getName())) {
+ processType(t, visited, depth+1);
+ }
+ }
+ } else {
+ System.out.println("INFO: tests failed on "+configType.getName()+" so not going into nested types");
+ }
+ return passesTests;
+ }
+
+ String fromLtoDotted(String lDescriptor) {
+ return lDescriptor.substring(1,lDescriptor.length()-1).replace("/", ".");
+ }
+
+ private Enumeration fetchResources(String resource) {
+ try {
+ Enumeration resources = Thread.currentThread().getContextClassLoader().getResources(resource);
+ return resources;
+ } catch (IOException e1) {
+ return Collections.enumeration(Collections.emptyList());
+ }
+ }
+
+ private void log(String msg) {
+ System.out.println(msg);
+ }
+
+ private String spaces(int depth) {
+ return " ".substring(0,depth*2);
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/support/SpringFeature.java b/src/main/java/org/springframework/graal/support/SpringFeature.java
new file mode 100644
index 000000000..a7a2a1d0c
--- /dev/null
+++ b/src/main/java/org/springframework/graal/support/SpringFeature.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.support;
+
+import java.nio.Buffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.graalvm.nativeimage.hosted.Feature;
+
+import com.oracle.svm.core.annotate.AutomaticFeature;
+import com.oracle.svm.hosted.ResourcesFeature;
+import com.oracle.svm.reflect.hosted.ReflectionFeature;
+import com.oracle.svm.reflect.proxy.hosted.DynamicProxyFeature;
+
+@AutomaticFeature
+public class SpringFeature implements Feature {
+
+ private ReflectionHandler reflectionHandler;
+
+ private DynamicProxiesHandler dynamicProxiesHandler;
+
+ private ResourcesHandler resourcesHandler;
+
+ private InitializationHandler buildTimeInitializationHandler;
+
+ public SpringFeature() {
+ System.out.println(
+ " ____ _ _____ _ \n"+
+ "/ ___| _ __ _ __(_)_ __ __ _ | ___|__ __ _| |_ _ _ _ __ ___ \n"+
+ "\\___ \\| '_ \\| '__| | '_ \\ / _` | | |_ / _ \\/ _` | __| | | | '__/ _ \\\n"+
+ " ___) | |_) | | | | | | | (_| | | _| __/ (_| | |_| |_| | | | __/\n"+
+ "|____/| .__/|_| |_|_| |_|\\__, | |_| \\___|\\__,_|\\__|\\__,_|_| \\___|\n"+
+ " |_| |___/ \n");
+ reflectionHandler = new ReflectionHandler();
+ dynamicProxiesHandler = new DynamicProxiesHandler();
+ resourcesHandler = new ResourcesHandler(reflectionHandler);
+ buildTimeInitializationHandler = new InitializationHandler();
+ }
+
+ public boolean isInConfiguration(IsInConfigurationAccess access) {
+ return true;
+ }
+
+ public List> getRequiredFeatures() {
+ List> fs = new ArrayList<>();
+ fs.add(DynamicProxyFeature.class); // Ensures DynamicProxyRegistry available
+ fs.add(ResourcesFeature.class); // Ensures ResourcesRegistry available
+ fs.add(ReflectionFeature.class); // Ensures RuntimeReflectionSupport available
+ return fs;
+ }
+
+ public void duringSetup(DuringSetupAccess access) {
+ reflectionHandler.register(access);
+ dynamicProxiesHandler.register(access);
+ }
+
+ public void beforeAnalysis(BeforeAnalysisAccess access) {
+ resourcesHandler.register(access);
+ buildTimeInitializationHandler.register(access);
+ // TODO who requires this, is it a netty thing?
+ try {
+ access.registerAsUnsafeAccessed(Buffer.class.getDeclaredField("address"));
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/support/Utils.java b/src/main/java/org/springframework/graal/support/Utils.java
new file mode 100644
index 000000000..6a9414426
--- /dev/null
+++ b/src/main/java/org/springframework/graal/support/Utils.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.support;
+
+import java.io.File;
+
+import org.springframework.graal.type.ConstantPoolScanner;
+
+public class Utils {
+
+ public static ConstantPoolScanner scanClass(File f) {
+ return new ConstantPoolScanner(f);
+ }
+
+}
diff --git a/src/main/java/org/springframework/graal/type/CompilationHint.java b/src/main/java/org/springframework/graal/type/CompilationHint.java
new file mode 100644
index 000000000..cbc9db563
--- /dev/null
+++ b/src/main/java/org/springframework/graal/type/CompilationHint.java
@@ -0,0 +1,27 @@
+package org.springframework.graal.type;
+
+public @interface CompilationHint {
+
+ // When attached to an annotation, this indicates which fields in that annotation
+ // hold type references, e.g. if on ConditionalOnClass, names={"value","name"}
+ String[] fieldNames();
+
+ // If difficult to tell the types involved from the thing being annotated, the info can be put here
+ // (e.g. you have an ImportSelector returning classnames, the possible names should be in here)
+ String[] name();
+ Class>[] value();
+
+ // If true then whatever class is annotated/meta-annotateed with this is useless if
+ // the types visible through the names() fields are not found.
+ boolean skipIfTypesMissing();
+
+ // If true, then whatever types are referenced need to be followed because they may
+ // be further annotated/meta-annotated with compilation hints
+ boolean follow();
+
+ // Do we need to specify what reflection should be accessible? (Fields/Methods/Ctors)?
+ // Reducing the amount could likely help the image size
+
+ // If true, whatever is (meta-)annotated with this must be accessible via getResource too.
+ boolean accessibleAsResource();
+}
diff --git a/src/main/java/org/springframework/graal/type/ConstantPoolScanner.java b/src/main/java/org/springframework/graal/type/ConstantPoolScanner.java
new file mode 100644
index 000000000..20fc7325a
--- /dev/null
+++ b/src/main/java/org/springframework/graal/type/ConstantPoolScanner.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2019 Contributors
+ *
+ * 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
+ *
+ * https://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 org.springframework.graal.type;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Enables us to check things quickly in the constant pool. Just parses the class up to the end of the constant pool.
+ *
+ * Useful reference: https://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html
+ *
+ * @author Andy Clement
+ */
+public class ConstantPoolScanner {
+
+ private static final boolean DEBUG = false;
+
+ private final static byte CONSTANT_Utf8 = 1;
+
+ private final static byte CONSTANT_Integer = 3;
+
+ private final static byte CONSTANT_Float = 4;
+
+ private final static byte CONSTANT_Long = 5;
+
+ private final static byte CONSTANT_Double = 6;
+
+ private final static byte CONSTANT_Class = 7;
+
+ private final static byte CONSTANT_String = 8;
+
+ private final static byte CONSTANT_Fieldref = 9;
+
+ private final static byte CONSTANT_Methodref = 10;
+
+ private final static byte CONSTANT_InterfaceMethodref = 11;
+
+ private final static byte CONSTANT_NameAndType = 12;
+
+ private final static byte CONSTANT_MethodHandle = 15;
+
+ private final static byte CONSTANT_MethodType = 16;
+
+ private final static byte CONSTANT_InvokeDynamic = 18;
+
+ private byte[] classbytes;
+
+ // Used during the parse step
+ private int ptr;
+
+ // Filled with strings and int[]
+ private Object[] cpdata;
+
+ private int cpsize;
+
+ private int[] type;
+
+ // Does not need to be a set as there are no dups in the ConstantPool (for a class from a decent compiler...)
+ private List referencedClasses = new ArrayList();
+
+ private List referencedMethods = new ArrayList();
+
+ private String slashedclassname;
+
+
+ public static References getReferences(byte[] classbytes) {
+ ConstantPoolScanner cpScanner = new ConstantPoolScanner(classbytes);
+ return new References(cpScanner.slashedclassname, cpScanner.referencedClasses, cpScanner.referencedMethods);
+ }
+
+ public static References getReferences(File f) {
+ try {
+ byte[] bytes = Files.readAllBytes(Paths.get(f.toURI()));
+ ConstantPoolScanner cpScanner = new ConstantPoolScanner(bytes);
+ return new References(cpScanner.slashedclassname, cpScanner.referencedClasses, cpScanner.referencedMethods);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public ConstantPoolScanner(File f) {
+ this(readBytes(f));
+ }
+
+ public static byte[] readBytes(File f) {
+ try {
+ byte[] bytes = Files.readAllBytes(Paths.get(f.toURI()));
+ return bytes;
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private ConstantPoolScanner(byte[] bytes) {
+ parseClass(bytes);
+// computeReferences();
+ }
+
+ // Format of a classfile:
+ // ClassFile {
+ // u4 magic;
+ // u2 minor_version;
+ // u2 major_version;
+ // u2 constant_pool_count;
+ // cp_info constant_pool[constant_pool_count-1];
+ // u2 access_flags;
+ // u2 this_class;
+ // u2 super_class;
+ // u2 interfaces_count;
+ // u2 interfaces[interfaces_count];
+ // u2 fields_count;
+ // field_info fields[fields_count];
+ // u2 methods_count;
+ // method_info methods[methods_count];
+ // u2 attributes_count;
+ // attribute_info attributes[attributes_count];
+ // }
+ private void parseClass(byte[] bytes) {
+ try {
+ this.classbytes = bytes;
+ this.ptr = 0;
+ int magic = readInt(); // magic 0xCAFEBABE
+ if (magic != 0xCAFEBABE) {
+ throw new IllegalStateException("not bytecode, magic was 0x" + Integer.toString(magic, 16));
+ }
+ ptr += 4; // skip minor and major versions
+ cpsize = readUnsignedShort();
+ if (DEBUG) {
+ System.out.println("Constant Pool Size =" + cpsize);
+ }
+ cpdata = new Object[cpsize];
+ type = new int[cpsize];
+ for (int cpentry = 1; cpentry < cpsize; cpentry++) {
+ boolean wasDoubleSlotItem = processConstantPoolEntry(cpentry);
+ if (wasDoubleSlotItem) {
+ cpentry++;
+ }
+ }
+ ptr += 2; // access flags
+ int thisclassname = readUnsignedShort();
+ int classindex = ((Integer) cpdata[thisclassname]);
+ slashedclassname = accessUtf8(classindex);
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Unexpected problem processing bytes for class", e);
+ }
+ }
+
+ public String getClassname() {
+ return slashedclassname;
+ }
+
+ /**
+ * Return the UTF8 at the specified index in the constant pool. The data found at the constant pool for that index
+ * may not have been unpacked yet if this is the first access of the string. If not unpacked the constant pool entry
+ * is a pair of ints in an array representing the offset and length within the classbytes where the UTF8 string is
+ * encoded. Once decoded the constant pool entry is flipped from an int array to a String for future fast access.
+ *
+ * @param cpIndex constant pool index
+ * @return UTF8 string at that constant pool index
+ */
+ private String accessUtf8(int cpIndex) {
+ Object object = cpdata[cpIndex];
+ if (object instanceof String) {
+ return (String) object;
+ }
+ int[] ptrAndLen = (int[]) object;
+ String value;
+ try {
+ value = new String(classbytes, ptrAndLen[0], ptrAndLen[1], "UTF8");
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException("Bad data found at constant pool position " + cpIndex + " offset="
+ + ptrAndLen[0] + " length=" + ptrAndLen[1], e);
+ }
+ cpdata[cpIndex] = value;
+ return value;
+ }
+
+ /**
+ * @return an int constructed from the next four bytes to be processed
+ */
+ private final int readInt() {
+ return ((classbytes[ptr++] & 0xFF) << 24) + ((classbytes[ptr++] & 0xFF) << 16)
+ + ((classbytes[ptr++] & 0xFF) << 8)
+ + (classbytes[ptr++] & 0xFF);
+ }
+
+ /**
+ * @return an unsigned short constructed from the next two bytes to be processed
+ */
+ private final int readUnsignedShort() {
+ return ((classbytes[ptr++] & 0xff) << 8) + (classbytes[ptr++] & 0xff);
+ }
+
+ private boolean processConstantPoolEntry(int index) throws IOException {
+ byte b = classbytes[ptr++];
+ switch (b) {
+ case CONSTANT_Integer: // CONSTANT_Integer_info { u1 tag; u4 bytes; }
+ case CONSTANT_Float: // CONSTANT_Float_info { u1 tag; u4 bytes; }
+ case CONSTANT_Fieldref: // CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
+ case CONSTANT_InterfaceMethodref: // CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
+ case CONSTANT_InvokeDynamic: // CONSTANT_InvokeDynamic_info { u1 tag; u2 bootstrap_method_attr_index; u2 name_and_type_index; }
+ ptr += 4;
+ break;
+ case CONSTANT_Utf8:
+ // CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
+ // Cache just the index and length - do not unpack it now
+ int len = readUnsignedShort();
+ cpdata[index] = new int[] { ptr, len };
+ ptr += len;
+ break;
+ case CONSTANT_Long: // CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; }
+ case CONSTANT_Double: // CONSTANT_Double_info { u1 tag; u4 high_bytes; u4 low_bytes; }
+ ptr += 8;
+ return true;
+ case CONSTANT_Class: // CONSTANT_Class_info { u1 tag; u2 name_index; }
+ type[index] = b;
+ cpdata[index] = readUnsignedShort();
+ break;
+ case CONSTANT_Methodref:
+ // CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
+ type[index] = b;
+ cpdata[index] = new int[] { readUnsignedShort(), readUnsignedShort() };
+ break;
+ case CONSTANT_NameAndType:
+ // The CONSTANT_NameAndType_info structure is used to represent a field or method, without indicating which class or interface type it belongs to:
+ // CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; }
+ // type[index] = b;
+ cpdata[index] = readUnsignedShort();
+ ptr += 2; // skip the descriptor for now
+ break;
+ case CONSTANT_MethodHandle:
+ // CONSTANT_MethodHandle_info { u1 tag; u1 reference_kind; u2 reference_index; }
+ ptr += 3;
+ break;
+ case CONSTANT_String: // CONSTANT_String_info { u1 tag; u2 string_index; }
+ case CONSTANT_MethodType: // CONSTANT_MethodType_info { u1 tag; u2 descriptor_index; }
+ ptr += 2;
+ break;
+ default:
+ throw new IllegalStateException("Entry: " + index + " " + Byte.toString(b));
+ }
+ return false;
+ }
+
+ private void computeReferences() {
+ for (int i = 0; i < cpsize; i++) {
+ switch (type[i]) {
+ case CONSTANT_Class:
+ int classindex = ((Integer) cpdata[i]);
+ String classname = accessUtf8(classindex);
+ if (classname == null) {
+ throw new IllegalStateException();
+ }
+ referencedClasses.add(classname);
+ break;
+ case CONSTANT_Methodref:
+ int[] indexes = (int[]) cpdata[i];
+ int classindex2 = indexes[0];
+ int nameAndTypeIndex = indexes[1];
+ StringBuilder s = new StringBuilder();
+ String theClassName = accessUtf8((Integer) cpdata[classindex2]);
+ if (theClassName.charAt(0) == 'j') {
+ s.append(theClassName);
+ s.append(".");
+ s.append(accessUtf8((Integer) cpdata[nameAndTypeIndex]));
+ referencedMethods.add(s.toString());
+ }
+ break;
+ // private final static byte CONSTANT_Utf8 = 1;
+ // private final static byte CONSTANT_Integer = 3;
+ // private final static byte CONSTANT_Float = 4;
+ // private final static byte CONSTANT_Long = 5;
+ // private final static byte CONSTANT_Double = 6;
+ // private final static byte CONSTANT_String = 8;
+ // private final static byte CONSTANT_Fieldref = 9;
+ // private final static byte CONSTANT_InterfaceMethodref = 11;
+ // private final static byte CONSTANT_NameAndType = 12;
+ }
+ }
+ }
+
+
+ public static class References {
+
+ public final String slashedClassName;
+
+ private final List referencedClasses;
+
+ private final List referencedMethods;
+
+ References(String slashedClassName, List rc, List rm) {
+ this.slashedClassName = slashedClassName;
+ this.referencedClasses = rc;
+ this.referencedMethods = rm;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder();
+ s.append("Class=").append(slashedClassName).append("\n");
+ s.append("ReferencedClasses=#").append(referencedClasses.size()).append("\n");
+ s.append("ReferencedMethods=#").append(referencedMethods.size()).append("\n");
+ return s.toString();
+ }
+
+ /**
+ * @return list of classes of the form org/springframework/boot/configurationprocessor/json/JSONException
+ */
+ public List getReferencedClasses() {
+ return referencedClasses;
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/graal/type/HintDescriptor.java b/src/main/java/org/springframework/graal/type/HintDescriptor.java
new file mode 100644
index 000000000..7b4765e3a
--- /dev/null
+++ b/src/main/java/org/springframework/graal/type/HintDescriptor.java
@@ -0,0 +1,51 @@
+package org.springframework.graal.type;
+
+import java.util.List;
+
+/**
+ * Represents a usage of @CompilationHint.
+ *
+ * @author Andy Clement
+ */
+public class HintDescriptor {
+
+ // This is the annotation 'chain' from the type that got asked about to the thing with @CompilationHint
+ // This chain may be short (e.g. if an autoconfig has @ConditionalOnClass on it which itself
+ // is meta annotated with @CompilationHint): chain will be [ConditionalOnClass]
+ // or it may be long: (e.g. if the autoconfig has an @EnableFoo on it which itself is marked
+ // with @ConditionalOnClass which in turn has CompilationHint) chain will be [EnableFoo, ConditionalOnClass]
+ private List annotationChain;
+
+ // If any types hinted at are missing, is this type effectively redundant?
+ private boolean skipIfTypesMissing;
+
+ // Should any types references be followed because they may also have further
+ // hints on them (e.g. @Import(Foo) where Foo has @Import(Bar) on it)
+ private boolean follow;
+
+ private String[] name;
+
+ public HintDescriptor(List annotationChain, boolean skipIfTypesMissing2, boolean follow, String[] name) {
+ this.annotationChain = annotationChain;
+ this.skipIfTypesMissing = skipIfTypesMissing2;
+ this.follow = follow;
+ this.name = name;
+ }
+
+ public List getAnnotationChain() {
+ return annotationChain;
+ }
+
+ public boolean isSkipIfTypesMissing() {
+ return skipIfTypesMissing;
+ }
+
+ public boolean isFollow() {
+ return follow;
+ }
+
+ public String[] getName() {
+ return name;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/graal/type/Method.java b/src/main/java/org/springframework/graal/type/Method.java
new file mode 100644
index 000000000..8bb046d77
--- /dev/null
+++ b/src/main/java/org/springframework/graal/type/Method.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.type;
+
+import org.objectweb.asm.tree.MethodNode;
+
+public class Method {
+
+ MethodNode mn;
+
+ public Method(MethodNode mn) {
+ this.mn = mn;
+ }
+
+ public String toString() {
+ return mn.name+mn.desc;
+ }
+
+ public String getName() {
+ return mn.name;
+ }
+
+ public String getDesc() {
+ return mn.desc;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/graal/type/MissingTypeException.java b/src/main/java/org/springframework/graal/type/MissingTypeException.java
new file mode 100644
index 000000000..e63af7f94
--- /dev/null
+++ b/src/main/java/org/springframework/graal/type/MissingTypeException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.type;
+
+public class MissingTypeException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+ private String typename;
+
+ public MissingTypeException(String slashedTypeName) {
+ this.typename = slashedTypeName;
+ }
+
+ @Override
+ public String getMessage() {
+ return "Unable to find class file for " + typename;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/springframework/graal/type/Type.java b/src/main/java/org/springframework/graal/type/Type.java
new file mode 100644
index 000000000..36eb3dbe1
--- /dev/null
+++ b/src/main/java/org/springframework/graal/type/Type.java
@@ -0,0 +1,1074 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * 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 org.springframework.graal.type;
+
+import java.lang.reflect.Modifier;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.Stack;
+import java.util.stream.Collectors;
+
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.signature.SignatureReader;
+import org.objectweb.asm.signature.SignatureVisitor;
+import org.objectweb.asm.tree.AnnotationNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.InnerClassNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/**
+ * @author Andy Clement
+ */
+public class Type {
+
+ // Spring Security
+ public final static String OAuth2ImportSelector = "Lorg/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector;";
+
+ public final static String SpringWebMvcImportSelector = "Lorg/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector;";
+
+ public final static String ImportSelector ="Lorg/springframework/context/annotation/ImportSelector;";
+
+ public final static String TransactionManagementConfigurationSelector = "Lorg/springframework/transaction/annotation/TransactionManagementConfigurationSelector;";
+
+ public final static String SpringDataWebConfigurationSelector = "Lorg/springframework/data/web/config/EnableSpringDataWebSupport$SpringDataWebConfigurationImportSelector;";
+
+ public final static String SpringDataWebQueryDslSelector = "Lorg/springframework/data/web/config/EnableSpringDataWebSupport$QuerydslActivator;";
+
+ public final static String AdviceModeImportSelector="Lorg/springframework/context/annotation/AdviceModeImportSelector;";
+
+ public final static String EnableConfigurationPropertiesImportSelector = "Lorg/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector;";
+
+ public final static String CacheConfigurationImportSelector = "Lorg/springframework/boot/autoconfigure/cache/CacheAutoConfiguration$CacheConfigurationImportSelector;";
+
+ public final static String RabbitConfigurationImportSelector = "Lorg/springframework/amqp/rabbit/annotation/RabbitListenerConfigurationSelector;";
+
+ public final static String AtBean = "Lorg/springframework/context/annotation/Bean;";
+
+ public final static String AtImports = "Lorg/springframework/context/annotation/Import;";
+
+ public final static String AtEnableConfigurationProperties = "Lorg/springframework/boot/context/properties/EnableConfigurationProperties;";
+
+ public final static String AtConditionalOnClass = "Lorg/springframework/boot/autoconfigure/condition/ConditionalOnClass;";
+
+ public final static String AtConditional = "Lorg/springframework/context/annotation/Conditional;";
+
+ public final static String AtConditionalOnMissingBean = "Lorg/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean;";
+
+ public final static String HypermediaConfigurationImportSelector = "Lorg/springframework/hateoas/config/HypermediaConfigurationImportSelector;";
+
+ public final static String WebStackImportSelector = "Lorg/springframework/hateoas/config/WebStackImportSelector;";
+
+ public final static Type MISSING = new Type(null, null);
+
+ public final static Type[] NO_INTERFACES = new Type[0];
+
+ protected static Set validBoxing = new HashSet();
+
+
+ private TypeSystem typeSystem;
+
+ private ClassNode node;
+
+ private Type[] interfaces;
+
+ public Type(TypeSystem typeSystem, ClassNode node) {
+ this.typeSystem = typeSystem;
+ this.node = node;
+ }
+
+ public static Type forClassNode(TypeSystem typeSystem, ClassNode node) {
+ return new Type(typeSystem, node);
+ }
+
+ /**
+ * @return typename in slashed form (aaa/bbb/ccc/Ddd$Eee)
+ */
+ public String getName() {
+ return node.name;
+ }
+
+ public String getDottedName() {
+ return node.name.replace("/", ".");
+ }
+
+ public Type getSuperclass() {
+ if (node.superName == null) {
+ return null;
+ }
+ return typeSystem.resolveSlashed(node.superName);
+ }
+
+ @Override
+ public String toString() {
+ return "Type:"+getName();
+ }
+
+ public Type[] getInterfaces() {
+ if (interfaces == null) {
+ List itfs = node.interfaces;
+ if (itfs.size() == 0) {
+ interfaces = NO_INTERFACES;
+ } else {
+ interfaces = new Type[itfs.size()];
+ for (int i = 0; i < itfs.size(); i++) {
+ interfaces[i] = typeSystem.resolveSlashed(itfs.get(i));
+ }
+ }
+ }
+ return interfaces;
+ }
+
+ /** @return List of slashed interface types */
+ public List getInterfacesStrings() {
+ return node.interfaces;
+ }
+
+ /** @return slashed supertype name */
+ public String getSuperclassString() {
+ return node.superName;
+ }
+
+ public List getTypesInSignature() {
+ if (node.signature == null) {
+ return Collections.emptyList();
+ } else {
+ SignatureReader reader = new SignatureReader(node.signature);
+ TypeCollector tc = new TypeCollector();
+ reader.accept(tc);
+ return tc.getTypes();
+ }
+ }
+
+ static class TypeCollector extends SignatureVisitor {
+
+ List types = null;
+
+ public TypeCollector() {
+ super(Opcodes.ASM7);
+ }
+
+ @Override
+ public void visitClassType(String name) {
+ if (types == null) {
+ types = new ArrayList();
+ }
+ types.add(name);
+ }
+
+ public List getTypes() {
+ if (types == null) {
+ return Collections.emptyList();
+ } else {
+ return types;
+ }
+ }
+
+ }
+
+ public boolean implementsInterface(String interfaceName) {
+ Type[] interfacesToCheck = getInterfaces();
+ for (Type interfaceToCheck : interfacesToCheck) {
+ if (interfaceToCheck.getName().equals(interfaceName)) {
+ return true;
+ }
+ if (interfaceToCheck.implementsInterface(interfaceName)) {
+ return true;
+ }
+ }
+ Type superclass = getSuperclass();
+ while (superclass != null) {
+ if (superclass.implementsInterface(interfaceName)) {
+ return true;
+ }
+ superclass = superclass.getSuperclass();
+ }
+ return false;
+ }
+
+ public List getMethodsWithAnnotation(String string) {
+ return node.methods.stream().filter(m -> hasAnnotation(m, string)).map(m -> wrap(m))
+ .collect(Collectors.toList());
+ }
+
+ public List getMethodsWithAtBean() {
+ return getMethodsWithAnnotation(AtBean);
+ }
+
+ public Method wrap(MethodNode mn) {
+ return new Method(mn);
+ }
+
+ private boolean hasAnnotation(MethodNode m, String string) {
+ List visibleAnnotations = m.visibleAnnotations;
+ Optional findAny = visibleAnnotations == null ? Optional.empty()
+ : visibleAnnotations.stream().filter(a -> a.desc.equals(string)).findFirst();
+ return findAny.isPresent();
+ }
+
+ static {
+ validBoxing.add("Ljava/lang/Byte;B");
+ validBoxing.add("Ljava/lang/Character;C");
+ validBoxing.add("Ljava/lang/Double;D");
+ validBoxing.add("Ljava/lang/Float;F");
+ validBoxing.add("Ljava/lang/Integer;I");
+ validBoxing.add("Ljava/lang/Long;J");
+ validBoxing.add("Ljava/lang/Short;S");
+ validBoxing.add("Ljava/lang/Boolean;Z");
+ validBoxing.add("BLjava/lang/Byte;");
+ validBoxing.add("CLjava/lang/Character;");
+ validBoxing.add("DLjava/lang/Double;");
+ validBoxing.add("FLjava/lang/Float;");
+ validBoxing.add("ILjava/lang/Integer;");
+ validBoxing.add("JLjava/lang/Long;");
+ validBoxing.add("SLjava/lang/Short;");
+ validBoxing.add("ZLjava/lang/Boolean;");
+ }
+
+ public boolean isAssignableFrom(Type other) {
+ if (other.isPrimitiveType()) {
+ if (validBoxing.contains(this.getName() + other.getName())) {
+ return true;
+ }
+ }
+ if (this == other) {
+ return true;
+ }
+
+ if (this.getName().equals("Ljava/lang/Object;")) {
+ return true;
+ }
+
+// if (!isTypeVariableReference()
+// && other.getSignature().equals("Ljava/lang/Object;")) {
+// return false;
+// }
+
+// boolean thisRaw = this.isRawType();
+// if (thisRaw && other.isParameterizedOrGenericType()) {
+// return isAssignableFrom(other.getRawType());
+// }
+//
+// boolean thisGeneric = this.isGenericType();
+// if (thisGeneric && other.isParameterizedOrRawType()) {
+// return isAssignableFrom(other.getGenericType());
+// }
+//
+// if (this.isParameterizedType()) {
+// // look at wildcards...
+// if (((ReferenceType) this.getRawType()).isAssignableFrom(other)) {
+// boolean wildcardsAllTheWay = true;
+// ResolvedType[] myParameters = this.getResolvedTypeParameters();
+// for (int i = 0; i < myParameters.length; i++) {
+// if (!myParameters[i].isGenericWildcard()) {
+// wildcardsAllTheWay = false;
+// } else {
+// BoundedReferenceType boundedRT = (BoundedReferenceType) myParameters[i];
+// if (boundedRT.isExtends() || boundedRT.isSuper()) {
+// wildcardsAllTheWay = false;
+// }
+// }
+// }
+// if (wildcardsAllTheWay && !other.isParameterizedType()) {
+// return true;
+// }
+// // we have to match by parameters one at a time
+// ResolvedType[] theirParameters = other
+// .getResolvedTypeParameters();
+// boolean parametersAssignable = true;
+// if (myParameters.length == theirParameters.length) {
+// for (int i = 0; i < myParameters.length
+// && parametersAssignable; i++) {
+// if (myParameters[i] == theirParameters[i]) {
+// continue;
+// }
+// // dont do this: pr253109
+// // if
+// // (myParameters[i].isAssignableFrom(theirParameters[i],
+// // allowMissing)) {
+// // continue;
+// // }
+// ResolvedType mp = myParameters[i];
+// ResolvedType tp = theirParameters[i];
+// if (mp.isParameterizedType()
+// && tp.isParameterizedType()) {
+// if (mp.getGenericType().equals(tp.getGenericType())) {
+// UnresolvedType[] mtps = mp.getTypeParameters();
+// UnresolvedType[] ttps = tp.getTypeParameters();
+// for (int ii = 0; ii < mtps.length; ii++) {
+// if (mtps[ii].isTypeVariableReference()
+// && ttps[ii]
+// .isTypeVariableReference()) {
+// TypeVariable mtv = ((TypeVariableReferenceType) mtps[ii])
+// .getTypeVariable();
+// boolean b = mtv
+// .canBeBoundTo((ResolvedType) ttps[ii]);
+// if (!b) {// TODO incomplete testing here
+// // I think
+// parametersAssignable = false;
+// break;
+// }
+// } else {
+// parametersAssignable = false;
+// break;
+// }
+// }
+// continue;
+// } else {
+// parametersAssignable = false;
+// break;
+// }
+// }
+// if (myParameters[i].isTypeVariableReference()
+// && theirParameters[i].isTypeVariableReference()) {
+// TypeVariable myTV = ((TypeVariableReferenceType) myParameters[i])
+// .getTypeVariable();
+// // TypeVariable theirTV =
+// // ((TypeVariableReferenceType)
+// // theirParameters[i]).getTypeVariable();
+// boolean b = myTV.canBeBoundTo(theirParameters[i]);
+// if (!b) {// TODO incomplete testing here I think
+// parametersAssignable = false;
+// break;
+// } else {
+// continue;
+// }
+// }
+// if (!myParameters[i].isGenericWildcard()) {
+// parametersAssignable = false;
+// break;
+// } else {
+// BoundedReferenceType wildcardType = (BoundedReferenceType) myParameters[i];
+// if (!wildcardType.alwaysMatches(theirParameters[i])) {
+// parametersAssignable = false;
+// break;
+// }
+// }
+// }
+// } else {
+// parametersAssignable = false;
+// }
+// if (parametersAssignable) {
+// return true;
+// }
+// }
+// }
+//
+// // eg this=T other=Ljava/lang/Object;
+// if (isTypeVariableReference() && !other.isTypeVariableReference()) {
+// TypeVariable aVar = ((TypeVariableReference) this)
+// .getTypeVariable();
+// return aVar.resolve(world).canBeBoundTo(other);
+// }
+//
+// if (other.isTypeVariableReference()) {
+// TypeVariableReferenceType otherType = (TypeVariableReferenceType) other;
+// if (this instanceof TypeVariableReference) {
+// return ((TypeVariableReference) this)
+// .getTypeVariable()
+// .resolve(world)
+// .canBeBoundTo(
+// otherType.getTypeVariable().getFirstBound()
+// .resolve(world));// pr171952
+// // return
+// // ((TypeVariableReference)this).getTypeVariable()==otherType
+// // .getTypeVariable();
+// } else {
+// // FIXME asc should this say canBeBoundTo??
+// return this.isAssignableFrom(otherType.getTypeVariable()
+// .getFirstBound().resolve(world));
+// }
+// }
+
+ Type[] interfaces = other.getInterfaces();
+ for (Type intface : interfaces) {
+ boolean b;
+// if (thisRaw && intface.isParameterizedOrGenericType()) {
+// b = this.isAssignableFrom(intface.getRawType(), allowMissing);
+// } else {
+ b = this.isAssignableFrom(intface);
+// }
+ if (b) {
+ return true;
+ }
+ }
+ Type superclass = other.getSuperclass();
+ if (superclass != null) {
+ boolean b;
+// if (thisRaw && superclass.isParameterizedOrGenericType()) {
+// b = this.isAssignableFrom(superclass.getRawType(), allowMissing);
+// } else {
+ b = this.isAssignableFrom(superclass);
+// }
+ if (b) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isPrimitiveType() {
+ return false;
+ }
+
+ public boolean isInterface() {
+ return Modifier.isInterface(node.access);
+ }
+
+ public boolean hasAnnotationInHierarchy(String lookingFor) {
+ return hasAnnotationInHierarchy(lookingFor, new ArrayList());
+ }
+
+ public boolean hasAnnotationInHierarchy(String lookingFor, List seen) {
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode anno : node.visibleAnnotations) {
+ if (seen.contains(anno.desc))
+ continue;
+ seen.add(anno.desc);
+ // System.out.println("Comparing "+anno.desc+" with "+lookingFor);
+ if (anno.desc.equals(lookingFor)) {
+ return true;
+ }
+ try {
+ Type resolve = typeSystem.Lresolve(anno.desc);
+ if (resolve.hasAnnotationInHierarchy(lookingFor, seen)) {
+ return true;
+ }
+ } catch (MissingTypeException mte) {
+ // not on classpath, that's ok
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean isMetaAnnotated(String slashedTypeDescriptor) {
+ return isMetaAnnotated(slashedTypeDescriptor, new HashSet<>());
+ }
+
+ public boolean isMetaAnnotated(String slashedTypeDescriptor, Set seen) {
+// System.out.println("Looking at "+this.getName()+" for "+slashedTypeDescriptor);
+ for (Type t : this.getAnnotations()) {
+ String tname = t.getName();
+ if (tname.equals(slashedTypeDescriptor)) {
+ return true;
+ }
+ if (!seen.contains(tname)) {
+ seen.add(tname);
+ if (t.isMetaAnnotated(slashedTypeDescriptor, seen)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ List annotations = null;
+
+ public static final List NO_ANNOTATIONS = Collections.emptyList();
+
+
+ public List findConditionalOnMissingBeanValue() {
+ List findAnnotationValue = findAnnotationValue(AtConditionalOnMissingBean, false);
+ if (findAnnotationValue==null) {
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an : node.visibleAnnotations) {
+ if (an.desc.equals(AtConditionalOnMissingBean)) {
+ System.out.println("??? found nothing on this @COC annotated thing "+this.getName());
+ }
+ }
+ }
+ }
+ return findAnnotationValue;
+ }
+
+ public List findConditionalOnClassValue() {
+ List findAnnotationValue = findAnnotationValue(AtConditionalOnClass, false);
+ if (findAnnotationValue==null) {
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an : node.visibleAnnotations) {
+ if (an.desc.equals(AtConditionalOnClass)) {
+ System.out.println("??? found nothing on this @COC annotated thing "+this.getName());
+ }
+ }
+ }
+ }
+ return findAnnotationValue;
+ }
+
+ public List findEnableConfigurationPropertiesValue() {
+ List values = findAnnotationValue(AtEnableConfigurationProperties, false);
+ return values;
+ }
+
+ public Map> findImports() {
+ return findAnnotationValueWithHostAnnotation(AtImports, true, new HashSet<>());
+ }
+
+ public List findAnnotationValue(String annotationType, boolean searchMeta) {
+ return findAnnotationValue(annotationType, searchMeta, new HashSet<>());
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map> findAnnotationValueWithHostAnnotation(String annotationType, boolean searchMeta, Set visited) {
+ if (!visited.add(this.getName())) {
+ return Collections.emptyMap();
+ }
+ Map> collectedResults = new LinkedHashMap<>();
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an : node.visibleAnnotations) {
+ if (an.desc.equals(annotationType)) {
+ List values = an.values;
+ if (values != null) {
+ for (int i=0;i importedReferences = ((List)values.get(i+1))
+ .stream()
+ .map(t -> t.getDescriptor())
+ .collect(Collectors.toList());
+ collectedResults.put(this.getName().replace("/", "."), importedReferences);
+ }
+ }
+ }
+ }
+ }
+ if (searchMeta) {
+ for (AnnotationNode an: node.visibleAnnotations) {
+ // For example @EnableSomething might have @Import on it
+ Type annoType = null;
+ try {
+ annoType = typeSystem.Lresolve(an.desc);
+ } catch (MissingTypeException mte) {
+ System.out.println("SBG: WARNING: Unable to find "+an.desc+" skipping...");
+ continue;
+ }
+ collectedResults.putAll(annoType.findAnnotationValueWithHostAnnotation(annotationType, searchMeta, visited));
+ }
+ }
+ }
+ return collectedResults;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public List findAnnotationValue(String annotationType, boolean searchMeta, Set visited) {
+ if (!visited.add(this.getName())) {
+ return Collections.emptyList();
+ }
+ List collectedResults = new ArrayList<>();
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an : node.visibleAnnotations) {
+ if (an.desc.equals(annotationType)) {
+ List values = an.values;
+ if (values != null) {
+ for (int i=0;i)values.get(i+1))
+ .stream()
+ .map(t -> t.getDescriptor())
+ .collect(Collectors.toCollection(() -> collectedResults));
+ }
+ }
+ }
+ }
+ }
+ if (searchMeta) {
+ for (AnnotationNode an: node.visibleAnnotations) {
+ // For example @EnableSomething might have @Import on it
+ Type annoType = typeSystem.Lresolve(an.desc);
+ collectedResults.addAll(annoType.findAnnotationValue(annotationType, searchMeta, visited));
+ }
+ }
+ }
+ return collectedResults;
+ }
+
+ private List getAnnotations() {
+ if (annotations == null) {
+ annotations = new ArrayList<>();
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an : node.visibleAnnotations) {
+ try {
+ annotations.add(this.typeSystem.Lresolve(an.desc));
+ } catch (MissingTypeException mte) {
+ // that's ok you weren't relying on it anyway!
+ }
+ }
+ }
+// if (node.invisibleAnnotations != null) {
+// for (AnnotationNode an: node.invisibleAnnotations) {
+// try {
+// annotations.add(this.typeSystem.Lresolve(an.desc));
+// } catch (MissingTypeException mte) {
+// // that's ok you weren't relying on it anyway!
+// }
+// }
+// }
+ if (annotations.size() == 0) {
+ annotations = NO_ANNOTATIONS;
+ }
+ }
+ return annotations;
+ }
+
+ public Type findAnnotation(Type searchType) {
+ List annos = getAnnotations();
+ for (Type anno : annos) {
+ if (anno.equals(searchType)) {
+ return anno;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return true if meta annotated with org.springframework.stereotype.Indexed
+ */
+ public Entry isIndexedOrEntity() {
+ Type indexedType = isMetaAnnotated2("Lorg/springframework/stereotype/Indexed;");
+ if (indexedType != null) {
+ return new AbstractMap.SimpleImmutableEntry(this.node.name.replace("/", "."),indexedType.getName().replace("/", "."));
+ } else {
+ indexedType = isMetaAnnotated2("Ljavax/persistence/Entity;");
+ if (indexedType != null) {
+ return new AbstractMap.SimpleImmutableEntry(this.node.name.replace("/", "."),"javax.persistence.Entity");
+ }
+ Type t = isIndexedInHierarchy();
+ if ( t != null) {
+ // This should catch repositories where the Repository interface is marked @Indexed
+ //app.main.model.FooRepository=org.springframework.data.repository.Repository")
+ return new AbstractMap.SimpleImmutableEntry(this.node.name.replace("/","."), t.node.name.replace("/","."));
+ }
+ return null;
+ }
+ }
+
+ private Type isIndexedInHierarchy() {
+ if (isAnnotated("Lorg/springframework/stereotype/Indexed;")) {
+ return this;
+ }
+ Type[] is = getInterfaces();
+ for (Type i: is) {
+ Type t = i.isIndexedInHierarchy();
+ if (t != null) {
+ return t;
+ }
+ }
+ Type sc = getSuperclass();
+ if (sc!=null) {
+ return sc.isIndexedInHierarchy();
+ }
+ return null;
+ }
+// app.main.model.FooRepository=org.springframework.data.repository.Repository
+// public List findConditionalOnClassValue() {
+// if (node.visibleAnnotations != null) {
+// for (AnnotationNode an : node.visibleAnnotations) {
+// if (an.desc.equals("Lorg/springframework/boot/autoconfigure/condition/ConditionalOnClass;")) {
+// List values = an.values;
+// for (int i=0;i)values.get(i+1))
+// .stream()
+// .map(t -> t.getDescriptor())
+// .collect(Collectors.toList());
+// }
+// }
+//// for (Object o: values) {
+//// System.out.println("<> "+o+" "+(o==null?"":o.ge
+
+ private Type isMetaAnnotated2(String Ldescriptor) {
+ return isMetaAnnotated2(Ldescriptor, new HashSet<>());
+ }
+
+ private boolean isAnnotated(String Ldescriptor) {
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an: node.visibleAnnotations) {
+ if (an.desc.equals(Ldescriptor)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private Type isMetaAnnotated2(String Ldescriptor, Set seen) {
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an: node.visibleAnnotations) {
+ if (seen.add(an.desc)) {
+ if (an.desc.equals(Ldescriptor)) {
+ return this;//typeSystem.Lresolve(an.desc);
+ } else {
+ Type annoType = typeSystem.Lresolve(an.desc);
+ Type meta = annoType.isMetaAnnotated2(Ldescriptor, seen);
+ if (meta != null) {
+ return meta;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public List getNestedTypes() {
+ List result = null;
+ List innerClasses = node.innerClasses;
+ for (InnerClassNode inner: innerClasses) {
+ if (inner.outerName==null || !inner.outerName.equals(getName())) {
+// System.out.println("SKIPPPING "+inner.name+" because outer is "+inner.outerName+" and we are looking at "+getName());
+ continue;
+ }
+ if (inner.name.equals(getName())) {
+ continue;
+ }
+ Type t = typeSystem.resolve(inner.name); // aaa/bbb/ccc/Ddd$Eee
+ if (result == null) {
+ result = new ArrayList<>();
+ }
+ result.add(t);
+ }
+ return result==null?Collections.emptyList():result;
+ }
+
+ public String getDescriptor() {
+ return "L"+node.name.replace(".", "/")+";";
+ }
+
+ /**
+ * Find @CompilationHints directly on this type or used as a meta-annotation on annotations on this type.
+ */
+ public Map> getHints() {
+ Map> hints = new LinkedHashMap<>();
+ CompilationHint hint = proposedAnnotations.get(getDescriptor());
+ if (hint !=null) {
+ List s = new ArrayList<>();
+ s.add(this);
+ hints.put(new HintDescriptor(s, hint.skipIfTypesMissing, hint.follow, hint.name), null);
+ }
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an: node.visibleAnnotations) {
+ Type annotationType = typeSystem.Lresolve(an.desc, true);
+ if (annotationType == null) {
+ System.out.println("Couldn't resolve "+an.desc);
+ } else {
+ Stack s = new Stack<>();
+ s.push(this);
+ annotationType.collectHints(an, hints, new HashSet<>(), s);
+ }
+ }
+ }
+ if (implementsImportSelector() && hints.size()==0) {
+ throw new IllegalStateException("No @CompilationHint found for import selector: "+getDottedName());
+ }
+
+ return hints.size()==0? Collections.emptyMap():hints;
+ }
+
+ // TODO repeatable annotations...
+
+ private void collectHints(AnnotationNode an, Map> hints, Set visited, Stack annotationChain) {
+ if (!visited.add(an)) {
+ return;
+ }
+ try {
+ annotationChain.push(this);
+ // Am I a compilation hint?
+ CompilationHint hint = proposedAnnotations.get(an.desc);
+ if (hint !=null) {
+ hints.put(new HintDescriptor(new ArrayList<>(annotationChain), hint.skipIfTypesMissing, hint.follow, hint.name), collectTypes(an));
+ }
+ // check for meta annotation
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an2: node.visibleAnnotations) {
+ Type annotationType = typeSystem.Lresolve(an2.desc, true);
+ if (annotationType == null) {
+ System.out.println("Couldn't resolve "+an2.desc);
+ } else {
+ annotationType.collectHints(an2, hints, visited, annotationChain);
+ }
+ }
+ }
+ } finally {
+ annotationChain.pop();
+ }
+ }
+
+ private CompilationHint findCompilationHintHelper(HashSet visited) {
+ if (!visited.add(this)) {
+ return null;
+ }
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an : node.visibleAnnotations) {
+ CompilationHint compilationHint = proposedAnnotations.get(an.desc);
+ if (compilationHint != null) {
+ return compilationHint;
+ }
+ Type resolvedAnnotation = typeSystem.Lresolve(an.desc);
+ compilationHint = resolvedAnnotation.findCompilationHintHelper(visited);
+ if (compilationHint != null) {
+ return compilationHint;
+ }
+ }
+ }
+ return null;
+ }
+
+ private List collectTypes(AnnotationNode an) {
+ List values = an.values;
+ if (values != null) {
+ for (int i=0;i importedReferences = ((List)values.get(i+1))
+ .stream()
+ .map(t -> t.getDescriptor())
+ .collect(Collectors.toList());
+ return importedReferences;
+ }
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ private static Map proposedAnnotations = new HashMap<>();
+
+ static {
+ // @ConditionalOnClass has @CompilationHint(skipIfTypesMissing=true, follow=false)
+ proposedAnnotations.put(AtConditionalOnClass, new CompilationHint(true,false));
+
+ // @ConditionalOnMissingBean has @CompilationHint(skipIfTypesMissing=true, follow=false)
+ proposedAnnotations.put(AtConditionalOnMissingBean, new CompilationHint(true, false));
+
+ // TODO can be {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
+ // @Imports has @CompilationHint(skipIfTypesMissing=false?, follow=true)
+ proposedAnnotations.put(AtImports, new CompilationHint(false, true));
+
+ // @Conditional has @CompilationHint(skipIfTypesMissing=false, follow=false)
+ proposedAnnotations.put(AtConditional, new CompilationHint(false, false));
+
+ // TODO do configuration properties chain?
+ // @EnableConfigurationProperties has @CompilationHint(skipIfTypesMissing=false, follow=false)
+ proposedAnnotations.put(AtEnableConfigurationProperties, new CompilationHint(false, false));
+
+ // @EnableConfigurationPropertiesImportSelector has
+ // @CompilationHint(skipIfTypesMissing=false, follow=false, name={
+ // ConfigurationPropertiesBeanRegistrar.class.getName(),
+ // ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() })
+ // proposedAnnotations.put(AtEnableConfigurationProperties, new CompilationHint(false, false));
+
+ // CacheConfigurationImportSelector has
+ // @CompilationHint(skipIfTypesMissing=true, follow=false, name={
+ // "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration",
+ // "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"})
+ proposedAnnotations.put(CacheConfigurationImportSelector,
+ new CompilationHint(false,false, new String[] {
+ "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration",
+ "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"}
+ ));
+
+ proposedAnnotations.put(RabbitConfigurationImportSelector,
+ new CompilationHint(true,true, new String[] {
+ "org.springframework.amqp.rabbit.annotation.RabbitBootstrapConfiguration"}
+ ));
+
+ // TransactionManagementConfigurationSelector has
+ // @CompilationHint(skipIfTypesMissing=true, follow=true, name={...})
+ proposedAnnotations.put(TransactionManagementConfigurationSelector,
+ new CompilationHint(true, true, new String[] {
+ "org.springframework.context.annotation.AutoProxyRegistrar",
+ "org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration",
+ "org.springframework.transaction.aspectj.AspectJJtaTransactionManagementConfiguration",
+ "org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration"}
+ ));
+
+ // EnableSpringDataWebSupport. TODO: there are others in spring.factories.
+ proposedAnnotations.put(SpringDataWebConfigurationSelector,
+ new CompilationHint(true, true, new String[] {
+ "org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration",
+ "org.springframework.data.web.config.SpringDataWebConfiguration"}
+ ));
+
+ // EnableSpringDataWebSupport. TODO: there are others in spring.factories.
+ proposedAnnotations.put(SpringDataWebQueryDslSelector,
+ new CompilationHint(true, true, new String[] {
+ "org.springframework.data.web.config.QuerydslWebConfiguration"}
+ ));
+
+ // EnableConfigurationPropertiesImportSelector has
+ // @CompilationHint(skipIfTypesMissing=true, follow=false, name={
+ // "org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector$ConfigurationPropertiesBeanRegistrar",
+ // "org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar"})
+ proposedAnnotations.put(EnableConfigurationPropertiesImportSelector,
+ new CompilationHint(false,false, new String[] {
+ "org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector$ConfigurationPropertiesBeanRegistrar",
+ "org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar"}
+ ));
+
+
+ // Not quite right... this is a superclass of a selector we've already added...
+ proposedAnnotations.put(AdviceModeImportSelector,
+ new CompilationHint(true, true, new String[0]
+ ));
+
+
+ // Spring Security!
+ proposedAnnotations.put(SpringWebMvcImportSelector,
+ new CompilationHint(false, true, new String[] {
+ "org.springframework.web.servlet.DispatcherServlet",
+ "org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration"
+ }));
+ proposedAnnotations.put(OAuth2ImportSelector,
+ new CompilationHint(false, true, new String[] {
+ "org.springframework.security.oauth2.client.registration.ClientRegistration",
+ "org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration"
+ }));
+
+ proposedAnnotations.put(HypermediaConfigurationImportSelector,
+ new CompilationHint(false, true, new String[] {
+ "org.springframework.hateoas.config.HypermediaConfigurationImportSelector"
+ }));
+
+ proposedAnnotations.put(WebStackImportSelector,
+ new CompilationHint(false, true, new String[] {
+ "org.springframework.hateoas.config.WebStackImportSelector"
+ }));
+ }
+
+ private boolean implementsImportSelector() {
+ return implementsInterface(fromLdescriptorToSlashed(ImportSelector));
+ }
+
+ private String fromLdescriptorToSlashed(String Ldescriptor) {
+ return Ldescriptor.substring(1,Ldescriptor.length()-1);
+ }
+
+ private CompilationHint findCompilationHint(Type annotationType) {
+ String descriptor = "L"+annotationType.getName().replace(".", "/")+";";
+ CompilationHint hint = proposedAnnotations.get(descriptor);
+ if (hint !=null) {
+ return hint;
+ } else {
+ // check for meta annotation
+ return annotationType.findCompilationHintHelper(new HashSet<>());
+ }
+ }
+
+ // TODO what about AliasFor usage in spring annotations themselves? does that trip this code up when we start looking at particular fields?
+
+ static class CompilationHint {
+ boolean follow;
+ boolean skipIfTypesMissing;
+ private String[] name;
+
+ public CompilationHint(boolean skipIfTypesMissing, boolean follow) {
+ this(skipIfTypesMissing,follow,new String[] {});
+ }
+
+ // TODO what about whether you need to reflect on ctors/methods/fields?
+ public CompilationHint(boolean skipIfTypesMissing, boolean follow, String[] name) {
+ this.skipIfTypesMissing = skipIfTypesMissing;
+ this.follow = follow;
+ this.name = name;
+ }
+ }
+
+ public void collectMissingAnnotationTypesHelper(Set missingAnnotationTypes, HashSet visited) {
+ if (!visited.add(this)) {
+ return;
+ }
+ if (node.visibleAnnotations != null) {
+ for (AnnotationNode an: node.visibleAnnotations) {
+ Type annotationType = typeSystem.Lresolve(an.desc, true);
+ if (annotationType == null) {
+ missingAnnotationTypes.add(an.desc.substring(0,an.desc.length()-1).replace("/", "."));
+ } else {
+ annotationType.collectMissingAnnotationTypesHelper(missingAnnotationTypes, visited);
+ }
+ }
+ }
+ }
+
+// @SuppressWarnings("unchecked")
+// public void findCompilationHints(String annotationType, Map> hintCollector, Set