/* * Copyright (c) 2014 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.xnode; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import javax.xml.namespace.QName; import com.evolveum.midpoint.prism.marshaller.XNodeProcessorEvaluationMode; import com.evolveum.midpoint.prism.util.CloneUtil; import com.evolveum.midpoint.prism.util.JavaTypeConverter; import com.evolveum.midpoint.prism.xml.XsdTypeMapper; import com.evolveum.midpoint.util.*; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import org.apache.commons.lang.StringUtils; import com.evolveum.midpoint.prism.Visitor; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.util.exception.SchemaException; import org.apache.commons.lang.Validate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class PrimitiveXNode<T> extends XNode implements Serializable { private static final Trace LOGGER = TraceManager.getTrace(PrimitiveXNode.class); /* * Invariants: * - At most one of value-valueParser may be null. * - If value is non-null, super.typeName must be non-null. */ private T value; private ValueParser<T> valueParser; /** * If set to true then this primitive value either came from an attribute * or we prefer this to be represented as an attribute (if the target format * is capable of representing attributes) */ private boolean isAttribute = false; public PrimitiveXNode() { super(); } public PrimitiveXNode(T value) { super(); this.value = value; } private void parseValue(@NotNull QName typeName, XNodeProcessorEvaluationMode mode) throws SchemaException { Validate.notNull(typeName, "Cannot parse primitive XNode without knowing its type"); if (valueParser != null) { value = valueParser.parse(typeName, mode); // Necessary. It marks that the value is parsed. It also frees some memory. valueParser = null; } } public T getValue() { return value; } @Deprecated public T getParsedValue(@NotNull QName typeName) throws SchemaException { return getParsedValue(typeName, null, XNodeProcessorEvaluationMode.STRICT); } public T getParsedValue(@NotNull QName typeName, @Nullable Class<T> expectedClass) throws SchemaException { return getParsedValue(typeName, expectedClass, XNodeProcessorEvaluationMode.STRICT); } public T getParsedValue(@NotNull QName typeName, @Nullable Class<T> expectedClass, XNodeProcessorEvaluationMode mode) throws SchemaException { if (!isParsed()) { parseValue(typeName, mode); } if (JavaTypeConverter.isTypeCompliant(value, expectedClass)) { return value; } else { throw new SchemaException("Expected " + expectedClass + " but got " + value.getClass() + " instead. Value is " + value); } } public ValueParser<T> getValueParser() { return valueParser; } public void setValueParser(ValueParser<T> valueParser) { Validate.notNull(valueParser, "Value parser cannot be null"); this.valueParser = valueParser; this.value = null; } public void setValue(T value, QName typeQName) { if (value != null) { if (typeQName == null) { // last desperate attempt to determine type name from the value type typeQName = XsdTypeMapper.getJavaToXsdMapping(value.getClass()); if (typeQName == null) { throw new IllegalStateException("Cannot determine type QName for a value of '" + value + "'"); // todo show only class? (security/size reasons) } } this.setTypeQName(typeQName); } this.value = value; this.valueParser = null; } public boolean isParsed() { return valueParser == null; } public boolean isAttribute() { return isAttribute; } public void setAttribute(boolean isAttribute) { this.isAttribute = isAttribute; } public boolean isEmpty() { if (!isParsed()) { return valueParser.isEmpty(); } if (value == null) { return true; } if (value instanceof String) { return StringUtils.isBlank((String)value); } return false; } /** * Returns parsed value without actually changing node state from UNPARSED to PARSED * (if node is originally unparsed). * * Useful when we are not sure about the type name and do not want to record parsed * value based on wrong type name. */ public T getParsedValueWithoutRecording(QName typeName) throws SchemaException { Validate.notNull(typeName, "typeName"); if (isParsed()) { return value; } else { return valueParser.parse(typeName, XNodeProcessorEvaluationMode.STRICT); } } /** * Returns a value that is correctly string-formatted according * to its type definition. Works properly only if definition is set. */ public String getFormattedValue() { // if (getTypeQName() == null) { // throw new IllegalStateException("Cannot fetch formatted value if type definition is not set"); // } if (!isParsed()) { throw new IllegalStateException("Cannot fetch formatted value if the xnode is not parsed"); } return formatValue(value); } /** * Returns formatted parsed value without actually changing node state from UNPARSED to PARSED * (if node is originally unparsed). * * Useful e.g. to serialize nodes that have a type declaration but were not parsed yet. * * Experimental. Should be thought through yet. * * @return properly formatted value */ public String getGuessedFormattedValue() throws SchemaException { if (isParsed()) { return getFormattedValue(); } if (getTypeQName() == null) { throw new IllegalStateException("Cannot fetch formatted value if type definition is not set"); } // brutal hack - fix this! MID-3616 try { T value = valueParser.parse(getTypeQName(), XNodeProcessorEvaluationMode.STRICT); return formatValue(value); } catch (SchemaException e) { return (String) valueParser.parse(DOMUtil.XSD_STRING, XNodeProcessorEvaluationMode.COMPAT); } } private String formatValue(T value) { if (value instanceof PolyString) { return ((PolyString) value).getOrig(); } if (value instanceof QName) { return QNameUtil.qNameToUri((QName) value); } if (value instanceof DisplayableValue) { return ((DisplayableValue) value).getValue().toString(); } if (value != null && value.getClass().isEnum()){ return value.toString(); } return XmlTypeConverter.toXmlTextContent(value, null); } @Override public void accept(Visitor visitor) { visitor.visit(this); } @Override public String debugDump(int indent) { StringBuilder sb = new StringBuilder(); DebugUtil.indentDebugDump(sb, indent); valueToString(sb); String dumpSuffix = dumpSuffix(); if (dumpSuffix != null) { sb.append(dumpSuffix); } return sb.toString(); } @Override public String getDesc() { return "primitive"; } @Override public String toString() { StringBuilder sb = new StringBuilder("XNode(primitive:"); valueToString(sb); if (isAttribute) { sb.append(",attr"); } sb.append(")"); return sb.toString(); } private void valueToString(StringBuilder sb) { if (value == null) { sb.append("parser ").append(valueParser); } else { sb.append(PrettyPrinter.prettyPrint(value)); sb.append(" (").append(value.getClass()).append(")"); } } /** * Returns the value represented as string - in the best format that we can. * There is no guarantee that the returned value will be precise. * This method is used as a "last instance" if everything else fails. * Invocation of this method will not change the state of this xnode, e.g. * it will NOT cause it to be parsed. */ public String getStringValue() { if (isParsed()) { if (getTypeQName() != null) { return getFormattedValue(); } else { if (value == null) { return null; } else { return value.toString(); } } } else { return valueParser.getStringValue(); } } /** * This method is used with conjunction with getStringValue, typically when serializing unparsed values. * Because the string value can represent QName or ItemPath, we have to provide relevant namespace declarations. * * Because we cannot know for sure, we are allowed to return namespace declarations that are not actually used. * We should minimize number of such declarations, however. * * Current implementation simply grabs all potential namespace declarations and searches * the xnode's string value for any 'prefix:' substrings. I'm afraid it is all we can do for now. * * THIS METHOD SHOULD BE CALLED ONLY ON EITHER UNPARSED OR EMPTY NODES. * * @return */ public Map<String, String> getRelevantNamespaceDeclarations() { Map<String,String> retval = new HashMap<>(); if (isEmpty()) { return retval; } if (valueParser == null) { throw new IllegalStateException("getRelevantNamespaceDeclarations called on parsed primitive XNode: " + this); } Map<String,String> candidateNamespaces = valueParser.getPotentiallyRelevantNamespaces(); if (candidateNamespaces == null) { return retval; } String stringValue = getStringValue(); if (stringValue == null) { return retval; } for (Map.Entry<String,String> candidateNamespace : candidateNamespaces.entrySet()) { String prefix = candidateNamespace.getKey(); if (stringValue.contains(prefix+":")) { retval.put(candidateNamespace.getKey(), candidateNamespace.getValue()); } } return retval; } @Override public boolean equals(Object obj) { if (!(obj instanceof PrimitiveXNode)) { return false; } PrimitiveXNode other = (PrimitiveXNode) obj; if (other.isParsed() && isParsed()){ return value.equals(other.value); } else if (!other.isParsed() && !isParsed()){ // TODO consider problem with namespaces (if string value is QName/ItemPath its meaning can depend on namespace declarations that are placed outside the element) String thisStringVal = this.getStringValue(); String otherStringVal = other.getStringValue(); return thisStringVal.equals(otherStringVal); } else if (other.isParsed() && !isParsed()){ String thisStringValue = this.getStringValue(); String otherStringValue = String.valueOf(other.value); return otherStringValue.equals(thisStringValue); } else if (!other.isParsed() && isParsed()){ String thisStringValue = String.valueOf(value); String otherStringValue = other.getStringValue(); return thisStringValue.equals(otherStringValue); } return false; } /** * The basic idea of equals() is: * - if parsed, compare the value; * - if unparsed, compare getStringValue() * Therefore the hashcode is generated based on value (if parsed) or getStringValue() (if unparsed). */ @Override public int hashCode() { Object objectToHash; if (isParsed()) { objectToHash = value; } else { // TODO consider problem with namespaces (if string value is QName/ItemPath its meaning can depend on namespace declarations that are placed outside the element) objectToHash = getStringValue(); } return objectToHash != null ? objectToHash.hashCode() : 0; } PrimitiveXNode cloneInternal() { PrimitiveXNode clone; if (value != null) { // if we are parsed, things are much simpler clone = new PrimitiveXNode(CloneUtil.clone(getValue())); } else { clone = new PrimitiveXNode(); clone.valueParser = valueParser; // for the time being we simply don't clone the valueParser } clone.isAttribute = this.isAttribute; clone.copyCommonAttributesFrom(this); return clone; } }