/*
* XmlElementWrapperPlugin.java
*
* Copyright (C) 2009, Bjarne Hansen, http://www.conspicio.dk.
* All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package com.sun.tools.xjc.addon.xew;
import static com.sun.tools.xjc.addon.xew.CommonUtils.addAnnotation;
import static com.sun.tools.xjc.addon.xew.CommonUtils.copyFields;
import static com.sun.tools.xjc.addon.xew.CommonUtils.generableToString;
import static com.sun.tools.xjc.addon.xew.CommonUtils.getAnnotation;
import static com.sun.tools.xjc.addon.xew.CommonUtils.getAnnotationMember;
import static com.sun.tools.xjc.addon.xew.CommonUtils.getAnnotationMemberExpression;
import static com.sun.tools.xjc.addon.xew.CommonUtils.getPrivateField;
import static com.sun.tools.xjc.addon.xew.CommonUtils.getXsdDeclaration;
import static com.sun.tools.xjc.addon.xew.CommonUtils.hasPropertyNameCustomization;
import static com.sun.tools.xjc.addon.xew.CommonUtils.isHiddenClass;
import static com.sun.tools.xjc.addon.xew.CommonUtils.isListedAsParametrisation;
import static com.sun.tools.xjc.addon.xew.CommonUtils.removeAnnotation;
import static com.sun.tools.xjc.addon.xew.CommonUtils.setPrivateField;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementRefs;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlMixed;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;
import com.sun.codemodel.JAnnotatable;
import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JAnnotationValue;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassContainer;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JJavaName;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPackage;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.addon.xew.config.AbstractConfigurablePlugin;
import com.sun.tools.xjc.addon.xew.config.ClassConfiguration;
import com.sun.tools.xjc.addon.xew.config.CommonConfiguration;
import com.sun.tools.xjc.model.CElementPropertyInfo;
import com.sun.tools.xjc.model.CElementPropertyInfo.CollectionMode;
import com.sun.tools.xjc.model.CPropertyInfo;
import com.sun.tools.xjc.model.CReferencePropertyInfo;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.FieldOutline;
import com.sun.tools.xjc.outline.Outline;
import com.sun.tools.xjc.reader.Ring;
import com.sun.xml.bind.api.impl.NameConverter;
import com.sun.xml.xsom.XSComponent;
import com.sun.xml.xsom.XSDeclaration;
import org.apache.commons.lang3.ObjectUtils;
import org.jvnet.jaxb2_commons.util.CustomizationUtils;
/**
* The XML Element Wrapper plugin is a JAXB plugin for the XJC compiler enabling generation of "natural" Java classes
* for handling collection types. The code generated will be annotated with {@link XmlElementWrapper} and
* {@link XmlElement} annotations and will have no extra inner classes representing the immediate collection type.
*
* @see <a href="https://github.com/dmak/jaxb-xew-plugin">plugin site</a>
* @see <a href="http://www.conspicio.dk/blog/bjarne/jaxb-xmlelementwrapper-plugin">original plugin site</a>
* @see <a href="http://www.conspicio.dk/projects/overview">source code and binary packages</a>
*
* @author Bjarne Hansen
* @author Dmitry Katsubo
*/
public class XmlElementWrapperPlugin extends AbstractConfigurablePlugin {
private JClass xmlElementDeclModelClass;
static final String FACTORY_CLASS_NAME = "ObjectFactory";
@Override
protected void runInternal(Outline outline) throws ClassNotFoundException, IOException {
JCodeModel codeModel = outline.getCodeModel();
JClass xmlElementWrapperModelClass = codeModel.ref(XmlElementWrapper.class);
JClass xmlElementModelClass = codeModel.ref(XmlElement.class);
JClass xmlAnyElementModelClass = codeModel.ref(XmlAnyElement.class);
JClass xmlMixedModelClass = codeModel.ref(XmlMixed.class);
JClass xmlElementRefModelClass = codeModel.ref(XmlElementRef.class);
JClass xmlElementRefsModelClass = codeModel.ref(XmlElementRefs.class);
JClass xmlElementsModelClass = codeModel.ref(XmlElements.class);
JClass xmlJavaTypeAdapterModelClass = codeModel.ref(XmlJavaTypeAdapter.class);
JClass xmlTypeModelClass = codeModel.ref(XmlType.class);
xmlElementDeclModelClass = codeModel.ref(XmlElementDecl.class);
Ring.begin();
Ring.add(outline.getModel());
logger.debug("JAXB Process Model (run)...");
applyConfigurationFromCustomizations(globalConfiguration,
CustomizationUtils.getCustomizations(outline.getModel()), false);
// Write summary information on the option for this compilation.
writeSummary("Compilation:");
writeSummary(" JAXB version : " + Options.getBuildID());
writeSummary(" Control file : "
+ ObjectUtils.defaultIfNull(globalConfiguration.getControlFileName(), "<none>"));
writeSummary(" Summary file : "
+ ObjectUtils.defaultIfNull(globalConfiguration.getSummaryFileName(), "<none>"));
writeSummary(" Instantiation mode : " + globalConfiguration.getInstantiationMode());
writeSummary(" Collection impl : " + globalConfiguration.getCollectionImplClass().getName());
writeSummary(" Collection interface : " + globalConfiguration.getCollectionInterfaceClass().getName());
writeSummary(" Plural form : " + globalConfiguration.isApplyPluralForm());
writeSummary("");
// Visit all classes generated by JAXB and find candidate classes for transformation.
Map<String, Candidate> candidatesMap = new HashMap<String, Candidate>();
// Write information on candidate classes to summary file.
writeSummary("Candidates:");
for (Iterator<Candidate> iter = findCandidateClasses(outline).iterator(); iter.hasNext();) {
Candidate candidate = iter.next();
if (globalConfiguration.isClassIncluded(candidate.getClassName())) {
if (globalConfiguration.isClassUnmarkedForRemoval(candidate.getClassName())) {
candidate.unmarkForRemoval();
writeSummary("\t[!]: " + candidate.getClassName());
}
else {
writeSummary("\t[+]: " + candidate.getClassName());
}
candidatesMap.put(candidate.getClassName(), candidate);
}
else {
writeSummary("\t[-]: " + candidate.getClassName());
}
}
writeSummary("\t" + candidatesMap.size() + " candidate(s) being considered.");
writeSummary("");
writeSummary("Modifications:");
int modificationCount = 0;
// Visit all classes again to check if the candidate is not eligible for removal:
// * If there are classes that extend the candidate
// * If there are class fields, that refer the candidate by e.g. @XmlElementRef annotation
for (ClassOutline outlineClass : outline.getClasses()) {
// Get the implementation class for the current class.
JDefinedClass targetClass = outlineClass.implClass;
ClassConfiguration classConfiguration = applyConfigurationFromCustomizations(globalConfiguration,
CustomizationUtils.getCustomizations(outlineClass), true);
// We cannot remove candidates that have parent classes, but we can still substitute them:
Candidate parentCandidate = candidatesMap.get(targetClass._extends().fullName());
if (parentCandidate != null) {
logger.debug("Candidate " + parentCandidate.getClassName() + " is a parent of " + targetClass.name()
+ " and hence won't be removed.");
parentCandidate.unmarkForRemoval();
}
// Visit all fields in this class.
for (FieldOutline field : outlineClass.getDeclaredFields()) {
// Only non-primitive fields are interesting.
// Consider only PropertyKind.ELEMENT as (for example) PropertyKind.ATTRIBUTE (stands for XSD attribute) is always simple type:
if (!(field.getRawType() instanceof JClass)
|| !(field.getPropertyInfo() instanceof CElementPropertyInfo)) {
continue;
}
final JClass fieldType = (JClass) field.getRawType();
final CPropertyInfo fieldPropertyInfo = field.getPropertyInfo();
String fieldName = fieldPropertyInfo.getName(false);
Candidate candidate = null;
for (Candidate c : candidatesMap.values()) {
// Skip fields with basic types as for example any class can be casted to Object.
if (fieldType.isAssignableFrom(c.getClazz()) && !isHiddenClass(fieldType)) {
// If the given field has type T, it cannot be also in the list of parametrisations (e.g. T<T>).
candidate = c;
break;
}
// If the candidate T is referred from list of parametrisations (e.g. List<T>), it cannot be removed.
// However field substitutions will take place.
else if (isListedAsParametrisation(c.getClazz(), fieldType)) {
logger.debug("Candidate " + c.getClassName() + " is listed as parametrisation of "
+ targetClass.fullName() + "#" + fieldName + " and hence won't be removed.");
c.unmarkForRemoval();
}
}
final JFieldVar originalImplField = targetClass.fields().get(fieldName);
if (candidate == null || !classConfiguration.isAnnotatable()) {
checkAnnotationReference(candidatesMap, originalImplField);
continue;
}
ClassConfiguration fieldConfiguration = applyConfigurationFromCustomizations(classConfiguration,
CustomizationUtils.getCustomizations(field), true);
if (!fieldConfiguration.isAnnotatable()) {
logger.debug("Field " + fieldName + " is excluded for processing.");
candidate.unmarkForRemoval();
continue;
}
// We have a candidate field to be replaced with a wrapped version. Report finding to summary file.
writeSummary("\tReplacing field [" + fieldType.name() + " " + targetClass.fullName() + "#" + fieldName
+ "]");
candidate.incrementSubstitutions();
modificationCount++;
// The container class has to be deleted. Check that inner class has to be moved to it's parent.
if (moveInnerClassToParent(outline, candidate)) {
modificationCount++;
}
List<JClass> fieldTypeParametrisations = candidate.getFieldClass().getTypeParameters();
// Create the new interface and collection classes using the specified interface and
// collection classes (configuration) with an element type corresponding to
// the element type from the collection present in the candidate class (narrowing).
JClass collectionInterfaceClass = codeModel.ref(fieldConfiguration.getCollectionInterfaceClass())
.narrow(fieldTypeParametrisations);
JClass collectionImplClass = codeModel.ref(fieldConfiguration.getCollectionImplClass())
.narrow(fieldTypeParametrisations);
boolean pluralFormWasApplied = false;
// Apply the plural form if there are no customizations. Assuming that customization is correct as may define the
// plural form in more correct way, e.g. "field[s]OfScience" instead of "fieldOfScience[s]".
if (fieldConfiguration.isApplyPluralForm() && !hasPropertyNameCustomization(fieldPropertyInfo)) {
String oldFieldName = fieldName;
// Taken from com.sun.tools.xjc.reader.xmlschema.ParticleBinder#makeJavaName():
fieldName = JJavaName.getPluralForm(fieldName);
// The field e.g. "return" was escaped as "_return", but after conversion to plural
// it became valid Java identifier, so we remove the leading "_":
if (fieldName.startsWith("_") && JJavaName.isJavaIdentifier(fieldName.substring(1))) {
fieldName = fieldName.substring(1);
}
if (!fieldName.equals(oldFieldName)) {
pluralFormWasApplied = true;
originalImplField.name(fieldName);
// Correct the @XmlType class-level annotation:
JAnnotationArrayMember propOrderValue = (JAnnotationArrayMember) getAnnotation(targetClass,
xmlTypeModelClass).getAnnotationMembers().get("propOrder");
if (propOrderValue != null) {
for (JAnnotationValue annotationValue : propOrderValue.annotations()) {
if (oldFieldName.equals(generableToString(annotationValue))) {
setPrivateField(annotationValue, "value", JExpr.lit(fieldName));
break;
}
}
}
}
}
// Transform the field accordingly.
originalImplField.type(collectionInterfaceClass);
// If instantiation is specified to be "early", add code for creating new instance of the collection class.
if (fieldConfiguration.getInstantiationMode() == CommonConfiguration.InstantiationMode.EARLY) {
logger.debug("Applying EARLY instantiation...");
// GENERATED CODE: ... fieldName = new C<T>();
originalImplField.init(JExpr._new(collectionImplClass));
}
// Annotate the field with the @XmlElementWrapper annotation using the original field name.
JAnnotationUse xmlElementWrapperAnnotation = originalImplField.annotate(xmlElementWrapperModelClass);
JAnnotationUse xmlElementOriginalAnnotation = getAnnotation(originalImplField, xmlElementModelClass);
// xmlElementOriginalAnnotation can be null:
JExpression wrapperXmlName = getAnnotationMemberExpression(xmlElementOriginalAnnotation, "name");
if (wrapperXmlName != null) {
xmlElementWrapperAnnotation.param("name", wrapperXmlName);
}
else if (fieldConfiguration.isApplyPluralForm()) {
xmlElementWrapperAnnotation.param("name", getXsdDeclaration(fieldPropertyInfo).getName());
}
JExpression wrapperXmlRequired = getAnnotationMemberExpression(xmlElementOriginalAnnotation,
"required");
if (wrapperXmlRequired != null) {
xmlElementWrapperAnnotation.param("required", wrapperXmlRequired);
}
JExpression wrapperXmlNillable = getAnnotationMemberExpression(xmlElementOriginalAnnotation,
"nillable");
if (wrapperXmlNillable != null) {
xmlElementWrapperAnnotation.param("nillable", wrapperXmlNillable);
}
// Namespace of the wrapper element
JExpression wrapperXmlNamespace = getAnnotationMemberExpression(xmlElementOriginalAnnotation,
"namespace");
if (wrapperXmlNamespace != null) {
xmlElementWrapperAnnotation.param("namespace", wrapperXmlNamespace);
}
if (xmlElementOriginalAnnotation != null) {
removeAnnotation(originalImplField, xmlElementOriginalAnnotation);
}
boolean xmlElementInfoWasTransferred = false;
// Transfer @XmlAnyElement, @XmlElementRefs, @XmlElements:
for (JClass annotationModelClass : new JClass[] { xmlAnyElementModelClass, xmlMixedModelClass,
xmlElementRefModelClass, xmlElementRefsModelClass, xmlElementsModelClass }) {
JAnnotationUse annotation = getAnnotation(candidate.getField(), annotationModelClass);
if (annotation != null) {
if (candidate.getFieldTargetNamespace() != null) {
JAnnotationArrayMember annotationArrayMember = (JAnnotationArrayMember) getAnnotationMember(
annotation, "value");
if (annotationArrayMember != null) {
for (JAnnotationUse subAnnotation : annotationArrayMember.annotations()) {
if (getAnnotationMemberExpression(subAnnotation, "namespace") == null) {
subAnnotation.param("namespace", candidate.getFieldTargetNamespace());
}
}
}
}
xmlElementInfoWasTransferred = true;
addAnnotation(originalImplField, annotation);
}
}
if (!xmlElementInfoWasTransferred) {
// Annotate the field with the @XmlElement annotation using the field name from the wrapped type as name.
// We cannot just re-use the same annotation object instance, as for example, we need to set XML name and this
// will impact the candidate field annotation in case candidate is unmarked from removal.
JAnnotationUse xmlElementAnnotation = originalImplField.annotate(xmlElementModelClass);
JAnnotationUse xmlElementCandidateAnnotation = getAnnotation(candidate.getField(),
xmlElementModelClass);
// xmlElementOriginalAnnotation can be null:
JExpression xmlName = getAnnotationMemberExpression(xmlElementCandidateAnnotation, "name");
if (xmlName != null) {
xmlElementAnnotation.param("name", xmlName);
}
else {
xmlElementAnnotation.param("name", candidate.getFieldName());
}
JExpression xmlNamespace = getAnnotationMemberExpression(xmlElementCandidateAnnotation,
"namespace");
if (xmlNamespace != null) {
xmlElementAnnotation.param("namespace", xmlNamespace);
}
else if (candidate.getFieldTargetNamespace() != null) {
xmlElementAnnotation.param("namespace", candidate.getFieldTargetNamespace());
}
JExpression type = getAnnotationMemberExpression(xmlElementCandidateAnnotation, "type");
if (type != null) {
xmlElementAnnotation.param("type", type);
}
JExpression required = getAnnotationMemberExpression(xmlElementCandidateAnnotation, "defaultValue");
if (required != null) {
xmlElementAnnotation.param("defaultValue", required);
}
JExpression nillable = getAnnotationMemberExpression(xmlElementCandidateAnnotation, "nillable");
if (nillable != null) {
xmlElementAnnotation.param("nillable", nillable);
}
}
JAnnotationUse adapterAnnotation = getAnnotation(candidate.getField(), xmlJavaTypeAdapterModelClass);
if (adapterAnnotation != null) {
addAnnotation(originalImplField, adapterAnnotation);
}
// Same as fieldName, but used as getter/setter method name:
String propertyName = fieldPropertyInfo.getName(true);
JDefinedClass implementationInterface = null;
for (Iterator<JClass> iter = targetClass._implements(); iter.hasNext();) {
JClass interfaceClass = iter.next();
// If value class implements some JVM interface it is not considered as such interface cannot be modified:
if (interfaceClass instanceof JDefinedClass
&& deleteSettersGetters((JDefinedClass) interfaceClass, propertyName)) {
implementationInterface = (JDefinedClass) interfaceClass;
break;
}
}
// Find original getter and setter methods to remove.
deleteSettersGetters(targetClass, propertyName);
// The type in property info should correspond to field type. For that we clone the candidate property info:
CPropertyInfo candidateFieldPropertyInfo = candidate.getFieldPropertyInfo();
CPropertyInfo propertyInfoClone = null;
if (candidateFieldPropertyInfo instanceof CElementPropertyInfo) {
propertyInfoClone = new CElementPropertyInfo("", CollectionMode.NOT_REPEATED, null, null, null,
null, null, false);
}
else if (candidateFieldPropertyInfo instanceof CReferencePropertyInfo) {
propertyInfoClone = new CReferencePropertyInfo("", false, false, false, null, null, null, false,
false, false);
}
else {
// There could be no other option as candidate field is a collection, hence not simple property.
assert false;
propertyInfoClone = candidateFieldPropertyInfo;
}
copyFields(candidateFieldPropertyInfo, propertyInfoClone);
if (pluralFormWasApplied) {
propertyName = JJavaName.getPluralForm(propertyName);
}
propertyInfoClone.setName(false, fieldName);
propertyInfoClone.setName(true, propertyName);
setPrivateField(field, "prop", propertyInfoClone);
// Add a new getter method returning the (wrapped) field added.
// GENERATED CODE: public I<T> getFieldName() { ... return fieldName; }
JMethod getterMethod = targetClass.method(JMod.PUBLIC, collectionInterfaceClass, "get" + propertyName);
if (fieldConfiguration.getInstantiationMode() == CommonConfiguration.InstantiationMode.LAZY) {
logger.debug("Applying LAZY instantiation...");
// GENERATED CODE: if (fieldName == null) fieldName = new C<T>();
getterMethod.body()._if(JExpr.ref(fieldName).eq(JExpr._null()))._then().assign(JExpr.ref(fieldName),
JExpr._new(collectionImplClass));
}
// GENERATED CODE: return "fieldName";
getterMethod.body()._return(JExpr.ref(fieldName));
// Add a new setter method:
// GENERATED CODE: public void setFieldName(I<T> fieldName) { this.fieldName = fieldName; }
JMethod setterMethod = targetClass.method(JMod.PUBLIC, codeModel.VOID, "set" + propertyName);
setterMethod.body().assign(JExpr._this().ref(fieldName),
setterMethod.param(collectionInterfaceClass, fieldName));
// Modify interface as well:
if (implementationInterface != null) {
writeSummary("\tCorrecting interface " + implementationInterface.fullName());
implementationInterface.method(JMod.PUBLIC, collectionInterfaceClass, "get" + propertyName);
setterMethod = implementationInterface.method(JMod.PUBLIC, codeModel.VOID, "set" + propertyName);
setterMethod.param(collectionInterfaceClass, fieldName);
}
// Adapt factory class:
for (JDefinedClass objectFactoryClass : candidate.getObjectFactoryClasses()) {
modificationCount += createScopedFactoryMethods(codeModel, objectFactoryClass,
candidate.getScopedElementInfos().values(), targetClass);
}
candidate.addObjectFactoryForClass(targetClass);
}
}
writeSummary("\t" + modificationCount + " modification(s) to original code.");
writeSummary("");
int deletionCount = deleteCandidates(outline, candidatesMap.values());
writeSummary("\t" + deletionCount + " deletion(s) from original code.");
writeSummary("");
globalConfiguration.closeSummary();
Ring.end(null);
logger.debug("Done");
}
/**
* If candidate class contains the inner class which is collection parametrisation (type), then this inner class has
* to be moved to top class. For example from<br>
* {@code TypeClass (is a collection type) -> ContainerClass (marked for removal) -> ElementClass}<br>
* we need to get<br>
* {@code TypeClass -> ElementClass}.<br>
* Also this move should be reflected on factory method names.
*/
private boolean moveInnerClassToParent(Outline outline, Candidate candidate) {
// Skip basic parametrisations like "List<String>":
if (candidate.getFieldParametrisationClass() == null) {
return false;
}
JDefinedClass fieldParametrisationImpl = candidate.getFieldParametrisationImpl();
if (candidate.getClazz() != fieldParametrisationImpl.parentContainer()) {
// Field parametrisation class is not inner class of the candidate:
return false;
}
JDefinedClass fieldParametrisationClass = candidate.getFieldParametrisationClass();
String oldFactoryMethodName = fieldParametrisationClass.outer().name() + fieldParametrisationClass.name();
moveClassLevelUp(outline, fieldParametrisationImpl);
renameFactoryMethod(fieldParametrisationImpl._package()._getClass(FACTORY_CLASS_NAME), oldFactoryMethodName,
fieldParametrisationClass.name());
if (candidate.isValueObjectDisabled()) {
moveClassLevelUp(outline, fieldParametrisationClass);
renameFactoryMethod(fieldParametrisationClass._package()._getClass(FACTORY_CLASS_NAME),
oldFactoryMethodName, fieldParametrisationClass.name());
}
return true;
}
/**
* Create additional factory methods with a new scope for elements that should be scoped.
*
* @param targetClass
* the class that is applied the transformation of properties
* @return number of created methods
* @see com.sun.tools.xjc.generator.bean.ObjectFactoryGenerator
*/
private int createScopedFactoryMethods(JCodeModel codeModel, JDefinedClass factoryClass,
Collection<ScopedElementInfo> scopedElementInfos, JDefinedClass targetClass) {
int createdMethods = 0;
NEXT: for (ScopedElementInfo info : scopedElementInfos) {
String dotClazz = targetClass.fullName() + ".class";
// First check that such factory method has not yet been created. It can be the case if target class
// is substituted with e.g. two candidates, each candidate having a field with the same name.
// FIXME: Could it be the case that these two fields have different namespaces?
for (JMethod method : factoryClass.methods()) {
JAnnotationUse xmlElementDeclAnnotation = getAnnotation(method, xmlElementDeclModelClass);
JExpression scope = getAnnotationMemberExpression(xmlElementDeclAnnotation, "scope");
JExpression name = getAnnotationMemberExpression(xmlElementDeclAnnotation, "name");
if (scope != null && dotClazz.equals(generableToString(scope))
&& generableToString(info.name).equals(generableToString(name))) {
continue NEXT;
}
}
// Generate the scoped factory method:
// @XmlElementDecl(..., scope = T.class)
// public JAXBElement<X> createT...(X value) { return new JAXBElement<...>(QNAME, X.class, T.class, value); }
StringBuilder methodName = new StringBuilder();
JDefinedClass container = targetClass;
// To avoid potential name conflicts method name starts with scope class name:
while (true) {
methodName.insert(0, container.name());
if (container.parentContainer().isClass()) {
container = (JDefinedClass) container.parentContainer();
}
else {
break;
}
}
methodName.insert(0, "create").append(NameConverter.standard.toPropertyName(generableToString(info.name)));
JClass wrapperType = codeModel.ref(JAXBElement.class).narrow(info.type);
JMethod method = factoryClass.method(JMod.PUBLIC, wrapperType, methodName.toString());
method.annotate(xmlElementDeclModelClass).param("namespace", info.namespace).param("name", info.name)
.param("scope", targetClass);
// FIXME: Make a try to load constants and (a) rename it appropriately (b) use it?
JInvocation qname = JExpr._new(codeModel.ref(QName.class)).arg(info.namespace).arg(info.name);
method.body()._return(JExpr._new(wrapperType).arg(qname).arg(info.type.boxify().dotclass())
.arg(targetClass.dotclass()).arg(method.param(info.type, "value")));
createdMethods++;
}
return createdMethods;
}
/**
* Locate the candidates classes for substitution/removal.
*
* @return a map className -> Candidate
*/
private Collection<Candidate> findCandidateClasses(Outline outline) {
Map<String, ClassOutline> interfaceImplementations = new HashMap<String, ClassOutline>();
// Visit all classes to create a map "interfaceName -> ClassOutline".
// This map is later used to resolve implementations from interfaces.
for (ClassOutline classOutline : outline.getClasses()) {
for (Iterator<JClass> iter = classOutline.implClass._implements(); iter.hasNext();) {
JClass interfaceClass = iter.next();
if (interfaceClass instanceof JDefinedClass) {
// Don't care if some interfaces collide: value classes have exactly one implementation
interfaceImplementations.put(interfaceClass.fullName(), classOutline);
}
}
}
Collection<Candidate> candidates = new ArrayList<Candidate>();
JClass collectionModelClass = outline.getCodeModel().ref(Collection.class);
JClass xmlSchemaModelClass = outline.getCodeModel().ref(XmlSchema.class);
// Visit all classes created by JAXB processing to collect all potential wrapper classes to be removed:
for (ClassOutline classOutline : outline.getClasses()) {
JDefinedClass candidateClass = classOutline.implClass;
// * The candidate class should not extend any other model class (as the total number of properties in this case will be more than 1)
if (!isHiddenClass(candidateClass._extends())) {
continue;
}
JFieldVar field = null;
// * The candidate class should have exactly one property
for (JFieldVar f : candidateClass.fields().values()) {
if ((f.mods().getValue() & JMod.STATIC) == JMod.STATIC) {
continue;
}
// If there are at least two non-static fields, we discard this candidate:
if (field != null) {
field = null;
break;
}
field = f;
}
// "field" is null if there are no fields (or all fields are static) or there are more then two fields.
// The only property should be a collection, hence it should be class:
if (field == null || !(field.type() instanceof JClass)) {
continue;
}
JClass fieldType = (JClass) field.type();
// * The property should be a collection
if (!collectionModelClass.isAssignableFrom(fieldType)) {
continue;
}
List<JClass> fieldParametrisations = fieldType.getTypeParameters();
// FIXME: All known collections have exactly one parametrisation type.
assert fieldParametrisations.size() == 1;
JDefinedClass fieldParametrisationClass = null;
JDefinedClass fieldParametrisationImpl = null;
// Parametrisations like "List<String>" or "List<Serialazable>" are not considered.
// They are substituted as is and do not require moving of classes.
if (fieldParametrisations.get(0) instanceof JDefinedClass) {
fieldParametrisationClass = (JDefinedClass) fieldParametrisations.get(0);
ClassOutline fieldParametrisationClassOutline = interfaceImplementations
.get(fieldParametrisationClass.fullName());
if (fieldParametrisationClassOutline != null) {
assert fieldParametrisationClassOutline.ref == fieldParametrisationClass;
fieldParametrisationImpl = fieldParametrisationClassOutline.implClass;
}
else {
fieldParametrisationImpl = fieldParametrisationClass;
}
}
// We have a candidate class:
Candidate candidate = new Candidate(candidateClass, classOutline.target, field, fieldParametrisationClass,
fieldParametrisationImpl, xmlElementDeclModelClass, xmlSchemaModelClass);
candidates.add(candidate);
logger.debug("Found " + candidate);
}
return candidates;
}
/**
* Delete all candidate classes together with setter/getter methods and helper methods from
* <code>ObjectFactory</code>.
*
* @return the number of deletions performed
*/
private int deleteCandidates(Outline outline, Collection<Candidate> candidates) {
int deletionCount = 0;
writeSummary("Deletions:");
// Visit all candidate classes.
for (Candidate candidate : candidates) {
if (!candidate.canBeRemoved()) {
continue;
}
// Get the defined class for candidate class.
JDefinedClass candidateClass = candidate.getClazz();
deleteClass(outline, candidateClass);
deletionCount++;
for (JDefinedClass objectFactoryClass : candidate.getObjectFactoryClasses()) {
deletionCount += deleteFactoryMethod(objectFactoryClass, candidate);
}
// Replay the same for interface:
if (candidate.isValueObjectDisabled()) {
for (Iterator<JClass> iter = candidateClass._implements(); iter.hasNext();) {
JClass interfaceClass = iter.next();
if (!isHiddenClass(interfaceClass)) {
deleteClass(outline, (JDefinedClass) interfaceClass);
deletionCount++;
}
}
}
}
return deletionCount;
}
//
// Model factory manipulation helpers.
//
/**
* Rename methods in factory class: {@code createABC() -> createAC()}.
*/
private void renameFactoryMethod(JDefinedClass factoryClass, String oldMethodName, String newMethodName) {
for (JMethod method : factoryClass.methods()) {
String methodName = method.name();
if (!methodName.contains(oldMethodName)) {
continue;
}
method.name(methodName.replace(oldMethodName, newMethodName));
writeSummary("\tRenamed " + methodName + " -> " + method.name() + " in " + factoryClass.fullName());
}
}
/**
* Remove method {@code ObjectFactory} that creates an object of a given {@code clazz}.
*
* @return {@code 1} if such method was successfully located and removed
*/
private int deleteFactoryMethod(JDefinedClass factoryClass, Candidate candidate) {
int deletedMethods = 0;
for (Iterator<JMethod> iter = factoryClass.methods().iterator(); iter.hasNext();) {
JMethod method = iter.next();
// Remove the methods:
// * public T createT() { return new T(); }
// * public JAXBElement<T> createT(T value) { return new JAXBElement<T>(QNAME, T.class, null, value); }
// * @XmlElementDecl(..., scope = X.class)
// public JAXBElement<T> createT...(T value) { return new JAXBElement<...>(QNAME, T.class, X.class, value); }
if ((method.type() instanceof JDefinedClass
&& ((JDefinedClass) method.type()).isAssignableFrom(candidate.getClazz()))
|| isListedAsParametrisation(candidate.getClazz(), method.type())
|| candidate.getScopedElementInfos().containsKey(method.name())) {
writeSummary("\tRemoving factory method [" + method.type().fullName() + "#" + method.name()
+ "()] from " + factoryClass.fullName());
iter.remove();
deletedMethods++;
}
}
return deletedMethods;
}
//
// Model manipulation helpers.
//
/**
* Returns {@code true} if setter/getter with given public name was successfully removed from given class/interface.
*/
private boolean deleteSettersGetters(JDefinedClass clazz, String fieldPublicName) {
boolean result = false;
for (Iterator<JMethod> iter = clazz.methods().iterator(); iter.hasNext();) {
JMethod m = iter.next();
if (m.name().equals("set" + fieldPublicName) || m.name().equals("get" + fieldPublicName)) {
iter.remove();
result = true;
}
}
return result;
}
/**
* Move the given class to his grandparent (either class or package). The given {@code clazz} should be inner class.
*/
private void moveClassLevelUp(Outline outline, JDefinedClass clazz) {
// Modify the container so it now refers the class. Container can be a class or package.
JDefinedClass parent = (JDefinedClass) clazz.parentContainer();
JClassContainer grandParent = parent.parentContainer();
Map<String, JDefinedClass> classes;
// FIXME: Pending https://java.net/jira/browse/JAXB-957
if (grandParent.isClass()) {
// Element class should be added as its container child:
JDefinedClass grandParentClass = (JDefinedClass) grandParent;
writeSummary("\tMoving inner class " + clazz.fullName() + " to class " + grandParentClass.fullName());
classes = getPrivateField(grandParentClass, "classes");
}
else {
JPackage grandParentPackage = (JPackage) grandParent;
writeSummary("\tMoving inner class " + clazz.fullName() + " to package " + grandParentPackage.name());
classes = getPrivateField(grandParentPackage, "classes");
// In this scenario class should have "static" modifier reset otherwise it won't compile:
setPrivateField(clazz.mods(), "mods", Integer.valueOf(clazz.mods().getValue() & ~JMod.STATIC));
for (ClassOutline classOutline : outline.getClasses()) {
if (classOutline.implClass == clazz) {
XSComponent sc = classOutline.target.getSchemaComponent();
// FIXME: Inner class is always a local declaration.
assert (sc instanceof XSDeclaration && ((XSDeclaration) sc).isLocal());
setPrivateField(sc, "anonymous", Boolean.FALSE);
break;
}
}
}
if (classes.containsKey(clazz.name())) {
writeSummary("\tRenaming class " + clazz.fullName() + " to class " + parent.name() + clazz.name());
setPrivateField(clazz, "name", parent.name() + clazz.name());
}
classes.put(clazz.name(), clazz);
// Finally modify the class so that it refers back the container:
setPrivateField(clazz, "outer", grandParent);
}
/**
* Remove the given class from it's parent class or package it is defined in.
*/
private void deleteClass(Outline outline, JDefinedClass clazz) {
if (clazz.parentContainer().isClass()) {
// The candidate class is an inner class. Remove the class from its parent class.
JDefinedClass parentClass = (JDefinedClass) clazz.parentContainer();
writeSummary("\tRemoving class " + clazz.fullName() + " from class " + parentClass.fullName());
for (Iterator<JDefinedClass> iter = parentClass.classes(); iter.hasNext();) {
if (iter.next().equals(clazz)) {
iter.remove();
break;
}
}
}
else {
// The candidate class is in a package. Remove the class from the package.
JPackage parentPackage = (JPackage) clazz.parentContainer();
writeSummary("\tRemoving class " + clazz.fullName() + " from package " + parentPackage.name());
parentPackage.remove(clazz);
// And also remove the class from model.
for (Iterator<? extends ClassOutline> iter = outline.getClasses().iterator(); iter.hasNext();) {
ClassOutline classOutline = iter.next();
if (classOutline.implClass == clazz) {
outline.getModel().beans().remove(classOutline.target);
Set<Object> packageClasses = getPrivateField(classOutline._package(), "classes");
packageClasses.remove(classOutline);
iter.remove();
break;
}
}
}
}
/**
* For the given annotatable check that all annotations (and all annotations within annotations recursively) do not
* refer any candidate for removal.
*/
private void checkAnnotationReference(Map<String, Candidate> candidatesMap, JAnnotatable annotatable) {
for (JAnnotationUse annotation : annotatable.annotations()) {
JAnnotationValue annotationMember = getAnnotationMember(annotation, "value");
if (annotationMember instanceof JAnnotationArrayMember) {
checkAnnotationReference(candidatesMap, (JAnnotationArrayMember) annotationMember);
continue;
}
JExpression type = getAnnotationMemberExpression(annotation, "type");
if (type == null) {
// Can be the case for @XmlElement(name = "publication-reference", namespace = "http://mycompany.org/exchange")
// or any other annotation without "type"
continue;
}
Candidate candidate = candidatesMap.get(generableToString(type).replace(".class", ""));
if (candidate != null) {
logger.debug("Candidate " + candidate.getClassName()
+ " is used in XmlElements/XmlElementRef and hence won't be removed.");
candidate.unmarkForRemoval();
}
}
}
}