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())); + } +}