Polish "Add GraalVM native JSON configuration generation"
See gh-28131
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Very basic json writer for the purposes of translating runtime hints to native
|
||||
* configuration.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class BasicJsonWriter {
|
||||
|
||||
private final IndentingWriter writer;
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified indent value.
|
||||
* @param writer the writer to use
|
||||
* @param singleIndent the value of one indent
|
||||
*/
|
||||
public BasicJsonWriter(Writer writer, String singleIndent) {
|
||||
this.writer = new IndentingWriter(writer, singleIndent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance using two whitespaces for the indent.
|
||||
* @param writer the writer to use
|
||||
*/
|
||||
public BasicJsonWriter(Writer writer) {
|
||||
this(writer, " ");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write an object with the specified attributes. Each attribute is
|
||||
* written according to its value type:
|
||||
* <ul>
|
||||
* <li>Map: write the value as a nested object</li>
|
||||
* <li>List: write the value as a nested array</li>
|
||||
* <li>Otherwise, write a single value</li>
|
||||
* </ul>
|
||||
* @param attributes the attributes of the object
|
||||
*/
|
||||
public void writeObject(Map<String, Object> attributes) {
|
||||
writeObject(attributes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an array with the specified items. Each item in the
|
||||
* list is written either as a nested object or as an attribute
|
||||
* depending on its type.
|
||||
* @param items the items to write
|
||||
* @see #writeObject(Map)
|
||||
*/
|
||||
public void writeArray(List<?> items) {
|
||||
writeArray(items, true);
|
||||
}
|
||||
|
||||
private void writeObject(Map<String, Object> attributes, boolean newLine) {
|
||||
if (attributes.isEmpty()) {
|
||||
this.writer.print("{ }");
|
||||
}
|
||||
else {
|
||||
this.writer.println("{").indented(writeAll(attributes.entrySet().iterator(),
|
||||
entry -> writeAttribute(entry.getKey(), entry.getValue()))).print("}");
|
||||
}
|
||||
if (newLine) {
|
||||
this.writer.println();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeArray(List<?> items, boolean newLine) {
|
||||
if (items.isEmpty()) {
|
||||
this.writer.print("[ ]");
|
||||
}
|
||||
else {
|
||||
this.writer.println("[")
|
||||
.indented(writeAll(items.iterator(), this::writeValue)).print("]");
|
||||
}
|
||||
if (newLine) {
|
||||
this.writer.println();
|
||||
}
|
||||
}
|
||||
|
||||
private <T> Runnable writeAll(Iterator<T> it, Consumer<T> writer) {
|
||||
return () -> {
|
||||
while (it.hasNext()) {
|
||||
writer.accept(it.next());
|
||||
if (it.hasNext()) {
|
||||
this.writer.println(",");
|
||||
}
|
||||
else {
|
||||
this.writer.println();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void writeAttribute(String name, Object value) {
|
||||
this.writer.print(quote(name) + ": ");
|
||||
writeValue(value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void writeValue(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
writeObject((Map<String, Object>) map, false);
|
||||
}
|
||||
else if (value instanceof List<?> list) {
|
||||
writeArray(list, false);
|
||||
}
|
||||
else if (value instanceof CharSequence string) {
|
||||
this.writer.print(quote(escape(string)));
|
||||
}
|
||||
else if (value instanceof Boolean flag) {
|
||||
this.writer.print(Boolean.toString(flag));
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("unsupported type: " + value.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private String quote(String name) {
|
||||
return "\"" + name + "\"";
|
||||
}
|
||||
|
||||
private static String escape(CharSequence input) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
input.chars().forEach(c -> {
|
||||
switch (c) {
|
||||
case '"':
|
||||
builder.append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
builder.append("\\\\");
|
||||
break;
|
||||
case '/':
|
||||
builder.append("\\/");
|
||||
break;
|
||||
case '\b':
|
||||
builder.append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
builder.append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
builder.append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
builder.append("\\t");
|
||||
break;
|
||||
default:
|
||||
if (c <= 0x1F) {
|
||||
builder.append(String.format("\\u%04x", c));
|
||||
}
|
||||
else {
|
||||
builder.append((char) c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
|
||||
static class IndentingWriter extends Writer {
|
||||
|
||||
private final Writer out;
|
||||
|
||||
private final String singleIndent;
|
||||
|
||||
private int level = 0;
|
||||
|
||||
private String currentIndent = "";
|
||||
|
||||
private boolean prependIndent = false;
|
||||
|
||||
IndentingWriter(Writer out, String singleIndent) {
|
||||
this.out = out;
|
||||
this.singleIndent = singleIndent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the specified text.
|
||||
* @param string the content to write
|
||||
*/
|
||||
public IndentingWriter print(String string) {
|
||||
write(string.toCharArray(), 0, string.length());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the specified text and append a new line.
|
||||
* @param string the content to write
|
||||
*/
|
||||
public IndentingWriter println(String string) {
|
||||
write(string.toCharArray(), 0, string.length());
|
||||
return println();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a new line.
|
||||
*/
|
||||
public IndentingWriter println() {
|
||||
String separator = System.lineSeparator();
|
||||
try {
|
||||
this.out.write(separator.toCharArray(), 0, separator.length());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
this.prependIndent = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the indentation level and execute the {@link Runnable}. Decrease the
|
||||
* indentation level on completion.
|
||||
* @param runnable the code to execute withing an extra indentation level
|
||||
*/
|
||||
public IndentingWriter indented(Runnable runnable) {
|
||||
indent();
|
||||
runnable.run();
|
||||
return outdent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the indentation level.
|
||||
*/
|
||||
private IndentingWriter indent() {
|
||||
this.level++;
|
||||
return refreshIndent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease the indentation level.
|
||||
*/
|
||||
private IndentingWriter outdent() {
|
||||
this.level--;
|
||||
return refreshIndent();
|
||||
}
|
||||
|
||||
private IndentingWriter refreshIndent() {
|
||||
this.currentIndent = this.singleIndent.repeat(Math.max(0, this.level));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] chars, int offset, int length) {
|
||||
try {
|
||||
if (this.prependIndent) {
|
||||
this.out.write(this.currentIndent.toCharArray(), 0, this.currentIndent.length());
|
||||
this.prependIndent = false;
|
||||
}
|
||||
this.out.write(chars, offset, length);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
this.out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.out.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.springframework.aot.hint.JavaSerializationHints;
|
||||
import org.springframework.aot.hint.ProxyHints;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.ResourceHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Generate the GraalVM native configuration files from runtime hints.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
public class FileNativeConfigurationGenerator implements NativeConfigurationGenerator {
|
||||
|
||||
private final Path basePath;
|
||||
|
||||
private final String groupId;
|
||||
|
||||
private final String artifactId;
|
||||
|
||||
public FileNativeConfigurationGenerator(Path basePath) {
|
||||
this(basePath, null, null);
|
||||
}
|
||||
|
||||
public FileNativeConfigurationGenerator(Path basePath, @Nullable String groupId, @Nullable String artifactId) {
|
||||
this.basePath = basePath;
|
||||
if ((groupId == null && artifactId != null) || (groupId != null && artifactId == null)) {
|
||||
throw new IllegalArgumentException("groupId and artifactId must be both null or both non-null");
|
||||
}
|
||||
this.groupId = groupId;
|
||||
this.artifactId = artifactId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate(RuntimeHints hints) {
|
||||
try {
|
||||
if (hints.javaSerialization().types().findAny().isPresent()) {
|
||||
generateFile(hints.javaSerialization());
|
||||
}
|
||||
if (hints.proxies().jdkProxies().findAny().isPresent()) {
|
||||
generateFile(hints.proxies());
|
||||
}
|
||||
if (hints.reflection().typeHints().findAny().isPresent()) {
|
||||
generateFile(hints.reflection());
|
||||
}
|
||||
if (hints.resources().resourcePatterns().findAny().isPresent() ||
|
||||
hints.resources().resourceBundles().findAny().isPresent()) {
|
||||
generateFile(hints.resources());
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Unexpected I/O error while writing the native configuration", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Java serialization native configuration file.
|
||||
*/
|
||||
private void generateFile(JavaSerializationHints hints) throws IOException {
|
||||
JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer();
|
||||
File file = createIfNecessary("serialization-config.json");
|
||||
FileWriter writer = new FileWriter(file);
|
||||
writer.write(serializer.serialize(hints));
|
||||
writer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the proxy native configuration file.
|
||||
*/
|
||||
private void generateFile(ProxyHints hints) throws IOException {
|
||||
ProxyHintsSerializer serializer = new ProxyHintsSerializer();
|
||||
File file = createIfNecessary("proxy-config.json");
|
||||
FileWriter writer = new FileWriter(file);
|
||||
writer.write(serializer.serialize(hints));
|
||||
writer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the reflection native configuration file.
|
||||
*/
|
||||
private void generateFile(ReflectionHints hints) throws IOException {
|
||||
ReflectionHintsSerializer serializer = new ReflectionHintsSerializer();
|
||||
File file = createIfNecessary("reflect-config.json");
|
||||
FileWriter writer = new FileWriter(file);
|
||||
writer.write(serializer.serialize(hints));
|
||||
writer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the resource native configuration file.
|
||||
*/
|
||||
private void generateFile(ResourceHints hints) throws IOException {
|
||||
ResourceHintsSerializer serializer = new ResourceHintsSerializer();
|
||||
File file = createIfNecessary("resource-config.json");
|
||||
FileWriter writer = new FileWriter(file);
|
||||
writer.write(serializer.serialize(hints));
|
||||
writer.close();
|
||||
}
|
||||
|
||||
private File createIfNecessary(String filename) throws IOException {
|
||||
Path outputDirectory = this.basePath.resolve("META-INF").resolve("native-image");
|
||||
if (this.groupId != null && this.artifactId != null) {
|
||||
outputDirectory = outputDirectory.resolve(this.groupId).resolve(this.artifactId);
|
||||
}
|
||||
outputDirectory.toFile().mkdirs();
|
||||
File file = outputDirectory.resolve(filename).toFile();
|
||||
file.createNewFile();
|
||||
return file;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link NativeConfigurationWriter} implementation that writes the
|
||||
* configuration to disk.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
public class FileNativeConfigurationWriter extends NativeConfigurationWriter {
|
||||
|
||||
private final Path basePath;
|
||||
|
||||
private final String groupId;
|
||||
|
||||
private final String artifactId;
|
||||
|
||||
public FileNativeConfigurationWriter(Path basePath) {
|
||||
this(basePath, null, null);
|
||||
}
|
||||
|
||||
public FileNativeConfigurationWriter(Path basePath, @Nullable String groupId, @Nullable String artifactId) {
|
||||
this.basePath = basePath;
|
||||
if ((groupId == null && artifactId != null) || (groupId != null && artifactId == null)) {
|
||||
throw new IllegalArgumentException("groupId and artifactId must be both null or both non-null");
|
||||
}
|
||||
this.groupId = groupId;
|
||||
this.artifactId = artifactId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeTo(String fileName, Consumer<BasicJsonWriter> writer) {
|
||||
try {
|
||||
File file = createIfNecessary(fileName);
|
||||
try (FileWriter out = new FileWriter(file)) {
|
||||
writer.accept(createJsonWriter(out));
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to write native configuration for " + fileName, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private File createIfNecessary(String filename) throws IOException {
|
||||
Path outputDirectory = this.basePath.resolve("META-INF").resolve("native-image");
|
||||
if (this.groupId != null && this.artifactId != null) {
|
||||
outputDirectory = outputDirectory.resolve(this.groupId).resolve(this.artifactId);
|
||||
}
|
||||
outputDirectory.toFile().mkdirs();
|
||||
File file = outputDirectory.resolve(filename).toFile();
|
||||
file.createNewFile();
|
||||
return file;
|
||||
}
|
||||
|
||||
private BasicJsonWriter createJsonWriter(Writer out) {
|
||||
return new BasicJsonWriter(out);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,35 +16,34 @@
|
||||
|
||||
package org.springframework.aot.nativex;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.aot.hint.JavaSerializationHints;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
|
||||
/**
|
||||
* Serialize a {@link JavaSerializationHints} to the JSON file expected by GraalVM {@code native-image} compiler,
|
||||
* typically named {@code serialization-config.json}.
|
||||
* Write a {@link JavaSerializationHints} to the JSON output expected by the
|
||||
* GraalVM {@code native-image} compiler, typically named
|
||||
* {@code serialization-config.json}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
class JavaSerializationHintsSerializer {
|
||||
class JavaSerializationHintsWriter {
|
||||
|
||||
public String serialize(JavaSerializationHints hints) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[\n");
|
||||
Iterator<TypeReference> typeIterator = hints.types().iterator();
|
||||
while (typeIterator.hasNext()) {
|
||||
TypeReference type = typeIterator.next();
|
||||
String name = JsonUtils.escape(type.getCanonicalName());
|
||||
builder.append("{ \"name\": \"").append(name).append("\" }");
|
||||
if (typeIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("\n]\n");
|
||||
return builder.toString();
|
||||
public static final JavaSerializationHintsWriter INSTANCE = new JavaSerializationHintsWriter();
|
||||
|
||||
public void write(BasicJsonWriter writer, JavaSerializationHints hints) {
|
||||
writer.writeArray(hints.types().map(this::toAttributes).toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(TypeReference typeReference) {
|
||||
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("name", typeReference.getCanonicalName());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
/**
|
||||
* Utility class for JSON.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
abstract class JsonUtils {
|
||||
|
||||
/**
|
||||
* Escape a JSON String.
|
||||
*/
|
||||
static String escape(String input) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
input.chars().forEach(c -> {
|
||||
switch (c) {
|
||||
case '"':
|
||||
builder.append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
builder.append("\\\\");
|
||||
break;
|
||||
case '/':
|
||||
builder.append("\\/");
|
||||
break;
|
||||
case '\b':
|
||||
builder.append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
builder.append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
builder.append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
builder.append("\\t");
|
||||
break;
|
||||
default:
|
||||
if (c <= 0x1F) {
|
||||
builder.append(String.format("\\u%04x", c));
|
||||
}
|
||||
else {
|
||||
builder.append((char) c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
|
||||
/**
|
||||
* Generate GraalVM native configuration.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
public interface NativeConfigurationGenerator {
|
||||
|
||||
/**
|
||||
* Generate the GraalVM native configuration from the provided hints.
|
||||
* @param hints the hints to serialize
|
||||
*/
|
||||
void generate(RuntimeHints hints);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.aot.hint.JavaSerializationHints;
|
||||
import org.springframework.aot.hint.ProxyHints;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.ResourceHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
|
||||
/**
|
||||
* Write {@link RuntimeHints} as GraalVM native configuration.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
public abstract class NativeConfigurationWriter {
|
||||
|
||||
/**
|
||||
* Write the GraalVM native configuration from the provided hints.
|
||||
* @param hints the hints to handle
|
||||
*/
|
||||
public void write(RuntimeHints hints) {
|
||||
if (hints.javaSerialization().types().findAny().isPresent()) {
|
||||
writeJavaSerializationHints(hints.javaSerialization());
|
||||
}
|
||||
if (hints.proxies().jdkProxies().findAny().isPresent()) {
|
||||
writeProxyHints(hints.proxies());
|
||||
}
|
||||
if (hints.reflection().typeHints().findAny().isPresent()) {
|
||||
writeReflectionHints(hints.reflection());
|
||||
}
|
||||
if (hints.resources().resourcePatterns().findAny().isPresent() ||
|
||||
hints.resources().resourceBundles().findAny().isPresent()) {
|
||||
writeResourceHints(hints.resources());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the specified GraalVM native configuration file, using the
|
||||
* provided {@link BasicJsonWriter}.
|
||||
* @param fileName the name of the file
|
||||
* @param writer a consumer for the writer to use
|
||||
*/
|
||||
protected abstract void writeTo(String fileName, Consumer<BasicJsonWriter> writer);
|
||||
|
||||
private void writeJavaSerializationHints(JavaSerializationHints hints) {
|
||||
writeTo("serialization-config.json", writer ->
|
||||
JavaSerializationHintsWriter.INSTANCE.write(writer, hints));
|
||||
}
|
||||
|
||||
private void writeProxyHints(ProxyHints hints) {
|
||||
writeTo("proxy-config.json", writer ->
|
||||
ProxyHintsWriter.INSTANCE.write(writer, hints));
|
||||
}
|
||||
|
||||
private void writeReflectionHints(ReflectionHints hints) {
|
||||
writeTo("reflect-config.json", writer ->
|
||||
ReflectionHintsWriter.INSTANCE.write(writer, hints));
|
||||
}
|
||||
|
||||
private void writeResourceHints(ResourceHints hints) {
|
||||
writeTo("resource-config.json", writer ->
|
||||
ResourceHintsWriter.INSTANCE.write(writer, hints));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,45 +16,37 @@
|
||||
|
||||
package org.springframework.aot.nativex;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.aot.hint.JdkProxyHint;
|
||||
import org.springframework.aot.hint.ProxyHints;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
|
||||
/**
|
||||
* Serialize {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON file expected by GraalVM
|
||||
* {@code native-image} compiler, typically named {@code proxy-config.json}.
|
||||
* Write {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON
|
||||
* output expected by the GraalVM {@code native-image} compiler, typically named
|
||||
* {@code proxy-config.json}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/DynamicProxy/">Dynamic Proxy in Native Image</a>
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
class ProxyHintsSerializer {
|
||||
class ProxyHintsWriter {
|
||||
|
||||
public String serialize(ProxyHints hints) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[\n");
|
||||
Iterator<JdkProxyHint> hintIterator = hints.jdkProxies().iterator();
|
||||
while (hintIterator.hasNext()) {
|
||||
builder.append("{ \"interfaces\": [ ");
|
||||
JdkProxyHint hint = hintIterator.next();
|
||||
Iterator<TypeReference> interfaceIterator = hint.getProxiedInterfaces().iterator();
|
||||
while (interfaceIterator.hasNext()) {
|
||||
String name = JsonUtils.escape(interfaceIterator.next().getCanonicalName());
|
||||
builder.append("\"").append(name).append("\"");
|
||||
if (interfaceIterator.hasNext()) {
|
||||
builder.append(", ");
|
||||
}
|
||||
}
|
||||
builder.append(" ] }");
|
||||
if (hintIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("\n]\n");
|
||||
return builder.toString();
|
||||
public static final ProxyHintsWriter INSTANCE = new ProxyHintsWriter();
|
||||
|
||||
public void write(BasicJsonWriter writer, ProxyHints hints) {
|
||||
writer.writeArray(hints.jdkProxies().map(this::toAttributes).toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(JdkProxyHint hint) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("interfaces", hint.getProxiedInterfaces().stream()
|
||||
.map(TypeReference::getCanonicalName).toList());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.aot.hint.ExecutableHint;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.FieldHint;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.TypeHint;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
|
||||
/**
|
||||
* Serialize {@link ReflectionHints} to the JSON file expected by GraalVM {@code native-image} compiler,
|
||||
* typically named {@code reflect-config.json}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/Reflection/">Reflection Use in Native Images</a>
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
class ReflectionHintsSerializer {
|
||||
|
||||
public String serialize(ReflectionHints hints) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[\n");
|
||||
Iterator<TypeHint> hintIterator = hints.typeHints().iterator();
|
||||
while (hintIterator.hasNext()) {
|
||||
TypeHint hint = hintIterator.next();
|
||||
String name = JsonUtils.escape(hint.getType().getCanonicalName());
|
||||
builder.append("{\n\"name\": \"").append(name).append("\"");
|
||||
serializeCondition(hint, builder);
|
||||
serializeMembers(hint, builder);
|
||||
serializeFields(hint, builder);
|
||||
serializeExecutables(hint, builder);
|
||||
builder.append(" }");
|
||||
if (hintIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("\n]");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void serializeCondition(TypeHint hint, StringBuilder builder) {
|
||||
if (hint.getReachableType() != null) {
|
||||
String name = JsonUtils.escape(hint.getReachableType().getCanonicalName());
|
||||
builder.append(",\n\"condition\": { \"typeReachable\": \"").append(name).append("\" }");
|
||||
}
|
||||
}
|
||||
|
||||
private void serializeFields(TypeHint hint, StringBuilder builder) {
|
||||
Iterator<FieldHint> fieldIterator = hint.fields().iterator();
|
||||
if (fieldIterator.hasNext()) {
|
||||
builder.append(",\n\"fields\": [\n");
|
||||
while (fieldIterator.hasNext()) {
|
||||
FieldHint fieldHint = fieldIterator.next();
|
||||
String name = JsonUtils.escape(fieldHint.getName());
|
||||
builder.append("{ \"name\": \"").append(name).append("\"");
|
||||
if (fieldHint.isAllowWrite()) {
|
||||
builder.append(", \"allowWrite\": ").append(fieldHint.isAllowWrite());
|
||||
}
|
||||
if (fieldHint.isAllowUnsafeAccess()) {
|
||||
builder.append(", \"allowUnsafeAccess\": ").append(fieldHint.isAllowUnsafeAccess());
|
||||
}
|
||||
builder.append(" }");
|
||||
if (fieldIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("\n]");
|
||||
}
|
||||
}
|
||||
|
||||
private void serializeExecutables(TypeHint hint, StringBuilder builder) {
|
||||
List<ExecutableHint> executables = Stream.concat(hint.constructors(), hint.methods()).toList();
|
||||
Iterator<ExecutableHint> methodIterator = executables.stream().filter(h -> h.getModes().contains(ExecutableMode.INVOKE) || h.getModes().isEmpty()).iterator();
|
||||
Iterator<ExecutableHint> queriedMethodIterator = executables.stream().filter(h -> h.getModes().contains(ExecutableMode.INTROSPECT)).iterator();
|
||||
if (methodIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
serializeMethods("methods", methodIterator, builder);
|
||||
}
|
||||
if (queriedMethodIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
serializeMethods("queriedMethods", queriedMethodIterator, builder);
|
||||
}
|
||||
}
|
||||
|
||||
private void serializeMethods(String fieldName, Iterator<ExecutableHint> methodIterator, StringBuilder builder) {
|
||||
builder.append("\"").append(JsonUtils.escape(fieldName)).append("\": [\n");
|
||||
while (methodIterator.hasNext()) {
|
||||
ExecutableHint hint = methodIterator.next();
|
||||
String name = JsonUtils.escape(hint.getName());
|
||||
builder.append("{\n\"name\": \"").append(name).append("\", ").append("\"parameterTypes\": [ ");
|
||||
Iterator<TypeReference> parameterIterator = hint.getParameterTypes().iterator();
|
||||
while (parameterIterator.hasNext()) {
|
||||
String parameterName = JsonUtils.escape(parameterIterator.next().getCanonicalName());
|
||||
builder.append("\"").append(parameterName).append("\"");
|
||||
if (parameterIterator.hasNext()) {
|
||||
builder.append(", ");
|
||||
}
|
||||
}
|
||||
builder.append(" ] }\n");
|
||||
if (methodIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("]\n");
|
||||
}
|
||||
|
||||
private void serializeMembers(TypeHint hint, StringBuilder builder) {
|
||||
Iterator<MemberCategory> categoryIterator = hint.getMemberCategories().iterator();
|
||||
if (categoryIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
while (categoryIterator.hasNext()) {
|
||||
switch (categoryIterator.next()) {
|
||||
case PUBLIC_FIELDS -> builder.append("\"allPublicFields\": true");
|
||||
case DECLARED_FIELDS -> builder.append("\"allDeclaredFields\": true");
|
||||
case INTROSPECT_PUBLIC_CONSTRUCTORS -> builder.append("\"queryAllPublicConstructors\": true");
|
||||
case INTROSPECT_DECLARED_CONSTRUCTORS -> builder.append("\"queryAllDeclaredConstructors\": true");
|
||||
case INVOKE_PUBLIC_CONSTRUCTORS -> builder.append("\"allPublicConstructors\": true");
|
||||
case INVOKE_DECLARED_CONSTRUCTORS -> builder.append("\"allDeclaredConstructors\": true");
|
||||
case INTROSPECT_PUBLIC_METHODS -> builder.append("\"queryAllPublicMethods\": true");
|
||||
case INTROSPECT_DECLARED_METHODS -> builder.append("\"queryAllDeclaredMethods\": true");
|
||||
case INVOKE_PUBLIC_METHODS -> builder.append("\"allPublicMethods\": true");
|
||||
case INVOKE_DECLARED_METHODS -> builder.append("\"allDeclaredMethods\": true");
|
||||
case PUBLIC_CLASSES -> builder.append("\"allPublicClasses\": true");
|
||||
case DECLARED_CLASSES -> builder.append("\"allDeclaredClasses\": true");
|
||||
}
|
||||
if (categoryIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.aot.hint.ExecutableHint;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.FieldHint;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.TypeHint;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Write {@link ReflectionHints} to the JSON output expected by the GraalVM
|
||||
* {@code native-image} compiler, typically named {@code reflect-config.json}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/Reflection/">Reflection Use in Native Images</a>
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
class ReflectionHintsWriter {
|
||||
|
||||
public static final ReflectionHintsWriter INSTANCE = new ReflectionHintsWriter();
|
||||
|
||||
public void write(BasicJsonWriter writer, ReflectionHints hints) {
|
||||
writer.writeArray(hints.typeHints().map(this::toAttributes).toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(TypeHint hint) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("name", hint.getType().getCanonicalName());
|
||||
handleCondition(attributes, hint);
|
||||
handleCategories(attributes, hint.getMemberCategories());
|
||||
handleFields(attributes, hint.fields());
|
||||
handleExecutables(attributes, Stream.concat(hint.constructors(), hint.methods()).toList());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private void handleCondition(Map<String, Object> attributes, TypeHint hint) {
|
||||
if (hint.getReachableType() != null) {
|
||||
Map<String, Object> conditionAttributes = new LinkedHashMap<>();
|
||||
conditionAttributes.put("typeReachable", hint.getReachableType().getCanonicalName());
|
||||
attributes.put("condition", conditionAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFields(Map<String, Object> attributes, Stream<FieldHint> fields) {
|
||||
addIfNotEmpty(attributes, "fields", fields.map(this::toAttributes).toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(FieldHint hint) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("name", hint.getName());
|
||||
if (hint.isAllowWrite()) {
|
||||
attributes.put("allowWrite", hint.isAllowWrite());
|
||||
}
|
||||
if (hint.isAllowUnsafeAccess()) {
|
||||
attributes.put("allowUnsafeAccess", hint.isAllowUnsafeAccess());
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private void handleExecutables(Map<String, Object> attributes, List<ExecutableHint> hints) {
|
||||
addIfNotEmpty(attributes, "methods", hints.stream()
|
||||
.filter(h -> h.getModes().contains(ExecutableMode.INVOKE) || h.getModes().isEmpty())
|
||||
.map(this::toAttributes).toList());
|
||||
addIfNotEmpty(attributes, "queriedMethods", hints.stream()
|
||||
.filter(h -> h.getModes().contains(ExecutableMode.INTROSPECT))
|
||||
.map(this::toAttributes).toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(ExecutableHint hint) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("name", hint.getName());
|
||||
attributes.put("parameterTypes", hint.getParameterTypes().stream().map(TypeReference::getCanonicalName).toList());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private void handleCategories(Map<String, Object> attributes, Set<MemberCategory> categories) {
|
||||
categories.forEach(category -> {
|
||||
switch (category) {
|
||||
case PUBLIC_FIELDS -> attributes.put("allPublicFields", true);
|
||||
case DECLARED_FIELDS -> attributes.put("allDeclaredFields", true);
|
||||
case INTROSPECT_PUBLIC_CONSTRUCTORS -> attributes.put("queryAllPublicConstructors", true);
|
||||
case INTROSPECT_DECLARED_CONSTRUCTORS -> attributes.put("queryAllDeclaredConstructors", true);
|
||||
case INVOKE_PUBLIC_CONSTRUCTORS -> attributes.put("allPublicConstructors", true);
|
||||
case INVOKE_DECLARED_CONSTRUCTORS -> attributes.put("allDeclaredConstructors", true);
|
||||
case INTROSPECT_PUBLIC_METHODS -> attributes.put("queryAllPublicMethods", true);
|
||||
case INTROSPECT_DECLARED_METHODS -> attributes.put("queryAllDeclaredMethods", true);
|
||||
case INVOKE_PUBLIC_METHODS -> attributes.put("allPublicMethods", true);
|
||||
case INVOKE_DECLARED_METHODS -> attributes.put("allDeclaredMethods", true);
|
||||
case PUBLIC_CLASSES -> attributes.put("allPublicClasses", true);
|
||||
case DECLARED_CLASSES -> attributes.put("allDeclaredClasses", true);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void addIfNotEmpty(Map<String, Object> attributes, String name, @Nullable Object value) {
|
||||
if (value != null && (value instanceof Collection<?> collection && !collection.isEmpty())) {
|
||||
attributes.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.aot.hint.ResourceBundleHint;
|
||||
import org.springframework.aot.hint.ResourceHints;
|
||||
import org.springframework.aot.hint.ResourcePatternHint;
|
||||
|
||||
/**
|
||||
* Serialize a {@link ResourceHints} to the JSON file expected by GraalVM {@code native-image} compiler,
|
||||
* typically named {@code resource-config.json}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/Resources/">Accessing Resources in Native Images</a>
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
class ResourceHintsSerializer {
|
||||
|
||||
public String serialize(ResourceHints hints) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("{\n\"resources\" : {\n");
|
||||
serializeInclude(hints, builder);
|
||||
serializeExclude(hints, builder);
|
||||
builder.append("},\n");
|
||||
serializeBundles(hints, builder);
|
||||
builder.append("}\n");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void serializeInclude(ResourceHints hints, StringBuilder builder) {
|
||||
builder.append("\"includes\" : [\n");
|
||||
Iterator<ResourcePatternHint> patternIterator = hints.resourcePatterns().iterator();
|
||||
while (patternIterator.hasNext()) {
|
||||
ResourcePatternHint hint = patternIterator.next();
|
||||
Iterator<String> includeIterator = hint.getIncludes().iterator();
|
||||
while (includeIterator.hasNext()) {
|
||||
String pattern = JsonUtils.escape(patternToRegexp(includeIterator.next()));
|
||||
builder.append("{ \"pattern\": \"").append(pattern).append("\" }");
|
||||
if (includeIterator.hasNext()) {
|
||||
builder.append(", ");
|
||||
}
|
||||
}
|
||||
if (patternIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("\n],\n");
|
||||
}
|
||||
|
||||
private void serializeExclude(ResourceHints hints, StringBuilder builder) {
|
||||
builder.append("\"excludes\" : [\n");
|
||||
Iterator<ResourcePatternHint> patternIterator = hints.resourcePatterns().iterator();
|
||||
while (patternIterator.hasNext()) {
|
||||
ResourcePatternHint hint = patternIterator.next();
|
||||
Iterator<String> excludeIterator = hint.getExcludes().iterator();
|
||||
while (excludeIterator.hasNext()) {
|
||||
String pattern = JsonUtils.escape(patternToRegexp(excludeIterator.next()));
|
||||
builder.append("{ \"pattern\": \"").append(pattern).append("\" }");
|
||||
if (excludeIterator.hasNext()) {
|
||||
builder.append(", ");
|
||||
}
|
||||
}
|
||||
if (patternIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("\n]\n");
|
||||
}
|
||||
|
||||
private void serializeBundles(ResourceHints hints, StringBuilder builder) {
|
||||
builder.append("\"bundles\" : [\n");
|
||||
Iterator<ResourceBundleHint> bundleIterator = hints.resourceBundles().iterator();
|
||||
while (bundleIterator.hasNext()) {
|
||||
String baseName = JsonUtils.escape(bundleIterator.next().getBaseName());
|
||||
builder.append("{ \"name\": \"").append(baseName).append("\" }");
|
||||
if (bundleIterator.hasNext()) {
|
||||
builder.append(",\n");
|
||||
}
|
||||
}
|
||||
builder.append("]\n");
|
||||
}
|
||||
|
||||
private String patternToRegexp(String pattern) {
|
||||
return Arrays.stream(pattern.split("\\*")).map(Pattern::quote).collect(Collectors.joining(".*"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.aot.hint.ResourceBundleHint;
|
||||
import org.springframework.aot.hint.ResourceHints;
|
||||
import org.springframework.aot.hint.ResourcePatternHint;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Write a {@link ResourceHints} to the JSON output expected by the GraalVM
|
||||
* {@code native-image} compiler, typically named {@code resource-config.json}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/Resources/">Accessing Resources in Native Images</a>
|
||||
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
|
||||
*/
|
||||
class ResourceHintsWriter {
|
||||
|
||||
public static final ResourceHintsWriter INSTANCE = new ResourceHintsWriter();
|
||||
|
||||
public void write(BasicJsonWriter writer, ResourceHints hints) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
addIfNotEmpty(attributes, "resources", toAttributes(hints));
|
||||
handleResourceBundles(attributes, hints.resourceBundles());
|
||||
writer.writeObject(attributes);
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(ResourceHints hint) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
addIfNotEmpty(attributes, "includes", hint.resourcePatterns().map(ResourcePatternHint::getIncludes)
|
||||
.flatMap(List::stream).distinct().map(this::toAttributes).toList());
|
||||
addIfNotEmpty(attributes, "excludes", hint.resourcePatterns().map(ResourcePatternHint::getExcludes)
|
||||
.flatMap(List::stream).distinct().map(this::toAttributes).toList());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private void handleResourceBundles(Map<String, Object> attributes, Stream<ResourceBundleHint> ressourceBundles) {
|
||||
addIfNotEmpty(attributes, "bundles", ressourceBundles.map(this::toAttributes).toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(ResourceBundleHint hint) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("name", hint.getBaseName());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private Map<String, Object> toAttributes(String pattern) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("pattern", patternToRegexp(pattern));
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private String patternToRegexp(String pattern) {
|
||||
return Arrays.stream(pattern.split("\\*")).map(Pattern::quote).collect(Collectors.joining(".*"));
|
||||
}
|
||||
|
||||
private void addIfNotEmpty(Map<String, Object> attributes, String name, @Nullable Object value) {
|
||||
if (value instanceof Collection<?> collection) {
|
||||
if (!collection.isEmpty()) {
|
||||
attributes.put(name, value);
|
||||
}
|
||||
}
|
||||
else if (value instanceof Map<?, ?> map) {
|
||||
if (!map.isEmpty()) {
|
||||
attributes.put(name, value);
|
||||
}
|
||||
}
|
||||
else if (value != null) {
|
||||
attributes.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link BasicJsonWriter}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class BasicJsonWriterTests {
|
||||
|
||||
private final StringWriter out = new StringWriter();
|
||||
|
||||
private final BasicJsonWriter json = new BasicJsonWriter(out, "\t");
|
||||
|
||||
@Test
|
||||
void writeObject() {
|
||||
Map<String, Object> attributes = orderedMap("test", "value");
|
||||
attributes.put("another", true);
|
||||
this.json.writeObject(attributes);
|
||||
assertThat(out.toString()).isEqualTo("""
|
||||
{
|
||||
"test": "value",
|
||||
"another": true
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeObjectWitNestedObject() {
|
||||
Map<String, Object> attributes = orderedMap("test", "value");
|
||||
attributes.put("nested", orderedMap("enabled", false));
|
||||
this.json.writeObject(attributes);
|
||||
assertThat(out.toString()).isEqualTo("""
|
||||
{
|
||||
"test": "value",
|
||||
"nested": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeObjectWitNestedArrayOfString() {
|
||||
Map<String, Object> attributes = orderedMap("test", "value");
|
||||
attributes.put("nested", List.of("test", "value", "another"));
|
||||
this.json.writeObject(attributes);
|
||||
assertThat(out.toString()).isEqualTo("""
|
||||
{
|
||||
"test": "value",
|
||||
"nested": [
|
||||
"test",
|
||||
"value",
|
||||
"another"
|
||||
]
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeObjectWitNestedArrayOfObject() {
|
||||
Map<String, Object> attributes = orderedMap("test", "value");
|
||||
LinkedHashMap<String, Object> secondNested = orderedMap("name", "second");
|
||||
secondNested.put("enabled", false);
|
||||
attributes.put("nested", List.of(orderedMap("name", "first"), secondNested, orderedMap("name", "third")));
|
||||
this.json.writeObject(attributes);
|
||||
assertThat(out.toString()).isEqualTo("""
|
||||
{
|
||||
"test": "value",
|
||||
"nested": [
|
||||
{
|
||||
"name": "first"
|
||||
},
|
||||
{
|
||||
"name": "second",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"name": "third"
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeObjectWithNestedEmptyArray() {
|
||||
Map<String, Object> attributes = orderedMap("test", "value");
|
||||
attributes.put("nested", Collections.emptyList());
|
||||
this.json.writeObject(attributes);
|
||||
assertThat(out.toString()).isEqualTo("""
|
||||
{
|
||||
"test": "value",
|
||||
"nested": [ ]
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeObjectWithNestedEmptyObject() {
|
||||
Map<String, Object> attributes = orderedMap("test", "value");
|
||||
attributes.put("nested", Collections.emptyMap());
|
||||
this.json.writeObject(attributes);
|
||||
assertThat(out.toString()).isEqualTo("""
|
||||
{
|
||||
"test": "value",
|
||||
"nested": { }
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeDoubleQuote() {
|
||||
assertEscapedValue("foo\"bar", "foo\\\"bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeBackslash() {
|
||||
assertEscapedValue("foo\"bar", "foo\\\"bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeBackspace() {
|
||||
assertEscapedValue("foo\bbar", "foo\\bbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeFormFeed() {
|
||||
assertEscapedValue("foo\fbar", "foo\\fbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeNewline() {
|
||||
assertEscapedValue("foo\nbar", "foo\\nbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeCarriageReturn() {
|
||||
assertEscapedValue("foo\rbar", "foo\\rbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeTab() {
|
||||
assertEscapedValue("foo\tbar", "foo\\tbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWithEscapeUnicode() {
|
||||
assertEscapedValue("foo\u001Fbar", "foo\\u001fbar");
|
||||
}
|
||||
|
||||
void assertEscapedValue(String value, String expectedEscapedValue) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
attributes.put("test", value);
|
||||
this.json.writeObject(attributes);
|
||||
assertThat(out.toString()).contains("\"test\": \"" + expectedEscapedValue + "\"");
|
||||
}
|
||||
|
||||
private static LinkedHashMap<String, Object> orderedMap(String key, Object value) {
|
||||
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
|
||||
map.put(key, value);
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,11 +45,11 @@ import org.springframework.util.MimeType;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link FileNativeConfigurationGenerator}.
|
||||
* Tests for {@link FileNativeConfigurationWriter}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class FileNativeConfigurationGeneratorTests {
|
||||
public class FileNativeConfigurationWriterTests {
|
||||
|
||||
@TempDir
|
||||
static Path tempDir;
|
||||
@@ -57,44 +57,44 @@ public class FileNativeConfigurationGeneratorTests {
|
||||
@Test
|
||||
void emptyConfig() {
|
||||
Path empty = tempDir.resolve("empty");
|
||||
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(empty);
|
||||
generator.generate(new RuntimeHints());
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(empty);
|
||||
generator.write(new RuntimeHints());
|
||||
assertThat(empty.toFile().listFiles()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializationConfig() throws IOException, JSONException {
|
||||
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
JavaSerializationHints serializationHints = hints.javaSerialization();
|
||||
serializationHints.registerType(Integer.class);
|
||||
serializationHints.registerType(Long.class);
|
||||
generator.generate(hints);
|
||||
generator.write(hints);
|
||||
assertEquals("""
|
||||
[
|
||||
{ "name" : "java.lang.Integer" },
|
||||
{ "name" : "java.lang.Long" }
|
||||
{ "name": "java.lang.Integer" },
|
||||
{ "name": "java.lang.Long" }
|
||||
]""", "serialization-config.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyConfig() throws IOException, JSONException {
|
||||
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ProxyHints proxyHints = hints.proxies();
|
||||
proxyHints.registerJdkProxy(Function.class);
|
||||
proxyHints.registerJdkProxy(Function.class, Consumer.class);
|
||||
generator.generate(hints);
|
||||
generator.write(hints);
|
||||
assertEquals("""
|
||||
[
|
||||
{ "interfaces" : [ "java.util.function.Function" ] },
|
||||
{ "interfaces" : [ "java.util.function.Function", "java.util.function.Consumer" ] }
|
||||
{ "interfaces": [ "java.util.function.Function" ] },
|
||||
{ "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] }
|
||||
]""", "proxy-config.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reflectionConfig() throws IOException, JSONException {
|
||||
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ReflectionHints reflectionHints = hints.reflection();
|
||||
reflectionHints.registerType(StringDecoder.class, builder -> {
|
||||
@@ -117,34 +117,34 @@ public class FileNativeConfigurationGeneratorTests {
|
||||
.withMethod("getDefaultCharset", Collections.emptyList(), constructorHint ->
|
||||
constructorHint.withMode(ExecutableMode.INTROSPECT));
|
||||
});
|
||||
generator.generate(hints);
|
||||
generator.write(hints);
|
||||
assertEquals("""
|
||||
[
|
||||
{
|
||||
"name" : "org.springframework.core.codec.StringDecoder",
|
||||
"condition" : { "typeReachable" : "java.lang.String" },
|
||||
"allPublicFields" : true,
|
||||
"allDeclaredFields" : true,
|
||||
"queryAllPublicConstructors" : true,
|
||||
"queryAllDeclaredConstructors" : true,
|
||||
"allPublicConstructors" : true,
|
||||
"allDeclaredConstructors" : true,
|
||||
"queryAllPublicMethods" : true,
|
||||
"queryAllDeclaredMethods" : true,
|
||||
"allPublicMethods" : true,
|
||||
"allDeclaredMethods" : true,
|
||||
"allPublicClasses" : true,
|
||||
"allDeclaredClasses" : true,
|
||||
"fields" : [
|
||||
{ "name" : "DEFAULT_CHARSET" },
|
||||
{ "name" : "defaultCharset", "allowWrite" = true, "allowUnsafeAccess" = true }
|
||||
"name": "org.springframework.core.codec.StringDecoder",
|
||||
"condition": { "typeReachable": "java.lang.String" },
|
||||
"allPublicFields": true,
|
||||
"allDeclaredFields": true,
|
||||
"queryAllPublicConstructors": true,
|
||||
"queryAllDeclaredConstructors": true,
|
||||
"allPublicConstructors": true,
|
||||
"allDeclaredConstructors": true,
|
||||
"queryAllPublicMethods": true,
|
||||
"queryAllDeclaredMethods": true,
|
||||
"allPublicMethods": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allPublicClasses": true,
|
||||
"allDeclaredClasses": true,
|
||||
"fields": [
|
||||
{ "name": "DEFAULT_CHARSET" },
|
||||
{ "name": "defaultCharset", "allowWrite": true, "allowUnsafeAccess": true }
|
||||
],
|
||||
"methods" : [
|
||||
{ "name" : "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
|
||||
"methods": [
|
||||
{ "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
|
||||
],
|
||||
"queriedMethods" : [
|
||||
{ "name" : "<init>", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
|
||||
{ "name" : "getDefaultCharset" }
|
||||
"queriedMethods": [
|
||||
{ "name": "<init>", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
|
||||
{ "name": "getDefaultCharset", "parameterTypes": [ ] }
|
||||
]
|
||||
}
|
||||
]""", "reflect-config.json");
|
||||
@@ -152,18 +152,18 @@ public class FileNativeConfigurationGeneratorTests {
|
||||
|
||||
@Test
|
||||
void resourceConfig() throws IOException, JSONException {
|
||||
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ResourceHints resourceHints = hints.resources();
|
||||
resourceHints.registerPattern("com/example/test.properties");
|
||||
resourceHints.registerPattern("com/example/another.properties");
|
||||
generator.generate(hints);
|
||||
generator.write(hints);
|
||||
assertEquals("""
|
||||
{
|
||||
"resources": {
|
||||
"includes" : [
|
||||
{"pattern" : "\\\\Qcom/example/test.properties\\\\E"},
|
||||
{"pattern" : "\\\\Qcom/example/another.properties\\\\E"}
|
||||
"includes": [
|
||||
{"pattern": "\\\\Qcom/example/test.properties\\\\E"},
|
||||
{"pattern": "\\\\Qcom/example/another.properties\\\\E"}
|
||||
]
|
||||
}
|
||||
}""", "resource-config.json");
|
||||
@@ -174,11 +174,11 @@ public class FileNativeConfigurationGeneratorTests {
|
||||
String groupId = "foo.bar";
|
||||
String artifactId = "baz";
|
||||
String filename = "resource-config.json";
|
||||
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir, groupId, artifactId);
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir, groupId, artifactId);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ResourceHints resourceHints = hints.resources();
|
||||
resourceHints.registerPattern("com/example/test.properties");
|
||||
generator.generate(hints);
|
||||
generator.write(hints);
|
||||
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(groupId).resolve(artifactId).resolve(filename);
|
||||
assertThat(jsonFile.toFile().exists()).isTrue();
|
||||
}
|
||||
@@ -186,7 +186,7 @@ public class FileNativeConfigurationGeneratorTests {
|
||||
private void assertEquals(String expectedString, String filename) throws IOException, JSONException {
|
||||
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(filename);
|
||||
String content = new String(Files.readAllBytes(jsonFile));
|
||||
JSONAssert.assertEquals(expectedString, content, JSONCompareMode.LENIENT);
|
||||
JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.aot.nativex;
|
||||
|
||||
import java.io.StringWriter;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
@@ -26,13 +28,11 @@ import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Tests for {@link JavaSerializationHintsSerializer}.
|
||||
* Tests for {@link JavaSerializationHintsWriter}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class JavaSerializationHintsSerializerTests {
|
||||
|
||||
private final JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer();
|
||||
public class JavaSerializationHintsWriterTests {
|
||||
|
||||
@Test
|
||||
void empty() throws JSONException {
|
||||
@@ -45,7 +45,7 @@ public class JavaSerializationHintsSerializerTests {
|
||||
JavaSerializationHints hints = new JavaSerializationHints().registerType(TypeReference.of(String.class));
|
||||
assertEquals("""
|
||||
[
|
||||
{ "name" : "java.lang.String" }
|
||||
{ "name": "java.lang.String" }
|
||||
]""", hints);
|
||||
}
|
||||
|
||||
@@ -56,13 +56,16 @@ public class JavaSerializationHintsSerializerTests {
|
||||
.registerType(TypeReference.of(Environment.class));
|
||||
assertEquals("""
|
||||
[
|
||||
{ "name" : "java.lang.String" },
|
||||
{ "name" : "org.springframework.core.env.Environment" }
|
||||
{ "name": "java.lang.String" },
|
||||
{ "name": "org.springframework.core.env.Environment" }
|
||||
]""", hints);
|
||||
}
|
||||
|
||||
private void assertEquals(String expectedString, JavaSerializationHints hints) throws JSONException {
|
||||
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
|
||||
StringWriter out = new StringWriter();
|
||||
BasicJsonWriter writer = new BasicJsonWriter(out, "\t");
|
||||
JavaSerializationHintsWriter.INSTANCE.write(writer, hints);
|
||||
JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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 org.springframework.aot.nativex;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JsonUtils}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class JsonUtilsTests {
|
||||
|
||||
@Test
|
||||
void unescaped() {
|
||||
assertThat(JsonUtils.escape("azerty")).isEqualTo("azerty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeDoubleQuote() {
|
||||
assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeBackslash() {
|
||||
assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeBackspace() {
|
||||
assertThat(JsonUtils.escape("foo\bbar")).isEqualTo("foo\\bbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeFormfeed() {
|
||||
assertThat(JsonUtils.escape("foo\fbar")).isEqualTo("foo\\fbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeNewline() {
|
||||
assertThat(JsonUtils.escape("foo\nbar")).isEqualTo("foo\\nbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeCarriageReturn() {
|
||||
assertThat(JsonUtils.escape("foo\rbar")).isEqualTo("foo\\rbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeTab() {
|
||||
assertThat(JsonUtils.escape("foo\tbar")).isEqualTo("foo\\tbar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeUnicode() {
|
||||
assertThat(JsonUtils.escape("foo\u001Fbar")).isEqualTo("foo\\u001fbar");
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.aot.nativex;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -27,13 +28,11 @@ import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||
import org.springframework.aot.hint.ProxyHints;
|
||||
|
||||
/**
|
||||
* Tests for {@link ProxyHintsSerializer}.
|
||||
* Tests for {@link ProxyHintsWriter}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class ProxyHintsSerializerTests {
|
||||
|
||||
private final ProxyHintsSerializer serializer = new ProxyHintsSerializer();
|
||||
public class ProxyHintsWriterTests {
|
||||
|
||||
@Test
|
||||
void empty() throws JSONException {
|
||||
@@ -47,7 +46,7 @@ public class ProxyHintsSerializerTests {
|
||||
hints.registerJdkProxy(Function.class);
|
||||
assertEquals("""
|
||||
[
|
||||
{ "interfaces" : [ "java.util.function.Function" ] }
|
||||
{ "interfaces": [ "java.util.function.Function" ] }
|
||||
]""", hints);
|
||||
}
|
||||
|
||||
@@ -58,14 +57,16 @@ public class ProxyHintsSerializerTests {
|
||||
hints.registerJdkProxy(Function.class, Consumer.class);
|
||||
assertEquals("""
|
||||
[
|
||||
{ "interfaces" : [ "java.util.function.Function" ] },
|
||||
{ "interfaces" : [ "java.util.function.Function", "java.util.function.Consumer" ] }
|
||||
{ "interfaces": [ "java.util.function.Function" ] },
|
||||
{ "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] }
|
||||
]""", hints);
|
||||
}
|
||||
|
||||
private void assertEquals(String expectedString, ProxyHints hints) throws JSONException {
|
||||
|
||||
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
|
||||
StringWriter out = new StringWriter();
|
||||
BasicJsonWriter writer = new BasicJsonWriter(out, "\t");
|
||||
ProxyHintsWriter.INSTANCE.write(writer, hints);
|
||||
JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.aot.nativex;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -33,13 +34,11 @@ import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReflectionHintsSerializer}.
|
||||
* Tests for {@link ReflectionHintsWriter}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class ReflectionHintsSerializerTests {
|
||||
|
||||
private final ReflectionHintsSerializer serializer = new ReflectionHintsSerializer();
|
||||
public class ReflectionHintsWriterTests {
|
||||
|
||||
@Test
|
||||
void empty() throws JSONException {
|
||||
@@ -73,30 +72,30 @@ public class ReflectionHintsSerializerTests {
|
||||
assertEquals("""
|
||||
[
|
||||
{
|
||||
"name" : "org.springframework.core.codec.StringDecoder",
|
||||
"condition" : { "typeReachable" : "java.lang.String" },
|
||||
"allPublicFields" : true,
|
||||
"allDeclaredFields" : true,
|
||||
"queryAllPublicConstructors" : true,
|
||||
"queryAllDeclaredConstructors" : true,
|
||||
"allPublicConstructors" : true,
|
||||
"allDeclaredConstructors" : true,
|
||||
"queryAllPublicMethods" : true,
|
||||
"queryAllDeclaredMethods" : true,
|
||||
"allPublicMethods" : true,
|
||||
"allDeclaredMethods" : true,
|
||||
"allPublicClasses" : true,
|
||||
"allDeclaredClasses" : true,
|
||||
"fields" : [
|
||||
{ "name" : "DEFAULT_CHARSET" },
|
||||
{ "name" : "defaultCharset", "allowWrite" = true, "allowUnsafeAccess" = true }
|
||||
"name": "org.springframework.core.codec.StringDecoder",
|
||||
"condition": { "typeReachable": "java.lang.String" },
|
||||
"allPublicFields": true,
|
||||
"allDeclaredFields": true,
|
||||
"queryAllPublicConstructors": true,
|
||||
"queryAllDeclaredConstructors": true,
|
||||
"allPublicConstructors": true,
|
||||
"allDeclaredConstructors": true,
|
||||
"queryAllPublicMethods": true,
|
||||
"queryAllDeclaredMethods": true,
|
||||
"allPublicMethods": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allPublicClasses": true,
|
||||
"allDeclaredClasses": true,
|
||||
"fields": [
|
||||
{ "name": "DEFAULT_CHARSET" },
|
||||
{ "name": "defaultCharset", "allowWrite": true, "allowUnsafeAccess": true }
|
||||
],
|
||||
"methods" : [
|
||||
{ "name" : "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
|
||||
"methods": [
|
||||
{ "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
|
||||
],
|
||||
"queriedMethods" : [
|
||||
{ "name" : "<init>", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
|
||||
{ "name" : "getDefaultCharset" }
|
||||
"queriedMethods": [
|
||||
{ "name": "<init>", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
|
||||
{ "name": "getDefaultCharset", "parameterTypes": [ ] }
|
||||
]
|
||||
}
|
||||
]""", hints);
|
||||
@@ -112,8 +111,8 @@ public class ReflectionHintsSerializerTests {
|
||||
|
||||
assertEquals("""
|
||||
[
|
||||
{ "name" : "java.lang.Integer" },
|
||||
{ "name" : "java.lang.Long" }
|
||||
{ "name": "java.lang.Integer" },
|
||||
{ "name": "java.lang.Long" }
|
||||
]""", hints);
|
||||
}
|
||||
|
||||
@@ -158,6 +157,7 @@ public class ReflectionHintsSerializerTests {
|
||||
]
|
||||
""", hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodAndQueriedMethods() throws JSONException {
|
||||
ReflectionHints hints = new ReflectionHints();
|
||||
@@ -188,7 +188,10 @@ public class ReflectionHintsSerializerTests {
|
||||
}
|
||||
|
||||
private void assertEquals(String expectedString, ReflectionHints hints) throws JSONException {
|
||||
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
|
||||
StringWriter out = new StringWriter();
|
||||
BasicJsonWriter writer = new BasicJsonWriter(out, "\t");
|
||||
ReflectionHintsWriter.INSTANCE.write(writer, hints);
|
||||
JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package org.springframework.aot.nativex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -26,22 +26,20 @@ import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||
import org.springframework.aot.hint.ResourceHints;
|
||||
|
||||
/**
|
||||
* Tests for {@link ResourceHintsSerializer}.
|
||||
* Tests for {@link ResourceHintsWriter}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class ResourceHintsSerializerTests {
|
||||
|
||||
private final ResourceHintsSerializer serializer = new ResourceHintsSerializer();
|
||||
public class ResourceHintsWriterTests {
|
||||
|
||||
@Test
|
||||
void empty() throws IOException, JSONException {
|
||||
void empty() throws JSONException {
|
||||
ResourceHints hints = new ResourceHints();
|
||||
assertEquals("{}", hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerExactMatch() throws JSONException {
|
||||
void registerExactMatch() throws JSONException {
|
||||
ResourceHints hints = new ResourceHints();
|
||||
hints.registerPattern("com/example/test.properties");
|
||||
hints.registerPattern("com/example/another.properties");
|
||||
@@ -49,8 +47,8 @@ public class ResourceHintsSerializerTests {
|
||||
{
|
||||
"resources": {
|
||||
"includes": [
|
||||
{ "pattern" : "\\\\Qcom/example/test.properties\\\\E"},
|
||||
{ "pattern" : "\\\\Qcom/example/another.properties\\\\E"}
|
||||
{ "pattern": "\\\\Qcom/example/test.properties\\\\E"},
|
||||
{ "pattern": "\\\\Qcom/example/another.properties\\\\E"}
|
||||
]
|
||||
}
|
||||
}""", hints);
|
||||
@@ -63,8 +61,8 @@ public class ResourceHintsSerializerTests {
|
||||
assertEquals("""
|
||||
{
|
||||
"resources": {
|
||||
"includes" : [
|
||||
{ "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}
|
||||
"includes": [
|
||||
{ "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}
|
||||
]
|
||||
}
|
||||
}""", hints);
|
||||
@@ -79,12 +77,12 @@ public class ResourceHintsSerializerTests {
|
||||
{
|
||||
"resources": {
|
||||
"includes": [
|
||||
{ "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"},
|
||||
{ "pattern" : "\\\\Qorg/example/\\\\E.*\\\\Q.properties\\\\E"}
|
||||
{ "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"},
|
||||
{ "pattern": "\\\\Qorg/example/\\\\E.*\\\\Q.properties\\\\E"}
|
||||
],
|
||||
"excludes": [
|
||||
{ "pattern" : "\\\\Qcom/example/to-ignore.properties\\\\E"},
|
||||
{ "pattern" : "\\\\Qorg/example/to-ignore.properties\\\\E"}
|
||||
{ "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"},
|
||||
{ "pattern": "\\\\Qorg/example/to-ignore.properties\\\\E"}
|
||||
]
|
||||
}
|
||||
}""", hints);
|
||||
@@ -97,8 +95,8 @@ public class ResourceHintsSerializerTests {
|
||||
assertEquals("""
|
||||
{
|
||||
"resources": {
|
||||
"includes" : [
|
||||
{ "pattern" : "\\\\Qjava/lang/String.class\\\\E"}
|
||||
"includes": [
|
||||
{ "pattern": "\\\\Qjava/lang/String.class\\\\E"}
|
||||
]
|
||||
}
|
||||
}""", hints);
|
||||
@@ -112,14 +110,17 @@ public class ResourceHintsSerializerTests {
|
||||
assertEquals("""
|
||||
{
|
||||
"bundles": [
|
||||
{ "name" : "com.example.message"},
|
||||
{ "name" : "com.example.message2"}
|
||||
{ "name": "com.example.message"},
|
||||
{ "name": "com.example.message2"}
|
||||
]
|
||||
}""", hints);
|
||||
}
|
||||
|
||||
private void assertEquals(String expectedString, ResourceHints hints) throws JSONException {
|
||||
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
|
||||
StringWriter out = new StringWriter();
|
||||
BasicJsonWriter writer = new BasicJsonWriter(out, "\t");
|
||||
ResourceHintsWriter.INSTANCE.write(writer, hints);
|
||||
JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user