/** * 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.cxf.aegis.type.basic; import java.beans.PropertyDescriptor; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.xml.namespace.QName; import org.apache.cxf.aegis.AegisContext; import org.apache.cxf.aegis.Context; import org.apache.cxf.aegis.DatabindingException; import org.apache.cxf.aegis.type.AbstractTypeCreator; import org.apache.cxf.aegis.type.AegisType; import org.apache.cxf.aegis.type.TypeMapping; import org.apache.cxf.aegis.type.TypeUtil; import org.apache.cxf.aegis.type.mtom.AbstractXOPType; import org.apache.cxf.aegis.xml.MessageReader; import org.apache.cxf.aegis.xml.MessageWriter; import org.apache.cxf.common.classloader.ClassLoaderUtils; import org.apache.cxf.common.xmlschema.XmlSchemaUtils; import org.apache.cxf.helpers.CastUtils; import org.apache.cxf.interceptor.Fault; import org.apache.ws.commons.schema.XmlSchema; import org.apache.ws.commons.schema.XmlSchemaAny; import org.apache.ws.commons.schema.XmlSchemaAnyAttribute; import org.apache.ws.commons.schema.XmlSchemaAttribute; import org.apache.ws.commons.schema.XmlSchemaComplexContent; import org.apache.ws.commons.schema.XmlSchemaComplexContentExtension; import org.apache.ws.commons.schema.XmlSchemaComplexType; import org.apache.ws.commons.schema.XmlSchemaElement; import org.apache.ws.commons.schema.XmlSchemaSequence; /** * Serializes JavaBeans. * * There's a really dangerous coding convention in this class, maintainers beware. * There are two constructor. The no-args constructor defers, until later, * the construction of a BeanTypeInfo. The one-arg constructor gets the BeanTypeInfo passed as a parameter. * Aegis doesn't have any uniform discipline of 'construct, set properties, initialize'. Instead, * each piece of code that uses the type info needs to call getTypeInfo() instead of referencing the * 'info' field. */ public class BeanType extends AegisType { private BeanTypeInfo info; private boolean isInterface; private boolean isException; /** * Construct a type info. Caller must pass in the type class via * setTypeClass later. */ public BeanType() { } /** * Construct a type info given a full BeanTypeInfo. * @param info */ public BeanType(BeanTypeInfo info) { this.info = info; this.typeClass = info.getTypeClass(); initTypeClass(); } private void initTypeClass() { // throw if someone tries to set up a generic bean. Class<?> plainClass = (Class<?>) typeClass; this.isInterface = plainClass.isInterface(); isException = Exception.class.isAssignableFrom(plainClass); } /** * {@inheritDoc} */ @Override public Object readObject(MessageReader reader, Context context) throws DatabindingException { BeanTypeInfo inf = getTypeInfo(); try { Class<?> clazz = getTypeClass(); Object object; // the target for properties; either the object or the proxy handler Object target; if (isInterface) { String impl = context.getGlobalContext().getBeanImplementationMap().get(clazz); if (impl == null) { InvocationHandler handler = new InterfaceInvocationHandler(); object = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, handler); target = handler; } else { try { clazz = ClassLoaderUtils.loadClass(impl, getClass()); object = clazz.newInstance(); target = object; } catch (ClassNotFoundException e) { throw new DatabindingException("Could not find implementation class " + impl + " for class " + clazz.getName()); } } } else if (isException) { object = createFromFault(context); target = object; } else { object = clazz.newInstance(); target = object; } // Read attributes while (reader.hasMoreAttributeReaders()) { MessageReader childReader = reader.getNextAttributeReader(); QName name = childReader.getName(); AegisType type = inf.getType(name); if (type != null) { Object writeObj = type.readObject(childReader, context); writeProperty(name, target, writeObj, clazz, inf); } } // Read child elements while (reader.hasMoreElementReaders()) { MessageReader childReader = reader.getNextElementReader(); QName name = childReader.getName(); // Find the BeanTypeInfo that contains a property for the element name BeanTypeInfo propertyTypeInfo = getBeanTypeInfoWithProperty(name); // Get the AegisType for the property AegisType type = getElementType(name, propertyTypeInfo, childReader, context); if (type != null) { if (!childReader.isXsiNil()) { Object writeObj; if (type.isFlatArray()) { ArrayType aType = (ArrayType) type; PropertyDescriptor desc = inf.getPropertyDescriptorFromMappedName(name); boolean isList = List.class.isAssignableFrom(desc.getPropertyType()); writeObj = aType.readObject(childReader, name, context, !isList); } else { writeObj = type.readObject(childReader, context); } writeProperty(name, target, writeObj, clazz, propertyTypeInfo); } else { if (!alwaysAllowNillables() && !propertyTypeInfo.isNillable(name)) { throw new DatabindingException(name.getLocalPart() + " is nil, but not nillable."); } childReader.readToEnd(); } } else { childReader.readToEnd(); } } return object; } catch (IllegalAccessException e) { throw new DatabindingException("Illegal access. " + e.getMessage(), e); } catch (InstantiationException e) { throw new DatabindingException("Couldn't instantiate class. " + e.getMessage(), e); } catch (SecurityException e) { throw new DatabindingException("Illegal access. " + e.getMessage(), e); } catch (IllegalArgumentException e) { throw new DatabindingException("Illegal argument. " + e.getMessage(), e); } catch (InvocationTargetException e) { throw new DatabindingException("Could not create class: " + e.getMessage(), e); } } protected boolean alwaysAllowNillables() { return false; } protected AegisType getElementType(QName name, BeanTypeInfo beanTypeInfo, MessageReader reader, Context context) { AegisType type = beanTypeInfo.getType(name); // AegisType can be overriden with a xsi:type attribute type = TypeUtil.getReadType(reader.getXMLStreamReader(), context.getGlobalContext(), type); return type; } /** * If the class is an exception, this will try and instantiate it with information from the XFireFault (if * it exists). */ protected Object createFromFault(Context context) throws SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Class<?> clazz = getTypeClass(); Constructor<?> ctr; Object o; Fault fault = context.getFault(); try { ctr = clazz.getConstructor(new Class[] { String.class, Throwable.class }); o = ctr.newInstance(new Object[] { fault.getMessage(), fault }); } catch (NoSuchMethodException e) { try { ctr = clazz.getConstructor(new Class[] { String.class, Exception.class }); o = ctr.newInstance(new Object[] { fault.getMessage(), fault }); } catch (NoSuchMethodException e1) { try { ctr = clazz.getConstructor(new Class[] { String.class }); o = ctr.newInstance(new Object[] { fault.getMessage() }); } catch (NoSuchMethodException e2) { return clazz.newInstance(); } } } return o; } /** * Write the specified property to a field. */ protected void writeProperty(QName name, Object object, Object property, Class<?> impl, BeanTypeInfo inf) throws DatabindingException { if (object instanceof InterfaceInvocationHandler) { InterfaceInvocationHandler delegate = (InterfaceInvocationHandler)object; delegate.writeProperty(name.getLocalPart(), property); return; } try { PropertyDescriptor desc = inf.getPropertyDescriptorFromMappedName(name); Method m = desc.getWriteMethod(); if (m == null) { if (getTypeClass().isInterface()) { m = getWriteMethodFromImplClass(impl, desc); } if (m == null && property instanceof List) { m = desc.getReadMethod(); List<Object> l = CastUtils.cast((List<?>)m.invoke(object)); List<Object> p = CastUtils.cast((List<?>)property); l.addAll(p); return; } if (m == null) { throw new DatabindingException("No write method for property " + name + " in " + object.getClass()); } } Class<?> propertyType = desc.getPropertyType(); if ((property == null && !propertyType.isPrimitive()) || (property != null)) { m.invoke(object, new Object[] { property }); } } catch (Exception e) { if (e instanceof DatabindingException) { throw (DatabindingException)e; } throw new DatabindingException("Couldn't set property " + name + " on " + object + ". " + e.getMessage(), e); } } /** * This is a hack to get the write method from the implementation class for an interface. */ private Method getWriteMethodFromImplClass(Class<?> impl, PropertyDescriptor pd) throws Exception { String name = pd.getName(); name = "set" + name.substring(0, 1).toUpperCase() + name.substring(1); return impl.getMethod(name, new Class[] { pd.getPropertyType() }); } /** * To avoid double-writing xsi:type attributes, ObjectType uses this special entrypoint. * * @param object * @param writer * @param context * @param wroteXsiType */ void writeObjectFromObjectType(Object object, MessageWriter writer, Context context, boolean wroteXsiType) { writeObjectInternal(object, writer, context, wroteXsiType); } /** * {@inheritDoc} */ @Override public void writeObject(Object object, MessageWriter writer, Context context) throws DatabindingException { writeObjectInternal(object, writer, context, false); } private void writeObjectInternal(Object object, MessageWriter writer, Context context, boolean wroteXsiType) throws DatabindingException { if (object == null) { return; } BeanTypeInfo inf = getTypeInfo(); if (!wroteXsiType && object.getClass() == getTypeClass() && context.isWriteXsiTypes()) { writer.writeXsiType(getSchemaType()); } for (QName name : inf.getAttributes()) { if (isInheritedProperty(inf, name)) { continue; } Object value = readProperty(object, name); if (value != null) { AegisType type = getType(inf, name); if (type == null) { throw new DatabindingException("Couldn't find type for " + value.getClass() + " for property " + name); } MessageWriter cwriter = writer.getAttributeWriter(name); type.writeObject(value, cwriter, context); cwriter.close(); } } if (inf.isExtension()) { AegisType t = getSuperType(); if (t != null) { t.writeObject(object, writer, context); } } for (QName name : inf.getElements()) { if (isInheritedProperty(inf, name)) { continue; } Object value = readProperty(object, name); AegisType defaultType = getType(inf, name); AegisType type = TypeUtil.getWriteType(context.getGlobalContext(), value, defaultType); // Write the value if it is not null. if (value != null) { if (type == null) { throw new DatabindingException("Couldn't find type for " + value.getClass() + " for property " + name); } writeElement(name, value, type, writer, context); } else if (inf.isNillable(name)) { MessageWriter cwriter = getWriter(writer, name, type); // Write the xsi:nil if it is null. cwriter.writeXsiNil(); cwriter.close(); } } } /** * @return true if the given beanType is extended and its given property is inherited from parent classes */ private boolean isInheritedProperty(BeanTypeInfo beanTypeInfo, QName propertyQName) { return beanTypeInfo.isExtension() && beanTypeInfo.getPropertyDescriptorFromMappedName(propertyQName).getReadMethod(). getDeclaringClass() != beanTypeInfo.getTypeClass(); } protected void writeElement(QName name, Object value, AegisType type, MessageWriter writer, Context context) { if (!type.isFlatArray()) { MessageWriter cwriter = null; cwriter = getWriter(writer, name, type); type.writeObject(value, cwriter, context); cwriter.close(); } else { ArrayType arrayType = (ArrayType)type; arrayType.writeObject(value, writer, context, name); } } private MessageWriter getWriter(MessageWriter writer, QName name, AegisType type) { MessageWriter cwriter; cwriter = writer.getElementWriter(name); return cwriter; } protected Object readProperty(Object object, QName name) { try { PropertyDescriptor desc = getTypeInfo().getPropertyDescriptorFromMappedName(name); Method m = desc.getReadMethod(); if (m == null) { throw new DatabindingException("No read method for property " + name + " in class " + object.getClass().getName()); } return m.invoke(object, new Object[0]); } catch (Exception e) { throw new DatabindingException("Couldn't get property " + name + " from bean " + object, e); } } private AegisType getType(BeanTypeInfo inf, QName name) { AegisType type = inf.getType(name); if (type == null) { throw new NullPointerException("Couldn't find type for" + name + " in class " + getTypeClass().getName()); } return type; } private void writeTypeReference(QName name, XmlSchemaElement element, AegisType type, XmlSchema schemaRoot) { if (type.isAbstract()) { element.setName(name.getLocalPart()); element.setSchemaTypeName(type.getSchemaType()); XmlSchemaUtils.addImportIfNeeded(schemaRoot, type.getSchemaType().getNamespaceURI()); /* * Here we have a semi-giant mess. If a parameter has a minOccurs > 1, it ends * up in the type info. However, it really got used in the array type. * All we really want to do here is manage 'optional' elements. If we * ever implement flat arrays, this will change. For now, we ignore * maxOccurs and we only look for 0's in the minOccurs. */ long minOccurs = getTypeInfo().getMinOccurs(name); /* If it is 1, that's the default, and if it's greater than one, it means * that there is a real array at work here. So the only value we want to pay * attention to is 0. */ if (minOccurs == 0) { element.setMinOccurs(minOccurs); } element.setNillable(getTypeInfo().isNillable(name)); } else { element.getRef().setTargetQName(type.getSchemaType()); } } /** * {@inheritDoc} */ @Override public void setTypeClass(Type typeClass) { super.setTypeClass(typeClass); initTypeClass(); } /** * We need to write a complex type schema for Beans, so return true. * * @see org.apache.cxf.aegis.type.AegisType#isComplex() */ @Override public boolean isComplex() { return true; } /** * {@inheritDoc} */ @Override public Set<AegisType> getDependencies() { Set<AegisType> deps = new HashSet<>(); BeanTypeInfo inf = getTypeInfo(); for (QName name : inf.getAttributes()) { if (isInheritedProperty(inf, name)) { continue; } deps.add(inf.getType(name)); } for (QName name : inf.getElements()) { if (isInheritedProperty(inf, name)) { continue; } deps.add(inf.getType(name)); } /* * Automagically add chain of superclasses if this is an an extension. */ AegisType sooperType = getSuperType(); if (sooperType != null) { deps.add(sooperType); } return deps; } protected BeanTypeInfo getBeanTypeInfoWithProperty(QName name) { // search the BeanType superType tree for the first BeanType with a property named 'name' BeanType beanType = this; AegisType type = null; while (type == null) { type = beanType.getTypeInfo().getType(name); if (type == null) { AegisType superType = beanType.getSuperType(); /* * The class might inherit from, say, 'Integer'. In * which case we've got no BeanType to work with. */ if (superType instanceof BeanType) { beanType = (BeanType)superType; } else { break; // give up. } } } return beanType.getTypeInfo(); } /** * Return the AegisType for the superclass if this type's class, if any. * @return */ public AegisType getSuperType() { BeanTypeInfo inf = getTypeInfo(); Class<?> c = inf.getTypeClass(); if (c.isInterface() && c.getInterfaces().length == 1) { c = c.getInterfaces()[0]; } else { c = c.getSuperclass(); } /* * Don't dig any deeper than Object or Exception */ if (c != null && c != Object.class && c != Exception.class && c != RuntimeException.class && c != Enum.class && c != Serializable.class && c != Cloneable.class) { TypeMapping tm = inf.getTypeMapping(); AegisType superType = tm.getType(c); if (superType == null) { // if we call createType, we know that we'll get a BeanType. */ superType = getTypeMapping().getTypeCreator().createType(c); if (superType != null) { tm.register(superType); this.info.setExtension(true); } } else { this.info.setExtension(true); } return superType; } else { return null; } } /** * Return the type info. * @return */ public BeanTypeInfo getTypeInfo() { if (info == null) { info = createTypeInfo(); } info.initialize(); return info; } /** * Create type info based in the type class. * @return */ public BeanTypeInfo createTypeInfo() { BeanTypeInfo inf = new BeanTypeInfo(getTypeClass(), getSchemaType().getNamespaceURI()); inf.setTypeMapping(getTypeMapping()); return inf; } /** * {@inheritDoc} */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getName()); sb.append(": [class="); Class<?> c = getTypeClass(); sb.append((c == null) ? "<null>" : c.getName()); sb.append(",\nQName="); QName q = getSchemaType(); sb.append((q == null) ? "<null>" : q.toString()); sb.append(",\ninfo="); sb.append(getTypeInfo().toString()); sb.append("]"); return sb.toString(); } private void addXmimeToSchema(XmlSchema root) { XmlSchemaUtils.addImportIfNeeded(root, AbstractXOPType.XML_MIME_NS); } /** * {@inheritDoc} */ @Override public long getMinOccurs() { return getTypeInfo().getMinOccurs(); } @Override public boolean hasMinOccurs() { return true; } @Override public void setTypeMapping(TypeMapping typeMapping) { super.setTypeMapping(typeMapping); if (info != null) { // this seems dangerous .. what if the type info is later created, it won't be passed the mapping. info.setTypeMapping(typeMapping); } } @Override public void writeSchema(XmlSchema root) { BeanTypeInfo inf = getTypeInfo(); XmlSchemaComplexType complex = new XmlSchemaComplexType(root, true); complex.setName(getSchemaType().getLocalPart()); AegisType sooperType = getSuperType(); /* * See Java Virtual Machine specification: * http://java.sun.com/docs/books/vmspec/2nd-edition/html/ClassFile.doc.html#75734 */ if (((inf.getTypeClass().getModifiers() & Modifier.ABSTRACT) != 0) && !inf.getTypeClass().isInterface()) { complex.setAbstract(true); } XmlSchemaSequence sequence = new XmlSchemaSequence(); /* * Decide if we're going to extend another type. If we are going to defer, then make sure that we * extend the type for our superclass. */ boolean isExtension = inf.isExtension(); if (isExtension && sooperType != null) { // if sooperType is null, things are confused. XmlSchemaComplexContent content = new XmlSchemaComplexContent(); complex.setContentModel(content); XmlSchemaComplexContentExtension extension = new XmlSchemaComplexContentExtension(); content.setContent(extension); extension.setBaseTypeName(sooperType.getSchemaType()); extension.setParticle(sequence); } else { complex.setParticle(sequence); } boolean needXmime = false; boolean needUtilityTypes = false; // Write out schema for elements for (QName name : inf.getElements()) { if (isInheritedProperty(inf, name)) { continue; } XmlSchemaElement element = new XmlSchemaElement(root, false); element.setName(name.getLocalPart()); sequence.getItems().add(element); AegisType type = getType(inf, name); if (type.isFlatArray()) { // ok, we need some tricks here element.setMinOccurs(type.getMinOccurs()); element.setMaxOccurs(type.getMaxOccurs()); // for now, assume ArrayType. Look at lists or more general solutions later. ArrayType aType = (ArrayType)type; type = aType.getComponentType(); element.setNillable(type.isNillable()); } else { if (AbstractTypeCreator. HTTP_CXF_APACHE_ORG_ARRAYS.equals(type.getSchemaType().getNamespaceURI())) { XmlSchemaUtils.addImportIfNeeded(root, AbstractTypeCreator.HTTP_CXF_APACHE_ORG_ARRAYS); } } writeTypeReference(name, element, type, root); needXmime |= type.usesXmime(); needUtilityTypes |= type.usesUtilityTypes(); } if (needXmime) { addXmimeToSchema(root); } if (needUtilityTypes) { AegisContext.addUtilityTypesToSchema(root); } /** * if future proof then add <xsd:any/> element */ if (inf.isExtensibleElements()) { XmlSchemaAny any = new XmlSchemaAny(); any.setMinOccurs(0); any.setMaxOccurs(Long.MAX_VALUE); sequence.getItems().add(any); } // Write out schema for attributes for (QName name : inf.getAttributes()) { if (isInheritedProperty(inf, name)) { continue; } XmlSchemaAttribute attribute = new XmlSchemaAttribute(root, false); complex.getAttributes().add(attribute); attribute.setName(name.getLocalPart()); AegisType type = getType(inf, name); attribute.setSchemaTypeName(type.getSchemaType()); String ns = name.getNamespaceURI(); if (!ns.equals(root.getTargetNamespace())) { XmlSchemaUtils.addImportIfNeeded(root, ns); } } /** * If extensible attributes then add <xsd:anyAttribute/> */ if (inf.isExtensibleAttributes()) { complex.setAnyAttribute(new XmlSchemaAnyAttribute()); } } }