diff --git a/README.md b/README.md
index 0cfbddc01..f5036c2cf 100644
--- a/README.md
+++ b/README.md
@@ -358,6 +358,144 @@ public final class HelloWorld {
}
```
+### $V for inlined values
+
+Sometimes you have an object that you want to reconstruct in generated code.
+But that object has over 20 fields, and each of those fields has over 20 fields.
+To simplify this reconstruction process, you can use **`$V`** to emit an **inlined** value:
+
+```java
+import com.squareup.javapoet.ObjectInliner;
+
+private MethodSpec computeSomething(String name, MyComplexConfig config) {
+ ObjectInliner inliner = new ObjectInliner()
+ .trustExactTypes(MyComplexConfig.class);
+ return MethodSpec.methodBuilder(name)
+ .returns(int.class)
+ .addStatement("return MyComplexCalculation.calculate($V);", inliner.inlined(config))
+ .build();
+}
+```
+
+Inlined values are constructed inside a generated `java.util.function.Supplier` lambda that constructs
+the value from its fields.
+For the above example, the generated code might look like this:
+```java
+int name() {
+ return MyComplexCalculation.calculate(
+ ((MyComplexConfig)((java.util.function.Supplier)(() -> {
+ MyComplexConfig $$javapoet$MyComplexConfig = new MyComplexConfig();
+ $$javapoet$MyComplexConfig.setParameter1(1);
+ $$javapoet$MyComplexConfig.setParameter2("abc");
+ // ...
+ return $$javapoet$MyComplexConfig;
+ })).get()));
+}
+```
+The generated code will invoke getters and setters on the passed instance,
+so make sure you **trust** any values you inline with **`$V`**.
+By default, only `java.lang.Object` is trusted.
+You can trust additional object types by calling `trustExactTypes` and
+`trustTypesAssignableTo`:
+
+```java
+import com.squareup.javapoet.ObjectInliner;
+
+private MethodSpec computeSomething(String name, MyComplexConfig config) {
+ ObjectInliner inliner = new ObjectInliner()
+ .trustTypesAssignableTo(List.class)
+ .trustExactTypes(MyComplexConfig.class);
+ //...
+}
+```
+If you trust everything, you can call `trustEverything`. Although please don't do this with user controlled instances.
+
+```java
+import com.squareup.javapoet.ObjectInliner;
+
+private MethodSpec computeSomething(String name, MyComplexConfig config) {
+ ObjectInliner inliner = new ObjectInliner()
+ .trustEverything();
+ //...
+}
+```
+
+If you don't pass an instance of `ObjectInliner.Inlined`, it will be
+inlined by `ObjectInliner.getDefault()`.
+
+```java
+import com.squareup.javapoet.ObjectInliner;
+
+private MethodSpec computeSomething(String name, MyComplexConfig config) {
+ ObjectInliner.getDefault()
+ .trustEverything();
+ return MethodSpec.methodBuilder(name)
+ .returns(int.class)
+ // config will be inlined by ObjectInliner.getDefault()
+ .addStatement("return MyComplexCalculation.calculate($V);", config)
+ .build();
+}
+```
+
+By default, the following objects can be inlined if trusted:
+
+- Primitives and their boxed types (trusted by default)
+- String (trusted by default)
+- `java.lang.Class` (trusted by default)
+- Instances of `java.lang.Enum` (trusted by default)
+- Arrays of inlinable trusted objects (trusted by default)
+- Lists, Sets, and Maps of inlineable trusted objects (not trusted by default)
+- Objects that have public setters for all non-public fields (not trusted by default)
+- Records of inlineable trusted objects (not trusted by default)
+
+You can register custom inliners for types not covered by the above using `TypeInliner`:
+
+```java
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.ObjectEmitter;
+import com.squareup.javapoet.TypeInliner;
+import com.squareup.javapoet.ObjectInliner;
+
+import java.time.Duration;
+
+private MethodSpec computeSomething(String name, MyComplexConfig config) {
+ ObjectInliner.getDefault()
+ .trustEverything()
+ .addTypeInliner(new TypeInliner() {
+ @Override
+ public boolean canInline(Object object) {
+ return object instanceof Duration;
+ }
+
+ @Override
+ public String inline(ObjectEmitter emitter, Object instance) {
+ Duration duration = (Duration) instance;
+ return CodeBlock.of("$T.ofNanos($V);", Duration.class, emitter.inlined(duration.toNanos())).toString();
+ }
+ });
+ return MethodSpec.methodBuilder(name)
+ .returns(int.class)
+ .addStatement("return MyComplexCalculation.calculate($V);", Duration.ofSeconds(1L))
+ .build();
+}
+```
+
+If the default name prefix `$$javapoet$` conflicts with any variables in your generated code,
+you can specify a different one using `useNamePrefix`:
+
+```java
+import com.squareup.javapoet.ObjectInliner;
+
+private MethodSpec computeSomething(String name, MyComplexConfig config) {
+ ObjectInliner.getDefault()
+ .useNamePrefix("myPrefix");
+ return MethodSpec.methodBuilder(name)
+ .returns(int.class)
+ .addStatement("return MyComplexCalculation.calculate($V);", config)
+ .build();
+}
+```
+
#### Import static
JavaPoet supports `import static`. It does it via explicitly collecting type member names. Let's
diff --git a/src/main/java/com/squareup/javapoet/CodeBlock.java b/src/main/java/com/squareup/javapoet/CodeBlock.java
index 02542f58b..fb1373e57 100644
--- a/src/main/java/com/squareup/javapoet/CodeBlock.java
+++ b/src/main/java/com/squareup/javapoet/CodeBlock.java
@@ -51,6 +51,13 @@
*
{@code $T} emits a type reference. Types will be imported if possible. Arguments
* for types may be {@linkplain Class classes}, {@linkplain javax.lang.model.type.TypeMirror
,* type mirrors}, and {@linkplain javax.lang.model.element.Element elements}.
+ * {@code $V} emits an inlined-value . The inlined-value can be a primitive,
+ * {@linkplain Enum an enum value}, {@link Class a class constant}, an instance of a class
+ * with setters for all non-public instance fields, and arrays, collections, and maps
+ * containing inlinable values. An inlined-value can be assigned to a variable, passed
+ * as a parameter to a method, and generally can be used as an object with all its methods
+ * available for use. The inlined-value will be a raw type; cast to a generic type if
+ * needed.
* {@code $$} emits a dollar sign.
* {@code $W} emits a space or a newline, depending on its position on the line. This prefers
* to wrap lines before 100 columns.
@@ -329,6 +336,9 @@ private void addArgument(String format, char c, Object arg) {
case 'T':
this.args.add(argToType(arg));
break;
+ case 'V':
+ this.args.add(argToInlinedValue(arg));
+ break;
default:
throw new IllegalArgumentException(
String.format("invalid format string: '%s'", format));
@@ -360,6 +370,13 @@ private TypeName argToType(Object o) {
throw new IllegalArgumentException("expected type but was " + o);
}
+ private ObjectInliner.Inlined argToInlinedValue(Object o) {
+ if (o instanceof ObjectInliner.Inlined) {
+ return (ObjectInliner.Inlined) o;
+ }
+ return ObjectInliner.getDefault().inlined(o);
+ }
+
/**
* @param controlFlow the control flow construct and its code, such as "if (foo == 5)".
* Shouldn't contain braces or newline characters.
diff --git a/src/main/java/com/squareup/javapoet/CodeWriter.java b/src/main/java/com/squareup/javapoet/CodeWriter.java
index da6d3c8e5..6d1ce50e8 100644
--- a/src/main/java/com/squareup/javapoet/CodeWriter.java
+++ b/src/main/java/com/squareup/javapoet/CodeWriter.java
@@ -268,6 +268,11 @@ public CodeWriter emit(CodeBlock codeBlock, boolean ensureTrailingNewline) throw
typeName.emit(this);
break;
+ case "$V":
+ ObjectInliner.Inlined inlined = (ObjectInliner.Inlined) codeBlock.args.get(a++);
+ inlined.emit(this);
+ break;
+
case "$$":
emitAndIndent("$");
break;
diff --git a/src/main/java/com/squareup/javapoet/ObjectEmitter.java b/src/main/java/com/squareup/javapoet/ObjectEmitter.java
new file mode 100644
index 000000000..9949d2173
--- /dev/null
+++ b/src/main/java/com/squareup/javapoet/ObjectEmitter.java
@@ -0,0 +1,609 @@
+package com.squareup.javapoet;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class ObjectEmitter {
+ private final IdentityHashMap objectIdentifierMap;
+ private final Set possibleCircularReferenceSet;
+ private final List typeInliners;
+
+ /**
+ * This is the Set of all exact types we trust,
+ * to prevent malicious classes with malicious setters
+ * from being generated.
+ */
+ private final Set> trustedExactTypes;
+
+ /**
+ * This is the Set of all assignable types we trust,
+ * which allows anything that can be assigned to them to
+ * be generated. Be careful with these; there can be a
+ * malicious subclass you do not know about.
+ */
+ private final Set> trustedAssignableTypes;
+
+ private final Function emitterFactory;
+ private final NameAllocator nameAllocator;
+ private final String namePrefix;
+ private final CodeWriter codeWriter;
+
+ ObjectEmitter(ObjectInliner objectInliner, CodeWriter codeWriter) {
+ this.typeInliners = new ArrayList<>(objectInliner.getTypeInliners());
+ this.trustedAssignableTypes = new HashSet<>(objectInliner.getTrustedAssignableTypes());
+ this.trustedExactTypes = new HashSet<>(objectInliner.getTrustedExactTypes());
+ this.namePrefix = objectInliner.getNamePrefix();
+ this.objectIdentifierMap = new IdentityHashMap<>();
+ this.possibleCircularReferenceSet = Collections.newSetFromMap(new IdentityHashMap<>());
+ this.nameAllocator = new NameAllocator();
+ this.codeWriter = codeWriter;
+ this.emitterFactory = newCodeWriter -> new ObjectEmitter(this, newCodeWriter);
+ }
+
+ private ObjectEmitter(ObjectEmitter objectEmitter, CodeWriter codeWriter) {
+ this.typeInliners = objectEmitter.typeInliners;
+ this.trustedAssignableTypes = objectEmitter.trustedAssignableTypes;
+ this.trustedExactTypes = objectEmitter.trustedExactTypes;
+ this.namePrefix = objectEmitter.namePrefix;
+ // Use a copy of the existing name map so sibling emitters cannot
+ // see variables defined in this emitter (since they cannot access them)
+ this.objectIdentifierMap = new IdentityHashMap<>(objectEmitter.objectIdentifierMap);
+ this.nameAllocator = objectEmitter.nameAllocator.clone();
+
+ // Use the same possibleCircularReferenceSet, so we can detect circular references
+ // caused by nested TypeInliners
+ this.possibleCircularReferenceSet = objectEmitter.possibleCircularReferenceSet;
+ this.codeWriter = codeWriter;
+ this.emitterFactory = newCodeWriter -> new ObjectEmitter(this, newCodeWriter);
+ }
+
+ public ObjectInliner.Inlined inlined(Object value) {
+ return new ObjectInliner.Inlined(emitterFactory, value);
+ }
+
+ public void emit(String s) throws IOException {
+ codeWriter.emit(s);
+ }
+
+ public void emit(String format, Object... args) throws IOException {
+ codeWriter.emit(format, args);
+ }
+
+ public String newName(String suggestedSuffix) {
+ return nameAllocator.newName(namePrefix + suggestedSuffix);
+ }
+
+ public String newName(String suggestedSuffix, Object tag) {
+ return nameAllocator.newName(namePrefix + suggestedSuffix, tag);
+ }
+
+ /**
+ * Reserves a {@link String} that can be used as an identifier
+ * for an object.
+ */
+ private String reserveObjectIdentifier(Object object, Class> expressionType) {
+ String reservedIdentifier = nameAllocator
+ .newName(namePrefix + expressionType.getSimpleName());
+ objectIdentifierMap.put(object, reservedIdentifier);
+ return reservedIdentifier;
+ }
+
+ /**
+ * Return a string that can be used in a {@link CodeWriter} to
+ * access a complex object.
+ *
+ * @param object The object to be accessed
+ * @return A string that can be used by a {@link CodeWriter} to
+ * access the object.
+ */
+ private String getObjectIdentifier(Object object) {
+ return objectIdentifierMap.get(object);
+ }
+
+ public String getName(Object tag) {
+ return nameAllocator.get(tag);
+ }
+
+ void emit(Object object) throws IOException {
+ if (object == null) {
+ codeWriter.emit("null");
+ return;
+ }
+
+ // Does a neat trick, so we can get a code block in a fragment
+ // It defines an inline supplier and immediately calls it.
+ codeWriter.emit("(($T)(($T)(()->{", getSerializedType(object.getClass()), Supplier.class);
+ codeWriter.emit("\nreturn $L;\n})).get())", getInlinedObject(object));
+ }
+
+ /**
+ * Serializes a Pojo to code that uses its no-args constructor
+ * and setters to create the object.
+ *
+ * @param object The object to be serialized.
+ * @return A string that can be used by a {@link CodeWriter} to access the object
+ */
+ private String getInlinedObject(Object object) throws IOException {
+ // Some inliners cannot inline circular references, so bail out if we detect an
+ // unsupported circular reference
+ if (possibleCircularReferenceSet.contains(object)) {
+ throw new IllegalArgumentException(
+ "Cannot serialize an object of type (" + object.getClass().getCanonicalName()
+ + ") because it contains a circular reference.");
+ }
+ // If we already serialized the object, we should just return
+ // its identifier
+ if (objectIdentifierMap.containsKey(object)) {
+ return getObjectIdentifier(object);
+ }
+ // First, check for primitives
+ if (object == null) {
+ return "null";
+ }
+ if (object instanceof Boolean) {
+ return object.toString();
+ }
+ if (object instanceof Byte) {
+ // Cast to byte
+ return "((byte) " + object + ")";
+ }
+ if (object instanceof Character) {
+ // A char is 16-bits, so its max value is 0xFFFF.
+ // So if we get the hex string of (value | 0x10000),
+ // we get a five-digit hex string 0x1abcd where 1
+ // is the known first digit and abcd are the hex
+ // digits for the character (with "a" being the most significant bit).
+ // Any 16-bit Java character can be accessed using the expression
+ // '\uABCD' where ABCD are the hex digits for the Java character.
+ return "'\\u" + Integer.toHexString(((char) object) | 0x10000)
+ .substring(1) + "'";
+ }
+ if (object instanceof Short) {
+ // Cast to short
+ return "((short) " + object + ")";
+ }
+ if (object instanceof Integer) {
+ return object.toString();
+ }
+ if (object instanceof Long) {
+ // Add long suffix to number string
+ return object + "L";
+ }
+ if (object instanceof Float) {
+ // Add float suffix to number string
+ return object + "f";
+ }
+ if (object instanceof Double) {
+ // Add double suffix to number string
+ return object + "d";
+ }
+
+ // Check for builtin classes
+ if (object instanceof String) {
+ return CodeBlock.builder().add("$S", object).build().toString();
+ }
+ if (object instanceof Class>) {
+ Class> value = (Class>) object;
+ if (!Modifier.isPublic(value.getModifiers())) {
+ throw new IllegalArgumentException("Cannot serialize (" + value
+ + ") because it is not a public class.");
+ }
+ return value.getCanonicalName() + ".class";
+ }
+ if (object.getClass().isEnum()) {
+ // Use field access to read the enum
+ Class> enumClass = object.getClass();
+ Enum> objectEnum = (Enum>) object;
+ if (!Modifier.isPublic(enumClass.getModifiers())) {
+ // Use name() since toString() can be malicious
+ throw new IllegalArgumentException(
+ "Cannot serialize (" + objectEnum.name()
+ + ") because its type (" + enumClass
+ + ") is not a public class.");
+ }
+
+ return enumClass.getCanonicalName() + "." + objectEnum.name();
+ }
+
+ // We need to use a custom in-liner, which will potentially
+ // call methods on a user-supplied instances. Make sure we trust
+ // the type before continuing
+ if (!isTrustedType(object.getClass())) {
+ throw new IllegalArgumentException("Cannot serialize instance of ("
+ + object.getClass().getCanonicalName()
+ + ") because it is not an instance of a trusted type.");
+ }
+ // Check if any registered TypeInliner matches the class
+ for (TypeInliner typeInliner : typeInliners) {
+ if (typeInliner.canInline(object)) {
+ Class> expressionType = typeInliner.getInlinedType(object);
+ String identifier = reserveObjectIdentifier(object, expressionType);
+ possibleCircularReferenceSet.add(object);
+ String out = typeInliner.inline(this, object);
+ possibleCircularReferenceSet.remove(object);
+ emit("\n$T $N = $L;", expressionType,
+ identifier,
+ out);
+ return identifier;
+ }
+ }
+
+ return getInlinedComplexObject(object);
+ }
+
+ private boolean isTrustedType(Class> query) {
+ if (query.isArray()) {
+ return query.getComponentType().isPrimitive()
+ || isTrustedType(query.getComponentType());
+ }
+ for (Class> trustedAssignableType : trustedAssignableTypes) {
+ if (trustedAssignableType.isAssignableFrom(query)) {
+ return true;
+ }
+ }
+ for (Class> trustedExactType : trustedExactTypes) {
+ if (trustedExactType.equals(query)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isRecord(Object object) {
+ Class> superClass = object.getClass().getSuperclass();
+ return superClass != null && superClass.getName()
+ .equals("java.lang.Record");
+ }
+
+ /**
+ * Serializes collections and complex POJOs to code.
+ */
+ private String getInlinedComplexObject(Object object) throws IOException {
+ if (isRecord(object)) {
+ // Records must set all fields at initialization time,
+ // so we delay the declaration of its variable
+ return getInlinedRecord(object);
+ }
+ // Object is not serialized yet
+ // Create a new variable to store its value when setting its fields
+ String newIdentifier = reserveObjectIdentifier(object,
+ getSerializedType(object.getClass()));
+
+ // First, check if it is a collection type
+ if (object.getClass().isArray()) {
+ return getInlinedArray(newIdentifier, object);
+ }
+ if (object instanceof List) {
+ return getInlinedList(newIdentifier, (List>) object);
+ }
+ if (object instanceof Set) {
+ return getInlinedSet(newIdentifier, (Set>) object);
+ }
+ if (object instanceof Map) {
+ return getInlinedMap(newIdentifier, (Map, ?>) object);
+ }
+
+ if (!Modifier.isPublic(object.getClass().getModifiers())) {
+ throw new IllegalArgumentException("Cannot serialize type ("
+ + object.getClass().getCanonicalName()
+ + ") because it is not public.");
+ }
+ codeWriter.emit("\n$T $N;", object.getClass(), newIdentifier);
+ try {
+ Constructor> constructor = object.getClass().getConstructor();
+ if (!Modifier.isPublic(constructor.getModifiers())) {
+ throw new IllegalArgumentException("Cannot serialize type ("
+ + object.getClass().getCanonicalName()
+ + ") because its no-args constructor is not public.");
+ }
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot serialize type ("
+ + object.getClass().getCanonicalName()
+ + ") because it does not have a public no-args constructor.");
+ }
+ codeWriter.emit("\n$N = new $T();", newIdentifier, object.getClass());
+ inlineFieldsOfPojo(object.getClass(), newIdentifier, object);
+ return getObjectIdentifier(object);
+ }
+
+ private String getInlinedArray(String newIdentifier, Object array) throws IOException {
+ Class> componentType = array.getClass().getComponentType();
+ if (!Modifier.isPublic(componentType.getModifiers())) {
+ throw new IllegalArgumentException(
+ "Cannot serialize array of type (" + componentType.getCanonicalName()
+ + ") because (" + componentType.getCanonicalName()
+ + ") is not public.");
+ }
+ codeWriter.emit("\n$T $N;", array.getClass(), newIdentifier);
+
+ // Get the length of the array
+ int length = Array.getLength(array);
+
+ // Create a new array from the component type with the given length
+ codeWriter.emit("\n$N = new $T[$L];", newIdentifier,
+ componentType, Integer.toString(length));
+ for (int i = 0; i < length; i++) {
+ // Set the elements of the array
+ codeWriter.emit("\n$N[$L] = $L;",
+ newIdentifier,
+ Integer.toString(i),
+ getInlinedObject(Array.get(array, i)));
+ }
+ return getObjectIdentifier(array);
+ }
+
+ private String getInlinedList(String newIdentifier, List> list) throws IOException {
+ codeWriter.emit("\n$T $N;", List.class, newIdentifier);
+
+ // Create an ArrayList
+ codeWriter.emit("\n$N = new $T($L);", newIdentifier, ArrayList.class,
+ Integer.toString(list.size()));
+ for (Object item : list) {
+ // Add each item of the list to the ArrayList
+ codeWriter.emit("\n$N.add($L);",
+ newIdentifier,
+ getInlinedObject(item));
+ }
+ return getObjectIdentifier(list);
+ }
+
+ private String getInlinedSet(String newIdentifier, Set> set) throws IOException {
+ codeWriter.emit("\n$T $N;", Set.class, newIdentifier);
+
+ // Create a new HashSet
+ codeWriter.emit("\n$N = new $T($L);", newIdentifier, LinkedHashSet.class,
+ Integer.toString(set.size()));
+ for (Object item : set) {
+ // Add each item of the set to the HashSet
+ codeWriter.emit("\n$N.add($L);",
+ newIdentifier,
+ getInlinedObject(item));
+ }
+ return getObjectIdentifier(set);
+ }
+
+ private String getInlinedMap(String newIdentifier,
+ Map, ?> map) throws IOException {
+ codeWriter.emit("\n$T $N;", Map.class, newIdentifier);
+
+ // Create a HashMap
+ codeWriter.emit("\n$N = new $T($L);", newIdentifier, LinkedHashMap.class,
+ Integer.toString(map.size()));
+ for (Map.Entry, ?> entry : map.entrySet()) {
+ // Put each entry of the map into the HashMap
+ codeWriter.emit("\n$N.put($L, $L);",
+ newIdentifier,
+ getInlinedObject(entry.getKey()),
+ getInlinedObject(entry.getValue()));
+ }
+ return getObjectIdentifier(map);
+ }
+
+ // Workaround for Java 8
+ private static final class RecordComponent {
+ private final Class> type;
+ private final String name;
+
+ private RecordComponent(Class> type, String name) {
+ this.type = type;
+ this.name = name;
+ }
+
+ public Class> getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Object getValue(Object record) {
+ try {
+ return record.getClass().getMethod(name).invoke(record);
+ } catch (InvocationTargetException | IllegalAccessException
+ | NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ // Workaround for Java 8
+ private static RecordComponent[] getRecordComponents(Class> recordClass) {
+ try {
+ Object[] components = (Object[]) recordClass.
+ getMethod("getRecordComponents").invoke(recordClass);
+ RecordComponent[] out = new RecordComponent[components.length];
+ for (int i = 0; i < components.length; i++) {
+ Object component = components[i];
+ Class> componentClass = component.getClass();
+ Class> type = (Class>) componentClass
+ .getMethod("getType").invoke(component);
+ String name = (String) componentClass
+ .getMethod("getName").invoke(component);
+ out[i] = new RecordComponent(type, name);
+ }
+ return out;
+ } catch (InvocationTargetException | IllegalAccessException
+ | NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String getInlinedRecord(Object record)
+ throws IOException {
+ possibleCircularReferenceSet.add(record);
+ Class> recordClass = record.getClass();
+ if (!Modifier.isPublic(recordClass.getModifiers())) {
+ throw new IllegalArgumentException(
+ "Cannot serialize record type (" + recordClass.getCanonicalName()
+ + ") because it is not public.");
+ }
+
+ RecordComponent[] recordComponents = getRecordComponents(recordClass);
+ String[] componentAccessors = new String[recordComponents.length];
+ for (int i = 0; i < recordComponents.length; i++) {
+ Object value;
+ Class> serializedType = getSerializedType(recordComponents[i].getType());
+ if (!recordComponents[i].getType().equals(serializedType)) {
+ throw new IllegalArgumentException(
+ "Cannot serialize type (" + recordClass
+ + ") as its component (" + recordComponents[i].getName()
+ + ") uses an implementation of a collection ("
+ + recordComponents[i].getType()
+ + ") instead of the interface type ("
+ + serializedType + ").");
+ }
+ value = recordComponents[i].getValue(record);
+ try {
+ componentAccessors[i] = getInlinedObject(value);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Cannot serialize record type ("
+ + record.getClass().getCanonicalName() + ") because the type of its value ("
+ + value.getClass().getCanonicalName() + ") for its component ("
+ + recordComponents[i].getName() + ") is not serializable.", e);
+ }
+ }
+ // All components serialized, so no circular references
+ possibleCircularReferenceSet.remove(record);
+ StringBuilder constructorArgs = new StringBuilder();
+ for (String componentAccessor : componentAccessors) {
+ constructorArgs.append(componentAccessor).append(", ");
+ }
+ if (componentAccessors.length != 0) {
+ constructorArgs.delete(constructorArgs.length() - 2,
+ constructorArgs.length());
+ }
+ String newIdentifier = nameAllocator.newName(namePrefix
+ + recordClass.getSimpleName());
+ objectIdentifierMap.put(record, newIdentifier);
+ codeWriter.emit("\n$T $N = new $T($L);", recordClass, newIdentifier,
+ recordClass, constructorArgs.toString());
+ return getObjectIdentifier(record);
+ }
+
+ static Class> getSerializedType(Class> query) {
+ if (List.class.isAssignableFrom(query)) {
+ return List.class;
+ }
+ if (Set.class.isAssignableFrom(query)) {
+ return Set.class;
+ }
+ if (Map.class.isAssignableFrom(query)) {
+ return Map.class;
+ }
+ return query;
+ }
+
+ private static Method getSetterMethod(Class> expectedArgumentType, Field field) {
+ Class> declaringClass = field.getDeclaringClass();
+ String fieldName = field.getName();
+
+ String methodName = "set" + Character.toUpperCase(fieldName.charAt(0))
+ + fieldName.substring(1);
+ try {
+ return declaringClass.getMethod(methodName, expectedArgumentType);
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Sets the fields of object declared in objectSuperClass and all its superclasses.
+ *
+ * @param objectSuperClass A class assignable to object containing some of its fields.
+ * @param identifier The name of the variable storing the serialized object.
+ * @param object The object being serialized.
+ */
+ private void inlineFieldsOfPojo(Class> objectSuperClass, String identifier,
+ Object object) throws IOException {
+ if (objectSuperClass == Object.class) {
+ // We are the top-level, no more fields to set
+ return;
+ }
+ Field[] fields = objectSuperClass.getDeclaredFields();
+ // Sort by name to guarantee a consistent ordering
+ Arrays.sort(fields, Comparator.comparing(Field::getName));
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ // We do not want to write static fields
+ continue;
+ }
+ if (Modifier.isPublic(field.getModifiers())) {
+ try {
+ codeWriter.emit("\n$N.$N = $L;", identifier,
+ field.getName(),
+ getInlinedObject(field.get(object)));
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ continue;
+ }
+ // Set the field accessible so we can read its value
+ field.setAccessible(true);
+ Class> serializedType = getSerializedType(field.getType());
+ Method setterMethod = getSetterMethod(serializedType, field);
+ // setterMethod guaranteed to be public
+ if (setterMethod == null) {
+ if (!field.getType().equals(serializedType)) {
+ throw new IllegalArgumentException(
+ "Cannot serialize type (" + objectSuperClass
+ + ") as its field (" + field.getName()
+ + ") uses an implementation of a collection ("
+ + field.getType()
+ + ") instead of the interface type ("
+ + serializedType + ").");
+ }
+ throw new IllegalArgumentException(
+ "Cannot serialize type (" + objectSuperClass
+ + ") as it is missing a public setter method for field ("
+ + field.getName() + ") of type (" + field.getType() + ").");
+ }
+ Object value;
+ try {
+ value = field.get(object);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ // Convert the field value to code, and call the setter
+ // corresponding to the field with the serialized field value.
+ codeWriter.emit("\n$N.$N($L);", identifier,
+ setterMethod.getName(),
+ getInlinedObject(value));
+ } catch (IllegalArgumentException e) {
+ // We trust object, but not necessary value
+ throw new IllegalArgumentException("Cannot serialize an instance of type ("
+ + object.getClass().getCanonicalName() + ") because the type of its value ("
+ + value.getClass().getCanonicalName()
+ + ") for its field (" + field.getName()
+ + ") is not serializable.", e);
+ }
+ }
+ try {
+ inlineFieldsOfPojo(objectSuperClass.getSuperclass(), identifier, object);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Cannot serialize type ("
+ + objectSuperClass + ") because its superclass ("
+ + objectSuperClass.getSuperclass() + ") is not serializable.", e);
+ }
+ }
+}
diff --git a/src/main/java/com/squareup/javapoet/ObjectInliner.java b/src/main/java/com/squareup/javapoet/ObjectInliner.java
new file mode 100644
index 000000000..f449b30b7
--- /dev/null
+++ b/src/main/java/com/squareup/javapoet/ObjectInliner.java
@@ -0,0 +1,148 @@
+package com.squareup.javapoet;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+public class ObjectInliner {
+ private static final String DEFAULT_NAME_PREFIX = "$$javapoet$";
+ private static final ObjectInliner DEFAULT = new ObjectInliner();
+ private final List typeInliners = new ArrayList<>();
+
+ /**
+ * This is the Set of all exact types we trust,
+ * to prevent malicious classes with malicious setters
+ * from being generated.
+ */
+ private final Set> trustedExactTypes = new HashSet<>();
+
+ /**
+ * This is the Set of all assignable types we trust,
+ * which allows anything that can be assigned to them to
+ * be generated. Be careful with these; there can be a
+ * malicious subclass you do not know about.
+ */
+ private final Set> trustedAssignableTypes = new HashSet<>();
+ private final Function emitterFactory;
+ private String namePrefix;
+
+ public ObjectInliner() {
+ this.namePrefix = DEFAULT_NAME_PREFIX;
+ this.emitterFactory = codeWriter -> new ObjectEmitter(this, codeWriter);
+ }
+
+ public static ObjectInliner getDefault() {
+ return DEFAULT;
+ }
+
+ public ObjectInliner useNamePrefix(String namePrefix) {
+ this.namePrefix = namePrefix;
+ return this;
+ }
+
+ public ObjectInliner addTypeInliner(TypeInliner typeInliner) {
+ typeInliners.add(typeInliner);
+ return this;
+ }
+
+ /**
+ * Trust everything assignable to the given type. Do not call this method
+ * if users can create subclasses of that type, since this will allow
+ * the user to execute arbitrary code in the generated class.
+ */
+ public ObjectInliner trustTypesAssignableTo(Class> assignableType) {
+ trustedAssignableTypes.add(assignableType);
+ return this;
+ }
+
+ /**
+ * Trust the exact types reachable from the given type.
+ * These are:
+ *
+ * The type of its fields.
+ * The type of any fields inherited from a superclass.
+ * The type and its supertypes.
+ *
+ */
+ public ObjectInliner trustExactTypes(Class> exactType) {
+ if (trustedExactTypes.contains(exactType)) {
+ return this;
+ }
+ trustedExactTypes.add(exactType);
+ for (Field field : exactType.getDeclaredFields()) {
+ trustExactTypes(field.getType());
+ }
+ if (exactType.getSuperclass() != null) {
+ trustExactTypes(exactType.getSuperclass());
+ }
+ return this;
+ }
+
+ /**
+ * Trust everything. Do not call this method if you are passing in
+ * user-generated instances of arbitrary types, since this will allow
+ * the user to execute arbitrary code in the generated class.
+ */
+ public ObjectInliner trustEverything() {
+ return trustTypesAssignableTo(Object.class);
+ }
+
+ List getTypeInliners() {
+ return typeInliners;
+ }
+
+ Set> getTrustedExactTypes() {
+ return trustedExactTypes;
+ }
+
+ Set> getTrustedAssignableTypes() {
+ return trustedAssignableTypes;
+ }
+
+ String getNamePrefix() {
+ return namePrefix;
+ }
+
+ public static class Inlined {
+ private final Function emitterFactory;
+ private final Object value;
+
+ Inlined(Function emitterFactory, Object value) {
+ this.emitterFactory = emitterFactory;
+ this.value = value;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ void emit(CodeWriter codeWriter) throws IOException {
+ emitterFactory.apply(codeWriter).emit(value);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object)
+ return true;
+ if (object == null || getClass() != object.getClass())
+ return false;
+ Inlined inlined = (Inlined) object;
+ return Objects.equals(emitterFactory, inlined.emitterFactory)
+ && Objects.equals(value, inlined.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(emitterFactory, value);
+ }
+ }
+
+ public Inlined inlined(Object object) {
+ return new Inlined(emitterFactory, object);
+ }
+}
diff --git a/src/main/java/com/squareup/javapoet/TypeInliner.java b/src/main/java/com/squareup/javapoet/TypeInliner.java
new file mode 100644
index 000000000..5f0960c63
--- /dev/null
+++ b/src/main/java/com/squareup/javapoet/TypeInliner.java
@@ -0,0 +1,49 @@
+package com.squareup.javapoet;
+
+import java.io.IOException;
+
+/**
+ * An interface that allows for additional classes to be inlined
+ * by the $V formatter.
+ */
+public interface TypeInliner {
+ /**
+ * Returns true if this {@link TypeInliner} can inline an {@link Object} of the
+ * given type.
+ *
+ * @param object The object to be inlined
+ * @return True if the {@link TypeInliner} can inline the given object,
+ * false otherwise.
+ */
+ boolean canInline(Object object);
+
+ /**
+ * Returns the type of the expression returned by {@link #inline(ObjectEmitter, Object)}.
+ * Defaults to the type of the passed instance.
+ * @param instance The object to be inlined.
+ * @return The type of the expression returned by {@link #inline(ObjectEmitter, Object)}.
+ */
+ default Class> getInlinedType(Object instance) {
+ return instance.getClass();
+ }
+
+ /**
+ * Inlines an {@link Object} that
+ * {@link #canInline(Object)} accepted.
+ * The argument for `$V` must be
+ * obtained by using the {@link ObjectEmitter#inlined(Object)}
+ * of the passed {@link ObjectEmitter}.
+ * Returns a {@link String} that represents a valid Java expression,
+ * that when evaluated, results in an {@link Object} equal to the
+ * passed {@link Object}.
+ *
+ * @param emitter An emitter that can be used to generate code.
+ * You can emit any valid Java statements.
+ * When {@link #inline(ObjectEmitter, Object)}
+ * returns, it is expected the emitted code is a set of complete Java statements.
+ *
+ * @param instance The object to inline.
+ * @return A Java expression that evaluates to the inlined {@link Object}.
+ */
+ String inline(ObjectEmitter emitter, Object instance) throws IOException;
+}
diff --git a/src/test/java/com/squareup/javapoet/CodeBlockTest.java b/src/test/java/com/squareup/javapoet/CodeBlockTest.java
index 11b75fa4f..d7a369395 100644
--- a/src/test/java/com/squareup/javapoet/CodeBlockTest.java
+++ b/src/test/java/com/squareup/javapoet/CodeBlockTest.java
@@ -124,6 +124,14 @@ public final class CodeBlockTest {
assertThat(block.toString()).isEqualTo("java.lang.String");
}
+ @Test public void inlinedValueFormatCanBeIndexed() {
+ CodeBlock block = CodeBlock.builder().add("$1V", "taco").build();
+ assertThat(block.toString().replaceAll("\\s+", "")).isEqualTo(
+ ("((java.lang.String)((java.util.function.Supplier)(()->{\n" +
+ " return \"taco\";\n" +
+ " })).get())").replaceAll("\\s+", ""));
+ }
+
@Test public void simpleNamedArgument() {
Map map = new LinkedHashMap<>();
map.put("text", "taco");
diff --git a/src/test/java/com/squareup/javapoet/ObjectInlinerTest.java b/src/test/java/com/squareup/javapoet/ObjectInlinerTest.java
new file mode 100644
index 000000000..c0eb7d1b1
--- /dev/null
+++ b/src/test/java/com/squareup/javapoet/ObjectInlinerTest.java
@@ -0,0 +1,795 @@
+package com.squareup.javapoet;
+
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.CompilationSubject;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.lang.model.element.Modifier;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.testing.compile.Compiler.javac;
+
+public class ObjectInlinerTest {
+ public static class TrustedPojo {
+ public static final String TYPE = TrustedPojo.class.getCanonicalName();
+ public String name;
+ private int value;
+ private TrustedPojo next;
+
+ public TrustedPojo() {
+ }
+
+ public TrustedPojo(String name, int value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public void setValue(int value) {
+ this.value = value;
+ }
+
+ public TrustedPojo getNext() {
+ return next;
+ }
+
+ public void setNext(TrustedPojo next) {
+ this.next = next;
+ }
+
+ public TrustedPojo withNext(TrustedPojo next) {
+ this.next = next;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object)
+ return true;
+ if (object == null || getClass() != object.getClass())
+ return false;
+ TrustedPojo that = (TrustedPojo) object;
+ return Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+ }
+
+ private static String getInlineResult(Object object) {
+ return getInlineResult(object, (inliner) -> {});
+ }
+
+ private static String getInlineResult(Object object,
+ Consumer adapter) {
+ StringBuilder result = new StringBuilder();
+ CodeWriter codeWriter = new CodeWriter(result);
+ ObjectInliner inliner = new ObjectInliner();
+ adapter.accept(inliner);
+ try {
+ inliner.inlined(object).emit(codeWriter);
+ } catch (IOException e) {
+ throw new AssertionError("IOFailure", e);
+ }
+ return result.toString();
+ }
+
+ private String expectedResult(Class> type, String lambdaBody) {
+ StringBuilder out = new StringBuilder();
+ out.append("((");
+ out.append(ObjectEmitter.getSerializedType(type).getCanonicalName());
+ out.append(")((");
+ out.append(Supplier.class.getCanonicalName());
+ out.append(")(()->{");
+ out.append(lambdaBody);
+ out.append("})).get())");
+ return out.toString().replaceAll("\\s+", "");
+ }
+
+ private String expectedInlinedTrustedPojo(TrustedPojo trustedPojo, String... identifiers) {
+ return expectedInlinedTrustedPojo(trustedPojo, Arrays.asList(identifiers));
+ }
+
+ private String expectedInlinedTrustedPojo(TrustedPojo trustedPojo, List identifiers) {
+ StringBuilder out = new StringBuilder()
+ .append(TrustedPojo.TYPE).append(identifiers.get(0)).append(";")
+ .append(identifiers.get(0)).append(" = new ")
+ .append(TrustedPojo.TYPE).append("();")
+ .append(identifiers.get(0)).append(".name = \"")
+ .append(trustedPojo.name).append("\";");
+
+ if (trustedPojo.next == null) {
+ out.append(identifiers.get(0)).append(".setNext(null);");
+ } else if (trustedPojo.next == trustedPojo) {
+ out.append(identifiers.get(0)).append(".setNext(")
+ .append(identifiers.get(0)).append(");");
+ } else {
+ out.append(expectedInlinedTrustedPojo(trustedPojo.next,
+ identifiers.subList(1, identifiers.size())));
+ out.append(identifiers.get(0)).append(".setNext(")
+ .append(identifiers.get(1)).append(");");
+ }
+ out.append(identifiers.get(0)).append(".setValue(").
+ append(trustedPojo.value).append(");");
+ return out.toString();
+ }
+
+ private void assertCompiles(Class> type, String value) {
+ JavaFile javaFile = JavaFile.builder("", TypeSpec
+ .classBuilder("TestClass")
+ .addField(ObjectEmitter.getSerializedType(type), "field", Modifier.STATIC)
+ .addStaticBlock(CodeBlock.of("field = $L;", value))
+ .build()).build();
+ Compilation compilation = javac().compile(javaFile.toJavaFileObject());
+ CompilationSubject.assertThat(compilation).succeeded();
+ }
+
+ private void assertResult(String expected,
+ Object object) {
+ assertResult(expected, object, inliner -> {});
+ }
+
+ private void assertResult(String expected,
+ Object object,
+ Consumer adapter) {
+ String result = getInlineResult(object, adapter);
+ assertThat(result.replaceAll("\\s+", "")).isEqualTo(expectedResult(object.getClass(), expected));
+ assertCompiles(object.getClass(), result);
+ }
+
+ private void assertThrows(String errorMessage,
+ Object object) {
+ assertThrows(errorMessage, object, inliner -> {});
+ }
+
+ private void assertThrows(String errorMessage,
+ Object object,
+ Consumer adapter) {
+ try {
+ getInlineResult(object, adapter);
+ Assert.fail("Expected an exception to be thrown.");
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessageThat().isEqualTo(errorMessage);
+ }
+
+ }
+
+ @Test
+ public void testInlineNull() {
+ assertThat(getInlineResult(null)).isEqualTo("null");
+ }
+
+ @Test
+ public void testInlineBool() {
+ assertResult("return true;", true);
+ }
+
+ @Test
+ public void testInlineByte() {
+ assertResult("return ((byte) 1);", (byte) 1);
+ }
+
+ @Test
+ public void testInlineChar() {
+ assertResult("return '\\u0061';", 'a');
+ }
+
+ @Test
+ public void testInlineShort() {
+ assertResult("return ((short) 1);", (short) 1);
+ }
+
+ @Test
+ public void testInlineInt() {
+ assertResult("return 1;", 1);
+ }
+
+ @Test
+ public void testInlineLong() {
+ assertResult("return 1L;", 1L);
+ }
+
+ @Test
+ public void testInlineFloat() {
+ assertResult("return 1.0f;", 1.0f);
+ }
+
+ @Test
+ public void testInlineDouble() {
+ assertResult("return 1.0d;", 1.0d);
+ }
+
+ @Test
+ public void testInlineClass() {
+ assertResult("return " + List.class.getCanonicalName() + ".class;", List.class);
+ }
+
+ public enum MyEnum {
+ VALUE
+ }
+
+ @Test
+ public void testInlineEnum() {
+ assertResult("return " + MyEnum.class.getCanonicalName() + ".VALUE;", MyEnum.VALUE);
+ }
+
+ @Test
+ public void testPrimitiveArrays() {
+ assertResult(new StringBuilder()
+ .append("int[] $$javapoet$int__;")
+ .append("$$javapoet$int__ = new int[3];")
+ .append("$$javapoet$int__[0] = 1;")
+ .append("$$javapoet$int__[1] = 2;")
+ .append("$$javapoet$int__[2] = 3;")
+ .append("return $$javapoet$int__;")
+ .toString(), new int[] {1, 2, 3});
+ }
+
+ @Test
+ public void testObjectArrays() {
+ TrustedPojo[] inlined = new TrustedPojo[] {
+ new TrustedPojo("a", 1),
+ new TrustedPojo("b", 2)
+ .withNext(new TrustedPojo("c", 3))
+ };
+ assertResult(new StringBuilder()
+ .append(TrustedPojo.TYPE).append("[] ")
+ .append("$$javapoet$TrustedPojo__;")
+ .append("$$javapoet$TrustedPojo__ = new ")
+ .append(TrustedPojo.TYPE).append("[2];")
+ .append(expectedInlinedTrustedPojo(inlined[0], "$$javapoet$TrustedPojo"))
+ .append("$$javapoet$TrustedPojo__[0] = $$javapoet$TrustedPojo;")
+ .append(expectedInlinedTrustedPojo(inlined[1],
+ "$$javapoet$TrustedPojo_",
+ "$$javapoet$TrustedPojo___"))
+ .append("$$javapoet$TrustedPojo__[1] = $$javapoet$TrustedPojo_;")
+ .append("return $$javapoet$TrustedPojo__;")
+ .toString(),
+ inlined,
+ inliner -> inliner.trustExactTypes(TrustedPojo.class));
+ }
+
+ @Test
+ public void testPrimitiveList() {
+ assertResult(new StringBuilder()
+ .append(List.class.getCanonicalName()).append(" $$javapoet$List;")
+ .append("$$javapoet$List = new ")
+ .append(ArrayList.class.getCanonicalName()).append("(3);")
+ .append("$$javapoet$List.add(1);")
+ .append("$$javapoet$List.add(2);")
+ .append("$$javapoet$List.add(3);")
+ .append("return $$javapoet$List;")
+ .toString(), Arrays.asList(1, 2, 3),
+ inliner -> inliner.trustTypesAssignableTo(List.class));
+ }
+
+ @Test
+ public void testObjectList() {
+ List inlined = Arrays.asList(
+ new TrustedPojo("a", 1),
+ new TrustedPojo("b", 2)
+ .withNext(new TrustedPojo("c", 3)));
+ assertResult(new StringBuilder()
+ .append(List.class.getCanonicalName()).append(" $$javapoet$List;")
+ .append("$$javapoet$List = new ")
+ .append(ArrayList.class.getCanonicalName()).append("(2);")
+ .append(expectedInlinedTrustedPojo(inlined.get(0), "$$javapoet$TrustedPojo"))
+ .append("$$javapoet$List.add($$javapoet$TrustedPojo);")
+ .append(expectedInlinedTrustedPojo(inlined.get(1),
+ "$$javapoet$TrustedPojo_",
+ "$$javapoet$TrustedPojo__"))
+ .append("$$javapoet$List.add($$javapoet$TrustedPojo_);")
+ .append("return $$javapoet$List;")
+ .toString(),
+ inlined,
+ inliner -> inliner.trustTypesAssignableTo(List.class)
+ .trustExactTypes(TrustedPojo.class));
+ }
+
+ @Test
+ public void testPrimitiveSet() {
+ assertResult(new StringBuilder()
+ .append(Set.class.getCanonicalName()).append(" $$javapoet$Set;")
+ .append("$$javapoet$Set = new ")
+ .append(LinkedHashSet.class.getCanonicalName()).append("(3);")
+ .append("$$javapoet$Set.add(1);")
+ .append("$$javapoet$Set.add(2);")
+ .append("$$javapoet$Set.add(3);")
+ .append("return $$javapoet$Set;")
+ .toString(), new LinkedHashSet<>(Arrays.asList(1, 2, 3)),
+ inliner -> inliner
+ .trustTypesAssignableTo(Set.class));
+ }
+
+ @Test
+ public void testObjectSet() {
+ Set inlined = new LinkedHashSet<>(Arrays.asList(
+ new TrustedPojo("a", 1),
+ new TrustedPojo("b", 2)
+ .withNext(new TrustedPojo("c", 3))));
+
+ Iterator iterator = inlined.iterator();
+ assertResult(new StringBuilder()
+ .append(Set.class.getCanonicalName()).append(" $$javapoet$Set;")
+ .append("$$javapoet$Set = new ")
+ .append(LinkedHashSet.class.getCanonicalName()).append("(2);")
+ .append(expectedInlinedTrustedPojo(iterator.next(), "$$javapoet$TrustedPojo"))
+ .append("$$javapoet$Set.add($$javapoet$TrustedPojo);")
+ .append(expectedInlinedTrustedPojo(iterator.next(),
+ "$$javapoet$TrustedPojo_",
+ "$$javapoet$TrustedPojo__"))
+ .append("$$javapoet$Set.add($$javapoet$TrustedPojo_);")
+ .append("return $$javapoet$Set;")
+ .toString(),
+ inlined,
+ inliner -> inliner
+ .trustTypesAssignableTo(Set.class)
+ .trustExactTypes(TrustedPojo.class));
+ }
+
+ @Test
+ public void testPrimitiveMap() {
+ Map map = new LinkedHashMap<>();
+ map.put("a", 1);
+ map.put("b", 2);
+ map.put("c", 3);
+ assertResult(new StringBuilder()
+ .append(Map.class.getCanonicalName()).append(" $$javapoet$Map;")
+ .append("$$javapoet$Map = new ")
+ .append(LinkedHashMap.class.getCanonicalName()).append("(3);")
+ .append("$$javapoet$Map.put(\"a\", 1);")
+ .append("$$javapoet$Map.put(\"b\", 2);")
+ .append("$$javapoet$Map.put(\"c\", 3);")
+ .append("return $$javapoet$Map;")
+ .toString(), map,
+ inliner -> inliner.trustTypesAssignableTo(Map.class));
+ }
+
+ @Test
+ public void testObjectValueMap() {
+ Map map = new LinkedHashMap<>();
+ map.put("a", new TrustedPojo("a", 1));
+ map.put("b", new TrustedPojo("b", 2).withNext(new TrustedPojo("c", 3)));
+ assertResult(new StringBuilder()
+ .append(Map.class.getCanonicalName()).append(" $$javapoet$Map;")
+ .append("$$javapoet$Map = new ")
+ .append(LinkedHashMap.class.getCanonicalName()).append("(2);")
+ .append(expectedInlinedTrustedPojo(map.get("a"), "$$javapoet$TrustedPojo"))
+ .append("$$javapoet$Map.put(\"a\", $$javapoet$TrustedPojo);")
+ .append(expectedInlinedTrustedPojo(map.get("b"), "$$javapoet$TrustedPojo_", "$$javapoet$TrustedPojo__"))
+ .append("$$javapoet$Map.put(\"b\", $$javapoet$TrustedPojo_);")
+ .append("return $$javapoet$Map;")
+ .toString(), map,
+ inliner -> inliner
+ .trustTypesAssignableTo(Map.class)
+ .trustExactTypes(TrustedPojo.class));
+ }
+
+ @Test
+ public void testObjectKeyMap() {
+ Map map = new LinkedHashMap<>();
+ map.put(new TrustedPojo("a", 1), 1);
+ map.put(new TrustedPojo("b", 2).withNext(new TrustedPojo("c", 3)), 2);
+ Iterator iterator = map.keySet().iterator();
+ assertResult(new StringBuilder()
+ .append(Map.class.getCanonicalName()).append(" $$javapoet$Map;")
+ .append("$$javapoet$Map = new ")
+ .append(LinkedHashMap.class.getCanonicalName()).append("(2);")
+ .append(expectedInlinedTrustedPojo(iterator.next(), "$$javapoet$TrustedPojo"))
+ .append("$$javapoet$Map.put($$javapoet$TrustedPojo, 1);")
+ .append(expectedInlinedTrustedPojo(iterator.next(), "$$javapoet$TrustedPojo_", "$$javapoet$TrustedPojo__"))
+ .append("$$javapoet$Map.put($$javapoet$TrustedPojo_, 2);")
+ .append("return $$javapoet$Map;")
+ .toString(), map,
+ inliner -> inliner
+ .trustTypesAssignableTo(Map.class)
+ .trustExactTypes(TrustedPojo.class));
+ }
+
+ @Test
+ public void testTrustedObject() {
+ TrustedPojo pojo = new TrustedPojo("a", 1)
+ .withNext(new TrustedPojo("b", 2));
+ assertResult(new StringBuilder()
+ .append(expectedInlinedTrustedPojo(pojo, "$$javapoet$TrustedPojo", "$$javapoet$TrustedPojo_"))
+ .append("return $$javapoet$TrustedPojo;")
+ .toString(), pojo, inliner -> inliner.trustExactTypes(TrustedPojo.class));
+ }
+
+ @Test
+ public void testTrustedObjectWithSelfReference() {
+ TrustedPojo pojo = new TrustedPojo("a", 1);
+ pojo.setNext(pojo);
+ assertResult(new StringBuilder()
+ .append(expectedInlinedTrustedPojo(pojo, "$$javapoet$TrustedPojo"))
+ .append("return $$javapoet$TrustedPojo;")
+ .toString(), pojo, inliner -> inliner.trustExactTypes(TrustedPojo.class));
+ }
+
+ @Test
+ public void testUntrustedObjectThrows() {
+ TrustedPojo pojo = new TrustedPojo("a", 1);
+ pojo.setNext(pojo);
+ assertThrows("Cannot serialize instance of (" + TrustedPojo.class.getCanonicalName() +
+ ") because it is not an instance of a trusted type.", pojo);
+ }
+
+ @Test
+ public void testUntrustedCollectionThrows() {
+ assertThrows("Cannot serialize instance of (" + ArrayList.class.getCanonicalName() +
+ ") because it is not an instance of a trusted type.", new ArrayList<>());
+ assertThrows("Cannot serialize instance of (" + LinkedHashSet.class.getCanonicalName() +
+ ") because it is not an instance of a trusted type.", new LinkedHashSet<>());
+ assertThrows("Cannot serialize instance of (" + LinkedHashMap.class.getCanonicalName() +
+ ") because it is not an instance of a trusted type.", new LinkedHashMap<>());
+ }
+
+ private static class PrivatePojo {
+
+ }
+
+ @Test
+ public void testPrivatePojoThrows() {
+ PrivatePojo pojo = new PrivatePojo();
+ assertThrows("Cannot serialize type (" + PrivatePojo.class.getCanonicalName() +
+ ") because it is not public.", pojo,
+ inliner -> inliner.trustExactTypes(PrivatePojo.class));
+ }
+
+ public static class MissingSetterPojo {
+ private String value;
+
+ public MissingSetterPojo() {
+ }
+
+ public MissingSetterPojo(String value) {
+ this.value = value;
+ }
+ }
+
+ @Test
+ public void testMissingSetterPojoThrows() {
+ MissingSetterPojo pojo = new MissingSetterPojo();
+ assertThrows("Cannot serialize type (" + MissingSetterPojo.class +
+ ") as it is missing a public setter method for field (value)" +
+ " of type (class java.lang.String).", pojo,
+ inliner -> inliner.trustExactTypes(MissingSetterPojo.class));
+ }
+
+ public static class PrivateSetterPojo {
+ private String value;
+
+ public PrivateSetterPojo() {
+ }
+
+ public PrivateSetterPojo(String value) {
+ this.value = value;
+ }
+
+ private String getValue() {
+ return value;
+ }
+
+ private void setValue(String value) {
+ this.value = value;
+ }
+ }
+
+ @Test
+ public void testPrivateSetterPojoThrows() {
+ PrivateSetterPojo pojo = new PrivateSetterPojo();
+ assertThrows("Cannot serialize type (" + PrivateSetterPojo.class +
+ ") as it is missing a public setter method for field (value)" +
+ " of type (class java.lang.String).", pojo,
+ inliner -> inliner.trustExactTypes(PrivateSetterPojo.class));
+ }
+
+ public static class MissingConstructorPojo {
+ public MissingConstructorPojo(String value) {
+ }
+ }
+
+ @Test
+ public void testMissingConstructorPojoThrows() {
+ MissingConstructorPojo pojo = new MissingConstructorPojo("a");
+ assertThrows("Cannot serialize type (" + MissingConstructorPojo.class.getCanonicalName() +
+ ") because it does not have a public no-args constructor.", pojo,
+ inliner -> inliner.trustExactTypes(MissingConstructorPojo.class));
+ }
+
+ public static class PrivateConstructorPojo {
+ private PrivateConstructorPojo() {
+ }
+ }
+
+ @Test
+ public void testPrivateConstructorPojoThrows() {
+ PrivateConstructorPojo pojo = new PrivateConstructorPojo();
+ assertThrows("Cannot serialize type (" + PrivateConstructorPojo.class.getCanonicalName() +
+ ") because it does not have a public no-args constructor.", pojo,
+ inliner -> inliner.trustExactTypes(PrivateConstructorPojo.class));
+ }
+
+ public static class RawTypePojo {
+ private final static String TYPE = RawTypePojo.class.getCanonicalName();
+ public Object value;
+
+ public RawTypePojo() {
+ }
+
+ public RawTypePojo(Object value) {
+ this.value = value;
+ }
+ }
+
+ @Test
+ public void testRawTypePojoWithTrustedValue() {
+ RawTypePojo pojo = new RawTypePojo("taco");
+ assertResult(new StringBuilder()
+ .append(RawTypePojo.TYPE).append(" $$javapoet$RawTypePojo;")
+ .append(" $$javapoet$RawTypePojo = new ")
+ .append(RawTypePojo.TYPE).append("();")
+ .append("$$javapoet$RawTypePojo.value = \"taco\";")
+ .append("return $$javapoet$RawTypePojo;")
+ .toString(),
+ pojo,
+ inliner -> inliner.trustExactTypes(RawTypePojo.class));
+ }
+
+ @Test
+ public void testRawTypePojoWithUntrustedFieldThrows() {
+ RawTypePojo pojo = new RawTypePojo(new ArrayList<>());
+ assertThrows("Cannot serialize instance of (" + ArrayList.class.getCanonicalName()
+ + ") because it is not an instance of a trusted type.",
+ pojo,
+ inliner -> inliner.trustExactTypes(RawTypePojo.class));
+ }
+
+ private static class DurationInliner implements TypeInliner {
+
+ @Override
+ public boolean canInline(Object object) {
+ return object instanceof Duration;
+ }
+
+ @Override
+ public String inline(ObjectEmitter emitter, Object instance) {
+ Duration duration = (Duration) instance;
+ return CodeBlock.of("$T.ofNanos($V)", Duration.class,
+ emitter.inlined(duration.toNanos())).toString();
+ }
+ }
+
+ @Test
+ public void testCustomInliner() {
+ Duration duration = Duration.ofSeconds(1L);
+ String inlinedNanos = CodeBlock.of("$V", duration.toNanos()).toString();
+ assertResult(new StringBuilder()
+ .append(Duration.class.getCanonicalName())
+ .append(" ")
+ .append("$$javapoet$")
+ .append(Duration.class.getSimpleName())
+ .append(" = ")
+ .append(Duration.class.getCanonicalName())
+ .append(".ofNanos(")
+ .append(inlinedNanos)
+ .append(");")
+ .append("return $$javapoet$")
+ .append(Duration.class.getSimpleName()).append(";")
+ .toString(),
+ duration,
+ inliner -> inliner
+ .trustExactTypes(Duration.class)
+ .addTypeInliner(new DurationInliner()));
+ }
+
+ @Test
+ public void testCustomInlinerNotCalledOnUntrustedTypes() {
+ Duration duration = Duration.ofSeconds(1L);
+ assertThrows("Cannot serialize instance of (" + Duration.class.getCanonicalName() + ") because it is not an instance of a trusted type.",
+ duration,
+ inliner -> inliner
+ .addTypeInliner(new DurationInliner()));
+ }
+
+ @Test
+ public void testCustomInlinerIgnoresUnknownTypes() {
+ TrustedPojo pojo = new TrustedPojo("a", 1);
+ assertResult(new StringBuilder()
+ .append(expectedInlinedTrustedPojo(pojo, "$$javapoet$TrustedPojo"))
+ .append("return $$javapoet$TrustedPojo;")
+ .toString(),
+ pojo,
+ inliner -> inliner
+ .trustExactTypes(TrustedPojo.class)
+ .addTypeInliner(new DurationInliner()));
+ }
+
+ @Test
+ public void testUseCustomPrefix() {
+ TrustedPojo pojo = new TrustedPojo("a", 1);
+ assertResult(new StringBuilder()
+ .append(expectedInlinedTrustedPojo(pojo, "test$TrustedPojo"))
+ .append("return test$TrustedPojo;")
+ .toString(),
+ pojo,
+ inliner -> inliner
+ .useNamePrefix("test$")
+ .trustExactTypes(TrustedPojo.class));
+ }
+
+ private static class RecursiveInliner implements TypeInliner {
+
+ @Override
+ public boolean canInline(Object object) {
+ return object instanceof TrustedPojo;
+ }
+
+ @Override
+ public String inline(ObjectEmitter emitter, Object instance) throws IOException {
+ TrustedPojo pojo = (TrustedPojo) instance;
+ emitter.newName(pojo.name, pojo);
+ emitter.emit("$T $N = new $T();", TrustedPojo.class, emitter.getName(pojo), TrustedPojo.class);
+ emitter.emit("$N.name = $S + $V;", emitter.getName(pojo), "Taco ", emitter.inlined(pojo.name));
+ emitter.emit("$N.setValue($V);", emitter.getName(pojo), emitter.inlined(pojo.value));
+ emitter.emit("$N.setNext($V);", emitter.getName(pojo), emitter.inlined(pojo.next));
+ return emitter.getName(pojo);
+ }
+ }
+
+ private String expectedCustomInlinedTrustedPojo(TrustedPojo trustedPojo, String prefix, String suffix) {
+ StringBuilder out = new StringBuilder()
+ .append(TrustedPojo.TYPE).append(prefix).append(trustedPojo.name)
+ .append(" = new ")
+ .append(TrustedPojo.TYPE).append("();")
+ .append(prefix).append(trustedPojo.name).append(".name = \"")
+ .append("Taco\" + ").append(CodeBlock.of("$V", trustedPojo.name)).append(";")
+ .append(prefix).append(trustedPojo.name).append(".setValue(")
+ .append(CodeBlock.of("$V", trustedPojo.value)).append(");");
+
+ if (trustedPojo.next == null) {
+ out.append(prefix).append(trustedPojo.name).append(".setNext(null);");
+ } else {
+ out.append(prefix).append(trustedPojo.name).append(".setNext(")
+ .append("((").append(TrustedPojo.class.getCanonicalName()).append(")((")
+ .append(Supplier.class.getCanonicalName()).append(")(()->{")
+ .append(expectedCustomInlinedTrustedPojo(trustedPojo.next, prefix, suffix + "_"))
+ .append("return ").append(prefix).append(TrustedPojo.class.getSimpleName()).append(suffix).append("_").append(";")
+ .append("})).get())")
+ .append(");");
+ }
+ out.append(TrustedPojo.class.getCanonicalName())
+ .append(prefix).append(TrustedPojo.class.getSimpleName()).append(suffix)
+ .append(" = ")
+ .append(prefix).append(trustedPojo.name)
+ .append(";");
+ return out.toString();
+ }
+
+ @Test
+ public void testCustomInlineTrustedObject() {
+ TrustedPojo pojo = new TrustedPojo("a", 1)
+ .withNext(new TrustedPojo("b", 2));
+ assertResult(new StringBuilder()
+ .append(expectedCustomInlinedTrustedPojo(pojo, "$$javapoet$", ""))
+ .append("return $$javapoet$").append(TrustedPojo.class.getSimpleName()).append(";")
+ .toString(), pojo, inliner -> inliner.trustExactTypes(TrustedPojo.class)
+ .addTypeInliner(new RecursiveInliner()));
+ }
+
+ @Test
+ public void testCustomInlineTrustedObjectWithSelfReference() {
+ TrustedPojo pojo = new TrustedPojo("a", 1);
+ pojo.setNext(pojo);
+ assertThrows("Cannot serialize an object of type ("
+ + TrustedPojo.class.getCanonicalName() + ") because it contains a circular reference.", pojo,
+ inliner -> inliner.trustExactTypes(TrustedPojo.class)
+ .addTypeInliner(new RecursiveInliner()));
+ }
+
+ public static class PairPojo {
+ private PairPojo left;
+ private PairPojo right;
+
+ public PairPojo(PairPojo left, PairPojo right) {
+ this.left = left;
+ this.right = right;
+ }
+
+ public PairPojo getLeft() {
+ return left;
+ }
+
+ public PairPojo getRight() {
+ return right;
+ }
+ }
+
+ private static class PairPojoInliner implements TypeInliner {
+
+ @Override
+ public boolean canInline(Object object) {
+ return object instanceof PairPojo;
+ }
+
+ @Override
+ public String inline(ObjectEmitter emitter, Object instance) {
+ PairPojo pair = (PairPojo) instance;
+ return CodeBlock.of("new $T($V, $V)",
+ PairPojo.class,
+ emitter.inlined(pair.getLeft()),
+ emitter.inlined(pair.getRight())).toString();
+ }
+ }
+
+ private String expectedCustomInlinedPairPojo(PairPojo pairPojo, String prefix, String suffix) {
+ StringBuilder out = new StringBuilder()
+ .append(PairPojo.class.getCanonicalName())
+ .append(" ")
+ .append(prefix).append(PairPojo.class.getSimpleName()).append(suffix)
+ .append(" = new ")
+ .append(PairPojo.class.getCanonicalName())
+ .append("(");
+
+ if (pairPojo.left == null) {
+ out.append("null");
+ } else {
+ out.append("((").append(PairPojo.class.getCanonicalName()).append(")((")
+ .append(Supplier.class.getCanonicalName()).append(")(()->{");
+ out.append(expectedCustomInlinedPairPojo(pairPojo.left, prefix, suffix + "_"))
+ .append("return ").append(prefix).append(PairPojo.class.getSimpleName()).append(suffix).append("_");
+ out.append(";})).get())");
+ }
+
+ out.append(", ");
+
+ if (pairPojo.right == null) {
+ out.append("null");
+ } else {
+ out.append("((").append(PairPojo.class.getCanonicalName()).append(")((")
+ .append(Supplier.class.getCanonicalName()).append(")(()->{");
+ out.append(expectedCustomInlinedPairPojo(pairPojo.right, prefix, suffix + "_"))
+ .append("return ").append(prefix).append(PairPojo.class.getSimpleName()).append(suffix).append("_");
+ out.append(";})).get())");
+ }
+
+ out.append(");");
+
+
+ return out.toString();
+ }
+
+ @Test
+ public void testCustomInlineTrustedObjectWithSiblingReference() {
+ PairPojo common = new PairPojo(null, null);
+ PairPojo root = new PairPojo(common, common);
+ // Make sure the sibling emitters do not try to share common, since
+ // they cannot access each other's variables
+ assertResult(new StringBuilder()
+ .append(expectedCustomInlinedPairPojo(root, "$$javapoet$", ""))
+ .append("return $$javapoet$").append(PairPojo.class.getSimpleName()).append(";")
+ .toString(),
+ root,
+ inliner -> inliner.trustExactTypes(PairPojo.class)
+ .addTypeInliner(new PairPojoInliner()));
+ }
+}