Java tutorial
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.shieldsbetter.sbomg; import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.lang.model.element.Modifier; import org.apache.commons.lang3.tuple.ImmutablePair; /** * * @author hamptos */ public class ViewModelGenerator { private static final Template LIST_ITERATOR_TEMPL; private static final String LIST_ITERATOR_CODE = "" + " return new Iterator<${elementType}>() {\n" + " private final Iterator<${contextName}Key> " + "myBaseIterator = ${baseIterable}.iterator();\n\n" + " @Override\n" + " public boolean hasNext() {\n" + " return myBaseIterator.hasNext();\n" + " }\n\n" + " @Override\n" + " public void remove() {\n" + " throw new UnsupportedOperationException();\n" + " }\n\n" + " @Override\n" + " public ${elementType} next() {\n" + " return myBaseIterator.next().getValue();\n" + " }\n" + " };\n"; private static final Template RETURN_LIST_METHOD_TEMPL; private static final String RETURN_LIST_METHOD_CODE = "" + "return new Iterable<${elementType}>() {\n" + " @Override\n" + " public $T iterator() {\n" + " ${iteratorCode}" + " }\n" + " };\n"; private static final Template REPLACE_LIST_METHOD_TEMPL; private static final String REPLACE_LIST_METHOD_CODE = "" + "if (${elementsParam} == null) {\n" + " throw new NullPointerException();\n" + "}\n\n" + "<#if !leafFlag>" + "for (${contextName}Key k : my${contextName}List) {\n" + " if (my${contextName}Subscriptions.containsKey(k)) {\n" + " my${contextName}Subscriptions.remove(k).unsubscribe();\n" + " }\n" + "}\n\n" + "</#if>" + "my${contextName}List = new LinkedList<>();\n" + "final List<${elementType}> valueList = new LinkedList<>();\n" + "for (${elementType} e : ${elementsParam}) {\n" + " valueList.add(e);\n" + " ${contextName}Key k = new ${contextName}Key(e);" + " my${contextName}List.add(k);\n" + "<#if !leafFlag>" + " subscribeIfNotNull(k);\n" + "</#if>" + "}\n\n" + "Event replaceEvent = new Event() {\n" + " @Override public void on(Listener l) {\n" + " l.on${contextName}Replaced(${parentType}.this, " + "java.util.Collections.unmodifiableList(valueList));\n" + " }\n" + "};\n" + "for (EventListener l : myListeners) {\n" + " l.on(replaceEvent);\n" + "}\n"; private static final Template SUBSCRIBE_IF_NOT_NULL_TEMPL; private static final String SUBSCRIBE_IF_NOT_NULL_CODE = "" + "final ${fieldType} value = ${keyParam}.getValue();\n" + "if (value != null) {\n" + " ${fieldType}.Subscription elSub =\n" + " value.addListener(\n" + " new ${fieldType}.EventListener() {\n" + " @Override " + "public void on(final ${fieldType}.Event e) {\n" + " Event parentE = new Event() {\n" + " @Override public void on(Listener l) {\n" + " int curIndex = \n" + " my${fieldName}List.indexOf(${keyParam});\n" + " l.on${fieldName}Updated(${parentType}.this,\n" + " value, curIndex, ${keyParam}, e);\n" + " }\n" + " };\n" + " for (EventListener l : myListeners) {\n" + " l.on(parentE);\n" + " }\n" + " }\n" + " });\n" + " my${fieldName}Subscriptions.put(${keyParam}, elSub);\n" + "}\n"; private static final Template LIST_SET_METHOD_BY_INDEX_TEMPL; private static final String LIST_SET_METHOD_BY_INDEX_CODE = "" + "${fieldName}Key slot = " + "my${fieldName}List.get(${indexParam});\n\n" + "set${fieldName}(${indexParam}, slot, ${valueParam});\n"; private static final Template LIST_CONTAINS_VALUE_METHOD_TEMPL; private static final String LIST_CONTAINS_VALUE_METHOD_CODE = "" + "return my${fieldName}List.contains(${valueParam});\n"; private static final Template LIST_SET_METHOD_BY_KEY_TEMPL; private static final String LIST_SET_METHOD_BY_KEY_CODE = "" + "int index = my${fieldName}List.indexOf(${keyParam});\n" + "if (index == -1) {\n" + " throw new IllegalArgumentException();\n" + "}\n\n" + "set${fieldName}(index, ${keyParam}, ${valueParam});\n"; private static final Template LIST_SET_METHOD_CORE_TEMPL; private static final String LIST_SET_METHOD_CORE_CODE = "" + "final ${fieldType} oldType = ${keyParam}.getValue();\n" + "${keyParam}.setValue(${valueParam});\n" + "<#if !leafFlag>" + "if (my${fieldName}Subscriptions.containsKey(${keyParam})) {\n" + " my${fieldName}Subscriptions.remove(" + "${keyParam}).unsubscribe();\n" + "}\n" + "subscribeIfNotNull(${keyParam});\n" + "</#if>" + "Event addEvent = new Event() {\n" + " @Override public void on(Listener l) {\n" + " l.on${fieldName}Set(${parentType}.this, oldType, " + "${valueParam}, ${indexParam}, ${keyParam});\n" + " }\n" + "};\n" + "for (EventListener l : myListeners) {\n" + " l.on(addEvent);\n" + "}\n"; private static final Template LIST_CLEAR_METHOD_TEMPL; private static final String LIST_CLEAR_METHOD_CODE = "" + "while (!my${fieldName}List.isEmpty()) {\n" + " remove${fieldName}(my${fieldName}List.get(0));\n" + "}\n"; private static final Template LIST_ADD_METHOD_TEMPL; private static final String LIST_ADD_METHOD_CODE = "" + "final ${fieldName}Key slot = " + "new ${fieldName}Key(${valueParam});\n" + "my${fieldName}List.add(slot);\n" + "final int addedAt = my${fieldName}List.size() - 1;\n" + "<#if !leafFlag>" + "subscribeIfNotNull(slot);\n" + "</#if>" + "Event addEvent = new Event() {\n" + " @Override\n" + " public void on(Listener l) {\n" + " l.on${fieldName}Added(${parentType}.this, ${valueParam}, " + "addedAt, slot);\n" + " }\n" + "};\n" + "for (EventListener l : myListeners) {\n" + " l.on(addEvent);\n" + "}\n" + "return slot;"; private static final Template LIST_REMOVE_METHOD_BY_INDEX_TEMPL; private static final String LIST_REMOVE_METHOD_BY_INDEX_CODE = "" + "${fieldName}Key slot = " + "my${fieldName}List.remove(${indexParam});\n\n" + "remove${fieldName}(${indexParam}, slot);\n"; private static final Template LIST_REMOVE_METHOD_BY_KEY_TEMPL; private static final String LIST_REMOVE_METHOD_BY_KEY_CODE = "" + "int index = my${fieldName}List.indexOf(${keyParam});\n" + "if (index == -1) {\n" + " throw new IllegalArgumentException();\n" + "}\n\n" + "remove${fieldName}(index, ${keyParam});\n"; private static final Template LIST_REMOVE_METHOD_CORE_TEMPL; private static final String LIST_REMOVE_METHOD_CORE_CODE = "" + "<#if !leafFlag>" + "if (my${fieldName}Subscriptions.containsKey(${keyParam})) {\n" + " my${fieldName}Subscriptions.remove(" + "${keyParam}).unsubscribe();\n" + "}\n\n" + "</#if>" + "my${fieldName}List.remove(${keyParam});\n" + "Event removeEvent = new Event() {\n" + " @Override public void on(Listener l) {\n" + " l.on${fieldName}Removed(${parentType}.this, " + "${keyParam}.getValue(), ${indexParam}, ${keyParam});\n" + " }\n" + "};\n" + "for (EventListener l : myListeners) {\n" + " l.on(removeEvent);\n" + "}\n"; private static final Template SUBSCRIBE_TEMPL; private static final String SUBSCRIBE_CODE = "" + "<#if !finalFlag>" + "if (my${fieldName} == null) {\n" + " my${fieldName}Subscription = null;\n" + "}\n" + "else {\n" + " my${fieldName}Subscription = " + "<#else>" + "if (my${fieldName} != null) {\n " + "</#if>" + "my${fieldName}.addListener(\n" + " new ${fieldType}.EventListener() {\n" + " public void on(final ${fieldType}.Event e) {\n" + " Event parentE = new Event() {\n" + " @Override public void on(Listener l) {\n" + " l.on${fieldName}Updated(${parentType}.this,\n" + " my${fieldName}, e);\n" + " }\n" + " };\n" + " for (EventListener l : myListeners) {\n" + " l.on(parentE);\n" + " }\n" + " }\n" + " });\n" + "}\n"; private static final Template SET_METHOD_TEMPL; private static final String SET_METHOD_CODE = "" + "my${fieldName} = ${valueParam};\n" + "<#if !leafFlag>\n" + "if (my${fieldName}Subscription != null) {\n" + " my${fieldName}Subscription.unsubscribe();\n" + "}\n" + "</#if>\n" + "${afterUnsubscribe}" + "Event setEvent = new Event() {\n" + " @Override public void on(Listener l) {\n" + " l.on${contextName}Set(${parentType}.this, my${fieldName});\n" + " }\n" + "};\n" + "for (EventListener l : myListeners) {\n" + " l.on(setEvent);\n" + "}\n"; static { Configuration c = new Configuration(Configuration.VERSION_2_3_24); SUBSCRIBE_TEMPL = template(SUBSCRIBE_CODE, c); SET_METHOD_TEMPL = template(SET_METHOD_CODE, c); LIST_CONTAINS_VALUE_METHOD_TEMPL = template(LIST_CONTAINS_VALUE_METHOD_CODE, c); LIST_ADD_METHOD_TEMPL = template(LIST_ADD_METHOD_CODE, c); LIST_REMOVE_METHOD_CORE_TEMPL = template(LIST_REMOVE_METHOD_CORE_CODE, c); LIST_REMOVE_METHOD_BY_INDEX_TEMPL = template(LIST_REMOVE_METHOD_BY_INDEX_CODE, c); LIST_REMOVE_METHOD_BY_KEY_TEMPL = template(LIST_REMOVE_METHOD_BY_KEY_CODE, c); LIST_SET_METHOD_CORE_TEMPL = template(LIST_SET_METHOD_CORE_CODE, c); LIST_SET_METHOD_BY_INDEX_TEMPL = template(LIST_SET_METHOD_BY_INDEX_CODE, c); LIST_SET_METHOD_BY_KEY_TEMPL = template(LIST_SET_METHOD_BY_KEY_CODE, c); LIST_CLEAR_METHOD_TEMPL = template(LIST_CLEAR_METHOD_CODE, c); REPLACE_LIST_METHOD_TEMPL = template(REPLACE_LIST_METHOD_CODE, c); RETURN_LIST_METHOD_TEMPL = template(RETURN_LIST_METHOD_CODE, c); LIST_ITERATOR_TEMPL = template(LIST_ITERATOR_CODE, c); SUBSCRIBE_IF_NOT_NULL_TEMPL = template(SUBSCRIBE_IF_NOT_NULL_CODE, c); } private static Template template(String code, Configuration config) { Template result; try { result = new Template(null, new StringReader(code), config); } catch (IOException ioe) { throw new RuntimeException(ioe); } return result; } public static void generate(String packageName, String rootModelName, Object descriptor, Appendable out) throws IOException { ModelClass r = new ModelClass(rootModelName, false); outfitModel(r, "", descriptor); JavaFile output = JavaFile.builder(packageName, r.buildTypeSpec()).build(); output.writeTo(out); } private static void outfitModel(ModelClass dest, String contextName, Object modelDesc) { if (modelDesc instanceof List) { List listDesc = (List) modelDesc; switch (listDesc.size()) { case 1: { outfitModelWithList(dest, contextName, new HashSet<>(), listDesc.get(0)); break; } case 2: { Set<String> options = new HashSet<>((List<String>) listDesc.get(0)); outfitModel(dest, contextName, options, listDesc.get(1)); break; } default: { throw new RuntimeException(); } } } else { outfitModel(dest, contextName, new HashSet<>(), modelDesc); } } private static void outfitModelWithList(ModelClass dest, String contextName, Set<String> listOptions, Object elDesc) { boolean immutableFlag = listOptions.contains("immutable"); boolean replaceableFlag = listOptions.contains("replaceable"); ListElementTypeData elTypeData = outfitModelWithListElementType(dest, contextName, elDesc); String elTypeRaw = elTypeData.getRawTypeName(); TypeName elType = ClassName.get("", elTypeRaw); String slotTypeRaw = contextName + "Key"; TypeName slotType = ClassName.get("", slotTypeRaw); TypeSpec.Builder keyType = TypeSpec.classBuilder(slotTypeRaw) .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE) .addParameter(elType, "value").addStatement("myValue = value").build()) .addMethod(MethodSpec.methodBuilder("getValue").addModifiers(Modifier.PRIVATE).returns(elType) .addStatement("return myValue").build()); if (elTypeData.isFinal() || immutableFlag) { keyType.addField(elType, "myValue", Modifier.PRIVATE, Modifier.FINAL); } else { keyType.addField(elType, "myValue", Modifier.PRIVATE); keyType.addMethod(MethodSpec.methodBuilder("setValue").addModifiers(Modifier.PRIVATE) .addParameter(elType, "value").addStatement("myValue = value").build()); } dest.addType(keyType.build()); FieldSpec.Builder field = FieldSpec .builder(ParameterizedTypeName.get(ClassName.get("java.util", "List"), slotType), "my" + contextName + "List", Modifier.PRIVATE) .initializer("new $T<>()", ClassName.get("java.util", "LinkedList")); if (!replaceableFlag) { field.addModifiers(Modifier.FINAL); } dest.addField(field.build()); dest.addRootMethod("get" + contextName + "Element", elType, ImmutableList.of(ImmutablePair.of("index", TypeName.INT)), CodeBlock.builder().addStatement("return my$LList.get(index).getValue()", contextName).build()); dest.addRootMethod("get" + contextName + "Element", elType, ImmutableList.of(ImmutablePair.of("key", slotType)), CodeBlock.builder().addStatement("int index = my$LList.indexOf(key)", contextName) .addStatement("return my$LList.get(index).getValue()", contextName).build()); // TODO: Fix this. The generated field contians keys not elements. /* dest.addRootMethod("contains" + contextName + "Element", TypeName.BOOLEAN, ImmutableList.of( ImmutablePair.of("element", elType)), renderCodeBlock(LIST_CONTAINS_VALUE_METHOD_TEMPL, "fieldName", contextName, "valueParam", "element")); */ if (contextName.isEmpty()) { String parentInterfaceName = elTypeRaw; if (elTypeData.isInnerClass()) { parentInterfaceName = dest.getName() + "." + parentInterfaceName; } dest.addImplements(ParameterizedTypeName.get(ClassName.get("", "Iterable"), ClassName.get("", parentInterfaceName))); dest.addOverriddenRootMethod("iterator", ParameterizedTypeName.get(ClassName.get(Iterator.class), elType), ImmutableList.of(), renderCodeBlock(LIST_ITERATOR_TEMPL, "elementType", elTypeRaw, "contextName", contextName, "baseIterable", "my" + contextName + "List")); } else { // Gross workaround here. JavaPoet doesn't provide any way to // include a random non-static import. So we render out a template // that happens to include an unresolved $T, then replace it with // the type we want to import. dest.addRootMethod(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, contextName), ParameterizedTypeName.get(ClassName.get("", "Iterable"), elType), ImmutableList.of(), CodeBlock.of( renderTemplate(RETURN_LIST_METHOD_TEMPL, "elementType", elTypeRaw, "iteratorCode", renderTemplate(LIST_ITERATOR_TEMPL, "elementType", elTypeRaw, "contextName", contextName, "baseIterable", "my" + contextName + "List")), ParameterizedTypeName.get(ClassName.get(Iterator.class), elType))); } if (immutableFlag) { String elementParam = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, contextName + "Elements"); dest.addInitializationParameter(elementParam, ParameterizedTypeName.get(ClassName.get("", "Iterable"), elType)); CodeBlock.Builder init = CodeBlock.builder().beginControlFlow("for ($T e : $L)", elType, elementParam) .addStatement("$LKey k = new $LKey(e);", contextName, contextName) .addStatement("my$LList.add(k)", contextName); if (!elTypeData.isLeaf()) { init.addStatement("subscribeIfNotNull(k)"); } init.endControlFlow(); dest.addInitializationCode(init.build()); } if (replaceableFlag) { dest.addListenerEvent(contextName + "Replaced", ImmutableList.of(ImmutablePair.of("newValue", ParameterizedTypeName.get(ClassName.get("java.util", "List"), elType)))); dest.addRootMethod("replace" + contextName, TypeName.VOID, ImmutableList.of(ImmutablePair.of("elements", ParameterizedTypeName.get(ClassName.get("", "Iterable"), elType))), renderCodeBlock(REPLACE_LIST_METHOD_TEMPL, "contextName", contextName, "elementType", elTypeRaw, "parentType", dest.getName(), "elementsParam", "elements", "leafFlag", elTypeData.isLeaf())); dest.addBuildCode(CodeBlock.builder().beginControlFlow("") .addStatement("List<$T> valueList = new $T<>()", elType, LinkedList.class) .beginControlFlow("for ($T s : my$LList)", slotType, contextName) .addStatement("valueList.add(s.getValue())").endControlFlow() .addStatement("l.on$LReplaced(this, " + "$T.unmodifiableList(valueList))", contextName, Collections.class) .endControlFlow().build()); } if (!elTypeData.isLeaf()) { dest.addField(FieldSpec .builder( ParameterizedTypeName.get(ClassName.get("java.util", "Map"), slotType, ClassName.get("", elTypeRaw + ".Subscription")), "my" + contextName + "Subscriptions", Modifier.PRIVATE, Modifier.FINAL) .initializer("new $T<>()", ClassName.get("java.util", "HashMap")).build()); dest.addListenerEvent(contextName + "Updated", ImmutableList.of(ImmutablePair.of("updatedElement", elType), ImmutablePair.of("index", TypeName.INT), ImmutablePair.of("key", slotType), ImmutablePair.of("event", ClassName.get("", elTypeRaw + ".Event")))); dest.addRootMethod(MethodSpec.methodBuilder("subscribeIfNotNull").returns(TypeName.VOID) .addModifiers(Modifier.PRIVATE).addParameter(slotType, "key", Modifier.FINAL) .addCode(renderCodeBlock(SUBSCRIBE_IF_NOT_NULL_TEMPL, "keyParam", "key", "fieldName", contextName, "fieldType", elTypeRaw, "parentType", dest.getName())) .build()); } if (!immutableFlag) { if (!replaceableFlag) { dest.addBuildCode(CodeBlock.builder().beginControlFlow("").addStatement("int i = 0") .beginControlFlow("for ($T s : my$LList)", slotType, contextName) .addStatement("l.on$LAdded(this, s.getValue(), i, s)", contextName).addStatement("i++") .endControlFlow().endControlFlow().build()); } dest.addListenerEvent(contextName + "Added", ImmutableList.of(ImmutablePair.of("addedElement", elType), ImmutablePair.of("index", TypeName.INT), ImmutablePair.of("key", slotType))); dest.addRootMethod("add" + contextName, slotType, ImmutableList.of(ImmutablePair.of("newElement", elType)), renderCodeBlock(LIST_ADD_METHOD_TEMPL, "valueParam", "newElement", "fieldName", contextName, "fieldType", elTypeRaw, "leafFlag", elTypeData.isLeaf(), "parentType", dest.getName())); dest.addListenerEvent(contextName + "Removed", ImmutableList.of(ImmutablePair.of("removedElement", elType), ImmutablePair.of("index", TypeName.INT), ImmutablePair.of("key", slotType))); dest.addRootMethod("remove" + contextName, TypeName.VOID, ImmutableList.of(ImmutablePair.of("index", TypeName.INT)), renderCodeBlock( LIST_REMOVE_METHOD_BY_INDEX_TEMPL, "indexParam", "index", "fieldName", contextName)); dest.addRootMethod("remove" + contextName, TypeName.VOID, ImmutableList.of(ImmutablePair.of("key", slotType)), renderCodeBlock(LIST_REMOVE_METHOD_BY_KEY_TEMPL, "keyParam", "key", "fieldName", contextName)); dest.addRootMethod(MethodSpec.methodBuilder("remove" + contextName).addModifiers(Modifier.PRIVATE) .returns(TypeName.VOID).addParameter(TypeName.INT, "index", Modifier.FINAL) .addParameter(slotType, "key", Modifier.FINAL) .addCode(renderCodeBlock(LIST_REMOVE_METHOD_CORE_TEMPL, "fieldName", contextName, "keyParam", "key", "indexParam", "index", "leafFlag", elTypeData.isLeaf(), "parentType", dest.getName())) .build()); dest.addRootMethod("clear" + contextName, TypeName.VOID, ImmutableList.<ImmutablePair<String, TypeName>>of(), renderCodeBlock(LIST_CLEAR_METHOD_TEMPL, "fieldName", contextName)); if (!elTypeData.isFinal()) { dest.addListenerEvent(contextName + "Set", ImmutableList.of(ImmutablePair.of("oldValue", elType), ImmutablePair.of("newValue", elType), ImmutablePair.of("index", TypeName.INT), ImmutablePair.of("key", slotType))); dest.addRootMethod("set" + contextName, TypeName.VOID, ImmutableList.of(ImmutablePair.of("index", TypeName.INT), ImmutablePair.of("newValue", elType)), renderCodeBlock(LIST_SET_METHOD_BY_INDEX_TEMPL, "fieldName", contextName, "indexParam", "index", "valueParam", "newValue")); dest.addRootMethod("set" + contextName, TypeName.VOID, ImmutableList.of(ImmutablePair.of("key", slotType), ImmutablePair.of("newValue", elType)), renderCodeBlock(LIST_SET_METHOD_BY_KEY_TEMPL, "keyParam", "key", "fieldName", contextName, "valueParam", "newValue")); dest.addRootMethod(MethodSpec.methodBuilder("set" + contextName).returns(TypeName.VOID) .addModifiers(Modifier.PRIVATE).addParameter(TypeName.INT, "index", Modifier.FINAL) .addParameter(slotType, "key", Modifier.FINAL).addParameter(elType, "value", Modifier.FINAL) .addCode(renderCodeBlock(LIST_SET_METHOD_CORE_TEMPL, "keyParam", "key", "indexParam", "index", "valueParam", "value", "leafFlag", elTypeData.isLeaf(), "fieldName", contextName, "fieldType", elTypeRaw, "parentType", dest.getName())) .build()); } } } private static ListElementTypeData outfitModelWithListElementType(ModelClass dest, String contextName, Object elDesc) { ListElementTypeData result; if (elDesc instanceof List) { List listElDesc = (List) elDesc; switch (listElDesc.size()) { case 2: { Set<String> options = ImmutableSet.copyOf((List<String>) listElDesc.get(0)); result = outfitModelWithListElementType(dest, contextName, options, listElDesc.get(1)); break; } default: { throw new RuntimeException(); } } } else { result = outfitModelWithListElementType(dest, contextName, new HashSet<>(), elDesc); } return result; } private static ListElementTypeData outfitModelWithListElementType(ModelClass dest, String contextName, Set<String> options, Object elDesc) { ListElementTypeData result; if (elDesc instanceof String) { result = new ListElementTypeData(options.contains("final"), options.contains("leaf"), (String) elDesc, false); } else if (elDesc instanceof Map) { String rawElementName = contextName + "Record"; ModelClass r = new ModelClass(rawElementName, true); outfitModel(r, "", elDesc); boolean leaf = options.contains("leaf") || r.getListenerEventCount() == 0; dest.addType(r.buildTypeSpec()); result = new ListElementTypeData(options.contains("final"), leaf, rawElementName, true); } else { throw new RuntimeException(); } return result; } private static void outfitModel(ModelClass dest, String contextName, Set<String> options, Object modelDesc) { boolean finalFlag = options.contains("final"); boolean leafFlag = options.contains("leaf"); String fieldName = contextName; if (fieldName.isEmpty()) { fieldName = "Value"; } if (modelDesc instanceof String) { String fieldTypeString = (String) modelDesc; TypeName fieldType = typeName(fieldTypeString); FieldSpec.Builder fieldBuild = FieldSpec.builder(fieldType, "my" + fieldName, Modifier.PRIVATE); dest.addRootMethod("get" + fieldName, fieldType, ImmutableList.of(), CodeBlock.builder().addStatement("return my$L", fieldName).build()); if (!leafFlag) { dest.addListenerEvent(contextName + "Updated", ImmutableList.of(ImmutablePair.of("value", fieldType), ImmutablePair.of("event", ClassName.get("", fieldTypeString + ".Event")))); } if (finalFlag) { fieldBuild.addModifiers(Modifier.FINAL); String paramName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, fieldName); dest.addInitializationParameter(paramName, fieldType); dest.addInitializationCode( CodeBlock.builder().addStatement("my$L = $L", fieldName, paramName).build()); if (!leafFlag) { dest.addInitializationCode(renderCodeBlock(SUBSCRIBE_TEMPL, "finalFlag", finalFlag, "fieldName", fieldName, "fieldType", fieldTypeString, "parentType", dest.getName())); } } else { String subscribeCode; if (leafFlag) { subscribeCode = ""; } else { FieldSpec.Builder subscriptionField = FieldSpec.builder( ClassName.get("", fieldTypeString + ".Subscription"), "my" + fieldName + "Subscription", Modifier.PRIVATE); dest.addField(subscriptionField.build()); subscribeCode = renderTemplate(SUBSCRIBE_TEMPL, "finalFlag", finalFlag, "fieldName", fieldName, "fieldType", fieldTypeString, "parentType", dest.getName()); } dest.addBuildCode( CodeBlock.builder().addStatement("l.on$LSet(this, my$L)", contextName, fieldName).build()); dest.addListenerEvent(contextName + "Set", ImmutableList.of(ImmutablePair.of("newValue", fieldType))); dest.addRootMethod("set" + contextName, TypeName.VOID, ImmutableList.of(ImmutablePair.of("newValue", fieldType)), renderCodeBlock(SET_METHOD_TEMPL, "parentType", dest.getName(), "fieldName", fieldName, "valueParam", "newValue", "leafFlag", leafFlag, "contextName", contextName, "afterUnsubscribe", subscribeCode)); } dest.addField(fieldBuild.build()); } else if (modelDesc instanceof List) { List listDesc = (List) modelDesc; switch (listDesc.size()) { case 1: { outfitModelWithList(dest, contextName, options, listDesc.get(0)); break; } case 2: { outfitModelWithList(dest, contextName, options, listDesc); break; } default: { throw new RuntimeException(); } } } else if (modelDesc instanceof Map) { Map<String, Object> mapDesc = (Map<String, Object>) modelDesc; for (Map.Entry<String, Object> field : mapDesc.entrySet()) { outfitModel(dest, contextName + field.getKey(), field.getValue()); } } else { throw new RuntimeException(modelDesc.getClass().getSimpleName()); } } private static CodeBlock renderCodeBlock(Template t, Object... data) { return CodeBlock.builder().add(renderTemplate(t, data)).build(); } private static String renderTemplate(Template t, Object... data) { Map<String, Object> dataMap = new HashMap<>(); for (int i = 0; i < data.length; i += 2) { dataMap.put((String) data[i], data[i + 1]); } StringWriter w = new StringWriter(); try { t.process(dataMap, w); } catch (TemplateException | IOException e) { throw new RuntimeException(e); } return w.toString(); } private static TypeName typeName(String rawName) { TypeName result; switch (rawName) { case "boolean": { result = TypeName.BOOLEAN; break; } case "byte": { result = TypeName.BYTE; break; } case "char": { result = TypeName.CHAR; break; } case "double": { result = TypeName.DOUBLE; break; } case "float": { result = TypeName.FLOAT; break; } case "int": { result = TypeName.INT; break; } case "long": { result = TypeName.LONG; break; } case "short": { result = TypeName.SHORT; break; } default: { result = ClassName.get("", rawName); break; } } return result; } private static class ListElementTypeData { private final boolean myFinalFlag; private final boolean myLeafFlag; private final boolean myInnerClassFlag; private final String myRawTypeName; public ListElementTypeData(boolean finalFlag, boolean leafFlag, String rawTypeName, boolean innerClassFlag) { myFinalFlag = finalFlag; myLeafFlag = leafFlag; myRawTypeName = rawTypeName; myInnerClassFlag = innerClassFlag; } public boolean isInnerClass() { return myInnerClassFlag; } public boolean isFinal() { return myFinalFlag; } public boolean isLeaf() { return myLeafFlag; } public String getRawTypeName() { return myRawTypeName; } } }