/* * 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.*; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.prism.schema.SchemaRegistry; import com.evolveum.midpoint.prism.xml.XsdTypeMapper; import com.evolveum.midpoint.prism.xnode.*; import com.evolveum.midpoint.util.DOMUtil; import com.evolveum.midpoint.util.Handler; import com.evolveum.midpoint.util.MiscUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.exception.TunnelException; 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.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.xml.bind.JAXBElement; import javax.xml.bind.annotation.XmlType; import javax.xml.namespace.QName; import java.lang.reflect.*; import java.util.*; public class BeanMarshaller { private static final Trace LOGGER = TraceManager.getTrace(BeanMarshaller.class); public static final String DEFAULT_PLACEHOLDER = "##default"; @NotNull private final PrismBeanInspector inspector; @NotNull private final PrismContext prismContext; @NotNull private final Map<Class,Marshaller> specialMarshallers = new HashMap<>(); @FunctionalInterface private interface Marshaller { XNode marshal(Object bean, SerializationContext sc) throws SchemaException; } private void createSpecialMarshallerMap() { specialMarshallers.put(XmlAsStringType.class, this::marshalXmlAsStringType); specialMarshallers.put(SchemaDefinitionType.class, this::marshalSchemaDefinition); specialMarshallers.put(ProtectedByteArrayType.class, this::marshalProtectedDataType); specialMarshallers.put(ProtectedStringType.class, this::marshalProtectedDataType); specialMarshallers.put(ItemPathType.class, this::marshalItemPathType); specialMarshallers.put(RawType.class, this::marshalRawType); // add(PolyString.class, this::unmarshalPolyStringFromPrimitive, this::unmarshalPolyStringFromMap); // add(PolyStringType.class, this::unmarshalPolyStringFromPrimitive, this::unmarshalPolyStringFromMap); } public BeanMarshaller(@NotNull PrismContext prismContext, @NotNull PrismBeanInspector inspector) { this.prismContext = prismContext; this.inspector = inspector; createSpecialMarshallerMap(); } @Nullable public <T> XNode marshall(@Nullable T bean) throws SchemaException { return marshall(bean, null); } @Nullable public <T> XNode marshall(@Nullable T bean, @Nullable SerializationContext ctx) throws SchemaException { if (bean == null) { return null; } Marshaller marshaller = specialMarshallers.get(bean.getClass()); if (marshaller != null) { return marshaller.marshal(bean, ctx); } // avoiding chatty PolyString serializations (namespace declaration + orig + norm) if (bean instanceof PolyString) { bean = (T) ((PolyString) bean).getOrig(); } else if (bean instanceof PolyStringType) { bean = (T) ((PolyStringType) bean).getOrig(); } if (bean instanceof Containerable) { return prismContext.xnodeSerializer().serializeRealValue(bean, new QName("dummy")).getSubnode(); } else if (bean instanceof Enum) { return marshalEnum((Enum) bean, ctx); } else if (bean.getClass().getAnnotation(XmlType.class) != null) { return marshalXmlType(bean, ctx); } else { return marshalToPrimitive(bean, ctx); } } private XNode marshalToPrimitive(Object bean, SerializationContext ctx) { return createPrimitiveXNode(bean, null, false); } private XNode marshalXmlType(Object bean, SerializationContext ctx) throws SchemaException { Class<?> beanClass = bean.getClass(); ComplexTypeDefinition ctd = getSchemaRegistry() .findTypeDefinitionByCompileTimeClass(beanClass, ComplexTypeDefinition.class); if (ctd != null && ctd.isListMarker()) { return marshalHeterogeneousList(bean, ctx); } else { return marshalXmlTypeToMap(bean, ctx); } } private XNode marshalHeterogeneousList(Object bean, SerializationContext ctx) throws SchemaException { // structurally similar to a specific path through marshalXmlTypeToMap Class<?> beanClass = bean.getClass(); QName propertyName = getHeterogeneousListPropertyName(beanClass); Method getter = inspector.findPropertyGetter(beanClass, propertyName.getLocalPart()); Object getterResult = getValue(bean, getter, propertyName.getLocalPart()); if (!(getterResult instanceof Collection)) { throw new IllegalStateException("Heterogeneous list property " + propertyName + " does not contain a collection but " + MiscUtil.getObjectName(getterResult)); } ListXNode xlist = new ListXNode(); for (Object value : (Collection) getterResult) { if (!(value instanceof JAXBElement)) { throw new IllegalStateException("Heterogeneous list contains a value that is not a JAXBElement: " + value); } JAXBElement jaxbElement = (JAXBElement) value; Object realValue = jaxbElement.getValue(); if (realValue == null) { throw new IllegalStateException("Heterogeneous list contains a null value"); // TODO } QName typeName = inspector.determineTypeForClass(realValue.getClass()); XNode marshaled = marshallValue(realValue, typeName, false, ctx); marshaled.setElementName(jaxbElement.getName()); setExplicitTypeDeclarationIfNeededForHeteroList(marshaled, realValue); xlist.add(marshaled); } return xlist; } private XNode marshalXmlTypeToMap(Object bean, SerializationContext ctx) throws SchemaException { Class<?> beanClass = bean.getClass(); MapXNode xmap; if (bean instanceof SearchFilterType) { // this hack is here because of c:ConditionalSearchFilterType - it is analogous to situation when unmarshalling this type (TODO: rework this in a nicer way) xmap = marshalSearchFilterType((SearchFilterType) bean); if (SearchFilterType.class.equals(bean.getClass())) { return xmap; // nothing more to serialize; otherwise we continue, because in that case we deal with a subclass of SearchFilterType } } else { xmap = new MapXNode(); } String namespace = inspector.determineNamespace(beanClass); if (namespace == null) { throw new IllegalArgumentException("Cannot determine namespace of "+beanClass); } List<String> propOrder = inspector.getPropOrder(beanClass); for (String fieldName: propOrder) { Method getter = inspector.findPropertyGetter(beanClass, fieldName); if (getter == null) { throw new IllegalStateException("No getter for field "+fieldName+" in "+beanClass); } Object getterResult = getValue(bean, getter, fieldName); if (getterResult == null) { continue; } Field field = inspector.findPropertyField(beanClass, fieldName); boolean isAttribute = inspector.isAttribute(field, getter); QName elementName = inspector.findFieldElementQName(fieldName, beanClass, namespace); if (getterResult instanceof Collection<?>) { Collection collection = (Collection) getterResult; if (collection.isEmpty()) { continue; } Iterator i = collection.iterator(); Object getterResultValue = i.next(); if (getterResultValue == null) { continue; } // elementName will be determined from the first item on the list // TODO make sure it will be correct with respect to other items as well! if (getterResultValue instanceof JAXBElement && ((JAXBElement) getterResultValue).getName() != null) { elementName = ((JAXBElement) getterResultValue).getName(); } ListXNode xlist = new ListXNode(); for (Object value: collection) { if (value == null) { continue; } Object valueToMarshal = value; if (value instanceof JAXBElement) { valueToMarshal = ((JAXBElement) value).getValue(); } QName typeName = inspector.findTypeName(field, valueToMarshal.getClass(), namespace); // note: fieldTypeName is used only for attribute values here (when constructing PrimitiveXNode) XNode marshaled = marshallValue(valueToMarshal, typeName, isAttribute, ctx); setExplicitTypeDeclarationIfNeeded(marshaled, getter, valueToMarshal, typeName); xlist.add(marshaled); } xmap.put(elementName, xlist); } else { QName fieldTypeName = inspector.findTypeName(field, getterResult.getClass(), namespace); Object valueToMarshall; if (getterResult instanceof JAXBElement){ valueToMarshall = ((JAXBElement) getterResult).getValue(); elementName = ((JAXBElement) getterResult).getName(); } else{ valueToMarshall = getterResult; } XNode marshaled = marshallValue(valueToMarshall, fieldTypeName, isAttribute, ctx); // TODO reconcile with setExplioitTypeDeclarationIfNeeded if (!getter.getReturnType().equals(valueToMarshall.getClass()) && getter.getReturnType().isAssignableFrom(valueToMarshall.getClass()) && !(valueToMarshall instanceof Enum)) { PrismObjectDefinition def = prismContext.getSchemaRegistry().determineDefinitionFromClass(valueToMarshall.getClass()); if (def != null){ QName type = def.getTypeName(); marshaled.setTypeQName(type); marshaled.setExplicitTypeDeclaration(true); } } xmap.put(elementName, marshaled); // setExplicitTypeDeclarationIfNeeded(getter, valueToMarshall, xmap, fieldTypeName); } } return xmap; } private Object getValue(Object bean, Method getter, String fieldOrPropertyName) { Object getterResult; try { getterResult = getter.invoke(bean); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException("Cannot invoke method for field/property "+fieldOrPropertyName+" in "+bean.getClass()+": "+e.getMessage(), e); } return getterResult; } private XNode marshalEnum(Enum enumValue, SerializationContext ctx) { Class<? extends Enum> enumClass = enumValue.getClass(); String enumStringValue = inspector.findEnumFieldValue(enumClass, enumValue.toString()); if (StringUtils.isEmpty(enumStringValue)){ enumStringValue = enumValue.toString(); } QName fieldTypeName = inspector.findTypeName(null, enumClass, DEFAULT_PLACEHOLDER); return createPrimitiveXNode(enumStringValue, fieldTypeName, false); } private XNode marshalXmlAsStringType(Object bean, SerializationContext sc) { PrimitiveXNode xprim = new PrimitiveXNode<>(); xprim.setValue(((XmlAsStringType) bean).getContentAsString(), DOMUtil.XSD_STRING); return xprim; } public void revive(Object bean, final PrismContext prismContext) throws SchemaException { Handler<Object> visitor = o -> { if (o instanceof Revivable) { try { ((Revivable)o).revive(prismContext); } catch (SchemaException e) { throw new TunnelException(e); } } return true; }; try { visit(bean, visitor); } catch (TunnelException te) { SchemaException e = (SchemaException) te.getCause(); throw e; } } public void visit(Object bean, Handler<Object> handler) { if (bean == null) { return; } Class<? extends Object> beanClass = bean.getClass(); handler.handle(bean); if (beanClass.isEnum() || beanClass.isPrimitive()){ //nothing more to do return; } // TODO: implement special handling for RawType, if necessary (it has no XmlType annotation any more) XmlType xmlType = beanClass.getAnnotation(XmlType.class); if (xmlType == null) { // no @XmlType annotation, we are not interested to go any deeper return; } List<String> propOrder = inspector.getPropOrder(beanClass); for (String fieldName: propOrder) { Method getter = inspector.findPropertyGetter(beanClass, fieldName); if (getter == null) { throw new IllegalStateException("No getter for field "+fieldName+" in "+beanClass); } Object getterResult = getValue(bean, getter, fieldName); if (getterResult == null) { continue; } if (getterResult instanceof Collection<?>) { Collection col = (Collection)getterResult; if (col.isEmpty()) { continue; } for (Object element: col) { visitValue(element, handler); } } else { visitValue(getterResult, handler); } } } private void visitValue(Object element, Handler<Object> handler) { Object elementToMarshall = element; if (element instanceof JAXBElement){ elementToMarshall = ((JAXBElement) element).getValue(); } visit(elementToMarshall, handler); } private void setExplicitTypeDeclarationIfNeededForHeteroList(XNode node, Object realValue) { QName elementName = node.getElementName(); QName typeName = inspector.determineTypeForClass(realValue.getClass()); if (typeName != null && !getSchemaRegistry().hasImplicitTypeDefinition(elementName, typeName) && getSchemaRegistry().findTypeDefinitionByType(typeName, TypeDefinition.class) != null) { node.setExplicitTypeDeclaration(true); node.setTypeQName(typeName); } } // TODO shouldn't we use here the same approach as above? private void setExplicitTypeDeclarationIfNeeded(XNode node, Method getter, Object getterResult, QName typeName) { Class getterReturnType = getter.getReturnType(); Class getterType = null; if (Collection.class.isAssignableFrom(getterReturnType)) { Type genericReturnType = getter.getGenericReturnType(); if (genericReturnType instanceof ParameterizedType) { Type actualType = inspector.getTypeArgument(genericReturnType, "explicit type declaration"); if (actualType instanceof Class) { getterType = (Class) actualType; } else if (actualType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) actualType; Type typeArgument = inspector.getTypeArgument(parameterizedType, "JAXBElement return type"); getterType = inspector.getUpperBound(typeArgument, "JAXBElement return type"); } } } if (getterType == null) { getterType = getterReturnType; } Class getterResultReturnType = getterResult.getClass(); if (node != null && getterType != getterResultReturnType && getterType.isAssignableFrom(getterResultReturnType)) { node.setExplicitTypeDeclaration(true); node.setTypeQName(typeName); } } // bean should have only two features: "list" attribute and multivalued property into which we should store the elements @NotNull <T> QName getHeterogeneousListPropertyName(Class<T> beanClass) throws SchemaException { List<String> properties = inspector.getPropOrder(beanClass); if (!properties.contains(DOMUtil.IS_LIST_ATTRIBUTE_NAME)) { throw new SchemaException("Couldn't unmarshal heterogeneous list into class without '" + DOMUtil.IS_LIST_ATTRIBUTE_NAME + "' attribute. Class: " + beanClass.getName() + " has the following properties: " + properties); } if (properties.size() > 2) { throw new SchemaException("Couldn't unmarshal heterogeneous list into class with more than one property " + "other than '" + DOMUtil.IS_LIST_ATTRIBUTE_NAME + "'. Class " + beanClass.getName() + " has the following properties: " + properties); } String contentProperty = properties.stream() .filter(p -> !DOMUtil.IS_LIST_ATTRIBUTE_NAME.equals(p)) .findFirst() .orElseThrow(() -> new SchemaException("Couldn't unmarshal heterogeneous list into class without " + "content-holding property. Class: " + beanClass.getName() + ".")); return new QName(inspector.determineNamespace(beanClass), contentProperty); } private <T> XNode marshallValue(T value, QName valueType, boolean isAttribute, SerializationContext ctx) throws SchemaException { if (value == null) { return null; } if (isAttribute) { // hoping the value fits into primitive! return createPrimitiveXNode(value, valueType, true); } else { return marshall(value, ctx); } } private <T> PrimitiveXNode<T> createPrimitiveXNode(T value, QName valueType, boolean isAttribute) { PrimitiveXNode<T> xprim = new PrimitiveXNode<>(); xprim.setValue(value, valueType); xprim.setAttribute(isAttribute); return xprim; } private <T> PrimitiveXNode<T> createPrimitiveXNode(T val, QName type) { return createPrimitiveXNode(val, type, false); } private XNode marshalRawType(Object value, SerializationContext sc) throws SchemaException { return ((RawType) value).serializeToXNode(); } private XNode marshalItemPathType(Object o, SerializationContext sc) { ItemPathType itemPath = (ItemPathType) o; PrimitiveXNode<ItemPathType> xprim = new PrimitiveXNode<>(); if (itemPath != null) { xprim.setValue(itemPath, ItemPathType.COMPLEX_TYPE); } return xprim; } private XNode marshalSchemaDefinition(Object o, SerializationContext ctx) { SchemaDefinitionType schemaDefinitionType = (SchemaDefinitionType) o; SchemaXNode xschema = new SchemaXNode(); xschema.setSchemaElement(schemaDefinitionType.getSchema()); MapXNode xmap = new MapXNode(); xmap.put(DOMUtil.XSD_SCHEMA_ELEMENT, xschema); return xmap; } // TODO create more appropriate interface to be able to simply serialize ProtectedStringType instances public <T> MapXNode marshalProtectedDataType(Object o, SerializationContext sc) throws SchemaException { ProtectedDataType<T> protectedType = (ProtectedDataType<T>) o; MapXNode xmap = new MapXNode(); if (protectedType.getEncryptedDataType() != null) { EncryptedDataType encryptedDataType = protectedType.getEncryptedDataType(); MapXNode xEncryptedDataType = (MapXNode) marshall(encryptedDataType); xmap.put(ProtectedDataType.F_ENCRYPTED_DATA, xEncryptedDataType); } else if (protectedType.getHashedDataType() != null) { HashedDataType hashedDataType = protectedType.getHashedDataType(); MapXNode xHashedDataType = (MapXNode) marshall(hashedDataType); xmap.put(ProtectedDataType.F_HASHED_DATA, xHashedDataType); } else if (protectedType.getClearValue() != null){ QName type = XsdTypeMapper.toXsdType(protectedType.getClearValue().getClass()); PrimitiveXNode xClearValue = createPrimitiveXNode(protectedType.getClearValue(), type); xmap.put(ProtectedDataType.F_CLEAR_VALUE, xClearValue); } // TODO: clearValue return xmap; } //region Specific marshallers ============================================================== private MapXNode marshalSearchFilterType(SearchFilterType value) throws SchemaException { if (value == null) { return null; } return value.serializeToXNode(); } //endregion @NotNull public PrismContext getPrismContext() { return prismContext; } private SchemaRegistry getSchemaRegistry() { return prismContext.getSchemaRegistry(); } public boolean canProcess(QName typeName) { Class<Object> clazz = getSchemaRegistry().determineClassForType(typeName); if (clazz != null && canProcess(clazz)) { return true; } TypeDefinition td = getSchemaRegistry().findTypeDefinitionByType(typeName); if (td instanceof SimpleTypeDefinition) { return true; // most probably dynamic enum, at this point } return false; } public boolean canProcess(@NotNull Class<?> clazz) { return !Containerable.class.isAssignableFrom(clazz) && (RawType.class.equals(clazz) || clazz.getAnnotation(XmlType.class) != null || XsdTypeMapper.getTypeFromClass(clazz) != null); } public QName determineTypeForClass(Class<?> clazz) { return inspector.determineTypeForClass(clazz); } } // TODO hacked, for now // private <T> String findEnumFieldValue(Class classType, Object bean){ // String name = bean.toString(); // for (Field field: classType.getDeclaredFields()) { // XmlEnumValue xmlEnumValue = field.getAnnotation(XmlEnumValue.class); // if (xmlEnumValue != null && field.getName().equals(name)) { // return xmlEnumValue.value(); // } // } // return null; // }