/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.mappingsmodel.xml; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.Vector; import javax.xml.namespace.QName; import org.eclipse.persistence.tools.workbench.mappingsmodel.MWDataField; import org.eclipse.persistence.tools.workbench.mappingsmodel.MWModel; import org.eclipse.persistence.tools.workbench.mappingsmodel.ProblemConstants; import org.eclipse.persistence.tools.workbench.mappingsmodel.schema.MWSchemaContextComponent; import org.eclipse.persistence.tools.workbench.mappingsmodel.schema.MWSimpleTypeDefinition; import org.eclipse.persistence.tools.workbench.utility.CollectionTools; import org.eclipse.persistence.tools.workbench.utility.iterators.NullIterator; import org.eclipse.persistence.internal.helper.DatabaseField; import org.eclipse.persistence.oxm.XMLConstants; import org.eclipse.persistence.oxm.XMLDescriptor; import org.eclipse.persistence.oxm.XMLField; import org.eclipse.persistence.oxm.XMLUnionField; import org.eclipse.persistence.oxm.mappings.XMLDirectMapping; public final class MWXmlField extends MWModel implements MWDataField, MWXmlNode { // **************** Variables ********************************************* /** The string representation of this xpath */ private volatile String xpath = ""; public final static String XPATH_PROPERTY = "xpath"; /** Whether this xpath is aggregated into its parent element (xpath = ".") */ private volatile boolean aggregated = false; public final static String AGGREGATED_PROPERTY = "aggregated"; /** * Whether the field represented by this xpath should use xsi:type. * This xpath must be a "text()" field in order for this to be true. */ private volatile boolean typed; public final static String TYPED_PROPERTY = "typed"; /** * Whether this xml field should collate text data into a single field * as opposed to writing multiple fields, each with a single text item. * This is only appropriate for direct collection mappings. */ private volatile boolean useSingleNode = false; public final static String USE_SINGLE_NODE_PROPERTY = "useSingleNode"; /** The xpath steps resolved to by the xpath string */ private transient Vector xpathSteps; /** Whether the xpath string resolves to a valid schema location */ private transient boolean resolved; public final static String RESOLVED_PROPERTY = "resolved"; /** Whether the xpath string resolves correctly to valid positions */ private transient boolean validPosition = true; public final static String VALID_POSITION_PROPERTY = "validPosition"; /** Whether the xpath string resolves correctly to "text()" */ private transient boolean validText = true; public final static String VALID_TEXT_PROPERTY = "validText"; /** Used for text() xpaths */ public static final String TEXT = "text()"; /** Used for runtime conversion */ private transient static Map jdbcTypeMap; // **************** Constructors ****************************************** /** TopLink use only */ private MWXmlField() { super(); } public MWXmlField(MWXpathContext parent) { super(parent); } // **************** Initialization **************************************** protected void initialize() { super.initialize(); this.xpathSteps = new Vector(); } protected void addTransientAspectNamesTo(Set transientAspectNames) { super.addTransientAspectNamesTo(transientAspectNames); transientAspectNames.add(RESOLVED_PROPERTY); transientAspectNames.add(VALID_POSITION_PROPERTY); transientAspectNames.add(VALID_TEXT_PROPERTY); } // **************** Convenience ******************************************* public MWXpathContext getXpathContext() { return (MWXpathContext) this.getParent(); } public MWSchemaContextComponent schemaContext() { return this.getXpathContext().schemaContext(this); } public MWXpathSpec xpathSpec() { return this.getXpathContext().xpathSpec(this); } protected Iterator xpathSteps() { return this.xpathSteps.iterator(); } // **************** Xpath string ****************************************** public String getXpath() { return this.xpath; } public void setXpath(String newXpath) { if (newXpath == null) { newXpath = ""; } String oldXpath = this.xpath; this.xpath = newXpath; if (this.attributeValueHasChanged(oldXpath, newXpath)) { this.firePropertyChanged(XPATH_PROPERTY, oldXpath, newXpath); this.firePropertyChanged(FIELD_NAME_PROPERTY, newXpath); this.resolve(); if ( ! this.xpath.equals("")) { this.setAggregated(false); } if (! this.xpath.endsWith(TEXT)) { this.setTyped(false); } } } /** Return true if ends with "text()" */ public boolean isTextXpath() { return this.getXpath().endsWith(TEXT); } /** Return true if contains "@" */ public boolean isAttributeXpath() { return this.getXpath().indexOf('@') != -1; } /** * Return true if contains any positional information (e.g. "[1]") * (For now, will assume that if the string contains '[' or ']', it does.) */ public boolean isPositionalXpath() { if (this.isResolved()) { for (Iterator stream = this.xpathSteps(); stream.hasNext(); ) { if (((MWXpathStep) stream.next()).isPositional()) { return true; } } } return false; } /** Return true if this points to an attribute or to the text node of an element */ public boolean isDirect() { if (this.isResolved() && ! this.isAggregated()) { MWXpathStep lastStep = (MWXpathStep) this.xpathSteps.lastElement(); return lastStep.isAttribute() || lastStep.isText(); } return false; } /** * Return true is this points to a singular node. * e.g. The XPath "foo" is not singular if there can be multiple foo's. * e.g. The XPath "foo[2]" is always singular. * e.g. The XPath "bar/foo[2]" is not singular if there can be multiple bar's. */ public boolean isSingular() { boolean singular = true; for (Iterator stream = this.xpathSteps(); stream.hasNext(); ) { singular &= ((MWXpathStep) stream.next()).isSingular(); } return singular; } /** * Return true if this field "contains" the other field. * Basically this just means that the other field's xpath * "starts with" this field's xpath (plus a "/" character). * * This won't make sense in *some* cases. Obviously, an attribute * xml field can't contain anything, but generally speaking, we won't * end up with any valid xpaths that don't start with elements. */ public boolean containsXmlField(MWXmlField otherField) { return otherField.getXpath().startsWith(this.getXpath()); } // **************** Aggregated ******************************************** public boolean isAggregated() { return this.aggregated; } public void setAggregated(boolean newValue) { boolean oldValue = this.aggregated; this.aggregated = newValue; this.firePropertyChanged(AGGREGATED_PROPERTY, oldValue, newValue); if (newValue) { this.setXpath(""); } } // **************** Typed ************************************************* public boolean isTyped() { return this.typed; } public void setTyped(boolean newValue) { boolean oldValue = this.typed; this.typed = newValue; this.firePropertyChanged(TYPED_PROPERTY, oldValue, newValue); } // **************** Use single node *************************************** public boolean usesSingleNode() { return this.useSingleNode; } public void setUseSingleNode(boolean newValue) { boolean oldValue = this.useSingleNode; this.useSingleNode = newValue; this.firePropertyChanged(USE_SINGLE_NODE_PROPERTY, oldValue, newValue); } // **************** Resolution/Validation ********************************* public boolean isSpecified() { return ! "".equals(this.xpath) || this.isAggregated(); } public boolean isResolved() { return this.resolved; } /** Should only be used internally */ private void setResolved(boolean newValue) { boolean oldValue = this.resolved; this.resolved = newValue; this.firePropertyChanged(RESOLVED_PROPERTY, oldValue, newValue); } /** Should only be used internally */ private void setValidText(boolean newValue) { boolean oldValue = this.validText; this.validText = newValue; this.firePropertyChanged(VALID_TEXT_PROPERTY, oldValue, newValue); } /** Should only be used internally */ private void setValidPosition(boolean newValue) { boolean oldValue = this.validPosition; this.validPosition = newValue; this.firePropertyChanged(VALID_POSITION_PROPERTY, oldValue, newValue); } public boolean isValid() { return this.isResolved() && this.validText && this.validPosition; } // **************** Internal ********************************************** private void resolve() { this.xpathSteps.clear(); StringTokenizer tokenizer = new StringTokenizer(this.xpath, "/"); while (tokenizer.hasMoreTokens()) { this.xpathSteps.add(new MWXpathStep(this, tokenizer.nextToken())); } MWSchemaContextComponent schemaContext = this.schemaContext(); boolean resolved = this.isSpecified(); for (Iterator stream = this.xpathSteps(); resolved && stream.hasNext(); ) { MWXpathStep nextStep = (MWXpathStep) stream.next(); schemaContext = nextStep.resolveContext(schemaContext); resolved &= nextStep.isResolved(); } if (! resolved) { this.xpathSteps.clear(); } this.setResolved(resolved); this.validateXpath(); } private void resynchXpath() { // if we are currently resolved, update the string if (this.isResolved()) { String xpath = ""; for (Iterator stream = this.xpathSteps.iterator(); stream.hasNext(); ) { MWXpathStep nextStep = (MWXpathStep) stream.next(); nextStep.updateStepString(); xpath += nextStep.getStepString(); if (stream.hasNext()) { xpath += "/"; } } this.setXpath(xpath); } // otherwise, attempt to resolve else { this.resolve(); } } private void validateXpath() { if (this.isTextXpath()) { if (this.isResolved()) { this.validateTextXpath(); } else { // if it's not resolved, it can't be valid text this.setValidText(false); } } else { // if it's not text, it can't be invalid text this.setValidText(true); } if (this.isPositionalXpath()) { if (this.isResolved()) { this.validatePositionalXpath(); } else { // if it's not resolved, it can't be valid position this.setValidPosition(false); } } else { // if it's not positional, it can't be invalid position this.setValidPosition(true); } } /** Only for resolved text xpaths */ private void validateTextXpath() { boolean valid = true; for (Iterator stream = this.xpathSteps(); stream.hasNext(); ) { MWXpathStep step = (MWXpathStep) stream.next(); if (step.isText()) { valid &= step.isValid(); } } this.setValidText(valid); } /** Only for resolved positional xpaths */ private void validatePositionalXpath() { boolean valid = true; for (Iterator stream = this.xpathSteps(); stream.hasNext(); ) { MWXpathStep step = (MWXpathStep) stream.next(); if (step.isPositional()) { valid &= step.isValid(); } } this.setValidPosition(valid); } // **************** Model synchronization ********************************* /** @see MWXmlNode#resolveXpaths() */ public void resolveXpaths() { this.resolve(); } /** @see MWXmlNode#schemaChanged(SchemaChange) */ public void schemaChanged(SchemaChange change) { if (change.getChangeType() == SchemaChange.SCHEMA_STRUCTURE_CHANGED) { this.resolve(); } else if (change.getChangeType() == SchemaChange.SCHEMA_NAMESPACE_PREFIXES_CHANGED) { this.resynchXpath(); } } // **************** MWDataField handling ************************************** public String fieldName() { return this.xpath; } // **************** Problem handling ************************************** protected void addProblemsTo(List currentProblems) { super.addProblemsTo(currentProblems); this.checkXpath(currentProblems); } private void checkXpath(List currentProblems) { if (this.isResolved()) { if ( ! this.validText) { currentProblems.add(this.buildProblem(ProblemConstants.XPATH_NOT_VALID_TEXT, this.getXpath())); } if ( ! this.validPosition) { currentProblems.add(this.buildProblem(ProblemConstants.XPATH_NOT_VALID_POSITION, this.getXpath())); } } else { if ( ! "".equals(this.getXpath())) { currentProblems.add(this.buildProblem(ProblemConstants.XPATH_NOT_RESOLVED, this.getXpath())); } } } // **************** Runtime conversion ************************************ public String runtimeFieldName() { return this.xpath; } public DatabaseField runtimeField() { if (! this.isSpecified()) { return null; } XMLField runtimeField = this.buildRuntimeField(); runtimeField.setXPath(this.runtimeXpath()); return runtimeField; } public DatabaseField runtimeField(MWXmlField groupingElement) { if (! this.isSpecified()) { return null; } XMLField runtimeField = this.buildRuntimeField(); if (groupingElement.isSpecified()) { runtimeField.setXPath(this.runtimeXpath(groupingElement)); } else { runtimeField.setXPath(this.runtimeXpath()); } return runtimeField; } private XMLField buildRuntimeField() { XMLField runtimeField = null; Vector dataTypes = CollectionTools.vector(this.baseBuiltInTypes()); if (dataTypes.size() > 1) { runtimeField = new XMLUnionField(); this.adjustUnionSchemaTypes((XMLUnionField) runtimeField, dataTypes); } else { runtimeField = new XMLField(); this.adjustNonUnionSchemaTypes(runtimeField, dataTypes); } if (this.isTyped()) { runtimeField.setIsTypedTextField(true); } if (this.usesSingleNode()) { runtimeField.setUsesSingleNode(true); } return runtimeField; } private String runtimeXpath() { if (this.isAggregated()) { return "."; } else { return this.getXpath(); } } private String runtimeXpath(MWXmlField groupingElement) { String runtimeXpath = this.runtimeXpath(); if (groupingElement.containsXmlField(this)) { String groupingRuntimeXpath = groupingElement.getXpath(); if (runtimeXpath.length() > groupingRuntimeXpath.length()) { return runtimeXpath.substring(groupingRuntimeXpath.length() + 1); // account for the additional "/" } return runtimeXpath; } else { return runtimeXpath; } } private Iterator baseBuiltInTypes() { MWSchemaContextComponent component = this.xpathComponent(); if (component == null) { return NullIterator.instance(); } else { return component.baseBuiltInTypes(); } } /** Return the component "pointed to" by this xml field */ private MWSchemaContextComponent xpathComponent() { MWSchemaContextComponent xpathComponent = null; if (TEXT.equals(this.getXpath())) { xpathComponent = this.schemaContext(); } for (Iterator stream = this.xpathSteps(); stream.hasNext(); ) { MWXpathStep nextStep = (MWXpathStep) stream.next(); if (nextStep.xpathComponent() != null) { xpathComponent = nextStep.xpathComponent(); } } return xpathComponent; } private void adjustUnionSchemaTypes(XMLUnionField runtimeField, Vector baseBuiltInTypes) { if (! this.isDirect()) { return; } for (Iterator stream = baseBuiltInTypes.iterator(); stream.hasNext(); ) { MWSimpleTypeDefinition baseBuiltInType = (MWSimpleTypeDefinition) stream.next(); if (XMLConstants.SCHEMA_URL.equals(baseBuiltInType.getNamespaceUrl())) { QName schemaType = this.runtimeSchemaType(baseBuiltInType); List runtimeSchemaTypes = runtimeField.getSchemaTypes(); if (runtimeSchemaTypes == null || ! runtimeSchemaTypes.contains(schemaType)) { runtimeField.addSchemaType(schemaType); } } } } private void adjustNonUnionSchemaTypes(XMLField runtimeField, Vector baseBuiltInTypes) { if (! this.isDirect()) { return; } // really should only have at most one type here, but iterating makes it convenient // for the case that the vector is empty for (Iterator stream = baseBuiltInTypes.iterator(); stream.hasNext(); ) { MWSimpleTypeDefinition baseBuiltInType = (MWSimpleTypeDefinition) stream.next(); if (XMLConstants.SCHEMA_URL.equals(baseBuiltInType.getNamespaceUrl())) { QName runtimeSchemaType = this.runtimeSchemaType(baseBuiltInType); if (this.shouldSetNonUnionRuntimeSchemaType(runtimeSchemaType)) { runtimeField.setSchemaType(runtimeSchemaType); } } } } /** These are the only types that are important if there is only one schema type */ private boolean shouldSetNonUnionRuntimeSchemaType(QName runtimeSchemaType) { return XMLConstants.DATE_QNAME.equals(runtimeSchemaType) || XMLConstants.TIME_QNAME.equals(runtimeSchemaType) || XMLConstants.DATE_TIME_QNAME.equals(runtimeSchemaType) || XMLConstants.BASE_64_BINARY_QNAME.equals(runtimeSchemaType) || XMLConstants.HEX_BINARY_QNAME.equals(runtimeSchemaType); } private QName runtimeSchemaType(MWSimpleTypeDefinition baseBuiltInType) { String baseDataType = (String) this.jdbcTypeMap().get(baseBuiltInType.getName()); return new QName(XMLConstants.SCHEMA_URL, baseDataType); } private Map jdbcTypeMap() { if (jdbcTypeMap == null) { buildJdbcTypeMap(); } return jdbcTypeMap; } private static void buildJdbcTypeMap() { Map typeMap = new HashMap(); // ur-type typeMap.put("anySimpleType", "anySimpleType"); // primitive types typeMap.put("duration", "anySimpleType"); typeMap.put("dateTime", "dateTime"); // transforms as java.util.Calendar typeMap.put("time", "time"); // transforms as java.util.Calendar typeMap.put("date", "date"); // transforms as java.util.Calendar typeMap.put("gYear", "anySimpleType"); typeMap.put("gYearMonth", "anySimpleType"); typeMap.put("gMonth", "anySimpleType"); typeMap.put("gMonthDay", "anySimpleType"); typeMap.put("gDay", "anySimpleType"); typeMap.put("boolean", "boolean"); // transforms as boolean typeMap.put("base64Binary", "base64Binary"); // transforms as byte[] typeMap.put("hexBinary", "hexBinary"); // transforms as byte[] typeMap.put("anyURI", "anySimpleType"); typeMap.put("QName", "anySimpleType"); typeMap.put("NOTATION", "anySimpleType"); // should be ignored ? // string types typeMap.put("string", "anySimpleType"); typeMap.put("normalizedString", "anySimpleType"); typeMap.put("token", "anySimpleType"); typeMap.put("language", "anySimpleType"); typeMap.put("Name", "anySimpleType"); typeMap.put("NMTOKEN", "anySimpleType"); typeMap.put("NCName", "anySimpleType"); typeMap.put("ID", "anySimpleType"); typeMap.put("IDREF", "IDREF"); // transforms as java.lang.Object typeMap.put("IDREFS", "IDREFS"); // transforms as ???? typeMap.put("ENTITY", "anySimpleType"); // should be ignored ? // number types typeMap.put("float", "float"); // transforms as float typeMap.put("double", "double"); // transforms as double typeMap.put("decimal", "decimal"); // transforms as java.math.BigDecimal typeMap.put("integer", "integer"); // transforms as java.math.BigInteger typeMap.put("nonPositiveInteger", "integer"); // transforms as java.math.BigInteger typeMap.put("negativeInteger", "integer"); // transforms as java.math.BigInteger typeMap.put("nonNegativeInteger", "integer"); // transforms as java.math.BigInteger typeMap.put("positiveInteger", "integer"); // transforms as java.math.BigInteger typeMap.put("unsignedLong", "integer"); // transforms as java.math.BigInteger typeMap.put("unsignedInt", "unsignedInt"); // transforms as long typeMap.put("unsignedShort", "unsignedShort"); // transforms as int typeMap.put("unsignedByte", "unsignedByte"); // transforms as short typeMap.put("long", "long"); // transforms as long typeMap.put("int", "int"); // transforms as int typeMap.put("short", "short"); // transforms as short typeMap.put("byte", "byte"); // transforms as byte jdbcTypeMap = typeMap; } /** * It is assumed that xmlField1 and xmlField2 are both relative to this schema context. * * Return -1 if xmlField1 comes before xmlField2, +1 if it comes after, or 0 * if there is no order constraint imposed by the schema on the two fields. */ public static int compareSchemaOrder(MWXmlField xmlField1, MWXmlField xmlField2) { if (xmlField1 == xmlField2 || xmlField1 == null || xmlField2 == null || ! xmlField1.isResolved() || ! xmlField2.isResolved() ) { return 0; } else if (xmlField1.schemaContext() != xmlField2.schemaContext()) { throw new IllegalStateException( "XML field \"" + xmlField1.getXpath() + "\" and XML field \"" + xmlField2.getXpath() + "\" are not in the same context."); } else { return compareSchemaOrder(xmlField1.schemaContext(), xmlField1.xpathSteps(), xmlField2.xpathSteps()); } } public static int compareSchemaOrder(MWSchemaContextComponent contextComponent, Iterator xpathSteps1, Iterator xpathSteps2) { if (! xpathSteps1.hasNext() || ! xpathSteps2.hasNext()) { return 0; } MWXpathStep step1 = (MWXpathStep) xpathSteps1.next(); MWXpathStep step2 = (MWXpathStep) xpathSteps2.next(); int comparison = MWXpathStep.compareSchemaOrder(contextComponent, step1, step2); if (comparison == 0 && step1.xpathComponent() == step2.xpathComponent()) { return compareSchemaOrder(step1.xpathComponent(), xpathSteps1, xpathSteps2); } else { return comparison; } } // **************** printing/displaying *************************************** public void toString(StringBuffer sb) { super.toString(sb); sb.append("\"" + this.xpath + "\""); } public String displayString() { return this.xpath; } // **************** TopLink methods *************************************** public static XMLDescriptor buildDescriptor() { XMLDescriptor descriptor = new XMLDescriptor(); descriptor.setJavaClass(MWXmlField.class); ((XMLDirectMapping) descriptor.addDirectMapping("xpath", "text()")).setNullValue(""); ((XMLDirectMapping) descriptor.addDirectMapping("aggregated", "@aggregated")).setNullValue(Boolean.FALSE); ((XMLDirectMapping) descriptor.addDirectMapping("typed", "@typed")).setNullValue(Boolean.FALSE); return descriptor; } public void postProjectBuild() { super.postProjectBuild(); this.resolve(); } }