/*
* 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.workday.autoparse.xml.annotations.XmlAttribute;
import com.workday.autoparse.xml.annotations.XmlChildElement;
import com.workday.autoparse.xml.annotations.XmlTextContent;
import com.workday.autoparse.xml.utils.CollectionUtils;
import com.workday.meta.MetaTypes;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
/**
* @author nathan.taylor
* @since 2013-10-11
*/
class AttributesAndElements {
private final ProcessingEnvironment processingEnv;
private final TypeElement classElement;
private Map<List<String>, Element> attributes = new HashMap<>();
private Collection<VariableElement> singletonFieldChildren = new HashSet<>();
private Collection<VariableElement> collectionFieldChildren = new HashSet<>();
private Collection<ExecutableElement> singletonSetterChildren = new HashSet<>();
private Collection<ExecutableElement> collectionSetterChildren = new HashSet<>();
private Element textContentElement;
private MetaTypes metaTypes;
public AttributesAndElements(ProcessingEnvironment processingEnv, TypeElement classElement) {
this.processingEnv = processingEnv;
metaTypes = new MetaTypes(processingEnv);
this.classElement = classElement;
findAttributesAndElements();
}
public Map<List<String>, Element> getAttributes() {
return attributes;
}
public Collection<VariableElement> getSingletonFieldChildren() {
return singletonFieldChildren;
}
public Collection<VariableElement> getCollectionFieldChildren() {
return collectionFieldChildren;
}
public Collection<ExecutableElement> getSingletonSetterChildren() {
return singletonSetterChildren;
}
public Collection<ExecutableElement> getCollectionSetterChildren() {
return collectionSetterChildren;
}
public Element getTextContentElement() {
return textContentElement;
}
private void findAttributesAndElements() {
Map<String, Element> visitedAttributes = new HashMap<>();
Map<TypeMirror, Element> visitedElements = new HashMap<>();
for (Element e : processingEnv.getElementUtils().getAllMembers(classElement)) {
XmlAttribute attributeAnnotation = e.getAnnotation(XmlAttribute.class);
XmlChildElement elementAnnotation = e.getAnnotation(XmlChildElement.class);
XmlTextContent textContentAnnotation = e.getAnnotation(XmlTextContent.class);
int numAnnotationsPresent =
numAnnotationsPresent(attributeAnnotation,
elementAnnotation,
textContentAnnotation);
if (numAnnotationsPresent > 1) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"An XML object can only be parsed as one"
+ " type.",
e);
}
if (attributeAnnotation != null) {
assignAsAttribute(e, attributeAnnotation, visitedAttributes);
} else if (textContentAnnotation != null) {
assignAsTextContent(e);
} else if (elementAnnotation != null) {
assignAsElement(e, visitedElements);
}
}
}
private void assignAsAttribute(Element e,
XmlAttribute annotation,
Map<String, Element> visitedAttributes) {
for (String name : annotation.value()) {
if (visitedAttributes.containsKey(name)) {
Element existingAttribute = visitedAttributes.get(name);
TypeElement enclosingClass = (TypeElement) existingAttribute.getEnclosingElement();
String currentAttributeName =
String.format("%s.%s",
enclosingClass.getQualifiedName(),
existingAttribute.getSimpleName());
String errorMessage =
String.format("%s also maps to attribute name \"%s\"",
currentAttributeName,
name);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, errorMessage, e);
} else {
visitedAttributes.put(name, e);
}
}
attributes.put(CollectionUtils.newArrayList(annotation.value()), e);
}
private void assignAsTextContent(Element e) {
if (textContentElement != null) {
String errorMessage = String.format(
"Only one field or one setter can be annotated with @%s",
XmlTextContent.class.getSimpleName());
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, errorMessage, e);
return;
}
TypeMirror type;
if (e instanceof ExecutableElement) {
// This is a setter
ExecutableElement method = (ExecutableElement) e;
if (!assertMethodHasSingleParameter(method)) {
return;
}
type = method.getParameters().get(0).asType();
} else {
// This is a field
type = e.asType();
}
if (!metaTypes.isString(type)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format(
"A field or setter annotated with %s must take type java.lang.String",
XmlTextContent.class.getSimpleName()));
return;
}
textContentElement = e;
}
private void assignAsElement(Element e, Map<TypeMirror, Element> visitedElements) {
if (e instanceof ExecutableElement) {
// This is a setter
ExecutableElement method = (ExecutableElement) e;
if (!assertMethodHasSingleParameter(method)) {
return;
}
DeclaredType parameterType = (DeclaredType) method.getParameters().get(0).asType();
if (!assertElementTypeNotAlreadyVisited(parameterType, e, visitedElements)) {
return;
}
if (metaTypes.isAssignable(parameterType, Collection.class)) {
collectionSetterChildren.add(method);
} else {
singletonSetterChildren.add(method);
}
} else {
// This is a field
TypeMirror fieldType = e.asType();
if (!assertElementTypeNotAlreadyVisited(fieldType, e, visitedElements)) {
return;
}
if (metaTypes.isAssignable(fieldType, Collection.class)) {
collectionFieldChildren.add((VariableElement) e);
} else {
singletonFieldChildren.add((VariableElement) e);
}
}
}
private int numAnnotationsPresent(XmlAttribute attributeAnnotation,
XmlChildElement elementAnnotation,
XmlTextContent textContentAnnotation) {
int count = 0;
if (attributeAnnotation != null) {
count++;
}
if (elementAnnotation != null) {
count++;
}
if (textContentAnnotation != null) {
count++;
}
return count;
}
private boolean assertMethodHasSingleParameter(ExecutableElement method) {
if (method.getParameters().size() != 1) {
String errorMessage = "Autoparse can only set values on single-parameter methods.";
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, errorMessage, method);
return false;
}
return true;
}
private boolean assertElementTypeNotAlreadyVisited(TypeMirror type, Element offendingElement,
Map<TypeMirror, Element> visitedElements) {
if (visitedElements.containsKey(type)) {
String currentElementName = visitedElements.get(type).getSimpleName().toString();
String errorMessage =
String.format("%s also takes elements of type %s", currentElementName,
type.toString());
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR, errorMessage, offendingElement);
return false;
}
return true;
}
}