/* * 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.*; import com.evolveum.midpoint.prism.lex.dom.DomLexicalProcessor; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.prism.schema.SchemaRegistry; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.prism.xml.XsdTypeMapper; import com.evolveum.midpoint.prism.xnode.*; import com.evolveum.midpoint.util.DOMUtil; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.logging.LoggingUtils; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.prism.xml.ns._public.query_3.SearchFilterType; import com.evolveum.prism.xml.ns._public.types_3.*; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.w3c.dom.Element; import javax.xml.bind.JAXBElement; import javax.xml.namespace.QName; import java.io.UnsupportedEncodingException; import java.lang.reflect.*; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; /** * Analogous to PrismUnmarshaller, this class unmarshals atomic values from XNode tree structures. * Atomic values are values that can be used as property values (i.e. either simple types, or * beans that are not containerables). */ public class BeanUnmarshaller { private static final Trace LOGGER = TraceManager.getTrace(BeanUnmarshaller.class); @NotNull private final PrismBeanInspector inspector; @NotNull private final PrismContext prismContext; @NotNull private final Map<Class,PrimitiveUnmarshaller> specialPrimitiveUnmarshallers = new HashMap<>(); @NotNull private final Map<Class,MapUnmarshaller> specialMapUnmarshallers = new HashMap<>(); public BeanUnmarshaller(@NotNull PrismContext prismContext, @NotNull PrismBeanInspector inspector) { this.prismContext = prismContext; this.inspector = inspector; createSpecialUnmarshallerMaps(); } @FunctionalInterface private interface PrimitiveUnmarshaller<T> { T unmarshal(PrimitiveXNode node, Class<T> beanClass, ParsingContext pc) throws SchemaException; } @FunctionalInterface private interface MapUnmarshaller<T> { T unmarshal(MapXNode node, Class<T> beanClass, ParsingContext pc) throws SchemaException; } private void add(Class<?> beanClass, PrimitiveUnmarshaller primitive, MapUnmarshaller map) { specialPrimitiveUnmarshallers.put(beanClass, primitive); specialMapUnmarshallers.put(beanClass, map); } private void createSpecialUnmarshallerMaps() { add(XmlAsStringType.class, this::unmarshalXmlAsStringFromPrimitive, this::unmarshalXmlAsStringFromMap); add(RawType.class, this::unmarshalRawType, this::unmarshalRawType); add(PolyString.class, this::unmarshalPolyStringFromPrimitive, this::unmarshalPolyStringFromMap); add(PolyStringType.class, this::unmarshalPolyStringFromPrimitive, this::unmarshalPolyStringFromMap); add(ItemPathType.class, this::unmarshalItemPath, this::notSupported); add(ProtectedStringType.class, this::unmarshalProtectedString, this::unmarshalProtectedString); add(ProtectedByteArrayType.class, this::unmarshalProtectedByteArray, this::unmarshalProtectedByteArray); add(SchemaDefinitionType.class, this::notSupported, this::unmarshalSchemaDefinitionType); } //region Main entry ========================================================================== /* * Preconditions: * 1. typeName is processable by unmarshaller - i.e. it corresponds to simple or complex type NOT of containerable character */ <T> T unmarshal(@NotNull XNode xnode, @NotNull QName typeQName, @NotNull ParsingContext pc) throws SchemaException { Class<T> classType = getSchemaRegistry().determineClassForType(typeQName); // TODO use correct method! if (classType == null) { TypeDefinition td = getSchemaRegistry().findTypeDefinitionByType(typeQName); if (td instanceof SimpleTypeDefinition) { // most probably dynamically defined enum (TODO clarify) classType = (Class<T>) String.class; } else { throw new IllegalArgumentException("Couldn't unmarshal " + typeQName + ". Type definition = " + td); } } return unmarshal(xnode, classType, pc); } /** * TODO: decide if this method should be marked @NotNull. * Basically the problem is with primitives. When parsed, they sometimes return null. The question is if it's correct. */ <T> T unmarshal(@NotNull XNode xnode, @NotNull Class<T> beanClass, @NotNull ParsingContext pc) throws SchemaException { T value = unmarshalInternal(xnode, beanClass, pc); if (PrismContextImpl.isExtraValidation() && value != null) { Class<?> requested = ClassUtils.primitiveToWrapper(beanClass); Class<?> actual = ClassUtils.primitiveToWrapper(value.getClass()); if (!requested.isAssignableFrom(actual)) { throw new AssertionError("Postcondition fail: unmarshal returned a value of " + value + " (" + actual + ") which is not of requested type (" + requested + ")"); } } return value; } private <T> T unmarshalInternal(@NotNull XNode xnode, @NotNull Class<T> beanClass, @NotNull ParsingContext pc) throws SchemaException { if (xnode instanceof RootXNode) { XNode subnode = ((RootXNode) xnode).getSubnode(); if (subnode == null) { throw new IllegalStateException("Couldn't parse " + beanClass + " from a root node with a null content: " + xnode.debugDump()); } else { return unmarshal(subnode, beanClass, pc); } } else if (!(xnode instanceof MapXNode) && !(xnode instanceof PrimitiveXNode) && !xnode.isHeterogeneousList()) { throw new IllegalStateException("Couldn't parse " + beanClass + " from non-map/non-primitive/non-hetero-list node: " + xnode.debugDump()); } // only maps and primitives and heterogeneous lists after this point if (xnode instanceof PrimitiveXNode) { PrimitiveXNode<T> prim = (PrimitiveXNode) xnode; if (XmlTypeConverter.canConvert(beanClass)) { QName xsdType = XsdTypeMapper.toXsdType(beanClass); Object parsedValue = prim.getParsedValue(xsdType, beanClass); return postConvertUnmarshal(parsedValue, pc); } else if (beanClass.isEnum()) { return unmarshalEnumFromPrimitive(prim, beanClass, pc); } @SuppressWarnings("unchecked") PrimitiveUnmarshaller<T> unmarshaller = specialPrimitiveUnmarshallers.get(beanClass); if (unmarshaller != null) { return unmarshaller.unmarshal(prim, beanClass, pc); } else if (prim.isEmpty()) { return instantiate(beanClass); // Special case. Just return empty object } else { throw new SchemaException("Cannot convert primitive value to bean of type " + beanClass); } } else { @SuppressWarnings("unchecked") MapUnmarshaller<T> unmarshaller = specialMapUnmarshallers.get(beanClass); if (xnode instanceof MapXNode && unmarshaller != null) { // TODO: what about special unmarshaller + hetero list? return unmarshaller.unmarshal((MapXNode) xnode, beanClass, pc); } return unmarshalFromMapOrHeteroList(xnode, beanClass, pc); } } boolean canProcess(QName typeName) { return getBeanMarshaller().canProcess(typeName); } boolean canProcess(Class<?> clazz) { return getBeanMarshaller().canProcess(clazz); } @NotNull private BeanMarshaller getBeanMarshaller() { return ((PrismContextImpl) getPrismContext()).getBeanMarshaller(); } //endregion @NotNull private <T> T unmarshalFromMapOrHeteroList(@NotNull XNode mapOrList, @NotNull Class<T> beanClass, @NotNull ParsingContext pc) throws SchemaException { if (Containerable.class.isAssignableFrom(beanClass)) { // This could have come from inside; note we MUST NOT parse this as PrismValue, because for objects we would lose oid/version return prismContext.parserFor(mapOrList.toRootXNode()).type(beanClass).parseRealValue(); } else if (SearchFilterType.class.isAssignableFrom(beanClass)) { if (mapOrList instanceof MapXNode) { T bean = (T) unmarshalSearchFilterType((MapXNode) mapOrList, (Class<? extends SearchFilterType>) beanClass, pc); // TODO fix this BRUTAL HACK - it is here because of c:ConditionalSearchFilterType return unmarshalFromMapOrHeteroListToBean(bean, mapOrList, Collections.singleton("condition"), pc); } else { throw new SchemaException("SearchFilterType is not supported in combination of heterogeneous list."); } } else { T bean = instantiate(beanClass); return unmarshalFromMapOrHeteroListToBean(bean, mapOrList, null, pc); } } private <T> T instantiate(@NotNull Class<T> beanClass) { T bean; try { bean = beanClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new SystemException("Cannot instantiate bean of type " + beanClass + ": " + e.getMessage(), e); } return bean; } @NotNull private <T> T unmarshalFromMapOrHeteroListToBean(@NotNull T bean, @NotNull XNode mapOrList, @Nullable Collection<String> keysToParse, @NotNull ParsingContext pc) throws SchemaException { @SuppressWarnings("unchecked") Class<T> beanClass = (Class<T>) bean.getClass(); if (mapOrList instanceof MapXNode) { MapXNode map = (MapXNode) mapOrList; for (Entry<QName, XNode> entry : map.entrySet()) { QName key = entry.getKey(); if (keysToParse != null && !keysToParse.contains(key.getLocalPart())) { continue; } if (entry.getValue() == null) { continue; } unmarshalEntry(bean, beanClass, entry.getKey(), entry.getValue(), mapOrList, false, pc); } } else if (mapOrList.isHeterogeneousList()) { QName keyQName = getBeanMarshaller().getHeterogeneousListPropertyName(beanClass); unmarshalEntry(bean, beanClass, keyQName, mapOrList, mapOrList, true, pc); } else { throw new IllegalStateException("Not a map nor heterogeneous list: " + mapOrList.debugDump()); } return bean; } /** * Parses either a map entry, or a fictitious heterogeneous list property. * * It makes sure that a 'key' property is inserted into 'bean' object, being sourced from 'node' structure. * Node itself can be single-valued or multi-valued, corresponding to single or multi-valued 'key' property. * --- * A notable (and quite ugly) exception is processing of fictitious heterogeneous lists. * In this case we have a ListXNode that should be interpreted as a MapXNode, inserting fictitious property * named after abstract multivalued property in the parent bean. * * For example, when we have (embedded in ExecuteScriptType): * { * pipeline: *[ * { element: search, ... }, * { element: sequence, ... } * ] * } * * ...it should be, in fact, read as if it would be: * * { * pipeline: { * scriptingExpression: [ * { type: SearchExpressionType, ... }, * { type: ExpressionSequenceType, ... } * ] * } * } * * (The only difference is in element names, which are missing in the latter snippet, but let's ignore that here.) * * Fictitious heterogeneous list entry here is "scriptingExpression", a property of pipeline (ExpressionPipelineType). * * We have to create the following data structure (corresponding to latter snippet): * * instance of ExecuteScriptType: * scriptingExpression = instance of JAXBElement(pipeline, ExpressionPipelineType): [1] * scriptingExpression = List of [2] * - JAXBElement(search, SearchExpressionType) * - JAXBElement(sequence, ExpressionSequenceType) * * We in fact invoke this method twice with the same node (a two-entry list, marked as '*' in the first snippet): * 1) bean=ExecuteScriptType, key=pipeline, node=HList(*), isHeteroListProperty=false * 2) bean=ExpressionPipelineType, key=scriptingExpression, node=HList(*), isHeteroListProperty=true <<< * * During the first call we fill in scriptingExpression (single value) in ExecuteScriptType [1]; during the second one * we fill in scriptingExpression (multivalued) in ExpressionPipelineType [2]. * * Now let's expand the sample. * * This XNode tree: * { * pipeline: *[ * { element: search, type: RoleType, searchFilter: {...}, action: log }, * { element: sequence, value: **[ * { element: action, type: delete }, * { element: action, type: assign, parameter: {...} }, * { element: search, type: UserType } * ] } * ] * } * * Should be interpreted as: * { * pipeline: { * scriptingExpression: [ * { type: SearchExpressionType, type: RoleType, searchFilter: {...}, action: log } * { type: ExpressionSequenceType, scriptingExpression: [ * { type: ActionExpressionType, type: delete }, * { type: ActionExpressionType, type: assign, parameter: {...} }, * { type: SearchExpressionType, type: UserType } * ] } * ] * } * } * * Producing the following data: * * instance of ExecuteScriptType: * scriptingExpression = instance of JAXBElement(pipeline, ExpressionPipelineType): [1] * scriptingExpression = List of [2] * - JAXBElement(search, instance of SearchExpressionType): * type: RoleType, * searchFilter: (...), * action: log, * - JAXBElement(sequence, instance of ExpressionSequenceType): * scriptingExpression = List of * - JAXBElement(action, instance of ActionExpressionType): * type: delete * - JAXBElement(action, instance of ActionExpressionType): * type: assign * parameter: (...), * - JAXBElement(search, instance of SearchExpressionType): * type: UserType * * Invocations of this method will be: * 1) bean=ExecuteScriptType, key=pipeline, node=HList(*), isHeteroListProperty=false * 2) bean=ExpressionPipelineType, key=scriptingExpression, node=HList(*), isHeteroListProperty=true <<< * 3) bean=SearchExpressionType, key=type, node='type: c:RoleType', isHeteroListProperty=false * 4) bean=SearchExpressionType, key=searchFilter, node=XNode(map:1 entries), isHeteroListProperty=false * 5) bean=SearchExpressionType, key=action, node=XNode(map:1 entries), isHeteroListProperty=false * 6) bean=ActionExpressionType, key=type, node='type: log', isHeteroListProperty=false * 7) bean=ExpressionSequenceType, key=scriptingExpression, node=HList(**), isHeteroListProperty=true <<< * 8) bean=ActionExpressionType, key=type, node='type: delete', isHeteroListProperty=false * 9) bean=ActionExpressionType, key=type, node='type: assign', isHeteroListProperty=false * 10) bean=ActionExpressionType, key=parameter, node=XNode(map:2 entries), isHeteroListProperty=false * 11) bean=ActionParameterValueType, key=name, node='name: role', isHeteroListProperty=false * 12) bean=ActionParameterValueType, key=value, node='value: rome555c-7797-11e2-94a6-001e8c717e5b', isHeteroListProperty=false * 13) bean=SearchExpressionType, key=type, node='type: UserType', isHeteroListProperty=false * * Here we have 2 calls with isHeteroListProperty=true; first for pipeline.scriptingExpression, second for * sequence.scriptingExpression. */ private <T> void unmarshalEntry(@NotNull T bean, @NotNull Class<T> beanClass, @NotNull QName key, @NotNull XNode node, @NotNull XNode containingNode, boolean isHeteroListProperty, @NotNull ParsingContext pc) throws SchemaException { //System.out.println("bean=" + bean.getClass().getSimpleName() + ", key=" + key.getLocalPart() + ", node=" + node + ", isHeteroListProperty=" + isHeteroListProperty); final String propName = key.getLocalPart(); // this code is just to keep this method reasonably short PropertyAccessMechanism mechanism = new PropertyAccessMechanism(); if (!mechanism.compute(bean, beanClass, propName, key, node, pc)) { return; } final String actualPropertyName = mechanism.actualPropertyName; final boolean storeAsRawType = mechanism.storeAsRawType; final Method getter = mechanism.getter; final Method setter = mechanism.setter; final boolean wrapInJaxbElement = mechanism.wrapInJaxbElement; if (Element.class.isAssignableFrom(mechanism.paramType)) { throw new IllegalArgumentException("DOM not supported in field "+actualPropertyName+" in "+beanClass); } // The type T that is expected by the bean, i.e. either by // - setMethod(T value), or // - Collection<T> getMethod() // We use it to retrieve the correct value when parsing the node. // We might specialize it using the information derived from the node (to deal with inclusive polymorphism, // i.e. storing ExclusionPolicyConstraintType where AbstractPolicyConstraintType is expected). @NotNull Class<?> paramType; if (!storeAsRawType && !isHeteroListProperty) { Class<?> t = specializeParamType(node, mechanism.paramType, pc); if (t == null) { // indicates a problem return; } else { paramType = t; } } else { paramType = mechanism.paramType; } if (!(node instanceof ListXNode) && Object.class.equals(paramType) && !storeAsRawType) { throw new IllegalArgumentException("Object property (without @Raw) not supported in field "+actualPropertyName+" in "+beanClass); } // String paramNamespace = inspector.determineNamespace(paramType); boolean problem = false; Object propValue = null; Collection<Object> propValues = null; // For heterogeneous lists we have to create multi-valued fictitious property first. So we have to treat node as a map // (instead of list) and process it as a single value. Only when if (node instanceof ListXNode && (!node.isHeterogeneousList() || isHeteroListProperty)) { ListXNode xlist = (ListXNode) node; if (setter != null) { try { Object value = unmarshalSinglePropValue(node, actualPropertyName, paramType, storeAsRawType, beanClass, pc); if (wrapInJaxbElement) { propValue = wrapInJaxbElement(value, mechanism.objectFactory, mechanism.elementFactoryMethod, propName, beanClass, pc); } else { propValue = value; } } catch (SchemaException e) { problem = processSchemaException(e, node, pc); } } else { // No setter, we have to use collection getter propValues = new ArrayList<>(xlist.size()); for (XNode xsubsubnode: xlist) { try { Object valueToAdd; Object value = unmarshalSinglePropValue(xsubsubnode, actualPropertyName, paramType, storeAsRawType, beanClass, pc); if (value != null) { if (isHeteroListProperty) { QName elementName = xsubsubnode.getElementName(); if (elementName == null) { // TODO better error handling throw new SchemaException("Heterogeneous list with a no-elementName node: " + xsubsubnode); } Class valueClass = value.getClass(); QName jaxbElementName; if (QNameUtil.hasNamespace(elementName)) { jaxbElementName = elementName; } else { // Approximate solution: find element in schema registry - check for type compatibility // in order to exclude accidental name matches (like c:expression/s:expression). Optional<ItemDefinition> itemDefOpt = getSchemaRegistry().findItemDefinitionsByElementName(elementName) .stream() .filter(def -> getSchemaRegistry().findTypeDefinitionsByType(def.getTypeName()).stream() .anyMatch(typeDef -> typeDef.getCompileTimeClass() != null && typeDef.getCompileTimeClass() .isAssignableFrom(valueClass))) .findFirst(); if (itemDefOpt.isPresent()) { jaxbElementName = itemDefOpt.get().getName(); } else { LOGGER.warn("Heterogeneous list member with unknown element name '" + elementName + "': " + value); jaxbElementName = elementName; // unqualified } } @SuppressWarnings("unchecked") JAXBElement jaxbElement = new JAXBElement<>(jaxbElementName, valueClass, value); valueToAdd = jaxbElement; } else { if (wrapInJaxbElement) { valueToAdd = wrapInJaxbElement(value, mechanism.objectFactory, mechanism.elementFactoryMethod, propName, beanClass, pc); } else { valueToAdd = value; } } propValues.add(valueToAdd); } } catch (SchemaException e) { problem = processSchemaException(e, xsubsubnode, pc); } } } } else { try { propValue = unmarshalSinglePropValue(node, actualPropertyName, paramType, storeAsRawType, beanClass, pc); if (wrapInJaxbElement) { propValue = wrapInJaxbElement(propValue, mechanism.objectFactory, mechanism.elementFactoryMethod, propName, beanClass, pc); } } catch (SchemaException e) { problem = processSchemaException(e, node, pc); } } if (setter != null) { try { setter.invoke(bean, propValue); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException("Cannot invoke setter "+setter+" on bean of type "+beanClass+": "+e.getMessage(), e); } } else if (getter != null) { Object getterReturn; Collection<Object> col; try { getterReturn = getter.invoke(bean); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException("Cannot invoke getter "+getter+" on bean of type "+beanClass+": "+e.getMessage(), e); } try { col = (Collection<Object>)getterReturn; } catch (ClassCastException e) { throw new SystemException("Getter "+getter+" on bean of type "+beanClass+" returned "+getterReturn+" instead of collection"); } if (propValue != null) { col.add(propValue); } else if (propValues != null) { for (Object propVal: propValues) { col.add(propVal); } } else if (!problem) { throw new IllegalStateException("Strange. Multival property "+propName+" in "+beanClass+" produced null values list, parsed from "+containingNode); } if (!isHeteroListProperty) { checkJaxbElementConsistence(col, pc); } } else { throw new IllegalStateException("Uh? No setter nor getter."); } } private Class<?> specializeParamType(@NotNull XNode node, @NotNull Class<?> expectedType, @NotNull ParsingContext pc) throws SchemaException { if (node.getTypeQName() != null) { Class explicitType = getSchemaRegistry().determineClassForType(node.getTypeQName()); return explicitType != null && expectedType.isAssignableFrom(explicitType) ? explicitType : expectedType; // (if not assignable, we hope the adaptation will do it) } else if (node.getElementName() != null) { Collection<TypeDefinition> candidateTypes = getSchemaRegistry() .findTypeDefinitionsByElementName(node.getElementName(), TypeDefinition.class); List<TypeDefinition> suitableTypes = candidateTypes.stream() .filter(def -> def.getCompileTimeClass() != null && expectedType.isAssignableFrom(def.getCompileTimeClass())) .collect(Collectors.toList()); if (suitableTypes.isEmpty()) { pc.warnOrThrow(LOGGER, "Couldn't derive suitable type based on element name (" + node.getElementName() + "). Candidate types: " + candidateTypes + "; expected type: " + expectedType); return null; } else if (suitableTypes.size() > 1) { pc.warnOrThrow(LOGGER, "Couldn't derive single suitable type based on element name (" + node.getElementName() + "). Suitable types: " + suitableTypes); return null; } return suitableTypes.get(0).getCompileTimeClass(); } else { return expectedType; } } private class PropertyAccessMechanism { Class<?> beanClass; // phase1 String actualPropertyName; // This is the name of property we will really use. (Considering e.g. substitutions.) boolean storeAsRawType; // Whether the data will be stored as RawType. Object objectFactory; // JAXB object factory instance (e.g. xxxx.common-3.ObjectFactory). Method elementFactoryMethod; // Method in object factory that creates a given JAXB element (e.g. createAsIs(value)) // phase2 Method getter, setter; // Getter or setter that will be used to put a value (getter in case of collections) Class<?> paramType; // Actual parameter type; unwrapped: Collection<X> -> X, JAXBElement<X> -> X boolean wrapInJaxbElement; // If the paramType contained JAXBElement, i.e. if the value should be wrapped into it before using // returns true if the processing is to be continued; // false in case of using alternative way of unmarshalling (e.g. use of "any" method), or in case of error (in COMPAT mode) private <T> boolean compute(T bean, Class<T> beanClass, String propName, QName key, XNode node, ParsingContext pc) throws SchemaException { this.beanClass = beanClass; // phase1 if (!computeActualPropertyName(bean, propName, key, node, pc)) { return false; } // phase2 return computeGetterAndSetter(propName, pc); } // computes actualPropertyName + storeAsRawType // if necessary, fills-in also objectFactory + elementFactoryMethod private <T> boolean computeActualPropertyName(T bean, String propName, QName key, XNode node, ParsingContext pc) throws SchemaException { Field propertyField = inspector.findPropertyField(beanClass, propName); Method propertyGetter = null; if (propertyField == null) { propertyGetter = inspector.findPropertyGetter(beanClass, propName); } // Maybe this is not used in this context, because node.elementName is filled-in only for heterogeneous list // members - and they are iterated through elsewhere. Nevertheless, it is more safe to include it also here. // QName realElementName = getRealElementName(node, key, pc); // String realElementLocalName = realElementName.getLocalPart(); elementFactoryMethod = null; objectFactory = null; if (propertyField == null && propertyGetter == null) { // We have to try to find a more generic field, such as xsd:any or substitution element // check for global element definition first elementFactoryMethod = findElementFactoryMethod(propName); // realElementLocalName if (elementFactoryMethod != null) { // great - global element found, let's look up the field propertyField = inspector.lookupSubstitution(beanClass, elementFactoryMethod); if (propertyField == null) { propertyField = inspector.findAnyField(beanClass); // Check for "any" field if (propertyField != null) { unmarshalToAnyUsingField(bean, propertyField, key, node, pc); } else { unmarshalToAnyUsingGetterIfExists(bean, key, node, pc, propName); } return false; } } else { unmarshalToAnyUsingGetterIfExists(bean, key, node, pc, propName); // e.g. "getAny()" return false; } } // At this moment, property getter is the exact getter matching key.localPart (propName). // Property field may be either exact field matching key.localPart (propName), or more generic one (substitution, any). //noinspection ConstantConditions assert propertyGetter != null || propertyField != null; if (elementFactoryMethod != null) { storeAsRawType = elementFactoryMethod.getAnnotation(Raw.class) != null; } else if (propertyGetter != null) { storeAsRawType = propertyGetter.getAnnotation(Raw.class) != null; } else { storeAsRawType = propertyField.getAnnotation(Raw.class) != null; } if (propertyField != null) { actualPropertyName = propertyField.getName(); } else { actualPropertyName = propName; } return true; } private Method findElementFactoryMethod(String propName) { Class objectFactoryClass = inspector.getObjectFactoryClass(beanClass.getPackage()); objectFactory = instantiateObjectFactory(objectFactoryClass); return inspector.findElementMethodInObjectFactory(objectFactoryClass, propName); } private boolean computeGetterAndSetter(String propName, ParsingContext pc) throws SchemaException { setter = inspector.findSetter(beanClass, actualPropertyName); wrapInJaxbElement = false; paramType = null; if (setter == null) { // No setter. But if the property is multi-value we need to look // for a getter that returns a collection (Collection<Whatever>) getter = inspector.findPropertyGetter(beanClass, actualPropertyName); if (getter == null) { pc.warnOrThrow(LOGGER, "Cannot find setter or getter for field " + actualPropertyName + " in " + beanClass); return false; } computeParamTypeFromGetter(propName, getter.getReturnType()); } else { getter = null; Class<?> setterType = setter.getParameterTypes()[0]; computeParamTypeFromSetter(propName, setterType); } return true; } private void computeParamTypeFromSetter(String propName, Class<?> setterParamType) { if (JAXBElement.class.equals(setterParamType)) { // TODO some handling for the returned generic parameter types Type[] genericTypes = setter.getGenericParameterTypes(); if (genericTypes.length != 1) { throw new IllegalArgumentException("Too lazy to handle this."); } Type genericType = genericTypes[0]; if (genericType instanceof ParameterizedType) { Type actualType = inspector.getTypeArgument(genericType, "add some description"); if (actualType instanceof WildcardType) { if (elementFactoryMethod == null) { elementFactoryMethod = findElementFactoryMethod(propName); } // This is the case of Collection<JAXBElement<?>> // we need to extract the specific type from the factory method if (elementFactoryMethod == null) { throw new IllegalArgumentException( "Wildcard type in JAXBElement field specification and no factory method found for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type (inner type argument)"); } Type factoryMethodGenericReturnType = elementFactoryMethod.getGenericReturnType(); Type factoryMethodTypeArgument = inspector.getTypeArgument(factoryMethodGenericReturnType, "in factory method " + elementFactoryMethod + " return type for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type"); if (factoryMethodTypeArgument instanceof Class) { // This is the case of JAXBElement<Whatever> paramType = (Class<?>) factoryMethodTypeArgument; if (Object.class.equals(paramType) && !storeAsRawType) { throw new IllegalArgumentException("Factory method " + elementFactoryMethod + " type argument is Object (without @Raw) for field " + actualPropertyName + " in " + beanClass + ", property " + propName); } } else { throw new IllegalArgumentException( "Cannot determine factory method return type, got " + factoryMethodTypeArgument + " - for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type (inner type argument)"); } } } // Class enclosing = paramType.getEnclosingClass(); // Class clazz = paramType.getClass(); // Class declaring = paramType.getDeclaringClass(); wrapInJaxbElement = true; } else { paramType = setterParamType; } } private void computeParamTypeFromGetter(String propName, Class<?> getterReturnType) throws SchemaException { if (!Collection.class.isAssignableFrom(getterReturnType)) { throw new SchemaException("Cannot find getter for field " + actualPropertyName + " in " + beanClass + " does not return collection, cannot use it to set value"); } // getter.genericReturnType = Collection<...> Type typeArgument = inspector.getTypeArgument(getter.getGenericReturnType(), "for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type"); if (typeArgument instanceof Class) { paramType = (Class<?>) typeArgument; // ok, like Collection<AssignmentType> } else if (typeArgument instanceof ParameterizedType) { // something more complex ParameterizedType paramTypeArgument = (ParameterizedType) typeArgument; Type rawTypeArgument = paramTypeArgument.getRawType(); if (rawTypeArgument.equals(JAXBElement.class)) { // This is the case of Collection<JAXBElement<....>> wrapInJaxbElement = true; Type innerTypeArgument = inspector.getTypeArgument(typeArgument, "for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type (inner type argument)"); if (innerTypeArgument instanceof Class) { // This is the case of Collection<JAXBElement<Whatever>> (note that wrapInJaxbElement is now true) paramType = (Class<?>) innerTypeArgument; } else if (innerTypeArgument instanceof WildcardType) { // This is the case of Collection<JAXBElement<?>> // we need to extract the specific type from the factory method if (elementFactoryMethod == null) { elementFactoryMethod = findElementFactoryMethod(propName); if (elementFactoryMethod == null) { throw new IllegalArgumentException( "Wildcard type in JAXBElement field specification and no factory method found for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type (inner type argument)"); } } // something like JAXBElement<AsIsExpressionEvaluatorType> Type factoryMethodGenericReturnType = elementFactoryMethod.getGenericReturnType(); Type factoryMethodTypeArgument = inspector.getTypeArgument(factoryMethodGenericReturnType, "in factory method " + elementFactoryMethod + " return type for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type"); if (factoryMethodTypeArgument instanceof Class) { // This is the case of JAXBElement<Whatever> paramType = (Class<?>) factoryMethodTypeArgument; if (Object.class.equals(paramType) && !storeAsRawType) { throw new IllegalArgumentException("Factory method " + elementFactoryMethod + " type argument is Object (and not @Raw) for field " + actualPropertyName + " in " + beanClass + ", property " + propName); } } else { throw new IllegalArgumentException( "Cannot determine factory method return type, got " + factoryMethodTypeArgument + " - for field " + actualPropertyName + " in " + beanClass + ", cannot determine collection type (inner type argument)"); } } else { throw new IllegalArgumentException( "Ejha! " + innerTypeArgument + " " + innerTypeArgument.getClass() + " from " + getterReturnType + " from " + actualPropertyName + " in " + propName + " " + beanClass); } } else { // The case of Collection<Whatever<Something>> if (rawTypeArgument instanceof Class) { // ??? rawTypeArgument is the 'Whatever' part paramType = (Class<?>) rawTypeArgument; } else { throw new IllegalArgumentException( "EH? Eh!? " + typeArgument + " " + typeArgument.getClass() + " from " + getterReturnType + " from " + actualPropertyName + " in " + propName + " " + beanClass); } } } else { throw new IllegalArgumentException( "EH? " + typeArgument + " " + typeArgument.getClass() + " from " + getterReturnType + " from " + actualPropertyName + " in " + propName + " " + beanClass); } } } private QName getRealElementName(XNode node, QName key, ParsingContext pc) throws SchemaException { if (node.getElementName() == null) { return key; } String elementNS = node.getElementName().getNamespaceURI(); String keyNS = key.getNamespaceURI(); if (StringUtils.isNotEmpty(elementNS) && StringUtils.isNotEmpty(keyNS) && !elementNS.equals(keyNS)) { pc.warnOrThrow(LOGGER, "Namespaces for actual element (" + node.getElementName() + ") and it's place in schema (" + key + " are different."); return key; // fallback } else { return node.getElementName(); } } private <T> void unmarshalToAnyUsingGetterIfExists(@NotNull T bean, @NotNull QName key, @NotNull XNode node, @NotNull ParsingContext pc, String propName) throws SchemaException { Method elementMethod = inspector.findAnyMethod(bean.getClass()); if (elementMethod != null) { unmarshallToAnyUsingGetter(bean, elementMethod, key, node, pc); } else { pc.warnOrThrow(LOGGER, "No field "+propName+" in class "+bean.getClass()+" (and no element method in object factory too)"); } } // // if (prismContext != null && bean instanceof Revivable) { // ((Revivable)bean).revive(prismContext); // } // // return bean; // } // // Prepares value to be stored into the bean - e.g. converts PolyString->PolyStringType, wraps a value to JAXB if specified, ... private Object wrapInJaxbElement(Object propVal, Object objectFactory, Method factoryMethod, String propName, Class beanClass, ParsingContext pc) { if (factoryMethod == null) { throw new IllegalArgumentException("Param type is JAXB element but no factory method found for it, property "+propName+" in "+beanClass); } try { return factoryMethod.invoke(objectFactory, propVal); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException("Unable to invoke factory method "+factoryMethod+" on "+objectFactory.getClass()+" for property "+propName+" in "+beanClass); } } /* * We want to avoid this: * <expression> * <script> * <code>'up'</code> * </script> * <value>up</value> * </expression> * * Because it cannot be reasonably serialized in XNode (<value> gets changed to <script>). */ private void checkJaxbElementConsistence(Collection<Object> collection, ParsingContext pc) throws SchemaException { QName elementName = null; for (Object object : collection) { if (!(object instanceof JAXBElement)) { continue; } JAXBElement element = (JAXBElement) object; if (elementName == null) { elementName = element.getName(); } else { if (!QNameUtil.match(elementName, element.getName())) { String m = "Mixing incompatible element names in one property: " + elementName + " and " + element.getName(); if (pc.isStrict()) { throw new SchemaException(m); } else { pc.warn(LOGGER, m); } } } } } private boolean processSchemaException(SchemaException e, XNode xsubnode, ParsingContext pc) throws SchemaException { if (pc.isStrict()) { throw e; } else { LoggingUtils.logException(LOGGER, "Couldn't parse part of the document. It will be ignored. Document part:\n{}", e, xsubnode); pc.warn("Couldn't parse part of the document. It will be ignored. Document part:\n" + xsubnode); return true; } } private <T,S> void unmarshallToAnyUsingGetter(T bean, Method getter, QName elementName, XNode xsubnode, ParsingContext pc) throws SchemaException{ Class<T> beanClass = (Class<T>) bean.getClass(); Class objectFactoryClass = inspector.getObjectFactoryClass(elementName.getNamespaceURI()); Object objectFactory = instantiateObjectFactory(objectFactoryClass); Method elementFactoryMethod = inspector.findElementMethodInObjectFactory(objectFactoryClass, elementName.getLocalPart()); Class<S> subBeanClass = (Class<S>) elementFactoryMethod.getParameterTypes()[0]; if (xsubnode instanceof ListXNode){ for (XNode xsubSubNode : ((ListXNode) xsubnode)){ S subBean = unmarshal(xsubSubNode, subBeanClass, pc); unmarshallToAnyValue(bean, beanClass, subBean, objectFactoryClass, objectFactory, elementFactoryMethod, getter, pc); } } else{ S subBean = unmarshal(xsubnode, subBeanClass, pc); unmarshallToAnyValue(bean, beanClass, subBean, objectFactoryClass, objectFactory, elementFactoryMethod, getter, pc); } } private <T, S> void unmarshallToAnyValue(T bean, Class beanClass, S subBean, Class objectFactoryClass, Object objectFactory, Method elementFactoryMethod, Method getter, ParsingContext pc) { JAXBElement<S> subBeanElement; try { subBeanElement = (JAXBElement<S>) elementFactoryMethod.invoke(objectFactory, subBean); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) { throw new IllegalArgumentException("Cannot invoke factory method "+elementFactoryMethod+" on "+objectFactoryClass+" with "+subBean+": "+e1, e1); } Collection<Object> col; Object getterReturn; try { getterReturn = getter.invoke(bean); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException("Cannot invoke getter "+getter+" on bean of type "+beanClass+": "+e.getMessage(), e); } try { col = (Collection<Object>)getterReturn; } catch (ClassCastException e) { throw new SystemException("Getter "+getter+" on bean of type "+beanClass+" returned "+getterReturn+" instead of collection"); } col.add(subBeanElement != null ? subBeanElement.getValue() : null); } private <T,S> void unmarshalToAnyUsingField(T bean, Field anyField, QName elementName, XNode xsubnode, ParsingContext pc) throws SchemaException{ Method getter = inspector.findPropertyGetter(bean.getClass(), anyField.getName()); unmarshallToAnyUsingGetter(bean, getter, elementName, xsubnode, pc); } private Object instantiateObjectFactory(Class objectFactoryClass) { try { return objectFactoryClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalArgumentException("Cannot instantiate object factory class "+objectFactoryClass.getName()+": "+e.getMessage(), e); } } private Object unmarshalSinglePropValue(XNode xsubnode, String fieldName, Class paramType, boolean storeAsRawType, Class classType, ParsingContext pc) throws SchemaException { Object propValue; if (xsubnode == null) { propValue = null; } else if (paramType.equals(XNode.class)) { propValue = xsubnode; } else if (storeAsRawType || paramType.equals(RawType.class)) { RawType raw = new RawType(xsubnode, prismContext); // FIXME UGLY HACK: parse value if possible if (xsubnode.getTypeQName() != null) { PrismValue value = prismContext.parserFor(xsubnode.toRootXNode()).parseItemValue(); // TODO what about objects? oid/version will be lost here if (value != null && !value.isRaw()) { raw = new RawType(value, prismContext); } } propValue = raw; } else { // paramType is what we expect e.g. based on parent definition // but actual type (given by xsi:type/@typeDef) may be different, e.g. more specific // (although we already specialized paramType within the caller, we do it again here, because the subnode // used here might be a child of node used in the caller) paramType = specializeParamType(xsubnode, paramType, pc); if (paramType == null) { return null; // skipping this element in case of error } if (xsubnode instanceof PrimitiveXNode<?> || xsubnode instanceof MapXNode || xsubnode.isHeterogeneousList()) { propValue = unmarshal(xsubnode, paramType, pc); } else if (xsubnode instanceof ListXNode) { ListXNode xlist = (ListXNode)xsubnode; if (xlist.size() > 1) { throw new SchemaException("Cannot set multi-value value to a single valued property "+fieldName+" of "+classType); } else { if (xlist.isEmpty()) { propValue = null; } else { propValue = xlist.get(0); } } } else { throw new IllegalArgumentException("Cannot parse "+xsubnode+" to a bean "+classType); } } if (propValue instanceof PolyString) { propValue = new PolyStringType((PolyString) propValue); } return propValue; } private <T> T postConvertUnmarshal(Object parsedPrimValue, ParsingContext pc) { if (parsedPrimValue == null) { return null; } if (parsedPrimValue instanceof ItemPath) { return (T) new ItemPathType((ItemPath)parsedPrimValue); } else { return (T) parsedPrimValue; } } private SchemaDefinitionType unmarshalSchemaDefinitionType(MapXNode xmap, Class<?> beanClass, ParsingContext pc) throws SchemaException { Entry<QName, XNode> subEntry = xmap.getSingleSubEntry("schema element"); if (subEntry == null) { return null; } XNode xsub = subEntry.getValue(); if (xsub == null) { return null; } if (!(xsub instanceof SchemaXNode)) { throw new SchemaException("Cannot parse schema from "+xsub); } return unmarshalSchemaDefinitionType((SchemaXNode) xsub); } SchemaDefinitionType unmarshalSchemaDefinitionType(SchemaXNode xsub) throws SchemaException{ Element schemaElement = xsub.getSchemaElement(); if (schemaElement == null) { throw new SchemaException("Empty schema in " + xsub); } SchemaDefinitionType schemaDefType = new SchemaDefinitionType(); schemaDefType.setSchema(schemaElement); return schemaDefType; } @NotNull public PrismContext getPrismContext() { return prismContext; } @NotNull private SchemaRegistry getSchemaRegistry() { return prismContext.getSchemaRegistry(); } //region Specific unmarshallers ========================================================= // parses any subtype of SearchFilterType private <T extends SearchFilterType> T unmarshalSearchFilterType(MapXNode xmap, Class<T> beanClass, ParsingContext pc) throws SchemaException { if (xmap == null) { return null; } T filterType = instantiate(beanClass); filterType.parseFromXNode(xmap, prismContext); return filterType; } private ItemPathType unmarshalItemPath(PrimitiveXNode<ItemPathType> primitiveXNode, Class beanClass, ParsingContext parsingContext) throws SchemaException { ItemPathType parsedValue = primitiveXNode.getParsedValue(ItemPathType.COMPLEX_TYPE, ItemPathType.class); return postConvertUnmarshal(parsedValue, parsingContext); } private Object unmarshalPolyStringFromPrimitive(PrimitiveXNode<?> node, Class<?> beanClass, ParsingContext parsingContext) throws SchemaException { Object value; if (node.isParsed()) { value = node.getValue(); // there can be e.g. PolyString there } else { value = ((PrimitiveXNode<String>) node).getParsedValue(DOMUtil.XSD_STRING, String.class); } return toCorrectPolyStringClass(value, beanClass, node); } private Object unmarshalPolyStringFromMap(MapXNode map, Class<?> beanClass, ParsingContext pc) throws SchemaException { String orig = map.getParsedPrimitiveValue(QNameUtil.nullNamespace(PolyString.F_ORIG), DOMUtil.XSD_STRING); if (orig == null) { throw new SchemaException("Null polystring orig in "+map); } String norm = map.getParsedPrimitiveValue(QNameUtil.nullNamespace(PolyString.F_NORM), DOMUtil.XSD_STRING); Object value = new PolyStringType(new PolyString(orig, norm)); return toCorrectPolyStringClass(value, beanClass, map); } private Object toCorrectPolyStringClass(Object value, Class<?> beanClass, XNode node) { PolyString polyString; if (value instanceof String) { polyString = new PolyString((String) value); } else if (value instanceof PolyStringType) { polyString = ((PolyStringType) value).toPolyString(); } else if (value instanceof PolyString) { polyString = (PolyString) value; // TODO clone? } else if (value == null) { polyString = null; } else { throw new IllegalStateException("Couldn't convert " + value + " to a PolyString; while parsing " + node.debugDump()); } if (polyString != null && polyString.getNorm() == null) { // TODO should we always use default normalizer? polyString.recompute(prismContext.getDefaultPolyStringNormalizer()); } if (PolyString.class.equals(beanClass)) { return polyString; } else if (PolyStringType.class.equals(beanClass)) { return new PolyStringType(polyString); } else { throw new IllegalArgumentException("Wrong class for PolyString value: " + beanClass); } } private Object notSupported(XNode node, Class<?> beanClass, ParsingContext parsingContext) { // TODO what if compat mode? throw new IllegalArgumentException("The following couldn't be parsed as " + beanClass + ": " + node.debugDump()); } private XmlAsStringType unmarshalXmlAsStringFromPrimitive(PrimitiveXNode node, Class<XmlAsStringType> beanClass, ParsingContext parsingContext) throws SchemaException { return new XmlAsStringType(((PrimitiveXNode<String>) node).getParsedValue(DOMUtil.XSD_STRING, String.class)); } private XmlAsStringType unmarshalXmlAsStringFromMap(MapXNode map, Class<XmlAsStringType> beanClass, ParsingContext parsingContext) throws SchemaException { // reading a string represented a XML-style content // used e.g. when reading report templates (embedded XML) // A necessary condition: there may be only one map entry. if (map.size() > 1) { throw new SchemaException("Map with more than one item cannot be parsed as a string: " + map); } else if (map.isEmpty()) { return new XmlAsStringType(); } else { Entry<QName, XNode> entry = map.entrySet().iterator().next(); DomLexicalProcessor domParser = ((PrismContextImpl) prismContext).getParserDom(); String value = domParser.write(entry.getValue(), entry.getKey(), null); return new XmlAsStringType(value); } } private RawType unmarshalRawType(XNode node, Class<RawType> beanClass, ParsingContext parsingContext) { // TODO We could probably try to parse the raw node content using information from explicit node type. return new RawType(node, prismContext); } private <T> T unmarshalEnumFromPrimitive(PrimitiveXNode prim, Class<T> beanClass, ParsingContext pc) throws SchemaException { String primValue = (String) prim.getParsedValue(DOMUtil.XSD_STRING, String.class); primValue = StringUtils.trim(primValue); if (StringUtils.isEmpty(primValue)) { return null; } String javaEnumString = inspector.findEnumFieldName(beanClass, primValue); if (javaEnumString == null) { for (Field field: beanClass.getDeclaredFields()) { if (field.getName().equals(primValue)) { javaEnumString = field.getName(); break; } } } if (javaEnumString == null) { throw new SchemaException("Cannot find enum value for string '"+primValue+"' in "+beanClass); } @SuppressWarnings("unchecked") T bean = (T) Enum.valueOf((Class<Enum>)beanClass, javaEnumString); return bean; } private ProtectedStringType unmarshalProtectedString(MapXNode map, Class beanClass, ParsingContext pc) throws SchemaException { ProtectedStringType protectedType = new ProtectedStringType(); XNodeProcessorUtil.parseProtectedType(protectedType, map, prismContext, pc); return protectedType; } private ProtectedStringType unmarshalProtectedString(PrimitiveXNode<String> prim, Class beanClass, ParsingContext pc) throws SchemaException { ProtectedStringType protectedType = new ProtectedStringType(); protectedType.setClearValue(prim.getParsedValue(DOMUtil.XSD_STRING, String.class)); return protectedType; } private ProtectedByteArrayType unmarshalProtectedByteArray(MapXNode map, Class beanClass, ParsingContext pc) throws SchemaException { ProtectedByteArrayType protectedType = new ProtectedByteArrayType(); XNodeProcessorUtil.parseProtectedType(protectedType, map, prismContext, pc); return protectedType; } private ProtectedByteArrayType unmarshalProtectedByteArray(PrimitiveXNode<String> prim, Class beanClass, ParsingContext pc) throws SchemaException { ProtectedByteArrayType protectedType = new ProtectedByteArrayType(); String stringValue = prim.getParsedValue(DOMUtil.XSD_STRING, String.class); if (stringValue == null) { return null; } try { protectedType.setClearValue(ArrayUtils.toObject(stringValue.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e) { throw new SystemException("UTF-8 encoding is not supported", e); } return protectedType; } //endregion }