Java tutorial
/* * Copyright 2005-2013 the original author or authors. * * Licensed 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.javelin.sws.ext.bind.internal.metadata; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.LinkedList; import java.util.List; import javax.xml.XMLConstants; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlList; import javax.xml.bind.annotation.XmlNsForm; import javax.xml.bind.annotation.XmlSchemaType; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlValue; import javax.xml.namespace.QName; import org.javelin.sws.ext.bind.TypedPatternRegistry; import org.javelin.sws.ext.bind.internal.model.AttributePattern; import org.javelin.sws.ext.bind.internal.model.ComplexTypePattern; import org.javelin.sws.ext.bind.internal.model.ElementPattern; import org.javelin.sws.ext.bind.internal.model.SimpleContentPattern; import org.javelin.sws.ext.bind.internal.model.TypedPattern; import org.javelin.sws.ext.bind.internal.model.XmlEventsPattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.FieldCallback; import org.springframework.util.ReflectionUtils.FieldFilter; import org.springframework.util.ReflectionUtils.MethodCallback; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; /** * <p>This class analyzes bean and field properties of JAXB-bound class and produces a {@link TypedPattern} * representing this class in terms of OXM.</p> * * <p>Every class instance maps to a sequence of patterns for each class' property. The nested patterns * are of three categories:<ul> * <li>XML element</li> * <li>XML attribute</li> * <li>XML value</li> * </ul>The pattern for analyzed class is <b>always</b> a {@link ComplexTypePattern} but in one special case (only singe {@link XmlValue} * annotated property), the {@link #analyze(Class, QName)} method may return {@link SimpleContentPattern}.</p> * * <p>TODO: ensure that built-in types, primitives, exceptions, etc., are <b>not</b> analyzed</p> * * @author Grzegorz Grzybek * @param <T> a type of bean containing analyzed properties */ public class PropertyCallback<T> implements FieldCallback, MethodCallback { private static Logger log = LoggerFactory.getLogger(PropertyCallback.class.getName()); /* Initialization data */ private Class<T> clazz; private QName typeName; private XmlAccessType accessType; private XmlNsForm elementFormDefault; private XmlNsForm attributeFormDefault; private TypedPatternRegistry patternRegistry; /* Post-analysis data */ // list of properties mapped to XML Attributes private final List<PropertyMetadata<T, ?>> childAttributeMetadata = new LinkedList<PropertyMetadata<T, ?>>(); // list of properties mapped to XML Elements (or text of mixed content). non-empty = complex content private final List<PropertyMetadata<T, ?>> childElementMetadata = new LinkedList<PropertyMetadata<T, ?>>(); // at most one property mapped to simple characters content. non-null means there must be no childElementMetadata private PropertyMetadata<T, ?> valueMetadata = null; /** * @param patternRegistry * @param clazz * @param typeName * @param accessType * @param elementFormDefault * @param attributeFormDefault */ public PropertyCallback(TypedPatternRegistry patternRegistry, Class<T> clazz, QName typeName, XmlAccessType accessType, XmlNsForm elementFormDefault, XmlNsForm attributeFormDefault) { this.patternRegistry = patternRegistry; this.clazz = clazz; this.typeName = typeName; this.accessType = accessType; this.elementFormDefault = elementFormDefault; this.attributeFormDefault = attributeFormDefault; } /** * <p>Reads class' metadata and returns a {@link XmlEventsPattern pattern of XML events} which may be used to marshal * an object of the analyzed class.<?p> * * @return */ public TypedPattern<T> analyze() { TypedPattern<T> result = this.patternRegistry.findPatternByClass(this.clazz); if (result != null) return result; log.trace("Analyzing {} class with {} type name", this.clazz.getName(), this.typeName); // analyze fields ReflectionUtils.doWithFields(this.clazz, this, new FieldFilter() { @Override public boolean matches(Field field) { return !Modifier.isStatic(field.getModifiers()); } }); // analyze get/set methods - even private ones! ReflectionUtils.doWithMethods(this.clazz, this, new MethodFilter() { @Override public boolean matches(Method method) { boolean match = true; // is it getter? match &= method.getName().startsWith("get"); match &= method.getParameterTypes().length == 0; match &= method.getReturnType() != Void.class; // is there a setter? if (match) { Method setter = ReflectionUtils.findMethod(clazz, method.getName().replaceFirst("^get", "set"), method.getReturnType()); // TODO: maybe allow non-void returning setters as Spring-Framework already does? Now: yes match = setter != null || Collection.class.isAssignableFrom(method.getReturnType()); } return match; } }); if (this.valueMetadata != null && this.childElementMetadata.size() == 0 && this.childAttributeMetadata.size() == 0) { // we have a special case, where Java class becomes simpleType: // - formatting the analyzed class is really formatting the value // - the type information of the analyzed class is not changed! log.trace("Changing {} class' pattern to SimpleContentPattern", this.clazz.getName()); SimpleContentPattern<T> valuePattern = SimpleContentPattern.newValuePattern(this.typeName, this.clazz); SimpleContentPattern<?> simpleTypePattern = (SimpleContentPattern<?>) this.valueMetadata.getPattern(); valuePattern.setFormatter( PeelingFormatter.newPeelingFormatter(this.valueMetadata, simpleTypePattern.getFormatter())); result = valuePattern; } else { if (this.valueMetadata != null && this.childElementMetadata.size() > 0) { throw new RuntimeException("TODO: can't mix @XmlValue and @XmlElements"); } // we have complex type (possibly with simpleContent, when there's @XmlValue + one or more @XmlAttributes) // @XmlAttributes first. then @XmlElements this.childAttributeMetadata.addAll(this.childElementMetadata); if (this.valueMetadata != null) this.childAttributeMetadata.add(this.valueMetadata); result = ComplexTypePattern.newContentModelPattern(this.typeName, this.clazz, this.childAttributeMetadata); } if (log.isTraceEnabled()) log.trace("-> Class {} was mapped to {} with {} XSD type", this.clazz.getName(), result, this.typeName); return result; } /** * Like {@link MethodCallback#doWith(Method)}, but for two methods. * * @see org.springframework.util.ReflectionUtils.MethodCallback#doWith(java.lang.reflect.Method) */ @Override public void doWith(Method getter) throws IllegalArgumentException, IllegalAccessException { Method setter = ReflectionUtils.findMethod(clazz, getter.getName().replaceFirst("^get", "set"), getter.getReturnType()); if (log.isTraceEnabled()) { if (setter != null) { log.trace(" - Analyzing pair of methods: {}.{}/{}.{}", getter.getDeclaringClass().getSimpleName(), getter.getName(), setter.getDeclaringClass().getSimpleName(), setter.getName()); } else { // we have java.util.Collection log.trace(" - Analyzing method: {}.{}", getter.getDeclaringClass().getSimpleName(), getter.getName()); } } // TODO: properly handle class hierarchies String propertyName = StringUtils.uncapitalize(getter.getName().substring(3)); // metadata for getter/setter PropertyMetadata<T, ?> metadata = PropertyMetadata.newPropertyMetadata(this.clazz, getter.getReturnType(), propertyName, getter, setter, PropertyKind.BEAN); this.doWithPropertySafe(metadata); } /* (non-Javadoc) * @see org.springframework.util.ReflectionUtils.FieldCallback#doWith(java.lang.reflect.Field) */ @Override public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { log.trace("- Analyzing field: {}.{}", field.getDeclaringClass().getSimpleName(), field.getName()); String fieldName = field.getName(); // metadata for a field ReflectionUtils.makeAccessible(field); PropertyMetadata<T, ?> metadata = PropertyMetadata.newPropertyMetadata(this.clazz, field.getType(), fieldName, field, PropertyKind.FIELD); this.doWithPropertySafe(metadata); } /** * @param field * @param class1 */ private <P> void doWithPropertySafe(PropertyMetadata<T, P> metadata) throws IllegalArgumentException, IllegalAccessException { AnnotatedElement[] accessors = metadata.getAccessors(); if (this.findJaxbAnnotation(accessors, XmlTransient.class) != null) return; XmlSchemaType xmlSchemaType = this.findJaxbAnnotation(accessors, XmlSchemaType.class); // a pattern for property's class - force creating TypedPattern<P> pattern = null; if (xmlSchemaType != null) { // the schema type determines the pattern - if it is not present in the registry, it will be present after determining it on the basis of Java class // of the property QName typeName = new QName(xmlSchemaType.namespace(), xmlSchemaType.name()); pattern = this.patternRegistry.findPatternByType(typeName, metadata.getPropertyClass()); if (log.isTraceEnabled() && pattern != null) log.trace("-- @XmlSchemaType points to {}", pattern.toString()); } if (pattern == null) pattern = this.patternRegistry.determineAndCacheXmlPattern(metadata.getPropertyClass()); // is it value or list? XmlValue xmlValue = this.findJaxbAnnotation(accessors, XmlValue.class); XmlList xmlList = this.findJaxbAnnotation(accessors, XmlList.class); if (xmlValue != null || xmlList != null) { // the field's class must be a simpleType, i.e., a type convertible to String, which is either: // - a type registered in org.javelin.sws.ext.bind.internal.BuiltInMappings.initialize() // - a type which has only one property annotated with @XmlValue // a type with one @XmlValue property + non-zero @XmlAttribute properties is complex type with simple content, not a simple type if (!(pattern.isSimpleType() && pattern instanceof SimpleContentPattern)) throw new RuntimeException("TODO: should be simpleType"); if (log.isTraceEnabled()) log.trace("-- @XmlValue property \"{}\" of type {} mapped to {}", metadata.getPropertyName(), pattern.getJavaType().getName(), pattern.toString()); metadata.setPattern(pattern); if (this.valueMetadata != null) throw new RuntimeException("TODO: Only one @XmlValue allowed!"); this.valueMetadata = metadata; return; } // is it an attribute? XmlAttribute xmlAttribute = this.findJaxbAnnotation(accessors, XmlAttribute.class); if (xmlAttribute != null) { String namespace = XMLConstants.NULL_NS_URI; if (this.attributeFormDefault == XmlNsForm.QUALIFIED) { // the attribute MUST have namespace namespace = "##default".equals(xmlAttribute.namespace()) ? this.typeName.getNamespaceURI() : xmlAttribute.namespace(); } else { // the attribute MAY have namespace // TODO: handle org.javelin.sws.ext.bind.annotations.XmlAttribute if (!"##default".equals(xmlAttribute.namespace())) namespace = xmlAttribute.namespace(); } String name = "##default".equals(xmlAttribute.name()) ? metadata.getPropertyName() : xmlAttribute.name(); if (!(pattern.isSimpleType() && pattern instanceof SimpleContentPattern)) throw new RuntimeException("TODO: should be simpleType"); QName attributeQName = new QName(namespace, name); if (log.isTraceEnabled()) log.trace("-- @XmlAttribute property \"{}\" of type {} mapped to {} attribute {}", metadata.getPropertyName(), pattern.getJavaType().getName(), attributeQName, pattern.toString()); metadata.setPattern( AttributePattern.newAttributePattern(attributeQName, (SimpleContentPattern<P>) pattern)); this.childAttributeMetadata.add(metadata); return; } // is it an element? XmlElement xmlElement = this.findJaxbAnnotation(accessors, XmlElement.class); // field is also an element when told so using XmlAccessorType boolean isElement = false; if (accessors[0] instanceof Field) { if (this.accessType == XmlAccessType.FIELD) isElement = true; else if (this.accessType == XmlAccessType.PUBLIC_MEMBER && Modifier.isPublic(((Field) accessors[0]).getModifiers())) isElement = true; } else if (accessors[0] instanceof Method) { if (this.accessType == XmlAccessType.PROPERTY) isElement = true; else if (this.accessType == XmlAccessType.PUBLIC_MEMBER && Modifier.isPublic(((Method) accessors[0]).getModifiers())) { // TODO: what if getter is private and setter is public? isElement = true; } } if (xmlElement != null || isElement) { String namespace = XMLConstants.NULL_NS_URI; if (this.elementFormDefault == XmlNsForm.QUALIFIED) { // the element MUST have namespace namespace = xmlElement == null || "##default".equals(xmlElement.namespace()) ? this.typeName.getNamespaceURI() : xmlElement.namespace(); } else { // the element MAY have namespace if (xmlElement != null && !"##default".equals(xmlElement.namespace())) namespace = xmlElement.namespace(); } String name = xmlElement == null || "##default".equals(xmlElement.name()) ? metadata.getPropertyName() : xmlElement.name(); QName elementQName = new QName(namespace, name); if (log.isTraceEnabled()) log.trace("-- @XmlElement property \"{}\" of type {} mapped to {} element with {}", metadata.getPropertyName(), pattern.getJavaType().getName(), elementQName, pattern.toString()); ElementPattern<?> elementPattern = ElementPattern.newElementPattern(elementQName, pattern); XmlElementWrapper xmlElementWrapper = this.findJaxbAnnotation(accessors, XmlElementWrapper.class); if (xmlElementWrapper != null) { if (!"##default".equals(xmlElementWrapper.namespace())) namespace = xmlElementWrapper.namespace(); name = !"##default".equals(xmlElementWrapper.name()) ? xmlElementWrapper.name() : metadata.getPropertyName(); // XmlElementWrapper creates (in XSD Category) a new complex, anonymous, nested (inside element declaration) type // DESIGNFLAW: XmlElementWrapper works, but not as clean as it should PropertyMetadata<T, ?> md = PropertyMetadata.newPropertyMetadata(this.clazz, metadata.getCollectionClass(), "", PropertyKind.PASSTHROUGH); md.setPattern(elementPattern); ComplexTypePattern<T> newAnonymousType = ComplexTypePattern.newContentModelPattern(null, this.clazz, md); // TODO: Handle @XmlElementWrapper for collection properties // TODO: JAXB2 doesn't allow this, but maybe it's a good idea to be able to wrap non-collection properties also? // TODO: maybe it's a good idea to create @XmlElementWrappers class (aggregating multime @XmlElementWrapper annotations?) elementPattern = ElementPattern.newElementPattern(new QName(namespace, name), newAnonymousType); metadata.setWrappedCollection(true); } metadata.setPattern(elementPattern); this.childElementMetadata.add(metadata); return; } } /** * @param accessors * @return */ private <A extends Annotation> A findJaxbAnnotation(AnnotatedElement[] accessors, Class<A> ann) { A annotation = null; for (AnnotatedElement ae : accessors) { // TODO: throw an exception when JAXB annotation is on both getter and setter if (ae != null && (annotation = AnnotationUtils.getAnnotation(ae, ann)) != null) return annotation; } return null; } }