blob: 7e42c660e739a32be42bcaeee74a4946c7aabbd7 [file] [log] [blame]
/*
* 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 org.apache.tuscany.sca.databinding.jaxb;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.JAXBIntrospector;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlType;
import javax.xml.namespace.QName;
import org.apache.tuscany.sca.databinding.TransformationContext;
import org.apache.tuscany.sca.databinding.TransformationException;
import org.apache.tuscany.sca.databinding.impl.SimpleTypeMapperImpl;
import org.apache.tuscany.sca.databinding.util.LRUCache;
import org.apache.tuscany.sca.interfacedef.DataType;
import org.apache.tuscany.sca.interfacedef.Interface;
import org.apache.tuscany.sca.interfacedef.Operation;
import org.apache.tuscany.sca.interfacedef.impl.DataTypeImpl;
import org.apache.tuscany.sca.interfacedef.java.JavaInterface;
import org.apache.tuscany.sca.interfacedef.util.WrapperInfo;
import org.apache.tuscany.sca.interfacedef.util.XMLType;
/**
*
* @version $Rev$ $Date$
*/
// FIXME: [rfeng] We probably should turn this into a pluggable system service
public class JAXBContextHelper {
private static final JAXBContextCache cache = new JAXBContextCache();
private JAXBContextHelper() {
}
/**
* Create a JAXBContext for a given class
* @param cls
* @return
* @throws JAXBException
*/
public static JAXBContext createJAXBContext(Class<?> cls) throws JAXBException {
return cache.getJAXBContext(cls);
}
public static JAXBContext createJAXBContext(TransformationContext tContext, boolean source) throws JAXBException {
if (tContext == null)
throw new TransformationException("JAXB context is not set for the transformation.");
// TODO: [rfeng] Need to figure out what's the best granularity to create the JAXBContext
// per interface, operation or parameter
Operation op = source ? tContext.getSourceOperation() : tContext.getTargetOperation();
if (op != null) {
synchronized (op) {
JAXBContext context = op.getInputType().getMetaData(JAXBContext.class);
if (context == null) {
context = createJAXBContext(getDataTypes(op, true));
op.getInputType().setMetaData(JAXBContext.class, context);
}
return context;
}
}
// For property transformation, the operation can be null
DataType<?> dataType = source ? tContext.getSourceDataType() : tContext.getTargetDataType();
return createJAXBContext(dataType);
}
private static Class<?>[] getSeeAlso(Class<?> interfaze) {
if (interfaze == null) {
return null;
}
XmlSeeAlso seeAlso = interfaze.getAnnotation(XmlSeeAlso.class);
if (seeAlso == null) {
return null;
} else {
return seeAlso.value();
}
}
@SuppressWarnings("unchecked")
public static JAXBContext createJAXBContext(DataType dataType) throws JAXBException {
return createJAXBContext(findClasses(dataType));
}
public static Unmarshaller getUnmarshaller(JAXBContext context) throws JAXBException {
return cache.getUnmarshaller(context);
}
public static void releaseJAXBUnmarshaller(JAXBContext context, Unmarshaller unmarshaller) {
cache.releaseJAXBUnmarshaller(context, unmarshaller);
}
public static Marshaller getMarshaller(JAXBContext context) throws JAXBException {
return cache.getMarshaller(context);
}
public static void releaseJAXBMarshaller(JAXBContext context, Marshaller marshaller) {
cache.releaseJAXBMarshaller(context, marshaller);
}
@SuppressWarnings("unchecked")
public static Object createJAXBElement(JAXBContext context, DataType dataType, Object value) {
Class<?> type = dataType == null ? value.getClass() : dataType.getPhysical();
QName name = JAXBDataBinding.ROOT_ELEMENT;
if (context != null) {
Object logical = dataType == null ? null : dataType.getLogical();
if (logical instanceof XMLType) {
XMLType xmlType = (XMLType)logical;
if (xmlType.isElement()) {
name = xmlType.getElementName();
} else {
/**
* Set the declared type to Object.class so that xsi:type
* will be produced
*/
type = Object.class;
}
} else {
type = Object.class;
}
}
JAXBIntrospector introspector = context.createJAXBIntrospector();
Object element = null;
if (value != null && introspector.isElement(value)) {
// NOTE: [rfeng] We cannot wrap an element in a JAXBElement
element = value;
}
if (element == null) {
element = new JAXBElement(name, Object.class, value);
}
return element;
}
@SuppressWarnings("unchecked")
public static Object createReturnValue(JAXBContext context, DataType dataType, Object value) {
Class<?> cls = getJavaType(dataType);
if (cls == JAXBElement.class) {
return createJAXBElement(context, dataType, value);
} else {
if (value instanceof JAXBElement) {
return ((JAXBElement)value).getValue();
} else {
return value;
}
}
}
/**
* Create a JAXContext for an array of classes
* @param classes
* @return
* @throws JAXBException
*/
public static JAXBContext createJAXBContext(Class<?>[] classes) throws JAXBException {
return cache.getJAXBContext(classes);
}
public static JAXBContext createJAXBContext(Set<Class<?>> classes) throws JAXBException {
return cache.getJAXBContext(classes);
}
/**
* Create a JAXBContext for a given java interface
* @param intf
* @return
* @throws JAXBException
*/
@SuppressWarnings("unchecked")
public static JAXBContext createJAXBContext(Interface intf, boolean useWrapper) throws JAXBException {
synchronized (cache) {
LRUCache<Object, JAXBContext> map = cache.getCache();
Integer key = new Integer(System.identityHashCode(intf));
JAXBContext context = map.get(key);
if (context != null) {
return context;
}
List<DataType> dataTypes = getDataTypes(intf, useWrapper);
context = createJAXBContext(dataTypes);
map.put(key, context);
return context;
}
}
@SuppressWarnings("unchecked")
public static JAXBContext createJAXBContext(List<DataType> dataTypes) throws JAXBException {
JAXBContext context;
Set<Class<?>> classes = new HashSet<Class<?>>();
Set<Type> visited = new HashSet<Type>();
for (DataType d : dataTypes) {
findClasses(d, classes, visited);
}
context = createJAXBContext(classes);
return context;
}
@SuppressWarnings("unchecked")
private static Set<Class<?>> findClasses(DataType d) {
Set<Class<?>> classes = new HashSet<Class<?>>();
Set<Type> visited = new HashSet<Type>();
findClasses(d, classes, visited);
return classes;
}
@SuppressWarnings("unchecked")
private static void findClasses(DataType d, Set<Class<?>> classes, Set<Type> visited) {
if (d == null) {
return;
}
String db = d.getDataBinding();
if (JAXBDataBinding.NAME.equals(db) || (db != null && db.startsWith("java:")) || db == null) {
if (!d.getPhysical().isInterface() && !JAXBElement.class.isAssignableFrom(d.getPhysical())) {
classes.add(d.getPhysical());
}
}
if (d.getPhysical() != d.getGenericType()) {
findClasses(d.getGenericType(), classes, visited);
}
}
/**
* Find referenced classes in the generic type
* @param type
* @param classSet
* @param visited
*/
@SuppressWarnings("unchecked")
private static void findClasses(Type type, Set<Class<?>> classSet, Set<Type> visited) {
if (visited.contains(type) || type == null) {
return;
}
visited.add(type);
if (type instanceof Class) {
Class<?> cls = (Class<?>)type;
if (!cls.isInterface()) {
classSet.add(cls);
}
return;
} else if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType)type;
findClasses(pType.getRawType(), classSet, visited);
for (Type t : pType.getActualTypeArguments()) {
findClasses(t, classSet, visited);
}
} else if (type instanceof TypeVariable) {
TypeVariable<?> tv = (TypeVariable<?>)type;
for (Type t : tv.getBounds()) {
findClasses(t, classSet, visited);
}
} else if (type instanceof GenericArrayType) {
GenericArrayType gType = (GenericArrayType)type;
findClasses(gType.getGenericComponentType(), classSet, visited);
} else if (type instanceof WildcardType) {
WildcardType wType = (WildcardType)type;
for (Type t : wType.getLowerBounds()) {
findClasses(t, classSet, visited);
}
for (Type t : wType.getUpperBounds()) {
findClasses(t, classSet, visited);
}
}
}
public static JAXBContext createJAXBContext(Interface intf) throws JAXBException {
return createJAXBContext(intf, true);
}
/**
* @param intf
* @param useWrapper Use wrapper classes?
* @return
*/
@SuppressWarnings("unchecked")
private static List<DataType> getDataTypes(Interface intf, boolean useWrapper) {
List<DataType> dataTypes = new ArrayList<DataType>();
for (Operation op : intf.getOperations()) {
getDataTypes(dataTypes, op, useWrapper);
}
return dataTypes;
}
@SuppressWarnings("unchecked")
private static List<DataType> getDataTypes(Operation op, boolean useWrapper) {
List<DataType> dataTypes = new ArrayList<DataType>();
getDataTypes(dataTypes, op, useWrapper);
// Adding classes referenced by @XmlSeeAlso in the java interface
Interface interface1 = op.getInterface();
if (interface1 instanceof JavaInterface) {
JavaInterface javaInterface = (JavaInterface)interface1;
Class<?>[] seeAlso = getSeeAlso(javaInterface.getJavaClass());
if (seeAlso != null) {
for (Class<?> cls : seeAlso) {
dataTypes.add(new DataTypeImpl<XMLType>(JAXBDataBinding.NAME, cls, XMLType.UNKNOWN));
}
}
seeAlso = getSeeAlso(javaInterface.getCallbackClass());
if (seeAlso != null) {
for (Class<?> cls : seeAlso) {
dataTypes.add(new DataTypeImpl<XMLType>(JAXBDataBinding.NAME, cls, XMLType.UNKNOWN));
}
}
}
return dataTypes;
}
@SuppressWarnings("unchecked")
private static void getDataTypes(List<DataType> dataTypes, Operation op, boolean useWrapper) {
WrapperInfo inputWrapperInfo = op.getInputWrapper();
WrapperInfo outputWrapperInfo = op.getOutputWrapper();
// TUSCANY-3298: Add the wrapper type instead of individual elements
// if possible. JAXB will implicitly add all types that are statically
// reachable from the wrapper class, with the exception of type arguments
// for parameterized types that aren't collections.
DataType dt1 = null;
if (useWrapper && (inputWrapperInfo != null)) {
dt1 = inputWrapperInfo.getWrapperType();
if (dt1 != null) {
dataTypes.add(dt1);
for (DataType in : op.getInputType().getLogical()) {
if (isParameterizedNonCollectionType(in)) {
// JAXB won't add the type arguments, so we need to add them
dataTypes.add(in);
}
}
}
}
if (dt1 == null) {
// We couldn't add the wrapper, so add the elements individually
for (DataType dt : op.getInputType().getLogical()) {
dataTypes.add(dt);
}
}
// TUSCANY-3298: Add the wrapper type instead of the output type
// if possible. JAXB will implicitly add all types that are statically
// reachable from the wrapper class, with the exception of type arguments
// for parameterized types that aren't collections or maps.
DataType dt2 = null;
if (useWrapper && (outputWrapperInfo != null)) {
dt2 = outputWrapperInfo.getWrapperType();
if (dt2 != null) {
dataTypes.add(dt2);
DataType out = op.getOutputType();
if (out != null && isParameterizedNonCollectionType(out)) {
// JAXB won't add the type arguments, so we need to add them
dataTypes.add(out);
}
}
}
if (dt2 == null) {
// We couldn't add the wrapper, so add the output type directly
dt2 = op.getOutputType();
if (dt2 != null) {
dataTypes.add(dt2);
}
}
for (DataType<DataType> dt3 : op.getFaultTypes()) {
DataType dt4 = dt3.getLogical();
if (dt4 != null) {
dataTypes.add(dt4);
}
}
}
/*
* We need to add parameterized non-collection types to the JAXB context
* explicitly, because type argument information for these types is erased
* from the generated wrapper bean.
*/
private static boolean isParameterizedNonCollectionType(DataType dt) {
Type type = dt.getGenericType();
if (type instanceof ParameterizedType) {
Class physical = dt.getPhysical();
if (!Collection.class.isAssignableFrom(physical) &&
!Map.class.isAssignableFrom(physical)) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
public static Class<?> getJavaType(DataType<?> dataType) {
if (dataType == null) {
return null;
}
Class type = dataType.getPhysical();
if (JAXBElement.class.isAssignableFrom(type)) {
Type generic = dataType.getGenericType();
type = Object.class;
}
if (type == Object.class && dataType.getLogical() instanceof XMLType) {
XMLType xType = (XMLType)dataType.getLogical();
Class javaType = SimpleTypeMapperImpl.getJavaType(xType.getTypeName());
if (javaType != null) {
type = javaType;
}
}
return type;
}
public static XMLType getXmlTypeName(Class<?> javaType) {
if (javaType.isInterface()) {
// JAXB doesn't support interfaces
return null;
}
String namespace = null;
String name = null;
Package pkg = javaType.getPackage();
if (pkg != null) {
XmlSchema schema = pkg.getAnnotation(XmlSchema.class);
if (schema != null) {
namespace = schema.namespace();
}
}
QName elementQName = null;
QName typeQName = null;
XmlRootElement rootElement = javaType.getAnnotation(XmlRootElement.class);
if (rootElement != null) {
String elementName = rootElement.name();
String elementNamespace = rootElement.namespace();
if (elementNamespace.equals("##default")) {
elementNamespace = namespace;
}
if (elementName.equals("##default")) {
elementName = jaxbDecapitalize(javaType.getSimpleName());
}
elementQName = new QName(elementNamespace, elementName);
}
XmlType type = javaType.getAnnotation(XmlType.class);
if (type != null) {
String typeNamespace = type.namespace();
String typeName = type.name();
if (typeNamespace.equals("##default")) {
// namespace is from the package
typeNamespace = namespace;
}
if (typeName.equals("##default")) {
typeName = jaxbDecapitalize(javaType.getSimpleName());
}
typeQName = new QName(typeNamespace, typeName);
} else {
XmlEnum xmlEnum = javaType.getAnnotation(XmlEnum.class);
// POJO can have the @XmlSchema on the package-info too
if (xmlEnum != null || namespace != null) {
name = jaxbDecapitalize(javaType.getSimpleName());
typeQName = new QName(namespace, name);
}
}
if (elementQName == null && typeQName == null) {
return null;
}
return new XMLType(elementQName, typeQName);
}
/**
* The JAXB RI doesn't implement the decapitalization algorithm in the
* JAXB spec. See Sun bug 6505643 for details. This means that instead
* of calling java.beans.Introspector.decapitalize() as the JAXB spec says,
* Tuscany needs to mimic the incorrect JAXB RI algorithm.
*/
public static String jaxbDecapitalize(String name) {
// find first lower case char in name
int lower = name.length();
for (int i = 0; i < name.length(); i++) {
if (Character.isLowerCase(name.charAt(i))) {
lower = i;
break;
}
}
int decap;
if (name.length() == 0) {
decap = 0; // empty string: nothing to do
} else if (lower == 0) {
decap = 0; // first char is lower case: nothing to do
} else if (lower == 1) {
decap = 1; // one upper followed by lower: decapitalize 1 char
} else if (lower < name.length()) {
decap = lower - 1; // n uppers followed by at least one lower: decapitalize n-1 chars
} else {
decap = name.length(); // all upper case: decapitalize all chars
}
return name.substring(0, decap).toLowerCase() + name.substring(decap);
}
}