/* * Copyright (c) 2010-2016 Evolveum * * 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 com.evolveum.midpoint.prism.marshaller; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.schema.PrismSchema; import com.evolveum.midpoint.prism.schema.SchemaDescription; import com.evolveum.midpoint.prism.xml.XsdTypeMapper; import com.evolveum.midpoint.util.Handler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.jetbrains.annotations.NotNull; import org.w3c.dom.Node; import javax.xml.bind.annotation.XmlAnyElement; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementDecl; import javax.xml.bind.annotation.XmlElementRef; import javax.xml.bind.annotation.XmlEnumValue; import javax.xml.bind.annotation.XmlSchema; import javax.xml.bind.annotation.XmlSchemaType; import javax.xml.bind.annotation.XmlType; import javax.xml.namespace.QName; import java.lang.reflect.*; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author mederly */ public class PrismBeanInspector { @NotNull private PrismContext prismContext; public PrismBeanInspector(@NotNull PrismContext prismContext) { Validate.notNull(prismContext, "prismContext"); this.prismContext = prismContext; } //region Caching mechanism (multiple dimensions) interface Getter1<V, P1> { V get(P1 param1); } private <V, P1> V find1(Map<P1,V> cache, P1 param1, Getter1<V, P1> getter) { if (cache.containsKey(param1)) { return cache.get(param1); } else { V value = getter.get(param1); cache.put(param1, value); return value; } } interface Getter2<V, P1, P2> { V get(P1 param1, P2 param2); } private <V, P1, P2> V find2(final Map<P1,Map<P2,V>> cache, final P1 param1, final P2 param2, final Getter2<V, P1, P2> getter) { Map<P2, V> cache2 = cache.get(param1); if (cache2 == null) { cache2 = Collections.synchronizedMap(new HashMap()); cache.put(param1, cache2); } return find1(cache2, param2, new Getter1<V, P2>() { @Override public V get(P2 p) { return getter.get(param1, p); } }); } interface Getter3<V, P1, P2, P3> { V get(P1 param1, P2 param2, P3 param3); } private <V, P1, P2, P3> V find3(final Map<P1,Map<P2,Map<P3,V>>> cache, final P1 param1, final P2 param2, final P3 param3, final Getter3<V, P1, P2, P3> getter) { Map<P2, Map<P3, V>> cache2 = cache.get(param1); if (cache2 == null) { cache2 = Collections.synchronizedMap(new HashMap()); cache.put(param1, cache2); } return find2(cache2, param2, param3, new Getter2<V, P2, P3>() { @Override public V get(P2 p, P3 q) { return getter.get(param1, p, q); } }); } //endregion //region Individual inspection methods - cached versions private Map<Class<? extends Object>, String> _determineNamespace = Collections.synchronizedMap(new HashMap()); String determineNamespace(Class<? extends Object> paramType) { return find1(_determineNamespace, paramType, this::determineNamespaceUncached); } private Map<Class<?>, QName> _determineTypeForClass = Collections.synchronizedMap(new HashMap()); QName determineTypeForClass(Class<?> paramType) { return find1(_determineTypeForClass, paramType, this::determineTypeForClassUncached); } private Map<Field,Map<Method,Boolean>> _isAttribute = Collections.synchronizedMap(new HashMap()); boolean isAttribute(Field field, Method getter) { return find2(_isAttribute, field, getter, this::isAttributeUncached); } private Map<Class,Map<String,Method>> _findSetter = Collections.synchronizedMap(new HashMap()); <T> Method findSetter(Class<T> beanClass, String fieldName) { return find2(_findSetter, beanClass, fieldName, new Getter2<Method,Class,String>() { @Override public Method get(Class c, String f) { return findSetterUncached(c, f); } }); } private Map<Package,Class> _getObjectFactoryClassPackage = Collections.synchronizedMap(new HashMap()); Class getObjectFactoryClass(Package aPackage) { return find1(_getObjectFactoryClassPackage, aPackage, new Getter1<Class,Package>() { @Override public Class get(Package p) { return getObjectFactoryClassUncached(p); } }); } private Map<String,Class> _getObjectFactoryClassNamespace = Collections.synchronizedMap(new HashMap()); Class getObjectFactoryClass(String namespaceUri) { return find1(_getObjectFactoryClassNamespace, namespaceUri, new Getter1<Class,String>() { @Override public Class get(String s) { return getObjectFactoryClassUncached(s); } }); } private Map<Class<? extends Object>, List<String>> _getPropOrder = Collections.synchronizedMap(new HashMap()); List<String> getPropOrder(Class<? extends Object> beanClass) { return find1(_getPropOrder, beanClass, this::getPropOrderUncached); } private Map<Class,Map<String,Method>> _findElementMethodInObjectFactory = Collections.synchronizedMap(new HashMap()); Method findElementMethodInObjectFactory(Class objectFactoryClass, String propName) { return find2(_findElementMethodInObjectFactory, objectFactoryClass, propName, (c, p) -> findElementMethodInObjectFactoryUncached(c, p)); } private Map<Class,Map<Method,Field>> _lookupSubstitution = Collections.synchronizedMap(new HashMap()); <T> Field lookupSubstitution(Class<T> beanClass, Method elementMethod) { return find2(_lookupSubstitution, beanClass, elementMethod, this::lookupSubstitutionUncached); } private Map<Class,Map<String,String>> _findEnumFieldName = Collections.synchronizedMap(new HashMap()); <T> String findEnumFieldName(Class<T> classType, String primValue) { return find2(_findEnumFieldName, classType, primValue, (c, v) -> findEnumFieldNameUncached(c, v)); } private Map<Class,Map<String,String>> _findEnumFieldValue = Collections.synchronizedMap(new HashMap()); <T> String findEnumFieldValue(Class<T> classType, String toStringValue) { return find2(_findEnumFieldValue, classType, toStringValue, new Getter2<String,Class,String>() { @Override public String get(Class c, String v) { return findEnumFieldValueUncached(c, v); } }); } private Map<Field,Map<Class<? extends Object>,Map<String,QName>>> _findTypeName = Collections.synchronizedMap(new HashMap()); // Determines type for field/content combination. Field information is used only for simple XSD types. QName findTypeName(Field field, Class<?> contentClass, String defaultNamespacePlaceholder) { return find3(_findTypeName, field, contentClass, defaultNamespacePlaceholder, this::findTypeNameUncached); } private Map<String,Map<Class<? extends Object>,Map<String,QName>>> _findFieldElementQName = Collections.synchronizedMap(new HashMap()); QName findFieldElementQName(String fieldName, Class<? extends Object> beanClass, String defaultNamespace) { return find3(_findFieldElementQName, fieldName, beanClass, defaultNamespace, new Getter3<QName, String, Class<? extends Object>, String>() { @Override public QName get(String fieldName, Class<? extends Object> beanClass, String defaultNamespace) { return findFieldElementQNameUncached(fieldName, beanClass, defaultNamespace); } }); } private Map<Class,Map<String,Method>> _findPropertyGetter = Collections.synchronizedMap(new HashMap()); public <T> Method findPropertyGetter(Class<T> beanClass, String propName) { return find2(_findPropertyGetter, beanClass, propName, this::findPropertyGetterUncached); } private Map<Class,Map<String,Field>> _findPropertyField = Collections.synchronizedMap(new HashMap()); public <T> Field findPropertyField(Class<T> beanClass, String propName) { return find2(_findPropertyField, beanClass, propName, this::findPropertyFieldUncached); } //endregion //region Uncached versions of the inspection methods private <T> Field findPropertyFieldUncached(Class<T> classType, String propName) { Field field = findPropertyFieldExactUncached(classType, propName); if (field != null) { return field; } // Fields for some reserved words are prefixed by underscore, so try also this. return findPropertyFieldExactUncached(classType, "_"+propName); } private <T> Field findPropertyFieldExactUncached(Class<T> classType, String propName) { for (Field field: classType.getDeclaredFields()) { XmlElement xmlElement = field.getAnnotation(XmlElement.class); if (xmlElement != null && xmlElement.name() != null && xmlElement.name().equals(propName)) { return field; } XmlAttribute xmlAttribute = field.getAnnotation(XmlAttribute.class); if (xmlAttribute != null && xmlAttribute.name() != null && xmlAttribute.name().equals(propName)) { return field; } } try { return classType.getDeclaredField(propName); } catch (NoSuchFieldException e) { // nothing found } Class<? super T> superclass = classType.getSuperclass(); if (superclass == null || Object.class.equals(superclass)) { return null; } return findPropertyField(superclass, propName); } private <T> Method findPropertyGetterUncached(Class<T> classType, String propName) { if (propName.startsWith("_")) { propName = propName.substring(1); } for (Method method: classType.getDeclaredMethods()) { XmlElement xmlElement = method.getAnnotation(XmlElement.class); if (xmlElement != null && xmlElement.name().equals(propName)) { return method; } XmlAttribute xmlAttribute = method.getAnnotation(XmlAttribute.class); if (xmlAttribute != null && xmlAttribute.name().equals(propName)) { return method; } } String getterName = "get"+ StringUtils.capitalize(propName); try { return classType.getDeclaredMethod(getterName); } catch (NoSuchMethodException e) { // nothing found } getterName = "is"+StringUtils.capitalize(propName); try { return classType.getDeclaredMethod(getterName); } catch (NoSuchMethodException e) { // nothing found } Class<? super T> superclass = classType.getSuperclass(); if (superclass == null || superclass.equals(Object.class)) { return null; } return findPropertyGetter(superclass, propName); } private boolean isAttributeUncached(Field field, Method getter){ if (field == null && getter == null){ return false; } if (field != null && field.isAnnotationPresent(XmlAttribute.class)){ return true; } if (getter != null && getter.isAnnotationPresent(XmlAttribute.class)){ return true; } return false; } private String determineNamespaceUncached(Class<? extends Object> beanClass) { XmlType xmlType = beanClass.getAnnotation(XmlType.class); if (xmlType == null) { return null; } String namespace = xmlType.namespace(); if (BeanMarshaller.DEFAULT_PLACEHOLDER.equals(namespace)) { XmlSchema xmlSchema = beanClass.getPackage().getAnnotation(XmlSchema.class); namespace = xmlSchema.namespace(); } if (StringUtils.isBlank(namespace) || BeanMarshaller.DEFAULT_PLACEHOLDER.equals(namespace)) { return null; } return namespace; } private QName determineTypeForClassUncached(Class<? extends Object> beanClass) { XmlType xmlType = beanClass.getAnnotation(XmlType.class); if (xmlType == null) { return null; } String namespace = xmlType.namespace(); if (BeanMarshaller.DEFAULT_PLACEHOLDER.equals(namespace)) { XmlSchema xmlSchema = beanClass.getPackage().getAnnotation(XmlSchema.class); namespace = xmlSchema.namespace(); } if (StringUtils.isBlank(namespace) || BeanMarshaller.DEFAULT_PLACEHOLDER.equals(namespace)) { return null; } return new QName(namespace, xmlType.name()); } private <T> Method findSetterUncached(Class<T> classType, String fieldName) { String setterName = getSetterName(fieldName); for(Method method: classType.getMethods()) { if (!method.getName().equals(setterName)) { continue; } Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length != 1) { continue; } Class<?> setterType = parameterTypes[0]; if (setterType.equals(Object.class) || Node.class.isAssignableFrom(setterType)) { // Leave for second pass, let's try find a better setter continue; } return method; } // Second pass for(Method method: classType.getMethods()) { if (!method.getName().equals(setterName)) { continue; } Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length != 1) { continue; } return method; } return null; } private String getSetterName(String fieldName) { if (fieldName.startsWith("_")) { fieldName = fieldName.substring(1); } return "set"+StringUtils.capitalize(fieldName); } private Class getObjectFactoryClassUncached(Package pkg) { try { return Class.forName(pkg.getName()+".ObjectFactory"); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Cannot find object factory class in package "+pkg.getName()+": "+e.getMessage(), e); } } private Class getObjectFactoryClassUncached(String namespaceUri) { SchemaDescription schemaDescription = prismContext.getSchemaRegistry().findSchemaDescriptionByNamespace(namespaceUri); if (schemaDescription == null) { throw new IllegalArgumentException("Cannot find object factory class for namespace "+namespaceUri+": unknown schema namespace"); } Package compileTimeClassesPackage = schemaDescription.getCompileTimeClassesPackage(); if (compileTimeClassesPackage == null) { throw new IllegalArgumentException("Cannot find object factory class for namespace "+namespaceUri+": not a compile-time schema"); } return getObjectFactoryClassUncached(compileTimeClassesPackage); } private Method findElementMethodInObjectFactoryUncached(Class objectFactoryClass, String propName) { for (Method method: objectFactoryClass.getDeclaredMethods()) { XmlElementDecl xmlElementDecl = method.getAnnotation(XmlElementDecl.class); if (xmlElementDecl == null) { continue; } if (propName.equals(xmlElementDecl.name())) { return method; } } return null; } private Field lookupSubstitutionUncached(Class beanClass, Method elementMethodInObjectFactory) { XmlElementDecl xmlElementDecl = elementMethodInObjectFactory.getAnnotation(XmlElementDecl.class); if (xmlElementDecl == null) { return null; } final String substitutionHeadName = xmlElementDecl.substitutionHeadName(); return findField(beanClass, field -> { XmlElementRef xmlElementRef = field.getAnnotation(XmlElementRef.class); return xmlElementRef != null && xmlElementRef.name().equals(substitutionHeadName); }); } private Field findField(Class classType, Handler<Field> selector) { for (Field field: classType.getDeclaredFields()) { if (selector.handle(field)) { return field; } } Class superclass = classType.getSuperclass(); if (superclass == null || superclass.equals(Object.class)) { return null; } return findField(superclass, selector); } private Method findMethod(Class classType, Handler<Method> selector) { for (Method field: classType.getDeclaredMethods()) { if (selector.handle(field)) { return field; } } Class superclass = classType.getSuperclass(); if (superclass == null || superclass.equals(Object.class)) { return null; } return findMethod(superclass, selector); } private List<String> getPropOrderUncached(Class<? extends Object> beanClass) { List<String> propOrder; // Superclass first! Class superclass = beanClass.getSuperclass(); if (superclass == null || superclass.equals(Object.class) || superclass.getAnnotation(XmlType.class) == null) { propOrder = new ArrayList<>(); } else { propOrder = new ArrayList<>(getPropOrder(superclass)); } XmlType xmlType = beanClass.getAnnotation(XmlType.class); if (xmlType == null) { throw new IllegalArgumentException("Cannot marshall "+beanClass+" it does not have @XmlType annotation"); } String[] myPropOrder = xmlType.propOrder(); for (String myProp: myPropOrder) { if (StringUtils.isNotBlank(myProp)) { // some properties starts with underscore..we don't want to serialize them with underscore, so remove it.. if (myProp.startsWith("_")){ myProp = myProp.replace("_", ""); } propOrder.add(myProp); } } Field[] fields = beanClass.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(XmlAttribute.class)) { propOrder.add(field.getName()); } } Method[] methods = beanClass.getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(XmlAttribute.class)) { propOrder.add(getPropertyNameFromGetter(method.getName())); } } return propOrder; } private <T> String findEnumFieldNameUncached(Class classType, T primValue){ for (Field field: classType.getDeclaredFields()) { XmlEnumValue xmlEnumValue = field.getAnnotation(XmlEnumValue.class); if (xmlEnumValue != null && xmlEnumValue.value().equals(primValue)) { return field.getName(); } } return null; } public static String findEnumFieldValueUncached(Class classType, String toStringValue){ for (Field field: classType.getDeclaredFields()) { XmlEnumValue xmlEnumValue = field.getAnnotation(XmlEnumValue.class); if (xmlEnumValue != null && field.getName().equals(toStringValue)) { return xmlEnumValue.value(); } } return null; } private String getPropertyNameFromGetter(String getterName) { if ((getterName.length() > 3) && getterName.startsWith("get") && Character.isUpperCase(getterName.charAt(3))) { String propPart = getterName.substring(3); return StringUtils.uncapitalize(propPart); } return getterName; } private QName findTypeNameUncached(Field field, Class contentClass, String schemaNamespace) { if (field != null) { XmlSchemaType xmlSchemaType = field.getAnnotation(XmlSchemaType.class); if (xmlSchemaType != null) { return new QName(xmlSchemaType.namespace(), xmlSchemaType.name()); } } QName typeName = XsdTypeMapper.getJavaToXsdMapping(contentClass); if (typeName != null) { return typeName; } // TODO the following code is similar to determineTypeForClass XmlType xmlType = (XmlType) contentClass.getAnnotation(XmlType.class); if (xmlType != null) { String propTypeLocalPart = xmlType.name(); String propTypeNamespace = xmlType.namespace(); if (propTypeNamespace.equals(BeanMarshaller.DEFAULT_PLACEHOLDER)) { PrismSchema schema = prismContext.getSchemaRegistry().findSchemaByCompileTimeClass(contentClass); if (schema != null && schema.getNamespace() != null) { propTypeNamespace = schema.getNamespace(); // should be non-null for properly initialized schemas } else { // schemaNamespace is only a poor indicator of required namespace (consider e.g. having c:UserType in apit:ObjectListType) // so we use it only if we couldn't find anything else propTypeNamespace = schemaNamespace; } } return new QName(propTypeNamespace, propTypeLocalPart); } return null; } private QName findFieldElementQNameUncached(String fieldName, Class beanClass, String defaultNamespace) { Field field; try { field = beanClass.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { return new QName(defaultNamespace, fieldName); // TODO implement this if needed (lookup the getter method instead of the field) } String realLocalName = fieldName; String realNamespace = defaultNamespace; XmlElement xmlElement = field.getAnnotation(XmlElement.class); if (xmlElement != null) { String name = xmlElement.name(); if (name != null && !BeanMarshaller.DEFAULT_PLACEHOLDER.equals(name)) { realLocalName = name; } String namespace = xmlElement.namespace(); if (namespace != null && !BeanMarshaller.DEFAULT_PLACEHOLDER.equals(namespace)) { realNamespace = namespace; } } return new QName(realNamespace, realLocalName); } //endregion public <T> Field findAnyField(Class<T> beanClass) { return findField(beanClass, field -> field.getAnnotation(XmlAnyElement.class) != null); } public <T> Method findAnyMethod(Class<T> beanClass) { return findMethod(beanClass, method -> method.getAnnotation(XmlAnyElement.class) != null); } // e.g. Collection<UserType> -> UserType @NotNull Type getTypeArgument(Type origType, String desc) { if (!(origType instanceof ParameterizedType)) { throw new IllegalArgumentException("Not a parametrized type "+desc); } ParameterizedType parametrizedType = (ParameterizedType)origType; Type[] actualTypeArguments = parametrizedType.getActualTypeArguments(); if (actualTypeArguments == null || actualTypeArguments.length == 0) { throw new IllegalArgumentException("No type arguments for getter "+desc); } if (actualTypeArguments.length > 1) { throw new IllegalArgumentException("Too many type arguments for getter for "+desc); } return actualTypeArguments[0]; } @NotNull public Class getUpperBound(Type type, String desc) { if (type instanceof Class) { return (Class) type; } else if (type instanceof WildcardType) { WildcardType wildcard = ((WildcardType) type); if (wildcard.getUpperBounds().length != 1) { throw new IllegalArgumentException("Wrong number of upper bounds for " + type + " (" + wildcard.getUpperBounds().length + "): " + desc); } Type upper = wildcard.getUpperBounds()[0]; if (upper instanceof Class) { return (Class) upper; } else { throw new IllegalArgumentException("Upper bound for " + type + " is not a class, it is " + type + ": " + desc); } } else { throw new IllegalArgumentException(type + "is not a class nor wildcard type: " + type + ": " + desc); } } }