/* * Copyright 2015 Workday, Inc. * * This software is available under the MIT license. * Please see the LICENSE.txt file in this project. */ package com.workday.autoparse.xml.codegen; import com.squareup.javawriter.JavaWriter; import com.workday.autoparse.xml.parser.ParseException; import com.workday.autoparse.xml.parser.UnexpectedChildException; import com.workday.autoparse.xml.parser.UnexpectedElementHandler; import com.workday.autoparse.xml.parser.UnknownElementException; import com.workday.autoparse.xml.parser.XmlElementParser; import com.workday.autoparse.xml.parser.XmlStreamReader; import com.workday.autoparse.xml.utils.CollectionUtils; import com.workday.meta.Initializers; import com.workday.meta.InvalidTypeException; import com.workday.meta.MetaTypes; import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.tools.Diagnostic; /** * Writes the {@code parseChildren} method in an {@link XmlElementParser}. * * @author nathan.taylor * @since 2013-10-14 */ class ParseChildrenMethodWriter { private final AttributesAndElements attributesAndElements; private final ProcessingEnvironment processingEnv; private final MetaTypes metaTypes; private final TypeElement classElement; private final Initializers initializers; public ParseChildrenMethodWriter(AttributesAndElements attributesAndElements, ProcessingEnvironment processingEnv, MetaTypes metaTypes, TypeElement classElement) { this.attributesAndElements = attributesAndElements; this.processingEnv = processingEnv; this.metaTypes = metaTypes; this.classElement = classElement; initializers = new Initializers(metaTypes); } public void writeParseChildrenMethod(TypeElement classElement, JavaWriter writer) throws IOException { List<String> parameters = new ArrayList<String>(4); parameters.add(classElement.getSimpleName().toString()); parameters.add("object"); parameters.add(XmlStreamReader.class.getSimpleName()); parameters.add("reader"); List<String> throwsTypes = CollectionUtils.newArrayList( ParseException.class.getSimpleName(), UnknownElementException.class.getSimpleName(), UnexpectedChildException.class.getSimpleName()); writer.beginMethod("void", "parseChildren", EnumSet.of(Modifier.PRIVATE), parameters, throwsTypes); initializeSetters(writer); writer.beginControlFlow("while (!reader.isEndElement())"); writer.emitStatement( "Preconditions.checkState(reader.isStartElement(), \"Expected to be at a start " + "element\")"); writer.emitStatement("String name = reader.getName()"); writer.emitStatement("Object child = ParserUtils.parseCurrentElement(reader)"); boolean first = true; first = writeChildIfStatements(writer, first); String unexpectedChildStatement = String.format("%s.handleUnexpectedChild(object, child, name)", UnexpectedElementHandler.class.getSimpleName()); if (first) { writer.emitStatement(unexpectedChildStatement); } else { writer.nextControlFlow("else if (child == null)"); writer.emitSingleLineComment("do nothing"); writer.nextControlFlow("else"); writer.emitStatement(unexpectedChildStatement); writer.endControlFlow(); } writer.endControlFlow(); writeSetterAssignments(writer); writer.endMethod(); } private void initializeSetters(JavaWriter writer) throws IOException { for (ExecutableElement singletonSetter : attributesAndElements.getSingletonSetterChildren()) { String methodName = singletonSetter.getSimpleName().toString(); DeclaredType parameterType = (DeclaredType) singletonSetter.getParameters().get(0).asType(); String parameterTypeName = writer.compressType(parameterType.toString()); writer.emitStatement("%s %sValue = %s", parameterTypeName, methodName, findSingletonInitialValue(parameterType)); } for (ExecutableElement collectionSetter : attributesAndElements .getCollectionSetterChildren()) { String methodName = collectionSetter.getSimpleName().toString(); VariableElement parameter = collectionSetter.getParameters().get(0); DeclaredType parameterType = (DeclaredType) parameter.asType(); String initializer = null; try { initializer = initializers.findCollectionInitializer(parameterType); } catch (InvalidTypeException e) { processingEnv.getMessager() .printMessage(Diagnostic.Kind.ERROR, e.getMessage(), collectionSetter); } writer.emitField(parameterType.toString(), methodName + "Value", EnumSet.noneOf(Modifier.class), initializer); } } private void writeSetterAssignments(JavaWriter writer) throws IOException { for (ExecutableElement singletonSetter : attributesAndElements.getSingletonSetterChildren()) { writer.emitStatement("object.%s(%sValue)", singletonSetter.getSimpleName(), singletonSetter.getSimpleName()); } for (ExecutableElement collectionSetter : attributesAndElements.getCollectionSetterChildren()) { writer.emitStatement("object.%s(%sValue)", collectionSetter.getSimpleName(), collectionSetter.getSimpleName()); } } private String findSingletonInitialValue(DeclaredType type) { String initialValue; if (metaTypes.isBoxable(type)) { TypeKind kind = metaTypes.asPrimitive(type).getKind(); if (kind == TypeKind.BOOLEAN) { initialValue = "false"; } else if (kind == TypeKind.CHAR) { initialValue = "'\0'"; } else { initialValue = "0"; } } else { initialValue = "null"; } return initialValue; } private String findCollectionInitializer(DeclaredType type) throws IllegalArgumentException { String initializer; if (metaTypes.isSameTypeErasure(type, List.class) || metaTypes.isSameTypeErasure(type, ArrayList.class)) { initializer = "Lists.newArrayList()"; } else if (metaTypes.isSameTypeErasure(type, LinkedList.class)) { initializer = "Lists.newLinkedList()"; } else if (metaTypes.isSameTypeErasure(type, Set.class) || metaTypes.isSameTypeErasure(type, HashSet.class)) { initializer = "Sets.newHashSet()"; } else if (metaTypes.isSameTypeErasure(type, LinkedHashSet.class)) { initializer = "Sets.newLinkedHashSet()"; } else if (metaTypes.isSameTypeErasure(type, TreeSet.class)) { initializer = "Sets.newTreeSet()"; } else { throw new IllegalArgumentException( String.format("Autoparse does not know how to instantiate Collection of type " + "%s", type.toString())); } return initializer; } private boolean writeChildIfStatements(JavaWriter writer, boolean first) throws IOException { // TODO: assign children according to the highest specificity metric for (Element e : attributesAndElements.getSingletonFieldChildren()) { first = writeSingletonChildIfStatement(e, writer, first); } for (Element e : attributesAndElements.getCollectionFieldChildren()) { first = writeCollectionChildIfStatement(e, writer, first); } for (Element e : attributesAndElements.getSingletonSetterChildren()) { first = writeSingletonChildIfStatement(e, writer, first); } for (Element e : attributesAndElements.getCollectionSetterChildren()) { first = writeCollectionChildIfStatement(e, writer, first); } return first; } private boolean writeSingletonChildIfStatement(Element element, JavaWriter writer, boolean first) throws IOException { String typeName = extractChildTypeName(element, writer, false); String assignmentStatement; String singletonValidationStatement; if (element instanceof ExecutableElement) { assignmentStatement = String.format("%sValue = (%s) child", element.getSimpleName(), typeName); singletonValidationStatement = String.format("%sValue == null", element.getSimpleName()); } else { assignmentStatement = String.format("object.%s = (%s) child", element.getSimpleName(), typeName); singletonValidationStatement = String.format("object.%s == null", element.getSimpleName()); } String ifStatement; ifStatement = String.format("if (child instanceof %s)", typeName); if (first) { first = false; writer.beginControlFlow(ifStatement); } else { writer.nextControlFlow("else " + ifStatement); } String errorMessage = String.format( "Expected only one child of type %s for parent type %s, but found multiple", typeName, classElement.getSimpleName()); writer.emitStatement("Preconditions.checkState(%s, \"%s\")", singletonValidationStatement, errorMessage); writer.emitStatement(assignmentStatement); return first; } private boolean writeCollectionChildIfStatement(Element element, JavaWriter writer, boolean first) throws IOException { String parameterTypeName = extractChildTypeName(element, writer, true); String addStatement; if (element instanceof ExecutableElement) { addStatement = String.format("%sValue.add((%s) child)", element.getSimpleName(), parameterTypeName); } else { addStatement = String.format("object.%s.add((%s) child)", element.getSimpleName(), parameterTypeName); } String ifStatement = String.format("if (child instanceof %s)", parameterTypeName); if (first) { first = false; writer.beginControlFlow(ifStatement); } else { writer.nextControlFlow("else " + ifStatement); } writer.emitStatement(addStatement); return first; } /** * @param element The method or field that takes either a parsed object or a collection of * parsed objects. * * @return The compressed name of the parsed type. */ private String extractChildTypeName(Element element, JavaWriter writer, boolean isCollection) { String typeName; if (element instanceof ExecutableElement) { DeclaredType declaredType = (DeclaredType) ((ExecutableElement) element).getParameters().get(0).asType(); if (isCollection) { declaredType = getCollectionParameterType(declaredType, element); } typeName = writer.compressType(declaredType.toString()); } else { DeclaredType declaredType = (DeclaredType) element.asType(); if (isCollection) { declaredType = getCollectionParameterType(declaredType, element); } typeName = writer.compressType(declaredType.toString()); } return typeName; } private DeclaredType getCollectionParameterType(DeclaredType collectionType, Element offendingElement) { DeclaredType declaredParameterType; TypeMirror parameterType = collectionType.getTypeArguments().get(0); if (!(parameterType instanceof DeclaredType)) { String errorMessage = "Autoparse cannot handle collections parametrised with non-declared types."; processingEnv.getMessager() .printMessage(Diagnostic.Kind.ERROR, errorMessage, offendingElement); Elements elementUtils = processingEnv.getElementUtils(); TypeElement typeElement = elementUtils.getTypeElement(Object.class.getCanonicalName()); declaredParameterType = (DeclaredType) typeElement.asType(); } else { declaredParameterType = (DeclaredType) parameterType; } return declaredParameterType; } }