/**
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.envoisolutions.sxc.jaxb;
import java.awt.Image;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
import javax.activation.DataHandler;
import javax.xml.bind.JAXBException;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import com.envoisolutions.sxc.builder.BuildException;
import com.envoisolutions.sxc.builder.impl.JBlankLine;
import com.envoisolutions.sxc.builder.impl.JIfElseBlock;
import com.envoisolutions.sxc.builder.impl.JLineComment;
import com.envoisolutions.sxc.builder.impl.JStaticImports;
import static com.envoisolutions.sxc.jaxb.JavaUtils.isPrivate;
import static com.envoisolutions.sxc.jaxb.JavaUtils.toClass;
import com.envoisolutions.sxc.jaxb.model.Bean;
import com.envoisolutions.sxc.jaxb.model.ElementMapping;
import com.envoisolutions.sxc.jaxb.model.EnumInfo;
import com.envoisolutions.sxc.jaxb.model.Model;
import com.envoisolutions.sxc.jaxb.model.Property;
import com.envoisolutions.sxc.util.Base64;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JCatchBlock;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JForEach;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JTryBlock;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;
import org.w3c.dom.Element;
public class WriterIntrospector {
private static final Logger logger = Logger.getLogger(WriterIntrospector.class.getName());
private final BuilderContext context;
private final Model model;
private final Map<Bean, JAXBObjectBuilder> builders = new LinkedHashMap<Bean, JAXBObjectBuilder>();
private final Map<Class, JAXBEnumBuilder> enumBuilders = new LinkedHashMap<Class, JAXBEnumBuilder>();
public WriterIntrospector(BuilderContext context, Model model) throws JAXBException {
this.context = context;
this.model = model;
List<Bean> mybeans = new ArrayList<Bean>(model.getBeans());
Collections.sort(mybeans, new BeanComparator());
// build all enum toString methods so they are available for use by bean readers
for (EnumInfo enumInfo : model.getEnums()) {
addEnum(enumInfo);
}
// declare all writer methods first, so everything exists when we build
for (Bean bean : mybeans) {
if (bean.getType().isEnum()) continue;
boolean mixed = false;
for (Property property : bean.getProperties()) {
if (property.isMixed() && property.getXmlName() == null) {
mixed = true;
break;
}
}
JAXBObjectBuilder builder = context.createJAXBObjectBuilder(bean.getType(), bean.getRootElementName(), bean.getSchemaTypeName(), mixed);
LinkedHashSet<Property> allProperties = new LinkedHashSet<Property>();
for (Bean b = bean; b != null; b = b.getBaseClass()) {
allProperties.addAll(b.getProperties());
}
// set the default namespace to the most popular namespace used in properties
String mostPopularNS = getMostPopularNS(allProperties);
if (mostPopularNS != null) builder.setWriterDefaultNS(mostPopularNS);
// declare all private field accessors (so they are grouped)
for (Property property : allProperties) {
Field field = property.getField();
if (field != null) {
if (isPrivate(field)) {
builder.getPrivateFieldAccessor(property.getField());
}
} else {
if (isPrivate(property.getGetter()) || isPrivate(property.getSetter())) {
builder.getPrivatePropertyAccessor(property.getGetter(), property.getSetter(), property.getName());
}
}
}
// declare all adapter classes
for (Property property : allProperties) {
if (property.getAdapterType() != null) {
builder.getAdapter(property.getAdapterType());
}
}
builders.put(bean, builder);
}
// build the writer methods
for (Bean bean : model.getBeans()) {
if (!bean.getType().isEnum()) {
JAXBObjectBuilder builder = builders.get(bean);
if (builder != null) {
add(builder, bean);
}
}
}
}
private void add(JAXBObjectBuilder builder, Bean bean) {
JBlock block = builder.getWriteMethod().body();
// perform instance checks
JIfElseBlock ifElseBlock = new JIfElseBlock();
block.add(ifElseBlock);
JInvocation unexpectedSubclass = builder.getWriteContextVar().invoke("unexpectedSubclass").arg(builder.getXSW()).arg(builder.getWriteObject()).arg(context.dotclass(bean.getType()));
for (Bean altBean : getSubstitutionTypes(builder.getType())) {
if (bean == altBean) continue;
// add condition
JBlock altBlock = ifElseBlock.addCondition(context.dotclass(altBean.getType()).eq(builder.getWriteObject().invoke("getClass")));
// write xsi:type
QName typeName = altBean.getSchemaTypeName();
altBlock.invoke(builder.getXSW(), "writeXsiType").arg(typeName.getNamespaceURI()).arg(typeName.getLocalPart());
// call alternate marshaller
writeClassWriter(builder, altBean, altBlock, JExpr.cast(context.toJClass(altBean.getType()), builder.getWriteObject()));
altBlock._return();
// add as expected subclass arg
unexpectedSubclass.arg(context.dotclass(altBean.getType()));
}
// if the class isn't exactally this bean's type, then we have an unexpceted subclass
JBlock unknownSubclassBlock = ifElseBlock.addCondition(context.dotclass(builder.getType()).ne(builder.getWriteObject().invoke("getClass")));
unknownSubclassBlock.add(unexpectedSubclass);
unknownSubclassBlock._return();
block.add(new JBlankLine());
// add beforeMarshal
JExpression lifecycleCallbackRef = builder.getLifecycleCallbackVar();
if (builder.getWriteVariableManager().containsId(builder.getLifecycleCallbackVar().name())) {
lifecycleCallbackRef = builder.getJAXBObjectClass().staticRef(builder.getLifecycleCallbackVar().name());
}
block.invoke(builder.getWriteContextVar(), "beforeMarshal").arg(builder.getWriteObject()).arg(lifecycleCallbackRef);
block.add(new JBlankLine());
writeProperties(builder, bean);
}
private void addEnum(EnumInfo enumInfo) {
JAXBEnumBuilder builder = context.createJAXBEnumBuilder(enumInfo.getType(), enumInfo.getRootElementName(), enumInfo.getSchemaTypeName());
JMethod method = builder.getToStringMethod();
JIfElseBlock enumSwitch = new JIfElseBlock();
method.body().add(enumSwitch);
for (Map.Entry<Enum, String> entry : enumInfo.getEnumMap().entrySet()) {
Enum enumValue = entry.getKey();
String enumText = entry.getValue();
JBlock enumCase = enumSwitch.addCondition(context.toJClass(enumInfo.getType()).staticRef(enumValue.name()).eq(builder.getToStringValue()));
enumCase._return(JExpr.lit(enumText));
}
JInvocation unexpectedInvoke = enumSwitch._else().invoke(builder.getToStringContext(), "unexpectedEnumConst")
.arg(builder.getToStringBean())
.arg(builder.getToStringParameterName())
.arg(builder.getToStringValue());
for (Enum expectedValue : enumInfo.getEnumMap().keySet()) {
unexpectedInvoke.arg(context.toJClass(enumInfo.getType()).staticRef(expectedValue.name()));
}
enumSwitch._else()._return(JExpr._null());
// switch statements don't seem to compile correctly
// JSwitch enumSwitch = method.body()._switch(value);
// for (Map.Entry<Enum, String> entry : bean.getEnumMap().entrySet()) {
// Enum enumValue = entry.getKey();
// String enumText = entry.getValue();
//
// JCase enumCase = enumSwitch._case(new JEnumLabel(enumValue.name()));
// enumCase.body()._return(JExpr.lit(enumText));
// }
//
// enumSwitch._default().body()._throw(JExpr._new(toJClass(IllegalArgumentException.class))
// .arg(JExpr.lit("No value mapped to ").plus(value).plus(JExpr.lit(" for enum " + bean.getType().getName()))));
enumBuilders.put(enumInfo.getType(), builder);
}
private void writeProperties(JAXBObjectBuilder builder, Bean bean) {
writeAttributes(builder, bean);
writeElementsAndValue(builder, bean);
}
private void writeAttributes(JAXBObjectBuilder builder, Bean bean) {
if (bean.getBaseClass() != null) {
writeAttributes(builder, bean.getBaseClass());
}
for (Property property : bean.getProperties()) {
if (property.getXmlStyle() == Property.XmlStyle.ATTRIBUTE) {
JBlock block = builder.getWriteMethod().body();
block.add(new JBlankLine());
block.add(new JLineComment(property.getXmlStyle() + ": " + property.getName()));
JExpression propertyVar = getValue(builder, property, block);
if (!property.isXmlAny()) {
if (!toClass(property.getType()).isPrimitive()) {
JConditional nullCond = block._if(propertyVar.ne(JExpr._null()));
block = nullCond._then();
}
writeSimpleTypeAttribute(builder, block, property, propertyVar);
} else {
// if (value != null)
JConditional nullCond = block._if(propertyVar.ne(JExpr._null()));
String entryName = builder.getWriteVariableManager().createId(property.getName() + "Entry");
boolean needsCast = true;
if (property.getType() instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) property.getType();
Type[] arguments = parameterizedType.getActualTypeArguments();
if (arguments.length == 2 &&
QName.class.equals(arguments[0]) &&
property.getComponentType().equals(arguments[1])) {
needsCast = false;
}
}
if (needsCast) {
propertyVar = JExpr.cast(context.toJClass(Map.class).narrow(context.toJClass(QName.class), context.getGenericType(property.getComponentType())), propertyVar);
}
JForEach each = nullCond._then().forEach(context.toJClass(Map.Entry.class).narrow(context.toJClass(QName.class), context.getGenericType(property.getComponentType())), entryName, propertyVar.invoke("entrySet"));
writeSimpleTypeAttribute(builder, each.body(), each.var().invoke("getKey"), toClass(property.getComponentType()), each.var().invoke("getValue"));
}
}
}
}
private void writeElementsAndValue(JAXBObjectBuilder builder, Bean bean) {
if (bean.getBaseClass() != null) {
writeElementsAndValue(builder, bean.getBaseClass());
}
for (Property property : bean.getProperties()) {
if (property.getXmlStyle() == Property.XmlStyle.ATTRIBUTE) continue;
builder.getWriteMethod().body().add(new JBlankLine());
builder.getWriteMethod().body().add(new JLineComment(property.getXmlStyle() + ": " + property.getName()));
JVar propertyVar = getValue(builder, property, builder.getWriteMethod().body());
switch (property.getXmlStyle()) {
case ELEMENT:
// if the element is required, add a null check that writes xsi nil
JVar outerVar = propertyVar;
JBlock outerBlock = builder.getWriteMethod().body();
JVar firstVar = null;
if (property.isCollection()) {
QName wrapperElement = property.getXmlName();
// if wrapper tag is required, start tag before null check
if (wrapperElement != null && (property.isRequired() || property.isNillable())) {
outerBlock.add(builder.getWriteStartElement(wrapperElement));
}
// if collection is not null, process it; otherwise write xsi:nil
JConditional nullCond = outerBlock._if(outerVar.ne(JExpr._null()));
if (property.isNillable()) {
nullCond._else().add(builder.getXSW().invoke("writeXsiNil"));
}
// if wrapper tag is not required, start the tag inside of the null block
if (wrapperElement != null && !property.isRequired() && !property.isNillable()) {
nullCond._then().add(builder.getWriteStartElement(wrapperElement));
}
JType itemType;
if (!toClass(property.getComponentType()).isPrimitive()) {
itemType = context.getGenericType(property.getComponentType());
} else {
itemType = context.toJType((Class<?>) toClass(property.getComponentType()));
}
// if xml list add code to properly space items
if (property.isXmlList()) {
firstVar = nullCond._then().decl(context.toJType(boolean.class), builder.getWriteVariableManager().createId(property.getName() + "First"), JExpr.TRUE);
}
String itemName = builder.getWriteVariableManager().createId(property.getName() + "Item");
JForEach each = nullCond._then().forEach(itemType, itemName, outerVar);
// write wraper element closing tag
if (wrapperElement != null) {
if (property.isRequired() || property.isNillable()) {
outerBlock.add(builder.getXSW().invoke("writeEndElement"));
} else {
nullCond._then().add(builder.getXSW().invoke("writeEndElement"));
}
}
outerBlock = each.body();
outerVar = each.var();
}
Class propertyType = toClass(property.getComponentType());
// process value through adapter
outerVar = writeAdapterConversion(builder, outerBlock, property, outerVar);
if (property.getAdapterType() != null) {
propertyType = property.getComponentAdaptedType();
}
// determine types that may be substuited for this value
Map<Class, ElementMapping> expectedTypes = new TreeMap<Class, ElementMapping>(new ClassComparator());
for (ElementMapping mapping : property.getElementMappings()) {
if (mapping.getComponentType() != null) {
expectedTypes.put(toClass(mapping.getComponentType()), mapping);
} else {
expectedTypes.put(toClass(property.getType()), mapping);
}
}
if (expectedTypes.size() == 1 && !property.isMixed()) {
ElementMapping mapping = property.getElementMappings().iterator().next();
// null check for non-nillable elements
JBlock block = outerBlock;
JConditional nullCond = null;
if (!mapping.isNillable() && !propertyType.isPrimitive()) {
nullCond = outerBlock._if(outerVar.ne(JExpr._null()));
block = nullCond._then();
}
// add space (' ') separator for XmlList
if (property.isXmlList()) {
// if (fooFirst) {
// writer.writeCharacters(" ");
// }
// fooFirst = false;
block._if(firstVar.not())._then().add(builder.getXSW().invoke("writeCharacters").arg(" "));
block.assign(firstVar, JExpr.FALSE);
}
// write element
writeElement(builder, block, mapping, outerVar, propertyType, mapping.isNillable(), property.isXmlList());
// property is required and does not support nill, then an error is reported if the value was null
if (property.isRequired() && !mapping.isNillable() && nullCond != null) {
nullCond._else().invoke(builder.getWriteContextVar(), "unexpectedNullValue").arg(builder.getWriteObject()).arg(property.getName());
}
} else {
JIfElseBlock conditional = new JIfElseBlock();
outerBlock.add(conditional);
if (property.isMixed()) {
// add instance of check
JExpression isInstance = outerVar._instanceof(context.toJClass(String.class));
JBlock block = conditional.addCondition(isInstance);
// declare item variable
JVar itemVar;
if (toClass(property.getComponentType()) == String.class) {
itemVar = outerVar;
} else {
String itemName = builder.getWriteVariableManager().createId("string");
itemVar = block.decl(context.toJClass(String.class), itemName, JExpr.cast(context.toJClass(String.class), outerVar));
}
writeSimpleTypeElement(builder, itemVar, String.class, block);
}
ElementMapping nilMapping = null;
for (Map.Entry<Class, ElementMapping> entry : expectedTypes.entrySet()) {
Class itemType = entry.getKey();
ElementMapping mapping = entry.getValue();
if (mapping.isNillable()) {
if (nilMapping != null && nilMapping != mapping) {
throw new BuildException("Property " + property + " mappings " + mapping.getXmlName() + " and " + nilMapping + " are both nillable. Only one mapping may of an property may be nilable");
}
nilMapping = mapping;
}
// add instance of check
JExpression isInstance = outerVar._instanceof(context.toJClass(itemType));
JBlock block = conditional.addCondition(isInstance);
// add space (' ') separator for XmlList
if (property.isXmlList()) {
// if (fooFirst) {
// writer.writeCharacters(" ");
// }
// fooFirst = false;
block._if(firstVar.not())._then().add(builder.getXSW().invoke("writeCharacters").arg(" "));
block.assign(firstVar, JExpr.FALSE);
}
// declare item variable
JVar itemVar;
if (toClass(property.getComponentType()) == itemType) {
itemVar = outerVar;
} else {
String itemName = builder.getWriteVariableManager().createId(itemType.getSimpleName());
itemVar = block.decl(context.toJClass(itemType), itemName, JExpr.cast(context.toJClass(itemType), outerVar));
}
writeElement(builder, block, mapping, itemVar, itemType, false, property.isXmlList());
}
// if item was null, write xsi:nil or report an error
JBlock nullBlock = conditional.addCondition(outerVar.eq(JExpr._null()));
if (nilMapping != null) {
// write start element
QName name = nilMapping.getXmlName();
nullBlock.add(builder.getWriteStartElement(name));
// write xsi:nil
nullBlock.add(builder.getXSW().invoke("writeXsiNil"));
// close element
nullBlock.add(builder.getXSW().invoke("writeEndElement"));
} else {
nullBlock.invoke(builder.getWriteContextVar(), "unexpectedNullValue").arg(builder.getWriteObject()).arg(property.getName());
}
// if not a recogonized type or null, report unknown type error
JInvocation unexpected = conditional._else().invoke(builder.getWriteContextVar(), "unexpectedElementType").arg(builder.getXSW()).arg(builder.getWriteObject()).arg(property.getName()).arg(outerVar);
for (Class expectedType : expectedTypes.keySet()) {
unexpected.arg(context.dotclass(expectedType));
}
}
break;
case ELEMENT_REF:
JBlock block = builder.getWriteMethod().body();
JVar itemVar = propertyVar;
if (property.isCollection()) {
JBlock collectionNotNull = block._if(propertyVar.ne(JExpr._null()))._then();
JType itemType;
if (!toClass(property.getComponentType()).isPrimitive()) {
itemType = context.getGenericType(property.getComponentType());
} else {
itemType = context.toJType((Class<?>) toClass(property.getComponentType()));
}
String itemName = builder.getWriteVariableManager().createId( property.getName() + "Item");
JForEach each = collectionNotNull.forEach(itemType, itemName, propertyVar);
JBlock newBody = each.body();
block = newBody;
itemVar = each.var();
}
// process value through adapter
itemVar = writeAdapterConversion(builder, block, property, itemVar);
if (property.isMixed()) {
// add instance of check
JExpression isInstance = itemVar._instanceof(context.toJClass(String.class));
JConditional conditional = block._if(isInstance);
// declare item variable
JVar stringVar;
if (toClass(property.getComponentType()) == String.class) {
stringVar = itemVar;
} else {
String itemName = builder.getWriteVariableManager().createId("string");
stringVar = conditional._then().decl(context.toJClass(String.class), itemName, JExpr.cast(context.toJClass(String.class), itemVar));
}
writeSimpleTypeElement(builder, stringVar, String.class, conditional._then());
block = conditional._else();
}
if (!property.isXmlAny()) {
block.invoke(builder.getWriteContextVar(), "unexpectedElementRef").arg(builder.getXSW()).arg(builder.getWriteObject()).arg(property.getName()).arg(itemVar);
} else {
block.invoke(builder.getWriteContextVar(), "writeXmlAny").arg(builder.getXSW()).arg(builder.getWriteObject()).arg(property.getName()).arg(itemVar);
}
break;
case VALUE:
block = builder.getWriteMethod().body();
itemVar = propertyVar;
firstVar = null;
if (property.isCollection()) {
JBlock collectionNotNull = block._if(propertyVar.ne(JExpr._null()))._then();
JType itemType;
if (!toClass(property.getComponentType()).isPrimitive()) {
itemType = context.getGenericType(property.getComponentType());
} else {
itemType = context.toJType((Class<?>) toClass(property.getComponentType()));
}
firstVar = collectionNotNull.decl(context.toJType(boolean.class), builder.getWriteVariableManager().createId(property.getName() + "First"), JExpr.TRUE);
String itemName = builder.getWriteVariableManager().createId( property.getName() + "Item");
JForEach each = collectionNotNull.forEach(itemType, itemName, propertyVar);
JBlock newBody = each.body();
block = newBody;
itemVar = each.var();
}
// add space (' ') separator for XmlList
if (property.isCollection()) {
// if (fooFirst) {
// writer.writeCharacters(" ");
// }
// fooFirst = false;
block._if(firstVar.not())._then().add(builder.getXSW().invoke("writeCharacters").arg(" "));
block.assign(firstVar, JExpr.FALSE);
}
// process value through adapter
propertyVar = writeAdapterConversion(builder, block, property, itemVar);
// write it
writeSimpleTypeElement(builder, propertyVar, toClass(property.getComponentType()), block);
break;
default:
throw new BuildException("Unknown XmlMapping type " + property.getXmlStyle());
}
}
}
private void writeElement(JAXBObjectBuilder builder, JBlock block, ElementMapping mapping, JVar itemVar, Class type, boolean nillable, boolean xmlList) {
// if this is an id ref we write the ID property of the target bean instead of the bean itself
if (mapping.getProperty().isIdref()) {
Property property = mapping.getProperty();
Property idProperty = findReferencedIdProperty(property);
// read the id value
itemVar = getValue(builder, itemVar, idProperty, property.getName() + JavaUtils.capitalize(idProperty.getName()), block);
// the written type is always a non-nillable String
type = String.class;
nillable = false;
// if (id != null) write the value
JConditional nullCond = block._if(itemVar.ne(JExpr._null()));
block = nullCond._then();
}
// write start element
if (!xmlList) {
QName name = mapping.getXmlName();
block.add(builder.getWriteStartElement(name));
}
// if nillable, we need to write xsi:nil when value is null
JBlock elementWriteBlock = block;
if (nillable && !type.isPrimitive() && !xmlList) {
JConditional nilCond = block._if(itemVar.ne(JExpr._null()));
elementWriteBlock = nilCond._then();
nilCond._else().add(builder.getXSW().invoke("writeXsiNil"));
}
// write element
Bean targetBean = model.getBean(type);
if (targetBean == null || targetBean.getType().isEnum()) {
// simple built in types like String
writeSimpleTypeElement(builder, itemVar, type, elementWriteBlock);
} else {
if (!mapping.getComponentType().equals(type)) {
QName typeName = targetBean.getSchemaTypeName();
elementWriteBlock.add(builder.getXSW().invoke("writeXsiType").arg(typeName.getNamespaceURI()).arg(typeName.getLocalPart()));
}
writeClassWriter(builder, targetBean, elementWriteBlock, itemVar);
}
// close element
if (!xmlList) {
block.add(builder.getXSW().invoke("writeEndElement"));
}
}
private JVar getValue(JAXBObjectBuilder builder, Property property, JBlock block) {
return getValue(builder, builder.getWriteObject(), property, block);
}
private JVar getValue(JAXBObjectBuilder builder, JExpression beanVar, Property property, JBlock block) {
String propertyName = property.getName();
if (property.getAdapterType() != null) {
propertyName += "Raw";
}
return getValue(builder, beanVar, property, propertyName, block);
}
private JVar getValue(JAXBObjectBuilder builder, JExpression beanVar, Property property, String propertyNameHint, JBlock block) {
Class propertyType = toClass(property.getType());
String propertyName = builder.getWriteVariableManager().createId(propertyNameHint);
JVar propertyVar = block.decl(
context.getGenericType(property.getType()),
propertyName);
if (property.getField() != null) {
Field field = property.getField();
if (!isPrivate(field)) {
propertyVar.init(beanVar.ref(field.getName()));
} else {
JFieldVar fieldAccessorField = builder.getPrivateFieldAccessor(field);
String methodName;
if (Boolean.TYPE.equals(propertyType)) {
methodName = "getBoolean";
} else if (Byte.TYPE.equals(propertyType)) {
methodName = "getByte";
} else if (Character.TYPE.equals(propertyType)) {
methodName = "getChar";
} else if (Short.TYPE.equals(propertyType)) {
methodName = "getShort";
} else if (Integer.TYPE.equals(propertyType)) {
methodName = "getInt";
} else if (Long.TYPE.equals(propertyType)) {
methodName = "getLong";
} else if (Float.TYPE.equals(propertyType)) {
methodName = "getFloat";
} else if (Double.TYPE.equals(propertyType)) {
methodName = "getDouble";
} else {
methodName = "getObject";
}
propertyVar.init(fieldAccessorField.invoke(methodName).arg(beanVar).arg(builder.getWriteContextVar()).arg(beanVar));
}
} else if (property.getGetter() != null) {
Method getter = property.getGetter();
if (!isPrivate(getter)) {
propertyVar.init(JExpr._null());
JTryBlock tryGetter = block._try();
tryGetter.body().assign(propertyVar, beanVar.invoke(getter.getName()));
JCatchBlock catchException = tryGetter._catch(context.toJClass(Exception.class));
catchException.body().invoke(builder.getReadContextVar(), "getterError")
.arg(beanVar)
.arg(property.getName())
.arg(context.dotclass(property.getBean().getType()))
.arg(getter.getName())
.arg(catchException.param("e"));
} else {
JFieldVar propertyAccessorField = builder.getPrivatePropertyAccessor(property.getGetter(), property.getSetter(), property.getName());
propertyVar.init(propertyAccessorField.invoke("getObject").arg(beanVar).arg(builder.getWriteContextVar()).arg(beanVar));
}
} else {
throw new BuildException("Property does not have a getter " + property.getBean().getClass().getName() + "." + property.getName());
}
return propertyVar;
}
private void writeClassWriter(JAXBObjectBuilder builder, Bean bean, JBlock block, JExpression propertyVar) {
// Complex type which will already have an element builder defined
JAXBObjectBuilder existingBuilder = builders.get(bean);
if (existingBuilder == null) {
throw new BuildException("Unknown bean " + bean);
}
// Declare dependency from builder to existingBuilder
builder.addDependency(existingBuilder.getJAXBObjectClass());
// Add a static import for the write method on the existing builder class
String methodName = "write" + bean.getType().getSimpleName();
if (builder != existingBuilder) {
JStaticImports staticImports = JStaticImports.getStaticImports(builder.getJAXBObjectClass());
staticImports.addStaticImport(existingBuilder.getJAXBObjectClass().fullName() + "." + methodName);
}
// Call the static method
JInvocation invocation = JExpr.invoke(methodName).arg(builder.getXSW()).arg(propertyVar).arg(builder.getWriteContextVar());
block.add(invocation);
}
private List<Bean> getSubstitutionTypes(Class<?> c) {
List<Bean> beans = new ArrayList<Bean>();
for (Bean bean : model.getBeans()) {
if (c.isAssignableFrom(bean.getType()) && bean.getSchemaTypeName() != null) {
beans.add(bean);
}
}
Collections.sort(beans, new BeanComparator());
return beans;
}
private <T extends JExpression> T writeAdapterConversion(JAXBObjectBuilder builder, JBlock block, Property property, T propertyVar) {
if (property.getAdapterType() != null) {
JVar adapterVar = builder.getAdapter(property.getAdapterType());
JVar valueVar = block.decl(context.toJClass(property.getComponentAdaptedType()), builder.getWriteVariableManager().createId(property.getName()), JExpr._null());
JTryBlock tryBlock = block._try();
tryBlock.body().assign(valueVar, adapterVar.invoke("marshal").arg(propertyVar));
JCatchBlock catchException = tryBlock._catch(context.toJClass(Exception.class));
JBlock catchBody = catchException.body();
catchBody.invoke(builder.getReadContextVar(), "xmlAdapterError")
.arg(builder.getWriteObject())
.arg(property.getName())
.arg(context.dotclass(property.getAdapterType()))
.arg(context.dotclass(toClass(property.getType()))) // currently we only support conversion between same type
.arg(context.dotclass(toClass(property.getType())))
.arg(catchException.param("e"));
//noinspection unchecked
propertyVar = (T) valueVar;
}
return propertyVar;
}
private void writeSimpleTypeElement(JAXBObjectBuilder builder, JExpression object, Class type, JBlock block) {
if(isBuiltinType(type)) {
block.add(builder.getXSW().invoke("writeCharacters").arg(toString(builder, object, type)));
} else if (type.equals(byte[].class)) {
block.add(context.toJClass(BinaryUtils.class).staticInvoke("encodeBytes").arg(builder.getXSW()).arg(object));
} else if (type.equals(QName.class)) {
block.add(builder.getXSW().invoke("writeQName").arg(object));
} else if (type.equals(DataHandler.class) || type.equals(Image.class)) {
// todo support AttachmentMarshaller
} else if (type.equals(Object.class)) {
block.add(builder.getXSW().invoke("writeDomElement").arg(JExpr.cast(context.toJClass(Element.class), object)).arg(JExpr.FALSE));
} else {
logger.info("(JAXB Writer) Cannot map simple type yet: " + type);
}
}
private void writeSimpleTypeAttribute(JAXBObjectBuilder builder, JBlock block, Property property, JExpression propertyVar) {
JVar stringBuilder = null;
JExpression itemVar = propertyVar;
JBlock itemBlock = block;
if (property.isCollection()) {
JType itemType;
if (!toClass(property.getComponentType()).isPrimitive()) {
itemType = context.getGenericType(property.getComponentType());
} else {
itemType = context.toJType((Class<?>) toClass(property.getComponentType()));
}
String valueName = builder.getWriteVariableManager().createId( property.getName() + "Value");
stringBuilder = block.decl(context.toJClass(StringBuilder.class), valueName, JExpr._new(context.toJClass(StringBuilder.class)));
String itemName = builder.getWriteVariableManager().createId( property.getName() + "Item");
JForEach each = block.forEach(itemType, itemName, propertyVar);
JBlock newBody = each.body();
itemBlock = newBody;
itemVar = each.var();
// if (booleanArrayAttribute.length != 0) booleanArrayAttributeValue.append(" ");
itemBlock._if(stringBuilder.invoke("length").ne(JExpr.lit(0)))._then().invoke(stringBuilder, "append").arg(" ");
// the final value assigned to the property is stringBuilder.toString()
propertyVar = stringBuilder.invoke("toString");
}
itemVar = writeAdapterConversion(builder, itemBlock, property, itemVar);
Class type;
if (property.getAdapterType() == null) {
type = toClass(property.getComponentType());
} else {
type = property.getComponentAdaptedType();
}
// if this is an id ref we write the ID property of the target bean instead of the bean itself
if (property.isIdref()) {
Property idProperty = findReferencedIdProperty(property);
// read the id value
itemVar = getValue(builder, itemVar, idProperty, property.getName() + JavaUtils.capitalize(idProperty.getName()), itemBlock);
// the written type is always String
type = String.class;
// if (id != null) write the value
JConditional nullCond = itemBlock._if(itemVar.ne(JExpr._null()));
itemBlock = nullCond._then();
}
if(isBuiltinType(type)) {
itemVar = toString(builder, itemVar, type);
} else if (type.equals(byte[].class)) {
itemVar = context.toJClass(Base64.class).staticInvoke("encode").arg(itemVar);
} else if (type.equals(QName.class)) {
itemVar = builder.getXSW().invoke("getQNameAsString").arg(itemVar);
} else if (type.equals(DataHandler.class) || type.equals(Image.class)) {
// todo support AttachmentMarshaller
} else {
logger.info("(JAXB Writer) Cannot map simple attribute type yet: " + type);
return;
}
if (stringBuilder != null) {
itemBlock.invoke(stringBuilder, "append").arg(itemVar);
} else {
propertyVar = itemVar;
}
QName name = property.getXmlName();
JExpression prefix;
if (name.getNamespaceURI().length() > 0) {
prefix = builder.getWriterPrefix(name.getNamespaceURI());
} else {
prefix = JExpr.lit("");
}
block.add(builder.getXSW().invoke("writeAttribute")
.arg(prefix)
.arg(JExpr.lit(name.getNamespaceURI()))
.arg(JExpr.lit(name.getLocalPart()))
.arg(propertyVar));
}
private Property findReferencedIdProperty(Property property) {
// find referenced bean
Bean targetBean = model.getBean(toClass(property.getComponentType()));
if (targetBean == null) {
throw new BuildException("Unknown bean " + toClass(property.getType()));
}
// find id property on referenced bean
Property idProperty = null;
while (idProperty == null) {
for (Property targetProperty : targetBean.getProperties()) {
if (targetProperty.isId()) {
idProperty = targetProperty;
break;
}
}
if (idProperty == null) {
if (targetBean.getBaseClass() == null) {
throw new BuildException("Property " + property + " is an IDREF, but property type " + toClass(property.getType()).getName() + " does not have an ID property");
}
targetBean = targetBean.getBaseClass();
}
}
return idProperty;
}
private void writeSimpleTypeAttribute(JAXBObjectBuilder builder, JBlock block, JExpression qnameVar, Class type, JExpression value) {
if(isBuiltinType(type)) {
value = toString(builder, value, type);
} else if (type.equals(byte[].class)) {
value = context.toJClass(Base64.class).staticInvoke("encode").arg(value);
} else if (type.equals(QName.class)) {
value = builder.getXSW().invoke("getQNameAsString").arg(value);
} else if (type.equals(DataHandler.class) || type.equals(Image.class)) {
// todo support AttachmentMarshaller
} else {
logger.info("(JAXB Writer) Cannot map simple attribute type yet: " + type);
return;
}
block.add(builder.getXSW().invoke("writeAttribute").arg(qnameVar).arg(value));
}
private String getMostPopularNS(Set<Property> properties) {
List<QName> names = new ArrayList<QName>();
for (Property property : properties) {
if (property.getXmlName() != null) {
names.add(property.getXmlName());
}
for (ElementMapping mapping : property.getElementMappings()) {
if (mapping.getXmlName() != null) {
names.add(mapping.getXmlName());
}
}
}
String mostPopularNS = null;
int mostPopularCount = 0;
Map<String, Integer> nsCount = new TreeMap<String, Integer>();
for (QName name : names) {
String namespace = name.getNamespaceURI();
if (namespace.length() > 0) {
Integer count = nsCount.get(namespace);
count = count == null ? 0 : count + 1;
nsCount.put(namespace, count);
if (count > mostPopularCount) {
mostPopularNS = namespace;
mostPopularCount = count;
}
}
}
return mostPopularNS;
}
private boolean isBuiltinType(Class type) {
return type.equals(boolean.class) ||
type.equals(byte.class) ||
type.equals(short.class) ||
type.equals(int.class) ||
type.equals(long.class) ||
type.equals(float.class) ||
type.equals(double.class) ||
type.equals(String.class) ||
type.equals(Boolean.class) ||
type.equals(Byte.class) ||
type.equals(Short.class) ||
type.equals(Integer.class) ||
type.equals(Long.class) ||
type.equals(Float.class) ||
type.equals(Double.class) ||
type.equals(XMLGregorianCalendar.class) ||
type.equals(Duration.class) ||
type.equals(BigDecimal.class) ||
type.equals(BigInteger.class) ||
type.isEnum();
}
private JExpression toString(JAXBObjectBuilder builder, JExpression value, Class<?> type) {
if (type.isPrimitive()) {
if (type.equals(boolean.class)) {
return context.toJClass(Boolean.class).staticInvoke("toString").arg(value);
} else if (type.equals(byte.class)) {
return context.toJClass(Byte.class).staticInvoke("toString").arg(value);
} else if (type.equals(short.class)) {
return context.toJClass(Short.class).staticInvoke("toString").arg(value);
} else if (type.equals(int.class)) {
return context.toJClass(Integer.class).staticInvoke("toString").arg(value);
} else if (type.equals(long.class)) {
return context.toJClass(Long.class).staticInvoke("toString").arg(value);
} else if (type.equals(float.class)) {
return context.toJClass(Float.class).staticInvoke("toString").arg(value);
} else if (type.equals(double.class)) {
return context.toJClass(Double.class).staticInvoke("toString").arg(value);
}
} else {
if (type.equals(String.class)) {
return value;
} else if (type.equals(Boolean.class)) {
return context.toJClass(Boolean.class).staticInvoke("toString").arg(value);
} else if (type.equals(Byte.class)) {
return context.toJClass(Byte.class).staticInvoke("toString").arg(value);
} else if (type.equals(Short.class)) {
return context.toJClass(Short.class).staticInvoke("toString").arg(value);
} else if (type.equals(Integer.class)) {
return context.toJClass(Integer.class).staticInvoke("toString").arg(value);
} else if (type.equals(Long.class)) {
return context.toJClass(Long.class).staticInvoke("toString").arg(value);
} else if (type.equals(Float.class)) {
return context.toJClass(Float.class).staticInvoke("toString").arg(value);
} else if (type.equals(Double.class)) {
return context.toJClass(Double.class).staticInvoke("toString").arg(value);
} else if (type.equals(XMLGregorianCalendar.class)) {
return value.invoke("toXMLFormat");
} else if (type.equals(Duration.class)) {
return value.invoke("toString");
} else if (type.equals(BigDecimal.class)) {
return value.invoke("toString");
} else if (type.equals(BigInteger.class)) {
return value.invoke("toString");
} else if (type.isEnum()) {
JAXBEnumBuilder enumBuilder = enumBuilders.get(type);
if (enumBuilder == null) {
throw new BuildException("Unknown enum type " + type);
}
return invokeEnumToString(builder, builder.getWriteObject(), JExpr._null(), enumBuilder, value);
}
}
throw new UnsupportedOperationException("Invalid type " + type);
}
private JInvocation invokeEnumToString(JAXBObjectBuilder caller, JVar beanVar, JExpression parameterName, JAXBEnumBuilder enumBuilder, JExpression value) {
// Declare dependency from caller to parser
caller.addDependency(enumBuilder.getJAXBEnumClass());
// Add a static import for the toString method on the existing builder class
String methodName = "toString" + enumBuilder.getType().getSimpleName();
JStaticImports staticImports = JStaticImports.getStaticImports(caller.getJAXBObjectClass());
staticImports.addStaticImport(enumBuilder.getJAXBEnumClass().fullName() + "." + methodName);
// Call the static method
JInvocation invocation = JExpr.invoke(methodName)
.arg(beanVar)
.arg(parameterName)
.arg(caller.getWriteContextVar())
.arg(value);
return invocation;
}
private static class BeanComparator implements Comparator<Bean> {
public int compare(Bean left, Bean right) {
if (left.equals(right)) return 0;
if (left.getType().isAssignableFrom(right.getType())) {
return 1;
}
return -1;
}
}
private static class ClassComparator implements Comparator<Class> {
public int compare(Class left, Class right) {
if (left.equals(right)) return 0;
if (left.isAssignableFrom(right)) {
return 1;
}
return -1;
}
}
}