/*
* Copyright (c) 2010-2015 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.repo.sql.data.common.any;
import com.evolveum.midpoint.prism.*;
import com.evolveum.midpoint.prism.marshaller.PrismBeanInspector;
import com.evolveum.midpoint.prism.polystring.PolyString;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.repo.sql.query.QueryException;
import com.evolveum.midpoint.repo.sql.type.XMLGregorianCalendarType;
import com.evolveum.midpoint.repo.sql.util.DtoTranslationException;
import com.evolveum.midpoint.repo.sql.util.RUtil;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.prism.xml.ns._public.types_3.PolyStringType;
import org.apache.commons.lang.Validate;
import org.w3c.dom.Element;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.*;
/**
* @author lazyman
*/
public class RAnyConverter {
private enum ValueType {
BOOLEAN, LONG, STRING, DATE, POLY_STRING;
}
private static final Trace LOGGER = TraceManager.getTrace(RAnyConverter.class);
private static final Map<QName, ValueType> TYPE_MAP = new HashMap<QName, ValueType>();
private PrismContext prismContext;
static {
TYPE_MAP.put(DOMUtil.XSD_BOOLEAN, ValueType.BOOLEAN);
TYPE_MAP.put(DOMUtil.XSD_INT, ValueType.LONG);
TYPE_MAP.put(DOMUtil.XSD_LONG, ValueType.LONG);
TYPE_MAP.put(DOMUtil.XSD_SHORT, ValueType.LONG);
TYPE_MAP.put(DOMUtil.XSD_INTEGER, ValueType.STRING);
TYPE_MAP.put(DOMUtil.XSD_DECIMAL, ValueType.STRING);
TYPE_MAP.put(DOMUtil.XSD_STRING, ValueType.STRING);
TYPE_MAP.put(DOMUtil.XSD_DOUBLE, ValueType.STRING);
TYPE_MAP.put(DOMUtil.XSD_FLOAT, ValueType.STRING);
TYPE_MAP.put(DOMUtil.XSD_DATETIME, ValueType.DATE);
TYPE_MAP.put(PolyStringType.COMPLEX_TYPE, ValueType.POLY_STRING);
}
public RAnyConverter(PrismContext prismContext) {
this.prismContext = prismContext;
}
//todo assignment parameter really messed up this method, proper interfaces must be introduced later [lazyman]
public Set<RAnyValue> convertToRValue(Item item, boolean assignment) throws DtoTranslationException {
Validate.notNull(item, "Object for converting must not be null.");
Validate.notNull(item.getDefinition(), "Item '" + item.getElementName() + "' without definition can't be saved.");
ItemDefinition definition = item.getDefinition();
Set<RAnyValue> rValues = new HashSet<>();
if (!isIndexed(definition, prismContext)) {
return rValues;
}
try {
RAnyValue rValue;
List<PrismValue> values = item.getValues();
for (PrismValue value : values) {
if (value instanceof PrismPropertyValue) {
PrismPropertyValue propertyValue = (PrismPropertyValue) value;
//todo omg, do something with this!!! [lazyman]
switch (getValueType(definition.getTypeName())) {
case BOOLEAN: {
Boolean repoValue = extractValue(propertyValue, Boolean.class);
if (assignment) {
rValue = new RAExtBoolean(repoValue);
} else {
rValue = new ROExtBoolean(repoValue);
}
break;
}
case LONG: {
Long repoValue = extractValue(propertyValue, Long.class);
if (assignment) {
rValue = new RAExtLong(repoValue);
} else {
rValue = new ROExtLong(repoValue);
}
break;
}
case DATE: {
Timestamp repoValue = extractValue(propertyValue, Timestamp.class);
if (assignment) {
rValue = new RAExtDate(repoValue);
} else {
rValue = new ROExtDate(repoValue);
}
break;
}
case POLY_STRING: {
PolyString repoValue = extractValue(propertyValue, PolyString.class);
if (assignment) {
rValue = new RAExtPolyString(repoValue);
} else {
rValue = new ROExtPolyString(repoValue);
}
break;
}
case STRING:
default: {
String repoValue = extractValue(propertyValue, String.class);
if (assignment) {
rValue = new RAExtString(repoValue);
} else {
rValue = new ROExtString(repoValue);
}
}
}
} else if (value instanceof PrismReferenceValue) {
if (assignment) {
PrismReferenceValue referenceValue = (PrismReferenceValue) value;
rValue = RAExtReference.createReference(referenceValue);
} else {
PrismReferenceValue referenceValue = (PrismReferenceValue) value;
rValue = ROExtReference.createReference(referenceValue);
}
} else if (value == null) {
continue; // shouldn't occur anyway
} else {
// shouldn't get here because if isIndexed test above
throw new AssertionError("Wrong value type: " + value);
}
rValue.setName(RUtil.qnameToString(definition.getName()));
rValue.setType(RUtil.qnameToString(definition.getTypeName()));
rValue.setValueType(getValueType(value.getParent()));
rValue.setDynamic(definition.isDynamic());
rValues.add(rValue);
}
} catch (Exception ex) {
throw new DtoTranslationException("Exception when translating " + item + ": " + ex.getMessage(), ex);
}
return rValues;
}
private static String getEnumStringValue(Enum<?> realValue) {
return PrismBeanInspector.findEnumFieldValueUncached(realValue.getClass(), realValue.toString());
}
private static boolean isIndexed(ItemDefinition definition, PrismContext prismContext) {
if (definition instanceof PrismContainerDefinition) {
return false;
}
if (definition instanceof PrismReferenceDefinition) {
return true; // TODO make reference indexing configurable
}
if (!(definition instanceof PrismPropertyDefinition)) {
throw new UnsupportedOperationException("Unknown definition type '"
+ definition + "', can't say if it's indexed or not.");
}
PrismPropertyDefinition pDefinition = (PrismPropertyDefinition) definition;
if (pDefinition.isIndexed() != null) {
return pDefinition.isIndexed();
}
return isIndexedByDefault(definition, prismContext);
}
private static boolean isIndexedByDefault(ItemDefinition definition, PrismContext prismContext) {
QName type = definition.getTypeName();
if (DOMUtil.XSD_DATETIME.equals(type)
|| DOMUtil.XSD_INT.equals(type)
|| DOMUtil.XSD_LONG.equals(type)
|| DOMUtil.XSD_SHORT.equals(type)
|| DOMUtil.XSD_INTEGER.equals(type)
|| DOMUtil.XSD_DOUBLE.equals(type)
|| DOMUtil.XSD_FLOAT.equals(type)
|| DOMUtil.XSD_STRING.equals(type)
|| DOMUtil.XSD_DECIMAL.equals(type)
|| DOMUtil.XSD_BOOLEAN.equals(type)
|| PolyStringType.COMPLEX_TYPE.equals(type)) {
return true;
}
Collection<? extends TypeDefinition> typeDefinitions = prismContext.getSchemaRegistry()
.findTypeDefinitionsByType(definition.getTypeName());
if (typeDefinitions.isEmpty() || typeDefinitions.size() > 1) {
return false; // shouldn't occur
}
TypeDefinition typeDef = typeDefinitions.iterator().next();
if (typeDef instanceof SimpleTypeDefinition) {
SimpleTypeDefinition simpleTypeDef = (SimpleTypeDefinition) typeDef;
return DOMUtil.XSD_STRING.equals(simpleTypeDef.getBaseTypeName())
&& simpleTypeDef.getDerivationMethod() == SimpleTypeDefinition.DerivationMethod.RESTRICTION;
} else {
return false;
}
}
private RValueType getValueType(Itemable itemable) {
Validate.notNull(itemable, "Value parent must not be null.");
if (!(itemable instanceof Item)) {
throw new IllegalArgumentException("Item type '" + itemable.getClass() + "' not supported in 'any' now.");
}
return RValueType.getTypeFromItemClass(((Item) itemable).getClass());
}
private <T> T extractValue(PrismPropertyValue value, Class<T> returnType) throws SchemaException {
ItemDefinition definition = value.getParent().getDefinition();
//todo raw types
Object object = value.getValue();
if (object instanceof Element) {
object = getRealRepoValue(definition, (Element) object);
} else {
object = getAggregatedRepoObject(object);
}
if (returnType.isAssignableFrom(object.getClass())) {
return (T) object;
}
throw new IllegalStateException("Can't extract value for saving from prism property value\n" + value);
}
private static ValueType getValueType(QName qname) {
if (qname == null) {
return ValueType.STRING;
}
ValueType type = TYPE_MAP.get(qname);
if (type == null) {
return ValueType.STRING;
}
return type;
}
public void convertFromRValue(RAnyValue value, PrismContainerValue any) throws DtoTranslationException {
Validate.notNull(value, "Value for converting must not be null.");
Validate.notNull(any, "Parent prism container value must not be null.");
try {
Item<?,?> item = any.findOrCreateItem(RUtil.stringToQName(value.getName()), value.getValueType().getItemClass());
if (item == null) {
throw new DtoTranslationException("Couldn't create item for value '" + value.getName() + "'.");
}
addValueToItem(value, item);
} catch (Exception ex) {
if (ex instanceof DtoTranslationException) {
throw (DtoTranslationException) ex;
}
throw new DtoTranslationException(ex.getMessage(), ex);
}
}
private void addValueToItem(RAnyValue value, Item item) throws SchemaException {
Object realValue = createRealValue(value, item.getDefinition().getTypeName());
if (!(value instanceof ROExtReference) && realValue == null) {
throw new SchemaException("Real value must not be null. Some error occurred when adding value "
+ value + " to item " + item);
}
switch (value.getValueType()) {
case REFERENCE:
PrismReferenceValue referenceValue = ROExtReference.createReference((ROExtReference) value);
item.add(referenceValue);
break;
case PROPERTY:
PrismPropertyValue propertyValue = new PrismPropertyValue(realValue, null, null);
item.add(propertyValue);
break;
case OBJECT:
case CONTAINER:
//todo implement
throw new UnsupportedOperationException("Not implemented yet.");
}
}
/**
* Method restores aggregated object type to its real type, e.g. number 123.1 is type of double, but was
* saved as string. This method takes RAnyValue instance and creates 123.1 double from string based on
* provided definition.
*
* @param rValue
* @return
* @throws SchemaException
*/
private Object createRealValue(RAnyValue rValue, QName type) throws SchemaException {
if (rValue instanceof ROExtReference || rValue instanceof RAExtReference) {
//this is special case, reference doesn't have value, it only has a few properties (oid, filter, etc.)
return null;
}
Object value = rValue.getValue();
if (rValue instanceof ROExtDate || rValue instanceof RAExtDate) {
if (value instanceof Date) {
return XMLGregorianCalendarType.asXMLGregorianCalendar((Date) value);
}
} else if (rValue instanceof ROExtLong || rValue instanceof RAExtLong) {
if (DOMUtil.XSD_LONG.equals(type)) {
return value;
} else if (DOMUtil.XSD_INT.equals(type)) {
return ((Long) value).intValue();
} else if (DOMUtil.XSD_SHORT.equals(type)) {
return ((Long) value).shortValue();
}
} else if (rValue instanceof ROExtString || rValue instanceof RAExtString) {
if (DOMUtil.XSD_STRING.equals(type)) {
return value;
} else if (DOMUtil.XSD_DOUBLE.equals(type)) {
return Double.parseDouble((String) value);
} else if (DOMUtil.XSD_FLOAT.equals(type)) {
return Float.parseFloat((String) value);
} else if (DOMUtil.XSD_INTEGER.equals(type)) {
return new BigInteger((String) value);
} else if (DOMUtil.XSD_DECIMAL.equals(type)) {
return new BigDecimal((String) value);
}
} else if (rValue instanceof ROExtPolyString) {
ROExtPolyString poly = (ROExtPolyString) rValue;
return new PolyString(poly.getValue(), poly.getNorm());
} else if (rValue instanceof RAExtPolyString) {
RAExtPolyString poly = (RAExtPolyString) rValue;
return new PolyString(poly.getValue(), poly.getNorm());
} else if (rValue instanceof RAExtBoolean || rValue instanceof ROExtBoolean) {
return rValue.getValue();
}
LOGGER.trace("Couldn't create real value of type '{}' from '{}'",
new Object[]{type, rValue.getValue()});
throw new IllegalStateException("Can't create real value of type '" + type
+ "' from value saved in DB as '" + rValue.getClass().getSimpleName() + "'.");
}
/**
* This method provides extension type (in real it's table) string for definition and value
* defined as parameters.
*
* @param definition
* @param prismContext
* @return One of "strings", "longs", "dates", "clobs"
* @throws SchemaException
*/
public static String getAnySetType(ItemDefinition definition, PrismContext prismContext) throws
SchemaException, QueryException {
if (!isIndexed(definition, prismContext)) {
throw new QueryException("Can't query non-indexed value, definition " + definition);
}
QName typeName = definition.getTypeName();
ValueType valueType = getValueType(typeName);
switch (valueType) {
case BOOLEAN:
return "booleans";
case DATE:
return "dates";
case LONG:
return "longs";
case STRING:
default:
return "strings";
}
}
/**
* This method provides transformation of {@link Element} value to its object form, e.g. <value>1</value> to
* {@link Integer} number 1. It's based on element definition from schema registry or xsi:type attribute
* in that element.
*
* Expects only property values (references are handled at other place).
*
* @param definition
* @param value
* @return
*/
public static Object getRealRepoValue(ItemDefinition definition, Element value) throws SchemaException {
ValueType willBeSaveAs = definition == null ? null : getValueType(definition.getTypeName());
QName typeName = definition == null ? DOMUtil.resolveXsiType(value) : definition.getTypeName();
Validate.notNull(typeName, "Definition was not defined for element value '"
+ DOMUtil.getQNameWithoutPrefix(value) + "' and it doesn't have xsi:type.");
Object object;
if (ValueType.STRING.equals(willBeSaveAs)) {
if (DOMUtil.listChildElements(value).isEmpty()) {
//simple values
return value.getTextContent();
} else {
//composite elements or containers
return DOMUtil.serializeDOMToString(value);
}
} else {
object = XmlTypeConverter.toJavaValue(value, typeName);
}
object = getAggregatedRepoObject(object);
if (object == null) {
throw new IllegalStateException("Can't extract value for saving from prism property value\n" + value);
}
return object;
}
/**
* Method provides aggregation of some java types (only simple types, which are indexed)
*
* @param object
* @return aggregated object
*/
public static Object getAggregatedRepoObject(Object object) {
//check float/double to string
if (object instanceof Float) {
object = object.toString();
} else if (object instanceof Double) {
object = object.toString();
} else if (object instanceof BigInteger) {
object = object.toString();
} else if (object instanceof BigDecimal)
object = object.toString();
//check short/integer to long
if (object instanceof Short) {
object = ((Short) object).longValue();
} else if (object instanceof Integer) {
object = ((Integer) object).longValue();
}
//check gregorian calendar, xmlgregorian calendar to date
if (object instanceof GregorianCalendar) {
object = ((GregorianCalendar) object).getTime();
} else if (object instanceof XMLGregorianCalendar) {
object = XMLGregorianCalendarType.asDate(((XMLGregorianCalendar) object));
}
if (object instanceof Date) {
object = new Timestamp(((Date) object).getTime());
}
//if object instance of boolean, nothing to do
if (object instanceof Enum<?>) {
object = getEnumStringValue((Enum<?>) object);
}
return object;
}
}