/* * Copyright (c) 2010-2016 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 java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import com.evolveum.midpoint.prism.ItemDefinition; import com.evolveum.midpoint.prism.PrismConstants; import com.evolveum.midpoint.prism.PrismContainerDefinition; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObjectDefinition; import com.evolveum.midpoint.prism.path.*; import com.evolveum.midpoint.util.QNameUtil; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.evolveum.midpoint.prism.xml.GlobalDynamicNamespacePrefixMapper; import com.evolveum.midpoint.util.DOMUtil; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; /** * Holds internal (parsed) form of midPoint-style XPath-like expressions. * It is able to retrieve/export these expressions from/to various forms (text, text in XML document, * XPathSegment list, prism path specification). * * Assumes relative XPath, but somehow can also work with absolute XPaths. * * @author semancik * @author mederly */ public class XPathHolder { private static final Trace LOGGER = TraceManager.getTrace(XPathHolder.class); public static final String DEFAULT_PREFIX = "c"; private boolean absolute; private List<XPathSegment> segments; Map<String, String> explicitNamespaceDeclarations; // Part 1: Import from external representations. /** * Sets "current node" Xpath. */ public XPathHolder() { absolute = false; segments = new ArrayList<XPathSegment>(); } // This should not really be used. There should always be a namespace public XPathHolder(String xpath) { parse(xpath, null, null); } public XPathHolder(String xpath, Map<String, String> namespaceMap) { parse(xpath, null, namespaceMap); } public XPathHolder(Element domElement) { String xpath = "."; if (null != domElement) { xpath = domElement.getTextContent(); } parse(xpath, domElement, null); } public XPathHolder(String xpath, Node domNode) { parse(xpath, domNode, null); } /** * Parses XPath-like expression (midPoint flavour), with regards to domNode from where the namespace declarations * (embedded in XML using xmlns attributes) are taken. * * @param xpath text representation of the XPath-like expression * @param domNode context (DOM node from which the expression was taken) * @param namespaceMap externally specified namespaces */ private void parse(String xpath, Node domNode, Map<String, String> namespaceMap) { segments = new ArrayList<>(); absolute = false; if (".".equals(xpath)) { return; } // Check for explicit namespace declarations. TrivialXPathParser parser = TrivialXPathParser.parse(xpath); explicitNamespaceDeclarations = parser.getNamespaceMap(); // Continue parsing with Xpath without the "preamble" xpath = parser.getPureXPathString(); String[] segArray = xpath.split("/"); for (int i = 0; i < segArray.length; i++) { if (segArray[i] == null || segArray[i].isEmpty()) { if (i == 0) { absolute = true; // ignore the first empty segment of absolute path continue; } else { throw new IllegalArgumentException("XPath " + xpath + " has an empty segment (number " + i + ")"); } } String segmentStr = segArray[i]; XPathSegment idValueFilterSegment; // is ID value filter attached to this segment? int idValuePosition = segmentStr.indexOf('['); if (idValuePosition >= 0) { if (!segmentStr.endsWith("]")) { throw new IllegalArgumentException("XPath " + xpath + " has a ID segment not ending with ']': '" + segmentStr + "'"); } String value = segmentStr.substring(idValuePosition+1, segmentStr.length()-1); segmentStr = segmentStr.substring(0, idValuePosition); idValueFilterSegment = new XPathSegment(value); } else { idValueFilterSegment = null; } // processing the rest (i.e. the first part) of the segment boolean variable = false; if (segmentStr.startsWith("$")) { // We have variable here variable = true; segmentStr = segmentStr.substring(1); } String[] qnameArray = segmentStr.split(":"); if (qnameArray.length > 2) { throw new IllegalArgumentException("Unsupported format: more than one colon in XPath segment: " + segArray[i]); } QName qname; if (qnameArray.length == 1 || qnameArray[1] == null || qnameArray[1].isEmpty()) { if (ParentPathSegment.SYMBOL.equals(qnameArray[0])) { qname = ParentPathSegment.QNAME; } else if (ObjectReferencePathSegment.SYMBOL.equals(qnameArray[0])) { qname = ObjectReferencePathSegment.QNAME; } else if (IdentifierPathSegment.SYMBOL.equals(qnameArray[0])) { qname = IdentifierPathSegment.QNAME; } else { // default namespace <= empty prefix String namespace = findNamespace(null, domNode, namespaceMap); qname = new QName(namespace, qnameArray[0]); } } else { String namespacePrefix = qnameArray[0]; String namespace = findNamespace(namespacePrefix, domNode, namespaceMap); if (namespace == null) { QNameUtil.reportUndeclaredNamespacePrefix(namespacePrefix, xpath); namespacePrefix = QNameUtil.markPrefixAsUndeclared(namespacePrefix); } qname = new QName(namespace, qnameArray[1], namespacePrefix); } segments.add(new XPathSegment(qname, variable)); if (idValueFilterSegment != null) { segments.add(idValueFilterSegment); } } } private String findNamespace(String prefix, Node domNode, Map<String, String> namespaceMap) { String ns = null; if (explicitNamespaceDeclarations != null) { if (prefix == null) { ns = explicitNamespaceDeclarations.get(""); } else { ns = explicitNamespaceDeclarations.get(prefix); } if (ns != null) { return ns; } } if (namespaceMap != null) { if (prefix == null) { ns = namespaceMap.get(""); } else { ns = namespaceMap.get(prefix); } if (ns != null) { return ns; } } if (domNode != null) { if (StringUtils.isNotEmpty(prefix)) { ns = domNode.lookupNamespaceURI(prefix); } else { // we don't want the default namespace declaration (xmlns="...") to propagate into path expressions // so here we do not try to obtain the namespace from the document } if (ns != null) { return ns; } } return ns; } public XPathHolder(List<XPathSegment> segments) { this(segments, false); } public XPathHolder(List<XPathSegment> segments, boolean absolute) { this.segments = new ArrayList<XPathSegment>(); for (XPathSegment segment : segments) { if (segment.getQName() != null && StringUtils.isEmpty(segment.getQName().getPrefix())) { QName qname = segment.getQName(); this.segments.add(new XPathSegment(new QName(qname.getNamespaceURI(), qname.getLocalPart()))); } else { this.segments.add(segment); } } // this.segments = segments; this.absolute = absolute; } public XPathHolder(QName... segmentQNames) { this.segments = new ArrayList<XPathSegment>(); for (QName segmentQName : segmentQNames) { XPathSegment segment = new XPathSegment(segmentQName); this.segments.add(segment); } this.absolute = false; } public XPathHolder(ItemPath propertyPath) { this.segments = new ArrayList<>(); for (ItemPathSegment segment: propertyPath.getSegments()) { XPathSegment xsegment; if (segment instanceof NameItemPathSegment) { boolean variable = ((NameItemPathSegment) segment).isVariable(); xsegment = new XPathSegment(((NameItemPathSegment)segment).getName(), variable); } else if (segment instanceof IdItemPathSegment) { xsegment = new XPathSegment(idToString(((IdItemPathSegment) segment).getId())); } else if (segment instanceof ObjectReferencePathSegment) { xsegment = new XPathSegment(PrismConstants.T_OBJECT_REFERENCE, false); } else if (segment instanceof ParentPathSegment) { xsegment = new XPathSegment(PrismConstants.T_PARENT, false); } else if (segment instanceof IdentifierPathSegment) { xsegment = new XPathSegment(PrismConstants.T_ID, false); } else { throw new IllegalStateException("Unknown segment: " + segment); } this.segments.add(xsegment); } this.explicitNamespaceDeclarations = propertyPath.getNamespaceMap(); this.absolute = false; } // Part 2: Export to external representations. public String getXPath() { StringBuilder sb = new StringBuilder(); // addPureXpath(sb); sb.append(getXPathWithDeclarations()); return sb.toString(); } public String getXPathWithoutDeclarations() { StringBuilder sb = new StringBuilder(); addPureXpath(sb); return sb.toString(); } public String getXPathWithDeclarations() { // StringBuilder sb = new StringBuilder(); // // addExplicitNsDeclarations(sb); // addPureXpath(sb); return getXPathWithDeclarations(false); // return sb.toString(); } public String getXPathWithDeclarations(boolean forceExplicitDeclaration) { StringBuilder sb = new StringBuilder(); addExplicitNsDeclarations(sb, forceExplicitDeclaration); addPureXpath(sb); return sb.toString(); } private void addPureXpath(StringBuilder sb) { if (!absolute && segments.isEmpty()) { // Empty segment list gives a "local node" XPath sb.append("."); return; } if (absolute) { sb.append("/"); } boolean first = true; for (XPathSegment seg : segments) { if (seg.isIdValueFilter()) { sb.append("["); sb.append(seg.getValue()); sb.append("]"); } else { if (!first) { sb.append("/"); } else { first = false; } if (seg.isVariable()) { sb.append("$"); } QName qname = seg.getQName(); if (ObjectReferencePathSegment.QNAME.equals(qname)) { sb.append(ObjectReferencePathSegment.SYMBOL); } else if (ParentPathSegment.QNAME.equals(qname)) { sb.append(ParentPathSegment.SYMBOL); } else if (IdentifierPathSegment.QNAME.equals(qname)) { sb.append(IdentifierPathSegment.SYMBOL); } else if (!StringUtils.isEmpty(qname.getPrefix())) { sb.append(qname.getPrefix() + ":" + qname.getLocalPart()); } else { if (StringUtils.isNotEmpty(qname.getNamespaceURI())) { String prefix = GlobalDynamicNamespacePrefixMapper.getPreferredPrefix(qname.getNamespaceURI()); seg.setQNamePrefix(prefix); // hack - we modify the path segment here (only the form, not the meaning), but nevertheless it's ugly sb.append(seg.getQName().getPrefix() + ":" + seg.getQName().getLocalPart()); } else { // no namespace, no prefix sb.append(qname.getLocalPart()); } } } } } public String toCanonicalPath(Class objectType, PrismContext prismContext) { StringBuilder sb = new StringBuilder("\\"); boolean first = true; PrismObjectDefinition objDef = null; if (objectType != null) { objDef = prismContext.getSchemaRegistry().findObjectDefinitionByCompileTimeClass(objectType); } ItemDefinition def = null; for (XPathSegment seg : segments) { if (seg.isIdValueFilter()) { //for now, we don't want to save concrete id, just the path continue; } else { QName qname = seg.getQName(); if (!first) { sb.append("\\"); if (StringUtils.isBlank(qname.getNamespaceURI()) && objDef != null) { if (def instanceof PrismContainerDefinition) { PrismContainerDefinition containerDef = (PrismContainerDefinition) def; def = containerDef.findItemDefinition(qname); } if (def != null) { qname = def.getName(); } } } else { if (StringUtils.isBlank(qname.getNamespaceURI()) && objDef != null) { def = objDef.findItemDefinition(qname); if (def != null) { qname = def.getName(); } } first = false; } sb.append(QNameUtil.qNameToUri(qname)); } } return sb.toString(); } public Map<String, String> getNamespaceMap() { Map<String, String> namespaceMap = new HashMap<String, String>(); Iterator<XPathSegment> iter = segments.iterator(); while (iter.hasNext()) { XPathSegment seg = iter.next(); QName qname = seg.getQName(); if (qname != null) { if (qname.getPrefix() != null && !qname.getPrefix().isEmpty()) { namespaceMap.put(qname.getPrefix(), qname.getNamespaceURI()); } // this code seems to be currently of no use // else { // // Default namespace // // HACK. See addPureXpath method // namespaceMap.put(DEFAULT_PREFIX, qname.getNamespaceURI()); // } } } return namespaceMap; } public Element toElement(String elementNamespace, String localElementName) { // TODO: is this efficient? try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder loader = factory.newDocumentBuilder(); return toElement(elementNamespace, localElementName, loader.newDocument()); } catch (ParserConfigurationException ex) { throw new AssertionError("Error on creating XML document " + ex.getMessage()); } } public Element toElement(QName elementQName, Document document) { return toElement(elementQName.getNamespaceURI(), elementQName.getLocalPart(), document); } // really ugly implementation... (ignores overall context of serialization, so produces <c:path> elements even if common is default namespace) TODO rework [med] public Element toElement(String elementNamespace, String localElementName, Document document) { Element element = document.createElementNS(elementNamespace, localElementName); if (!StringUtils.isBlank(elementNamespace)) { String prefix = GlobalDynamicNamespacePrefixMapper.getPreferredPrefix(elementNamespace); if (!StringUtils.isBlank(prefix)) { try { element.setPrefix(prefix); } catch (DOMException e) { throw new SystemException("Error setting XML prefix '"+prefix+"' to element {"+elementNamespace+"}"+localElementName+": "+e.getMessage(), e); } } } element.setTextContent(getXPathWithDeclarations()); Map<String, String> namespaceMap = getNamespaceMap(); if (namespaceMap != null) { for (Entry<String, String> entry : namespaceMap.entrySet()) { DOMUtil.setNamespaceDeclaration(element, entry.getKey(), entry.getValue()); } } return element; } public List<XPathSegment> toSegments() { // FIXME !!! return Collections.unmodifiableList(segments); } @NotNull public ItemPath toItemPath() { List<XPathSegment> xsegments = toSegments(); List<ItemPathSegment> segments = new ArrayList<ItemPathSegment>(xsegments.size()); for (XPathSegment segment : xsegments) { if (segment.isIdValueFilter()) { segments.add(new IdItemPathSegment(idToLong(segment.getValue()))); } else { QName qName = segment.getQName(); boolean variable = segment.isVariable(); segments.add(ItemPath.createSegment(qName, variable)); } } ItemPath path = new ItemPath(segments); path.setNamespaceMap(explicitNamespaceDeclarations); return path; } // Part 3: Various /** * Returns new XPath with a specified element prepended to the path. Useful * for "transposing" relative paths to a absolute root. * * @param parentPath * @return */ public XPathHolder transposedPath(QName parentPath) { XPathSegment segment = new XPathSegment(parentPath); List<XPathSegment> segments = new ArrayList<XPathSegment>(); segments.add(segment); return transposedPath(segments); } /** * Returns new XPath with a specified element prepended to the path. Useful * for "transposing" relative paths to a absolute root. * * @param parentPath * @return */ public XPathHolder transposedPath(List<XPathSegment> parentPath) { List<XPathSegment> allSegments = new ArrayList<XPathSegment>(); allSegments.addAll(parentPath); allSegments.addAll(toSegments()); return new XPathHolder(allSegments); } private void addExplicitNsDeclarations(StringBuilder sb, boolean forceExplicitDeclaration) { boolean emptyExplicit = false; if (explicitNamespaceDeclarations == null || explicitNamespaceDeclarations.isEmpty()) { // if (!forceExplicitDeclaration){ return; // } // throw new IllegalStateException("Expecting explicit namespace declaration."); } // if (!emptyExplicit){ for (String prefix : explicitNamespaceDeclarations.keySet()) { sb.append("declare "); if (prefix.equals("")) { sb.append("default namespace '"); sb.append(explicitNamespaceDeclarations.get(prefix)); sb.append("'; "); } else { sb.append("namespace "); sb.append(prefix); sb.append("='"); sb.append(explicitNamespaceDeclarations.get(prefix)); sb.append("'; "); } } // } else{ // for (XPathSegment segment : this.toSegments()){ // sb.append("declare "); // QName s = segment.getQName(); // if (s.getPrefix() // } // } } public boolean isEmpty() { return segments.isEmpty(); } @Override public String toString() { // TODO: more verbose toString later return getXPath(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (absolute ? 1231 : 1237); result = prime * result + ((segments == null) ? 0 : segments.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; // Special case if (obj instanceof QName) { if (segments.size() != 1) { return false; } XPathSegment segment = segments.get(0); return segment.getQName().equals((QName)obj); } if (getClass() != obj.getClass()) return false; XPathHolder other = (XPathHolder) obj; if (absolute != other.absolute) return false; if (segments == null) { if (other.segments != null) return false; } else if (!segments.equals(other.segments)) return false; return true; } /** * Returns true if this path is below a specified path. */ public boolean isBelow(XPathHolder path) { if (this.segments.size() < 1){ return false; } for(int i = 0; i < path.segments.size(); i++) { if (i > this.segments.size()) { // We have run beyond all of local segments, therefore // this path cannot be below specified path return false; } if (!this.segments.get(i).equals(path.segments.get(i))) { // Segments don't match. We are not below. return false; } } return true; } /** * Returns a list of segments that are the "tail" after specified path. * The path in the parameter is assumed to be a "superpath" to this path, e.i. * this path is below specified path. This method returns all the segments * of this path that are below the specified path. * Returns null if the assumption is false. */ public List<XPathSegment> getTail(XPathHolder path) { int i = 0; while(i < path.segments.size()) { if (i > this.segments.size()) { // We have run beyond all of local segments, therefore // this path cannot be below specified path return null; } if (!this.segments.get(i).equals(path.segments.get(i))) { // Segments don't match. We are not below. return null; } i++; } return segments.subList(i, this.segments.size()); } public static boolean isDefault(Element pathElement) { if (pathElement == null) { return true; } XPathHolder xpath = new XPathHolder(pathElement); if (xpath.isEmpty()) { return true; } return false; } private Long idToLong(String stringVal) { if (stringVal == null) { return null; } return Long.valueOf(stringVal); } private String idToString(Long longVal) { if (longVal == null) { return null; } return longVal.toString(); } }