diff --git a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomIterativeReader.java b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomIterativeReader.java new file mode 100644 index 00000000000..aba8de12897 --- /dev/null +++ b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomIterativeReader.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020 Evolveum and contributors + * + * This work is dual-licensed under the Apache License 2.0 + * and European Union Public License. See LICENSE file for details. + */ + +package com.evolveum.midpoint.prism.impl.lex.dom; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import com.evolveum.midpoint.prism.schema.SchemaRegistry; + +import org.apache.commons.io.IOUtils; +import org.codehaus.staxmate.dom.DOMConverter; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.evolveum.midpoint.prism.ParserSource; +import com.evolveum.midpoint.prism.impl.lex.LexicalProcessor; +import com.evolveum.midpoint.prism.impl.xnode.RootXNodeImpl; +import com.evolveum.midpoint.util.DOMUtil; +import com.evolveum.midpoint.util.QNameUtil; +import com.evolveum.midpoint.util.exception.SchemaException; +import com.evolveum.midpoint.util.exception.SystemException; + +/** + * + */ +class DomIterativeReader { + + private final ParserSource source; + private final LexicalProcessor.RootXNodeHandler handler; + private final SchemaRegistry schemaRegistry; + + DomIterativeReader(ParserSource source, LexicalProcessor.RootXNodeHandler handler, SchemaRegistry schemaRegistry) { + this.source = source; + this.handler = handler; + this.schemaRegistry = schemaRegistry; + } + + // code taken from Validator class + public void readObjectsIteratively() throws SchemaException, IOException { + InputStream is = source.getInputStream(); + XMLStreamReader stream = null; + try { + stream = getXMLInputFactory().createXMLStreamReader(is); + + int eventType = stream.nextTag(); + if (eventType != XMLStreamConstants.START_ELEMENT) { + throw new SystemException("StAX Malfunction?"); + } + DOMConverter domConverter = new DOMConverter(); + Map rootNamespaceDeclarations = new HashMap<>(); + + QName objectsMarker = schemaRegistry.getPrismContext().getObjectsElementName(); + if (objectsMarker != null && !QNameUtil.match(stream.getName(), objectsMarker)) { + readSingleObjectIteratively(stream, rootNamespaceDeclarations, domConverter, handler); + } + for (int i = 0; i < stream.getNamespaceCount(); i++) { + rootNamespaceDeclarations.put(stream.getNamespacePrefix(i), stream.getNamespaceURI(i)); + } + while (stream.hasNext()) { + eventType = stream.next(); + if (eventType == XMLStreamConstants.START_ELEMENT) { + if (!readSingleObjectIteratively(stream, rootNamespaceDeclarations, domConverter, handler)) { + return; + } + } + } + } catch (XMLStreamException ex) { + String lineInfo = stream != null + ? " on line " + stream.getLocation().getLineNumber() + : ""; + throw new SchemaException( + "Exception while parsing XML" + lineInfo + ": " + ex.getMessage(), ex); + } finally { + if (source.closeStreamAfterParsing()) { + IOUtils.closeQuietly(is); + } + } + + } + + private boolean readSingleObjectIteratively( + XMLStreamReader stream, Map rootNamespaceDeclarations, + DOMConverter domConverter, LexicalProcessor.RootXNodeHandler handler) + throws XMLStreamException, SchemaException { + Document objectDoc = domConverter.buildDocument(stream); + Element objectElement = DOMUtil.getFirstChildElement(objectDoc); + DOMUtil.setNamespaceDeclarations(objectElement, rootNamespaceDeclarations); + RootXNodeImpl rootNode = new DomReader(objectElement, schemaRegistry).read(); + return handler.handleData(rootNode); + } + + private XMLInputFactory getXMLInputFactory() { + XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); + xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); + // TODO: cache? static? prism context? + return xmlInputFactory; + } +} diff --git a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalProcessor.java b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalProcessor.java index d6326325b4b..2cd06c92e5a 100644 --- a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalProcessor.java +++ b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalProcessor.java @@ -10,22 +10,14 @@ import java.io.IOException; import java.io.InputStream; import java.util.*; -import java.util.Map.Entry; import java.util.regex.Pattern; import javax.xml.namespace.QName; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamConstants; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamReader; + +import com.evolveum.midpoint.prism.marshaller.XNodeProcessorEvaluationMode; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.Validate; -import org.apache.commons.lang3.StringUtils; -import org.codehaus.staxmate.dom.DOMConverter; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -34,15 +26,12 @@ import com.evolveum.midpoint.prism.impl.lex.LexicalProcessor; import com.evolveum.midpoint.prism.impl.lex.LexicalUtils; import com.evolveum.midpoint.prism.impl.xnode.*; -import com.evolveum.midpoint.prism.marshaller.XNodeProcessorEvaluationMode; import com.evolveum.midpoint.prism.schema.SchemaRegistry; import com.evolveum.midpoint.prism.xnode.MapXNode; import com.evolveum.midpoint.prism.xnode.RootXNode; import com.evolveum.midpoint.prism.xnode.XNode; import com.evolveum.midpoint.util.DOMUtil; -import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; -import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; @@ -50,8 +39,6 @@ public class DomLexicalProcessor implements LexicalProcessor { public static final Trace LOGGER = TraceManager.getTrace(DomLexicalProcessor.class); - private static final QName SCHEMA_ELEMENT_QNAME = DOMUtil.XSD_SCHEMA_ELEMENT; - @NotNull private final SchemaRegistry schemaRegistry; public DomLexicalProcessor(@NotNull SchemaRegistry schemaRegistry) { @@ -62,16 +49,17 @@ public DomLexicalProcessor(@NotNull SchemaRegistry schemaRegistry) { @Override public RootXNodeImpl read(@NotNull ParserSource source, @NotNull ParsingContext parsingContext) throws SchemaException, IOException { if (source instanceof ParserElementSource) { - return read(((ParserElementSource) source).getElement()); - } - - InputStream is = source.getInputStream(); - try { - Document document = DOMUtil.parse(is); - return read(document); - } finally { - if (source.closeStreamAfterParsing()) { - IOUtils.closeQuietly(is); + return new DomReader(((ParserElementSource) source).getElement(), schemaRegistry) + .read(); + } else { + InputStream is = source.getInputStream(); + try { + Document document = DOMUtil.parse(is); + return new DomReader(document, schemaRegistry).read(); + } finally { + if (source.closeStreamAfterParsing()) { + IOUtils.closeQuietly(is); + } } } } @@ -82,7 +70,7 @@ public List readObjects(@NotNull ParserSource source, @NotNull Pa InputStream is = source.getInputStream(); try { Document document = DOMUtil.parse(is); - return readObjects(document); + return new DomReader(document, schemaRegistry).readObjects(); } finally { if (source.closeStreamAfterParsing()) { IOUtils.closeQuietly(is); @@ -90,327 +78,14 @@ public List readObjects(@NotNull ParserSource source, @NotNull Pa } } - private XMLInputFactory getXMLInputFactory() { - XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); - xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); - xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); - // TODO: cache? static? prism context? - return xmlInputFactory; - } - - // code taken from Validator class @Override public void readObjectsIteratively(@NotNull ParserSource source, @NotNull ParsingContext parsingContext, RootXNodeHandler handler) throws SchemaException, IOException { - - InputStream is = source.getInputStream(); - XMLStreamReader stream = null; - try { - stream = getXMLInputFactory().createXMLStreamReader(is); - - int eventType = stream.nextTag(); - if (eventType != XMLStreamConstants.START_ELEMENT) { - throw new SystemException("StAX Malfunction?"); - } - DOMConverter domConverter = new DOMConverter(); - Map rootNamespaceDeclarations = new HashMap<>(); - - QName objectsMarker = schemaRegistry.getPrismContext().getObjectsElementName(); - if (objectsMarker != null && !QNameUtil.match(stream.getName(), objectsMarker)) { - readSingleObjectIteratively(stream, rootNamespaceDeclarations, domConverter, handler); - } - for (int i = 0; i < stream.getNamespaceCount(); i++) { - rootNamespaceDeclarations.put(stream.getNamespacePrefix(i), stream.getNamespaceURI(i)); - } - while (stream.hasNext()) { - eventType = stream.next(); - if (eventType == XMLStreamConstants.START_ELEMENT) { - if (!readSingleObjectIteratively(stream, rootNamespaceDeclarations, domConverter, handler)) { - return; - } - } - } - } catch (XMLStreamException ex) { - String lineInfo = stream != null - ? " on line " + stream.getLocation().getLineNumber() - : ""; - throw new SchemaException( - "Exception while parsing XML" + lineInfo + ": " + ex.getMessage(), ex); - } finally { - if (source.closeStreamAfterParsing()) { - IOUtils.closeQuietly(is); - } - } - } - - private boolean readSingleObjectIteratively( - XMLStreamReader stream, Map rootNamespaceDeclarations, - DOMConverter domConverter, RootXNodeHandler handler) - throws XMLStreamException, SchemaException { - Document objectDoc = domConverter.buildDocument(stream); - Element objectElement = DOMUtil.getFirstChildElement(objectDoc); - DOMUtil.setNamespaceDeclarations(objectElement, rootNamespaceDeclarations); - RootXNodeImpl rootNode = read(objectElement); - return handler.handleData(rootNode); - } - - private List readObjects(Document document) throws SchemaException { - Element root = DOMUtil.getFirstChildElement(document); - QName objectsMarker = schemaRegistry.getPrismContext().getObjectsElementName(); - if (objectsMarker != null && !QNameUtil.match(DOMUtil.getQName(root), objectsMarker)) { - return Collections.singletonList(read(root)); - } else { - List rv = new ArrayList<>(); - for (Element child : DOMUtil.listChildElements(root)) { - rv.add(read(child)); - } - return rv; - } - } - - @NotNull - public RootXNodeImpl read(Document document) throws SchemaException { - Element rootElement = DOMUtil.getFirstChildElement(document); - return read(rootElement); - } - - @NotNull - public RootXNodeImpl read(Element rootElement) throws SchemaException { - QName rootElementName = DOMUtil.getQName(rootElement); - QName rootElementXsiType = DOMUtil.resolveXsiType(rootElement); - - RootXNodeImpl xroot = new RootXNodeImpl(rootElementName); - extractCommonMetadata(rootElement, rootElementXsiType, xroot); - XNodeImpl xnode = parseElementContent(rootElement, rootElementName, false); - xroot.setSubnode(xnode); - return xroot; - } - - private void extractCommonMetadata(Element element, QName xsiType, XNodeImpl xnode) - throws SchemaException { - - if (xsiType != null) { - xnode.setTypeQName(xsiType); - xnode.setExplicitTypeDeclaration(true); - } - - String maxOccursString = element.getAttributeNS( - PrismConstants.A_MAX_OCCURS.getNamespaceURI(), - PrismConstants.A_MAX_OCCURS.getLocalPart()); - if (!StringUtils.isBlank(maxOccursString)) { - int maxOccurs = parseMultiplicity(maxOccursString, element); - xnode.setMaxOccurs(maxOccurs); - } - } - - private int parseMultiplicity(String maxOccursString, Element element) throws SchemaException { - if (PrismConstants.MULTIPLICITY_UNBONUNDED.equals(maxOccursString)) { - return -1; - } - if (maxOccursString.startsWith("-")) { - return -1; - } - if (StringUtils.isNumeric(maxOccursString)) { - return Integer.parseInt(maxOccursString); - } else { - throw new SchemaException("Expected numeric value for " + PrismConstants.A_MAX_OCCURS.getLocalPart() - + " attribute on " + DOMUtil.getQName(element) + " but got " + maxOccursString); - } - } - - /** - * Parses the content of the element. - * - * @param knownElementName Pre-fetched element name. Might be null (this is expected if storeElementName is true). - */ - @NotNull - private XNodeImpl parseElementContent(@NotNull Element element, QName knownElementName, boolean storeElementName) throws SchemaException { - XNodeImpl node; - - QName xsiType = DOMUtil.resolveXsiType(element); - QName elementName = knownElementName != null ? knownElementName : DOMUtil.getQName(element); - - if (DOMUtil.hasChildElements(element) || DOMUtil.hasApplicationAttributes(element)) { - if (isList(element, elementName, xsiType)) { - node = parseElementContentToList(element); - } else { - node = parseElementContentToMap(element); - } - } else if (DOMUtil.isMarkedAsIncomplete(element)) { - // Note that it is of no use to check for "incomplete" on non-leaf elements. In XML the incomplete attribute - // must be attached to an empty element. - node = new IncompleteMarkerXNodeImpl(); - } else { - node = parsePrimitiveElement(element); - } - if (storeElementName) { - node.setElementName(elementName); - } - extractCommonMetadata(element, xsiType, node); - return node; - } - - // all the sub-elements should be compatible (this is not enforced here, however) - private ListXNodeImpl parseElementContentToList(Element element) throws SchemaException { - if (DOMUtil.hasApplicationAttributes(element)) { - throw new SchemaException("List should have no application attributes: " + element); - } - return parseElementList(DOMUtil.listChildElements(element), null, true); + new DomIterativeReader(source, handler, schemaRegistry) + .readObjectsIteratively(); } - private MapXNodeImpl parseElementContentToMap(Element element) throws SchemaException { - MapXNodeImpl xmap = new MapXNodeImpl(); - - // Attributes - for (Attr attr : DOMUtil.listApplicationAttributes(element)) { - QName attrQName = DOMUtil.getQName(attr); - XNodeImpl subnode = parseAttributeValue(attr); - xmap.put(attrQName, subnode); - } - - // Sub-elements - QName lastElementName = null; - List lastElements = null; - for (Element childElement : DOMUtil.listChildElements(element)) { - QName childName = DOMUtil.getQName(childElement); - if (!match(childName, lastElementName)) { - parseSubElementsGroupAsMapEntry(xmap, lastElementName, lastElements); - lastElementName = childName; - lastElements = new ArrayList<>(); - } - lastElements.add(childElement); - } - parseSubElementsGroupAsMapEntry(xmap, lastElementName, lastElements); - return xmap; - } - - private boolean isList(@NotNull Element element, @NotNull QName elementName, @Nullable QName xsiType) { - String isListAttribute = DOMUtil.getAttribute(element, DOMUtil.IS_LIST_ATTRIBUTE_NAME); - if (StringUtils.isNotEmpty(isListAttribute)) { - return Boolean.parseBoolean(isListAttribute); - } - // enable this after schema registry is optional (now it's mandatory) -// if (schemaRegistry == null) { -// return false; -// } - - SchemaRegistry.IsList fromSchema = schemaRegistry.isList(xsiType, elementName); - if (fromSchema != SchemaRegistry.IsList.MAYBE) { - return fromSchema == SchemaRegistry.IsList.YES; - } - - // checking the content - if (DOMUtil.hasApplicationAttributes(element)) { - return false; // TODO - or should we fail in this case? - } - //System.out.println("Elements are compatible: " + DOMUtil.listChildElements(element) + ": " + rv); - return elementsAreCompatible(DOMUtil.listChildElements(element)); - } - - private boolean elementsAreCompatible(List elements) { - QName unified = null; - for (Element element : elements) { - QName root = getHierarchyRoot(DOMUtil.getQName(element)); - if (unified == null) { - unified = root; - } else if (!QNameUtil.match(unified, root)) { - return false; - } else if (QNameUtil.noNamespace(unified) && QNameUtil.hasNamespace(root)) { - unified = root; - } - } - return true; - } - - private QName getHierarchyRoot(QName name) { - ItemDefinition def = schemaRegistry.findItemDefinitionByElementName(name); - if (def == null || !def.isHeterogeneousListItem()) { - return name; - } else { - return def.getSubstitutionHead(); - } - } - - private boolean match(QName name, QName existing) { - if (existing == null) { - return false; - } else { - return QNameUtil.match(name, existing); - } - } - - // All elements share the same elementName - private void parseSubElementsGroupAsMapEntry(MapXNodeImpl xmap, QName elementName, List elements) throws SchemaException { - if (elements == null || elements.isEmpty()) { - return; - } - XNodeImpl xsub; - // We really want to have equals here, not match - // we want to be very explicit about namespace here - if (elementName.equals(SCHEMA_ELEMENT_QNAME)) { - if (elements.size() == 1) { - xsub = parseSchemaElement(elements.iterator().next()); - } else { - throw new SchemaException("Too many schema elements"); - } - } else if (elements.size() == 1) { - xsub = parseElementContent(elements.get(0), elementName, false); - } else { - xsub = parseElementList(elements, elementName, false); - } - xmap.merge(elementName, xsub); - } - - /** - * Parses elements that should form the list. - *

- * Either they have the same element name, or they are stored as a sub-elements of "list" parent element. - */ - @NotNull - @Contract("!null, null, false -> fail") - private ListXNodeImpl parseElementList(List elements, QName elementName, boolean storeElementNames) throws SchemaException { - if (!storeElementNames && elementName == null) { - throw new IllegalArgumentException("When !storeElementNames the element name must be specified"); - } - ListXNodeImpl xlist = new ListXNodeImpl(); - for (Element element : elements) { - xlist.add(parseElementContent(element, elementName, storeElementNames)); - } - return xlist; - } - - // @pre element has no children nor application attributes - private PrimitiveXNodeImpl parsePrimitiveElement(@NotNull Element element) { - PrimitiveXNodeImpl xnode = new PrimitiveXNodeImpl<>(); - xnode.setValueParser(new ElementValueParser<>(element)); - return xnode; - } - - static T processIllegalArgumentException(String value, QName typeName, IllegalArgumentException e, - XNodeProcessorEvaluationMode mode) { - if (mode == XNodeProcessorEvaluationMode.COMPAT) { - LOGGER.warn("Value of '{}' couldn't be parsed as '{}' -- interpreting as null because of COMPAT mode set", value, - typeName, e); - return null; - } else { - throw e; - } - } - - private PrimitiveXNodeImpl parseAttributeValue(@NotNull Attr attr) { - PrimitiveXNodeImpl xnode = new PrimitiveXNodeImpl<>(); - xnode.setValueParser(new AttributeValueParser<>(attr)); - xnode.setAttribute(true); - return xnode; - } - - @NotNull - private SchemaXNodeImpl parseSchemaElement(Element schemaElement) { - SchemaXNodeImpl xschema = new SchemaXNodeImpl(); - xschema.setSchemaElement(schemaElement); - return xschema; - } @Override public boolean canRead(@NotNull File file) { @@ -428,17 +103,17 @@ public boolean canRead(@NotNull String dataString) { @NotNull @Override public String write(@NotNull XNode xnode, @NotNull QName rootElementName, SerializationContext serializationContext) throws SchemaException { - DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry, serializationContext); + DomLexicalWriter writer = new DomLexicalWriter(schemaRegistry, serializationContext); RootXNodeImpl xroot = LexicalUtils.createRootXNode((XNodeImpl) xnode, rootElementName); - Element element = serializer.serialize(xroot); + Element element = writer.write(xroot); return DOMUtil.serializeDOMToString(element); } @NotNull @Override public String write(@NotNull RootXNode xnode, SerializationContext serializationContext) throws SchemaException { - DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry, serializationContext); - Element element = serializer.serialize((RootXNodeImpl) xnode); + DomLexicalWriter writer = new DomLexicalWriter(schemaRegistry, serializationContext); + Element element = writer.write((RootXNodeImpl) xnode); return DOMUtil.serializeDOMToString(element); } @@ -451,56 +126,35 @@ public String write(@NotNull List roots, @Nullable SerializationC @NotNull public Element writeXRootListToElement(@NotNull List roots) throws SchemaException { - DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry, null); - return serializer.serialize(roots); - } - - public Element serializeXMapToElement(MapXNodeImpl xmap, QName elementName) throws SchemaException { - DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry, null); - return serializer.serializeToElement(xmap, elementName); + return new DomLexicalWriter(schemaRegistry, null) + .write(roots); } - private Element serializeXPrimitiveToElement(PrimitiveXNodeImpl xprim, QName elementName) throws SchemaException { - DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry, null); - return serializer.serializeXPrimitiveToElement(xprim, elementName); + public Element writeXMapToElement(MapXNodeImpl xmap, QName elementName) throws SchemaException { + return new DomLexicalWriter(schemaRegistry, null) + .writeToElement(xmap, elementName); } @NotNull public Element writeXRootToElement(@NotNull RootXNodeImpl xroot) throws SchemaException { - DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry, null); - return serializer.serialize(xroot); + return new DomLexicalWriter(schemaRegistry, null) + .write(xroot); } - private Element serializeToElement(XNodeImpl xnode, QName elementName) throws SchemaException { - Validate.notNull(xnode); - Validate.notNull(elementName); - if (xnode instanceof MapXNodeImpl) { - return serializeXMapToElement((MapXNodeImpl) xnode, elementName); - } else if (xnode instanceof PrimitiveXNodeImpl) { - return serializeXPrimitiveToElement((PrimitiveXNodeImpl) xnode, elementName); - } else if (xnode instanceof RootXNodeImpl) { - return writeXRootToElement((RootXNodeImpl) xnode); - } else if (xnode instanceof ListXNodeImpl) { - ListXNodeImpl xlist = (ListXNodeImpl) xnode; - if (xlist.size() == 0) { - return null; - } else if (xlist.size() > 1) { - throw new IllegalArgumentException("Cannot serialize list xnode with more than one item: " + xlist); - } else { - return serializeToElement(xlist.get(0), elementName); - } - } else { - throw new IllegalArgumentException("Cannot serialize " + xnode + " to element"); - } + public Element serializeSingleElementMapToElement(MapXNode map) throws SchemaException { + return new DomLexicalWriter(schemaRegistry, null) + .writeSingleElementMapToElement(map); } - public Element serializeSingleElementMapToElement(MapXNode map) throws SchemaException { - MapXNodeImpl xmap = (MapXNodeImpl) map; - if (xmap == null || xmap.isEmpty()) { + // TODO move somewhere + static T processIllegalArgumentException(String value, QName typeName, IllegalArgumentException e, + XNodeProcessorEvaluationMode mode) { + if (mode == XNodeProcessorEvaluationMode.COMPAT) { + LOGGER.warn("Value of '{}' couldn't be parsed as '{}' -- interpreting as null because of COMPAT mode set", value, + typeName, e); return null; + } else { + throw e; } - Entry subEntry = xmap.getSingleSubEntry(xmap.toString()); - Element parent = serializeToElement(xmap, subEntry.getKey()); - return DOMUtil.getFirstChildElement(parent); } } diff --git a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalWriter.java b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalWriter.java index b28311a4969..48655c64d7a 100644 --- a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalWriter.java +++ b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomLexicalWriter.java @@ -20,13 +20,14 @@ import com.evolveum.midpoint.prism.impl.xnode.SchemaXNodeImpl; import com.evolveum.midpoint.prism.impl.xnode.XNodeImpl; import com.evolveum.midpoint.prism.xnode.IncompleteMarkerXNode; +import com.evolveum.midpoint.prism.xnode.MapXNode; import com.evolveum.midpoint.util.DOMUtil; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.prism.xml.ns._public.types_3.ItemPathType; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -81,12 +82,12 @@ private void initializeWithExistingDocument(Document document) { } @NotNull - public Element serialize(@NotNull RootXNodeImpl rootxnode) throws SchemaException { + public Element write(@NotNull RootXNodeImpl rootxnode) throws SchemaException { initialize(); return serializeInternal(rootxnode, null); } - public Element serialize(@NotNull List roots) throws SchemaException { + public Element write(@NotNull List roots) throws SchemaException { initialize(); QName aggregateElementName = schemaRegistry.getPrismContext().getObjectsElementName(); if (aggregateElementName == null) { @@ -155,7 +156,7 @@ private void addDefaultNamespaceDeclaration(Element top) { DOMUtil.setNamespaceDeclaration(top, "", namespaces.iterator().next()); } - Element serializeToElement(MapXNodeImpl xmap, QName elementName) throws SchemaException { + Element writeToElement(MapXNodeImpl xmap, QName elementName) throws SchemaException { initialize(); Element element = createElement(elementName, null); serializeMap(xmap, element); @@ -226,7 +227,7 @@ private void serializeExplicitList(ListXNodeImpl list, Element parent) throws Sc DOMUtil.setAttributeValue(parent, DOMUtil.IS_LIST_ATTRIBUTE_NAME, "true"); } - Element serializeXPrimitiveToElement(PrimitiveXNodeImpl xprim, QName elementName) throws SchemaException { + private Element writeXPrimitiveToElement(PrimitiveXNodeImpl xprim, QName elementName) throws SchemaException { initialize(); Element parent = DOMUtil.createElement(doc, new QName("fake","fake")); serializePrimitiveElementOrAttribute(xprim, parent, elementName, false); @@ -396,8 +397,41 @@ private QName setQNamePrefixExplicit(QName qname) { return namespacePrefixMapper.setQNamePrefixExplicit(qname); } - public Element serializeItemPathTypeToElement(ItemPathType itemPathType, QName elementName, Document ownerDocument) { + private Element serializeItemPathTypeToElement(ItemPathType itemPathType, QName elementName, Document ownerDocument) { return ItemPathHolder.serializeToElement(itemPathType.getItemPath(), elementName, ownerDocument); } + Element writeSingleElementMapToElement(MapXNode map) throws SchemaException { + MapXNodeImpl xmap = (MapXNodeImpl) map; + if (xmap == null || xmap.isEmpty()) { + return null; + } + Entry subEntry = xmap.getSingleSubEntry(xmap.toString()); + Element parent = writeToElement2(xmap, subEntry.getKey()); + return DOMUtil.getFirstChildElement(parent); + } + + // TODO correlate to writeToElement + private Element writeToElement2(XNodeImpl xnode, QName elementName) throws SchemaException { + Validate.notNull(xnode); + Validate.notNull(elementName); + if (xnode instanceof MapXNodeImpl) { + return writeToElement((MapXNodeImpl) xnode, elementName); + } else if (xnode instanceof PrimitiveXNodeImpl) { + return writeXPrimitiveToElement((PrimitiveXNodeImpl) xnode, elementName); + } else if (xnode instanceof RootXNodeImpl) { + return write((RootXNodeImpl) xnode); + } else if (xnode instanceof ListXNodeImpl) { + ListXNodeImpl xlist = (ListXNodeImpl) xnode; + if (xlist.size() == 0) { + return null; + } else if (xlist.size() > 1) { + throw new IllegalArgumentException("Cannot serialize list xnode with more than one item: " + xlist); + } else { + return writeToElement2(xlist.get(0), elementName); + } + } else { + throw new IllegalArgumentException("Cannot serialize " + xnode + " to element"); + } + } } diff --git a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomReader.java b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomReader.java new file mode 100644 index 00000000000..cef9fd163e8 --- /dev/null +++ b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/lex/dom/DomReader.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2020 Evolveum and contributors + * + * This work is dual-licensed under the Apache License 2.0 + * and European Union Public License. See LICENSE file for details. + */ + +package com.evolveum.midpoint.prism.impl.lex.dom; + +import com.evolveum.midpoint.prism.ItemDefinition; +import com.evolveum.midpoint.prism.PrismConstants; +import com.evolveum.midpoint.prism.impl.xnode.*; + +import com.evolveum.midpoint.prism.schema.SchemaRegistry; +import com.evolveum.midpoint.util.DOMUtil; +import com.evolveum.midpoint.util.QNameUtil; +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.lang3.StringUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * TODO + */ +class DomReader { + + public static final Trace LOGGER = TraceManager.getTrace(DomLexicalProcessor.class); + + private static final QName SCHEMA_ELEMENT_QNAME = DOMUtil.XSD_SCHEMA_ELEMENT; + private static final String VALUE_LOCAL_PART = "_value"; + + private final Element root; + private final SchemaRegistry schemaRegistry; + + DomReader(@NotNull Element root, SchemaRegistry schemaRegistry) { + this.root = root; + this.schemaRegistry = schemaRegistry; + } + + DomReader(Document document, SchemaRegistry schemaRegistry) { + this(DOMUtil.getFirstChildElement(document), schemaRegistry); + } + + @NotNull + List readObjects() throws SchemaException { + QName objectsMarker = schemaRegistry.getPrismContext().getObjectsElementName(); + if (objectsMarker != null && !QNameUtil.match(DOMUtil.getQName(root), objectsMarker)) { + return Collections.singletonList(read()); + } else { + List rv = new ArrayList<>(); + for (Element child : DOMUtil.listChildElements(root)) { + rv.add(new DomReader(child, schemaRegistry).read()); + } + return rv; + } + } + + @NotNull RootXNodeImpl read() throws SchemaException { + QName rootElementName = DOMUtil.getQName(root); + + RootXNodeImpl xroot = new RootXNodeImpl(rootElementName); + XNodeImpl xnode = readElementContent(root, rootElementName, false); + xroot.setSubnode(xnode); + return xroot; + } + + private void extractCommonMetadata(Element element, QName xsiType, XNodeImpl xnode) + throws SchemaException { + + if (xsiType != null) { + xnode.setTypeQName(xsiType); + xnode.setExplicitTypeDeclaration(true); + } + + String maxOccursString = element.getAttributeNS( + PrismConstants.A_MAX_OCCURS.getNamespaceURI(), + PrismConstants.A_MAX_OCCURS.getLocalPart()); + if (!StringUtils.isBlank(maxOccursString)) { + int maxOccurs = parseMultiplicity(maxOccursString, element); + xnode.setMaxOccurs(maxOccurs); + } + } + + private int parseMultiplicity(String maxOccursString, Element element) throws SchemaException { + if (PrismConstants.MULTIPLICITY_UNBONUNDED.equals(maxOccursString)) { + return -1; + } + if (maxOccursString.startsWith("-")) { + return -1; + } + if (StringUtils.isNumeric(maxOccursString)) { + return Integer.parseInt(maxOccursString); + } else { + throw new SchemaException("Expected numeric value for " + PrismConstants.A_MAX_OCCURS.getLocalPart() + + " attribute on " + DOMUtil.getQName(element) + " but got " + maxOccursString); + } + } + + /** + * Reads the content of the element. + * + * @param knownElementName Pre-fetched element name. Might be null (this is expected if storeElementName is true). + */ + @NotNull + private XNodeImpl readElementContent(@NotNull Element element, QName knownElementName, boolean storeElementName) throws SchemaException { + XNodeImpl node; + + QName xsiType = DOMUtil.resolveXsiType(element); + QName elementName = knownElementName != null ? knownElementName : DOMUtil.getQName(element); + +// Element valueChild = DOMUtil.getChildElement(element, VALUE_LOCAL_PART); +// if (valueChild != null) { +// node = readElementContent(valueChild, elementName, false); +// } else + if (DOMUtil.hasChildElements(element) || DOMUtil.hasApplicationAttributes(element)) { + if (isList(element, elementName, xsiType)) { + node = parseElementContentToList(element); + } else { + node = parseElementContentToMap(element); + } + } else if (DOMUtil.isMarkedAsIncomplete(element)) { + // Note that it is of no use to check for "incomplete" on non-leaf elements. In XML the incomplete attribute + // must be attached to an empty element. + node = new IncompleteMarkerXNodeImpl(); + } else { + node = parsePrimitiveElement(element); + } + if (storeElementName) { + node.setElementName(elementName); + } + extractCommonMetadata(element, xsiType, node); + return node; + } + + // all the sub-elements should be compatible (this is not enforced here, however) + private ListXNodeImpl parseElementContentToList(Element element) throws SchemaException { + if (DOMUtil.hasApplicationAttributes(element)) { + throw new SchemaException("List should have no application attributes: " + element); + } + return parseElementList(DOMUtil.listChildElements(element), null, true); + } + + private MapXNodeImpl parseElementContentToMap(Element element) throws SchemaException { + MapXNodeImpl xmap = new MapXNodeImpl(); + + // Attributes + for (Attr attr : DOMUtil.listApplicationAttributes(element)) { + QName attrQName = DOMUtil.getQName(attr); + XNodeImpl subnode = parseAttributeValue(attr); + xmap.put(attrQName, subnode); + } + + // Sub-elements + QName lastElementName = null; + List lastElements = null; + for (Element childElement : DOMUtil.listChildElements(element)) { + QName childName = DOMUtil.getQName(childElement); + if (!match(childName, lastElementName)) { + parseSubElementsGroupAsMapEntry(xmap, lastElementName, lastElements); + lastElementName = childName; + lastElements = new ArrayList<>(); + } + lastElements.add(childElement); + } + parseSubElementsGroupAsMapEntry(xmap, lastElementName, lastElements); + return xmap; + } + + private boolean isList(@NotNull Element element, @NotNull QName elementName, @Nullable QName xsiType) { + String isListAttribute = DOMUtil.getAttribute(element, DOMUtil.IS_LIST_ATTRIBUTE_NAME); + if (StringUtils.isNotEmpty(isListAttribute)) { + return Boolean.parseBoolean(isListAttribute); + } + // enable this after schema registry is optional (now it's mandatory) +// if (schemaRegistry == null) { +// return false; +// } + + SchemaRegistry.IsList fromSchema = schemaRegistry.isList(xsiType, elementName); + if (fromSchema != SchemaRegistry.IsList.MAYBE) { + return fromSchema == SchemaRegistry.IsList.YES; + } + + // checking the content + if (DOMUtil.hasApplicationAttributes(element)) { + return false; // TODO - or should we fail in this case? + } + //System.out.println("Elements are compatible: " + DOMUtil.listChildElements(element) + ": " + rv); + return elementsAreCompatible(DOMUtil.listChildElements(element)); + } + + private boolean elementsAreCompatible(List elements) { + QName unified = null; + for (Element element : elements) { + QName root = getHierarchyRoot(DOMUtil.getQName(element)); + if (unified == null) { + unified = root; + } else if (!QNameUtil.match(unified, root)) { + return false; + } else if (QNameUtil.noNamespace(unified) && QNameUtil.hasNamespace(root)) { + unified = root; + } + } + return true; + } + + private QName getHierarchyRoot(QName name) { + ItemDefinition def = schemaRegistry.findItemDefinitionByElementName(name); + if (def == null || !def.isHeterogeneousListItem()) { + return name; + } else { + return def.getSubstitutionHead(); + } + } + + private boolean match(QName name, QName existing) { + if (existing == null) { + return false; + } else { + return QNameUtil.match(name, existing); + } + } + + // All elements share the same elementName + private void parseSubElementsGroupAsMapEntry(MapXNodeImpl xmap, QName elementName, List elements) throws SchemaException { + if (elements == null || elements.isEmpty()) { + return; + } + XNodeImpl xsub; + // We really want to have equals here, not match + // we want to be very explicit about namespace here + if (elementName.equals(SCHEMA_ELEMENT_QNAME)) { + if (elements.size() == 1) { + xsub = parseSchemaElement(elements.iterator().next()); + } else { + throw new SchemaException("Too many schema elements"); + } + } else if (elements.size() == 1) { + xsub = readElementContent(elements.get(0), elementName, false); + } else { + xsub = parseElementList(elements, elementName, false); + } + xmap.merge(elementName, xsub); + } + + /** + * Parses elements that should form the list. + *

+ * Either they have the same element name, or they are stored as a sub-elements of "list" parent element. + */ + @NotNull + @Contract("!null, null, false -> fail") + private ListXNodeImpl parseElementList(List elements, QName elementName, boolean storeElementNames) throws SchemaException { + if (!storeElementNames && elementName == null) { + throw new IllegalArgumentException("When !storeElementNames the element name must be specified"); + } + ListXNodeImpl xlist = new ListXNodeImpl(); + for (Element element : elements) { + xlist.add(readElementContent(element, elementName, storeElementNames)); + } + return xlist; + } + + // @pre element has no children nor application attributes + private PrimitiveXNodeImpl parsePrimitiveElement(@NotNull Element element) { + PrimitiveXNodeImpl xnode = new PrimitiveXNodeImpl<>(); + xnode.setValueParser(new ElementValueParser<>(element)); + return xnode; + } + + private PrimitiveXNodeImpl parseAttributeValue(@NotNull Attr attr) { + PrimitiveXNodeImpl xnode = new PrimitiveXNodeImpl<>(); + xnode.setValueParser(new AttributeValueParser<>(attr)); + xnode.setAttribute(true); + return xnode; + } + + @NotNull + private SchemaXNodeImpl parseSchemaElement(Element schemaElement) { + SchemaXNodeImpl xschema = new SchemaXNodeImpl(); + xschema.setSchemaElement(schemaElement); + return xschema; + } +} diff --git a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/marshaller/JaxbDomHackImpl.java b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/marshaller/JaxbDomHackImpl.java index 6eeb3433b9d..3dffc34d907 100644 --- a/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/marshaller/JaxbDomHackImpl.java +++ b/infra/prism-impl/src/main/java/com/evolveum/midpoint/prism/impl/marshaller/JaxbDomHackImpl.java @@ -46,11 +46,11 @@ public class JaxbDomHackImpl implements JaxbDomHack { private static final Trace LOGGER = TraceManager.getTrace(JaxbDomHack.class); private PrismContext prismContext; - private DomLexicalProcessor domParser; + private DomLexicalProcessor domLexicalProcessor; - public JaxbDomHackImpl(DomLexicalProcessor domParser, PrismContext prismContext) { + public JaxbDomHackImpl(DomLexicalProcessor domLexicalProcessor, PrismContext prismContext) { super(); - this.domParser = domParser; + this.domLexicalProcessor = domLexicalProcessor; this.prismContext = prismContext; } @@ -216,7 +216,7 @@ public Object toAny(PrismValue value) throws SchemaException { if (pval.isRaw() && parent.getDefinition() == null) { XNodeImpl rawElement = (XNodeImpl) pval.getRawElement(); if (rawElement instanceof MapXNodeImpl) { - return domParser.serializeXMapToElement((MapXNodeImpl)rawElement, elementName); + return domLexicalProcessor.writeXMapToElement((MapXNodeImpl)rawElement, elementName); } else if (rawElement instanceof PrimitiveXNodeImpl) { PrimitiveXNodeImpl xprim = (PrimitiveXNodeImpl)rawElement; String stringValue = xprim.getStringValue(); diff --git a/infra/prism-impl/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java b/infra/prism-impl/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java index 46b82bb67a3..635f859a822 100644 --- a/infra/prism-impl/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java +++ b/infra/prism-impl/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java @@ -388,7 +388,7 @@ public void test600AccountBarbossa() throws Exception { )), names); } - @Test + @Test(enabled = false) public void test700UserAliceMetadata() throws Exception { given(); PrismContext prismContext = getPrismContext();