/*
* 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.xml;
import com.evolveum.midpoint.prism.PrismConstants;
import com.evolveum.midpoint.prism.marshaller.XPathHolder;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.polystring.PolyString;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.JAXBUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.bind.annotation.XmlEnumValue;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import java.io.File;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
/**
* Simple implementation that converts XSD primitive types to Java (and vice
* versa).
* <p/>
* It convert type names (xsd types to java classes) and also the values.
* <p/>
* The implementation is very simple now. In fact just a bunch of ifs. We don't
* need much more now. If more complex thing will be needed, we will extend the
* implementation later.
*
* @author Radovan Semancik
*/
public class XmlTypeConverter {
private static DatatypeFactory datatypeFactory = null;
private static final Trace LOGGER = TraceManager.getTrace(XmlTypeConverter.class);
private static DatatypeFactory getDatatypeFactory() {
if (datatypeFactory == null) {
try {
datatypeFactory = DatatypeFactory.newInstance();
} catch (DatatypeConfigurationException ex) {
throw new IllegalStateException("Cannot construct DatatypeFactory: " + ex.getMessage(), ex);
}
}
return datatypeFactory;
}
public static <T> T toJavaValue(Element xmlElement, Class<T> type) throws SchemaException {
if (type.equals(Element.class)) {
return (T) xmlElement;
} else if (type.equals(QName.class)) {
return (T) DOMUtil.getQNameValue(xmlElement);
} else if (PolyString.class.isAssignableFrom(type)) {
return (T) polyStringToJava(xmlElement);
} else {
String stringContent = xmlElement.getTextContent();
if (stringContent == null) {
return null;
}
T javaValue = toJavaValue(stringContent, type);
if (javaValue == null) {
throw new IllegalArgumentException("Unknown type for conversion: " + type + "(element " + DOMUtil.getQName(xmlElement) + ")");
}
return javaValue;
}
}
public static <T> T toJavaValue(String stringContent, Class<T> type) {
return toJavaValue(stringContent, type, false);
}
public static <T> T toJavaValue(String stringContent, QName typeQName) {
Class<T> javaClass = XsdTypeMapper.getXsdToJavaMapping(typeQName);
return toJavaValue(stringContent, javaClass, false);
}
@SuppressWarnings("unchecked")
public static <T> T toJavaValue(String stringContent, Class<T> type, boolean exceptionOnUnknown) {
if (type.equals(String.class)) {
return (T) stringContent;
} else if (type.equals(char.class)) {
return (T) (new Character(stringContent.charAt(0)));
} else if (type.equals(File.class)) {
return (T) new File(stringContent);
} else if (type.equals(Integer.class)) {
return (T) Integer.valueOf(stringContent);
} else if (type.equals(int.class)) {
return (T) Integer.valueOf(stringContent);
} else if (type.equals(Short.class) || type.equals(short.class)) {
return (T) Short.valueOf(stringContent);
} else if (type.equals(Long.class)) {
return (T) Long.valueOf(stringContent);
} else if (type.equals(long.class)) {
return (T) Long.valueOf(stringContent);
} else if (type.equals(Byte.class)) {
return (T) Byte.valueOf(stringContent);
} else if (type.equals(byte.class)) {
return (T) Byte.valueOf(stringContent);
} else if (type.equals(float.class)) {
return (T) Float.valueOf(stringContent);
} else if (type.equals(Float.class)) {
return (T) Float.valueOf(stringContent);
} else if (type.equals(double.class)) {
return (T) Double.valueOf(stringContent);
} else if (type.equals(Double.class)) {
return (T) Double.valueOf(stringContent);
} else if (type.equals(BigInteger.class)) {
return (T) new BigInteger(stringContent);
} else if (type.equals(BigDecimal.class)) {
return (T) new BigDecimal(stringContent);
} else if (type.equals(byte[].class)) {
byte[] decodedData = Base64.decodeBase64(stringContent);
return (T) decodedData;
} else if (type.equals(boolean.class) || Boolean.class.isAssignableFrom(type)) {
// TODO: maybe we will need more inteligent conversion, e.g. to trim spaces, case insensitive, etc.
return (T) Boolean.valueOf(stringContent);
} else if (type.equals(GregorianCalendar.class)) {
return (T) getDatatypeFactory().newXMLGregorianCalendar(stringContent).toGregorianCalendar();
} else if (XMLGregorianCalendar.class.isAssignableFrom(type)) {
return (T) getDatatypeFactory().newXMLGregorianCalendar(stringContent);
} else if (Duration.class.isAssignableFrom(type)) {
return (T) getDatatypeFactory().newDuration(stringContent);
} else if (type.equals(PolyString.class)) {
return (T) new PolyString(stringContent);
} else if (type.equals(ItemPath.class)) {
throw new UnsupportedOperationException("Path conversion not supported yet");
} else {
if (exceptionOnUnknown) {
throw new IllegalArgumentException("Unknown conversion type "+type);
} else {
return null;
}
}
}
public static Object toJavaValue(Element xmlElement, QName type) throws SchemaException {
return toJavaValue(xmlElement, XsdTypeMapper.getXsdToJavaMapping(type));
}
/**
* Expects type information in xsi:type
*
* @param xmlElement
* @return
* @throws JAXBException
*/
public static Object toJavaValue(Element xmlElement) throws SchemaException {
return toTypedJavaValueWithDefaultType(xmlElement, null).getValue();
}
/**
* Try to locate element type from xsi:type, fall back to specified default
* type.
*
* @param element
* @param defaultType
* @return converted java value
* @throws JAXBException
* @throws SchemaException if no xsi:type or default type specified
*/
public static TypedValue toTypedJavaValueWithDefaultType(Element xmlElement, QName defaultType) throws SchemaException {
QName xsiType = DOMUtil.resolveXsiType(xmlElement);
if (xsiType == null) {
xsiType = defaultType;
if (xsiType == null) {
QName elementQName = JAXBUtil.getElementQName(xmlElement);
throw new SchemaException("Cannot convert element " + elementQName + " to java, no type information available", elementQName);
}
}
return new TypedValue(toJavaValue(xmlElement, xsiType), xsiType, DOMUtil.getQName(xmlElement));
}
public static Object toXsdElement(Object val, QName typeName, QName elementName, Document doc, boolean recordType) throws SchemaException {
// Just ignore the typeName for now. The java type will determine the conversion
Object createdObject = toXsdElement(val, elementName, doc, false);
if (createdObject instanceof Element) {
Element createdElement = (Element) createdObject;
// But record the correct type is asked to
if (recordType) {
if (typeName == null) {
// if no type was specified, just record the one that was used for automatic conversion
typeName = XsdTypeMapper.toXsdType(val.getClass());
}
DOMUtil.setXsiType(createdElement, typeName);
}
}
return createdObject;
}
public static Object toXsdElement(Object val, QName elementName, Document doc) throws SchemaException {
return toXsdElement(val, elementName, doc, false);
}
/**
* @param val
* @param elementName
* @param doc
* @param recordType
* @return created element
* @throws SchemaException
*/
public static Element toXsdElement(Object val, QName elementName, Document doc, boolean recordType) throws SchemaException {
if (val == null) {
// if no value is specified, do not create element
return null;
}
Class type = XsdTypeMapper.getTypeFromClass(val.getClass());
if (type == null) {
throw new IllegalArgumentException("No type mapping for conversion: " + val.getClass() + "(element " + elementName + ")");
}
if (doc == null) {
doc = DOMUtil.getDocument();
}
Element element = doc.createElementNS(elementName.getNamespaceURI(), elementName.getLocalPart());
//TODO: switch to global namespace prefixes map
// element.setPrefix(MidPointNamespacePrefixMapper.getPreferredPrefix(elementName.getNamespaceURI()));
toXsdElement(val, element, recordType);
return element;
}
public static void toXsdElement(Object val, Element element, boolean recordType) throws SchemaException {
if (val instanceof Element) {
return;
} else if (val instanceof QName) {
QName qname = (QName) val;
DOMUtil.setQNameValue(element, qname);
} else if (val instanceof PolyString) {
polyStringToElement(element, (PolyString)val);
} else {
element.setTextContent(toXmlTextContent(val, DOMUtil.getQName(element)));
}
if (recordType) {
QName xsdType = XsdTypeMapper.toXsdType(val.getClass());
DOMUtil.setXsiType(element, xsdType);
}
}
public static String toXmlTextContent(Object val, QName elementName) {
if (val == null) {
// if no value is specified, do not create element
return null;
}
Class type = XsdTypeMapper.getTypeFromClass(val.getClass());
if (type == null) {
throw new IllegalArgumentException("No type mapping for conversion: " + val.getClass() + "(element " + elementName + ")");
}
if (type.equals(String.class)) {
return (String) val;
} if (type.equals(PolyString.class)){
return ((PolyString) val).getNorm();
} else if (type.equals(char.class) || type.equals(Character.class)) {
return ((Character) val).toString();
} else if (type.equals(File.class)) {
return ((File) val).getPath();
} else if (type.equals(int.class) || type.equals(Integer.class)) {
return ((Integer) val).toString();
} else if (type.equals(long.class) || type.equals(Long.class)) {
return ((Long) val).toString();
} else if (type.equals(byte.class) || type.equals(Byte.class)) {
return ((Byte) val).toString();
} else if (type.equals(float.class) || type.equals(Float.class)) {
return ((Float) val).toString();
} else if (type.equals(double.class) || type.equals(Double.class)) {
return ((Double) val).toString();
} else if (type.equals(byte[].class)) {
byte[] binaryData = (byte[]) val;
return Base64.encodeBase64String(binaryData);
} else if (type.equals(Boolean.class)) {
Boolean bool = (Boolean) val;
if (bool.booleanValue()) {
return XsdTypeMapper.BOOLEAN_XML_VALUE_TRUE;
} else {
return XsdTypeMapper.BOOLEAN_XML_VALUE_FALSE;
}
} else if (type.equals(BigInteger.class)) {
return ((BigInteger) val).toString();
} else if (type.equals(BigDecimal.class)) {
return ((BigDecimal) val).toString();
} else if (type.equals(GregorianCalendar.class)) {
XMLGregorianCalendar xmlCal = createXMLGregorianCalendar((GregorianCalendar) val);
return xmlCal.toXMLFormat();
} else if (XMLGregorianCalendar.class.isAssignableFrom(type)) {
return ((XMLGregorianCalendar) val).toXMLFormat();
} else if (Duration.class.isAssignableFrom(type)) {
return ((Duration) val).toString();
} else if (type.equals(ItemPath.class)){
XPathHolder xpath = new XPathHolder((ItemPath)val);
return xpath.getXPath();
} else {
throw new IllegalArgumentException("Unknown type for conversion: " + type + "(element " + elementName + ")");
}
}
public static boolean canConvert(Class<?> clazz) {
return (XsdTypeMapper.getJavaToXsdMapping(clazz) != null);
}
public static boolean canConvert(QName xsdType) {
return (XsdTypeMapper.getXsdToJavaMapping(xsdType) != null);
}
public static <T> T convertValueElementAsScalar(Element valueElement, Class<T> type) throws SchemaException {
return toJavaValue(valueElement, type);
}
public static Object convertValueElementAsScalar(Element valueElement, QName xsdType) throws SchemaException {
return toJavaValue(valueElement, xsdType);
}
public static List<Object> convertValueElementAsList(Element valueElement) throws SchemaException {
return convertValueElementAsList(valueElement.getChildNodes(), Object.class);
}
public static <T> List<T> convertValueElementAsList(Element valueElement, Class<T> type) throws SchemaException {
if (type.equals(Object.class)) {
if (DOMUtil.hasXsiType(valueElement)) {
Object scalarValue = convertValueElementAsScalar(valueElement, DOMUtil.resolveXsiType(valueElement));
List<Object> list = new ArrayList<Object>(1);
list.add(scalarValue);
return (List<T>) list;
}
}
return convertValueElementAsList(valueElement.getChildNodes(), type);
}
public static List<?> convertValueElementAsList(Element valueElement, QName xsdType) throws SchemaException {
Class<?> type = XsdTypeMapper.toJavaType(xsdType);
return convertValueElementAsList(valueElement.getChildNodes(), type);
}
public static <T> List<T> convertValueElementAsList(NodeList valueNodes, Class<T> type) throws SchemaException {
// We need to determine whether this is single value or multi value
// no XML elements = single (primitive) value
// XML elements = multi value
List<T> values = new ArrayList<T>();
if (valueNodes == null) {
return values;
}
T scalarAttempt = tryConvertPrimitiveScalar(valueNodes, type);
if (scalarAttempt != null) {
values.add(scalarAttempt);
return values;
}
for (int i = 0; i < valueNodes.getLength(); i++) {
Node node = valueNodes.item(i);
if (DOMUtil.isJunk(node)) {
continue;
}
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
T value = null;
if (type.equals(Object.class)) {
Class<?> overrideType = String.class;
if (DOMUtil.hasXsiType(element)) {
QName xsiType = DOMUtil.resolveXsiType(element);
overrideType = XsdTypeMapper.toJavaType(xsiType);
}
value = (T) XmlTypeConverter.toJavaValue(element, overrideType);
} else {
value = XmlTypeConverter.toJavaValue(element, type);
}
values.add(value);
}
}
return values;
}
private static <T> T tryConvertPrimitiveScalar(NodeList valueNodes, Class<T> type) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < valueNodes.getLength(); i++) {
Node node = valueNodes.item(i);
if (DOMUtil.isJunk(node)) {
continue;
}
if (node.getNodeType() == Node.TEXT_NODE) {
sb.append(node.getTextContent());
} else {
// We have failed
return null;
}
}
if (type.equals(Object.class)) {
// Try string as default type
return (T) XmlTypeConverter.toJavaValue(sb.toString(), String.class);
}
return XmlTypeConverter.toJavaValue(sb.toString(), type);
}
public static void appendBelowNode(Object val, QName xsdType, QName name, Node parentNode,
boolean recordType) throws SchemaException {
Object xsdElement = toXsdElement(val, xsdType, name, parentNode.getOwnerDocument(), recordType);
if (xsdElement == null) {
return;
}
if (xsdElement instanceof Element) {
parentNode.appendChild((Element) xsdElement);
// } else if (xsdElement instanceof JAXBElement) {
// try {
// JAXBUtil.marshal(xsdElement, parentNode);
// } catch (JAXBException e) {
// throw new SchemaException("Error marshalling element " + xsdElement + ": " + e.getMessage(), e);
// }
} else {
throw new IllegalStateException("The XSD type converter returned unknown element type: " + xsdElement + " (" + xsdElement.getClass().getName() + ")");
}
}
public static XMLGregorianCalendar createXMLGregorianCalendar(long timeInMillis) {
GregorianCalendar gregorianCalendar = new GregorianCalendar();
gregorianCalendar.setTimeInMillis(timeInMillis);
return createXMLGregorianCalendar(gregorianCalendar);
}
public static XMLGregorianCalendar createXMLGregorianCalendar(Date date) {
if (date == null) {
return null;
}
GregorianCalendar gregorianCalendar = new GregorianCalendar();
gregorianCalendar.setTime(date);
return createXMLGregorianCalendar(gregorianCalendar);
}
public static XMLGregorianCalendar createXMLGregorianCalendar(String string) {
return getDatatypeFactory().newXMLGregorianCalendar(string);
}
public static XMLGregorianCalendar createXMLGregorianCalendar(GregorianCalendar cal) {
return getDatatypeFactory().newXMLGregorianCalendar(cal);
}
// in some environments, XMLGregorianCalendar.clone does not work
public static XMLGregorianCalendar createXMLGregorianCalendar(XMLGregorianCalendar cal) {
if (cal == null) {
return null;
}
return getDatatypeFactory().newXMLGregorianCalendar(cal.toGregorianCalendar()); // TODO find a better way
}
public static XMLGregorianCalendar createXMLGregorianCalendar(int year, int month, int day, int hour, int minute,
int second, int millisecond, int timezone) {
return getDatatypeFactory().newXMLGregorianCalendar(year, month, day, hour, minute, second, millisecond, timezone);
}
public static XMLGregorianCalendar createXMLGregorianCalendar(int year, int month, int day, int hour, int minute,
int second) {
return getDatatypeFactory().newXMLGregorianCalendar(year, month, day, hour, minute, second, 0, 0);
}
public static long toMillis(XMLGregorianCalendar xmlCal) {
if (xmlCal == null){
return 0;
}
return xmlCal.toGregorianCalendar().getTimeInMillis();
}
public static Date toDate(XMLGregorianCalendar xmlCal) {
return xmlCal != null ? new Date(xmlCal.toGregorianCalendar().getTimeInMillis()) : null;
}
public static Duration createDuration(long durationInMilliSeconds) {
return getDatatypeFactory().newDuration(durationInMilliSeconds);
}
public static Duration createDuration(String lexicalRepresentation) {
return lexicalRepresentation != null ? getDatatypeFactory().newDuration(lexicalRepresentation) : null;
}
public static Duration createDuration(boolean isPositive, int years, int months, int days, int hours, int minutes, int seconds) {
return getDatatypeFactory().newDuration(isPositive, years, months, days, hours, minutes, seconds);
}
/**
* Parse PolyString from DOM element.
*/
private static PolyString polyStringToJava(Element polyStringElement) throws SchemaException {
Element origElement = DOMUtil.getChildElement(polyStringElement, PrismConstants.POLYSTRING_ELEMENT_ORIG_QNAME);
if (origElement == null) {
// Check for a special syntactic short-cut. If the there are no child elements use the text content of the
// element as the value of orig
if (DOMUtil.hasChildElements(polyStringElement)) {
throw new SchemaException("Missing element "+PrismConstants.POLYSTRING_ELEMENT_ORIG_QNAME+" in polystring element "+
DOMUtil.getQName(polyStringElement));
}
String orig = polyStringElement.getTextContent();
return new PolyString(orig);
}
String orig = origElement.getTextContent();
String norm = null;
Element normElement = DOMUtil.getChildElement(polyStringElement, PrismConstants.POLYSTRING_ELEMENT_NORM_QNAME);
if (normElement != null) {
norm = normElement.getTextContent();
}
return new PolyString(orig, norm);
}
/**
* Serialize PolyString to DOM element.
*/
private static void polyStringToElement(Element polyStringElement, PolyString polyString) {
if (polyString.getOrig() != null) {
Element origElement = DOMUtil.createSubElement(polyStringElement, PrismConstants.POLYSTRING_ELEMENT_ORIG_QNAME);
origElement.setTextContent(polyString.getOrig());
}
if (polyString.getNorm() != null) {
Element origElement = DOMUtil.createSubElement(polyStringElement, PrismConstants.POLYSTRING_ELEMENT_NORM_QNAME);
origElement.setTextContent(polyString.getNorm());
}
}
public static <T> T toXmlEnum(Class<T> expectedType, String stringValue) {
if (stringValue == null) {
return null;
}
for (T enumConstant: expectedType.getEnumConstants()) {
Field field;
try {
field = expectedType.getField(((Enum)enumConstant).name());
} catch (SecurityException e) {
throw new IllegalArgumentException("Error getting field from '"+enumConstant+"' in "+expectedType, e);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Error getting field from '"+enumConstant+"' in "+expectedType, e);
}
XmlEnumValue annotation = field.getAnnotation(XmlEnumValue.class);
if (annotation.value().equals(stringValue)) {
return enumConstant;
}
}
throw new IllegalArgumentException("No enum value '"+stringValue+"' in "+expectedType);
}
public static <T> String fromXmlEnum(T enumValue) {
if (enumValue == null) {
return null;
}
String fieldName = ((Enum)enumValue).name();
Field field;
try {
field = enumValue.getClass().getField(fieldName);
} catch (SecurityException e) {
throw new IllegalArgumentException("Error getting field from "+enumValue, e);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Error getting field from "+enumValue, e);
}
XmlEnumValue annotation = field.getAnnotation(XmlEnumValue.class);
return annotation.value();
}
public static XMLGregorianCalendar addDuration(XMLGregorianCalendar now, Duration duration) {
XMLGregorianCalendar later = createXMLGregorianCalendar(toMillis(now));
later.add(duration);
return later;
}
public static XMLGregorianCalendar addDuration(XMLGregorianCalendar now, String duration) {
XMLGregorianCalendar later = createXMLGregorianCalendar(toMillis(now));
later.add(createDuration(duration));
return later;
}
public static XMLGregorianCalendar addMillis(XMLGregorianCalendar now, long duration) {
return createXMLGregorianCalendar(toMillis(now) + duration);
}
public static String formatDateXml(Date date) {
return createXMLGregorianCalendar(date).toXMLFormat();
}
public static int compare(XMLGregorianCalendar o1, XMLGregorianCalendar o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
return o1.compare(o2);
}
}