Java tutorial
/* * Copyright (c) 2010-2017 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.ComplexTypeDefinition; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.TypeDefinition; import com.evolveum.midpoint.prism.schema.PrismSchema; import com.evolveum.midpoint.prism.schema.SchemaDescription; import com.evolveum.midpoint.prism.schema.SchemaRegistry; import com.evolveum.midpoint.prism.xml.XsdTypeMapper; import com.evolveum.midpoint.util.Handler; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.prism.xml.ns._public.types_3.RawType; 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.*; import static java.util.Comparator.naturalOrder; import static java.util.Comparator.nullsLast; /** * @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) @FunctionalInterface 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; } } @FunctionalInterface 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.computeIfAbsent(param1, k -> Collections.synchronizedMap(new HashMap<>())); return find1(cache2, param2, p -> getter.get(param1, p)); } @FunctionalInterface 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.computeIfAbsent(param1, k -> Collections.synchronizedMap(new HashMap<>())); return find2(cache2, param2, param3, (p, q) -> getter.get(param1, p, q)); } //endregion //region Individual inspection methods - cached versions private Map<Class<?>, String> _determineNamespace = Collections.synchronizedMap(new HashMap<>()); String determineNamespace(Class<?> paramType) { return find1(_determineNamespace, paramType, this::determineNamespaceUncached); } private Map<Class<?>, QName> _determineTypeForClass = Collections.synchronizedMap(new HashMap<>()); QName determineTypeForClass(Class<?> paramType) { return find1(_determineTypeForClass, paramType, PrismBeanInspector::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) { //noinspection unchecked return find2(_findSetter, beanClass, fieldName, (c, f) -> findSetterUncached(c, f)); } private Map<Package, Class> _getObjectFactoryClassPackage = Collections.synchronizedMap(new HashMap<>()); Class getObjectFactoryClass(Package aPackage) { return find1(_getObjectFactoryClassPackage, aPackage, p -> getObjectFactoryClassUncached(p)); } private Map<String, Class> _getObjectFactoryClassNamespace = Collections.synchronizedMap(new HashMap<>()); Class getObjectFactoryClass(String namespaceUri) { return find1(_getObjectFactoryClassNamespace, namespaceUri, s -> getObjectFactoryClassUncached(s)); } private Map<Class<?>, List<String>> _getPropOrder = Collections.synchronizedMap(new HashMap<>()); List<String> getPropOrder(Class<?> 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, (c, v) -> findEnumFieldValueUncached(c, v)); } private Map<Field, Map<Class<?>, 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<?>, Map<String, QName>>> _findFieldElementQName = Collections .synchronizedMap(new HashMap<>()); QName findFieldElementQName(String fieldName, Class<?> beanClass, String defaultNamespace) { return find3(_findFieldElementQName, fieldName, beanClass, defaultNamespace, (fieldName1, beanClass1, defaultNamespace1) -> findFieldElementQNameUncached(fieldName1, beanClass1, defaultNamespace1)); } 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().equals(propName)) { return field; } XmlAttribute xmlAttribute = field.getAnnotation(XmlAttribute.class); if (xmlAttribute != 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; } else { return field != null && field.isAnnotationPresent(XmlAttribute.class) || getter != null && getter.isAnnotationPresent(XmlAttribute.class); } } private String determineNamespaceUncached(Class<?> 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; } public static QName determineTypeForClassUncached(Class<?> 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<?> 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 (RawType.class.equals(contentClass)) { // RawType is a meta-type. We do not really want to use field types of RawType class. return null; } 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 (!BeanMarshaller.DEFAULT_PLACEHOLDER.equals(name)) { realLocalName = name; } String namespace = xmlElement.namespace(); if (!BeanMarshaller.DEFAULT_PLACEHOLDER.equals(namespace)) { realNamespace = namespace; } } return new QName(realNamespace, realLocalName); } //endregion //region Other 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); } } @NotNull public <T> Class<? extends T> findMatchingSubclass(Class<T> beanClass, Collection<QName> fields) throws SchemaException { SchemaRegistry schemaRegistry = prismContext.getSchemaRegistry(); TypeDefinition typeDef = schemaRegistry.findTypeDefinitionByCompileTimeClass(beanClass, TypeDefinition.class); if (typeDef == null) { throw new SchemaException("No type definition for " + beanClass); } List<TypeDefinition> subTypes = new ArrayList<>(typeDef.getStaticSubTypes()); subTypes.sort(Comparator.comparing(TypeDefinition::getInstantiationOrder, nullsLast(naturalOrder()))); TypeDefinition matchingDefinition = null; for (TypeDefinition subType : subTypes) { if (matchingDefinition != null && !Objects.equals(matchingDefinition.getInstantiationOrder(), subType.getInstantiationOrder())) { break; // found something and went to lower orders -> we can stop searching } if (matches(subType, fields)) { if (matchingDefinition != null) { throw new SchemaException("Couldn't unambiguously determine a subclass for " + beanClass + " instantiation (fields: " + fields + "). Candidates: " + matchingDefinition + ", " + subType); } matchingDefinition = subType; } } if (matchingDefinition == null) { final int MAX = 5; throw new SchemaException("Couldn't find a subclass of " + beanClass + " that would contain fields " + fields + ". Considered " + subTypes.subList(0, Math.min(subTypes.size(), MAX)) + (subTypes.size() >= MAX ? " (...)" : "")); } //noinspection unchecked Class<? extends T> compileTimeClass = (Class<? extends T>) matchingDefinition.getCompileTimeClass(); if (compileTimeClass != null) { return compileTimeClass; } else { throw new SchemaException("No compile time class defined for " + matchingDefinition); } } private boolean matches(TypeDefinition type, Collection<QName> fields) { if (!(type instanceof ComplexTypeDefinition)) { return false; } ComplexTypeDefinition ctd = (ComplexTypeDefinition) type; return fields.stream().allMatch(field -> ctd.containsItemDefinition(field)); } //endregion }