/*
* 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.lex.dom;
import com.evolveum.midpoint.prism.*;
import com.evolveum.midpoint.prism.marshaller.XNodeProcessorEvaluationMode;
import com.evolveum.midpoint.prism.marshaller.XPathHolder;
import com.evolveum.midpoint.prism.lex.LexicalProcessor;
import com.evolveum.midpoint.prism.lex.LexicalUtils;
import com.evolveum.midpoint.prism.schema.SchemaRegistry;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.prism.xnode.*;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.PrettyPrinter;
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 com.evolveum.prism.xml.ns._public.types_3.ItemPathType;
import org.apache.commons.io.IOUtils;
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.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DomLexicalProcessor implements LexicalProcessor<String> {
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) {
this.schemaRegistry = schemaRegistry;
}
@Deprecated
public XNode read(File file, ParsingContext parsingContext) throws SchemaException, IOException {
return read(new ParserFileSource(file), parsingContext);
}
@NotNull
@Override
public RootXNode 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);
}
}
}
@NotNull
@Override
public List<RootXNode> readObjects(ParserSource source, ParsingContext parsingContext) throws SchemaException, IOException {
InputStream is = source.getInputStream();
try {
Document document = DOMUtil.parse(is);
return readObjects(document);
} finally {
if (source.closeStreamAfterParsing()) {
IOUtils.closeQuietly(is);
}
}
}
private List<RootXNode> readObjects(Document document) throws SchemaException{
Element root = DOMUtil.getFirstChildElement(document);
// TODO: maybe some check if this is a collection of other objects???
List<Element> children = DOMUtil.listChildElements(root);
List<RootXNode> nodes = new ArrayList<>();
for (Element child : children){
RootXNode xroot = read(child);
nodes.add(xroot);
}
return nodes;
}
@NotNull
public RootXNode read(Document document) throws SchemaException {
Element rootElement = DOMUtil.getFirstChildElement(document);
return read(rootElement);
}
@NotNull
public RootXNode read(Element rootElement) throws SchemaException{
RootXNode xroot = new RootXNode(DOMUtil.getQName(rootElement));
extractCommonMetadata(rootElement, xroot);
XNode xnode = parseElementContent(rootElement, false);
xroot.setSubnode(xnode);
return xroot;
}
private void extractCommonMetadata(Element element, XNode xnode) throws SchemaException {
QName xsiType = DOMUtil.resolveXsiType(element);
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.valueOf(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 (the name of the provided element is ignored (unless storeElementName=true),
* only the content is parsed).
*/
@Nullable
private XNode parseElementContent(Element element, boolean storeElementName) throws SchemaException {
if (DOMUtil.isNil(element)) { // TODO: ok?
return null;
}
XNode node;
if (DOMUtil.hasChildElements(element) || DOMUtil.hasApplicationAttributes(element)) {
if (isList(element)) {
node = parseElementContentToList(element);
} else {
node = parseElementContentToMap(element);
}
} else {
node = parsePrimitiveElement(element);
}
if (storeElementName) {
node.setElementName(DOMUtil.getQName(element));
}
extractCommonMetadata(element, node);
return node;
}
// all the sub-elements should be compatible (this is not enforced here, however)
private ListXNode parseElementContentToList(Element element) throws SchemaException {
if (DOMUtil.hasApplicationAttributes(element)) {
throw new SchemaException("List should have no application attributes: " + element);
}
return parseElementList(DOMUtil.listChildElements(element), true);
}
private MapXNode parseElementContentToMap(Element element) throws SchemaException {
MapXNode xmap = new MapXNode();
// Attributes
for (Attr attr: DOMUtil.listApplicationAttributes(element)) {
QName attrQName = DOMUtil.getQName(attr);
XNode subnode = parseAttributeValue(attr);
xmap.put(attrQName, subnode);
}
// Sub-elements
QName lastElementQName = null;
List<Element> lastElements = null;
for (Element childElement: DOMUtil.listChildElements(element)) {
QName childQName = DOMUtil.getQName(childElement);
if (match(childQName, lastElementQName)) {
lastElements.add(childElement);
} else {
parseSubElementsGroupAsMapEntry(xmap, lastElementQName, lastElements);
lastElementQName = childQName;
lastElements = new ArrayList<>();
lastElements.add(childElement);
}
}
parseSubElementsGroupAsMapEntry(xmap, lastElementQName, lastElements);
return xmap;
}
private boolean isList(Element element) throws SchemaException {
String isListAttribute = DOMUtil.getAttribute(element, new QName(DOMUtil.IS_LIST_ATTRIBUTE_NAME));
if (StringUtils.isNotEmpty(isListAttribute)) {
return Boolean.valueOf(isListAttribute);
}
// enable this after schema registry is optional (now it's mandatory)
// if (schemaRegistry == null) {
// return false;
// }
// checking parent element fitness
QName typeName = DOMUtil.resolveXsiType(element);
if (typeName != null) {
Collection<? extends ComplexTypeDefinition> definitions = schemaRegistry
.findTypeDefinitionsByType(typeName, ComplexTypeDefinition.class);
if (definitions.isEmpty()) {
return false; // to be safe (we support this heuristic only for known types)
}
if (QNameUtil.hasNamespace(typeName)) {
assert definitions.size() <= 1;
return definitions.iterator().next().isListMarker();
} else {
if (definitions.stream().allMatch(ComplexTypeDefinition::isListMarker)) {
// great -- we are very probably OK -- so let's continue
} else {
return false; // sorry, there's a possibility of failure
}
}
} else { // typeName == null
Collection<? extends ComplexTypeDefinition> definitions =
schemaRegistry.findTypeDefinitionsByElementName(DOMUtil.getQName(element), ComplexTypeDefinition.class);
// TODO - or allMatch here? - allMatch would mean that if there's an extension (or resource item) with a name
// of e.g. formItems, pipeline, sequence, ... - it would not be recognizable as list=true anymore. That's why
// we will use anyMatch here.
if (definitions.stream().anyMatch(ComplexTypeDefinition::isListMarker)) {
// we are very hopefully OK -- so let's continue
} else {
return false;
}
}
// 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<Element> 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);
}
}
private void parseSubElementsGroupAsMapEntry(MapXNode xmap, QName elementQName, List<Element> elements) throws SchemaException {
if (elements == null || elements.isEmpty()) {
return;
}
XNode xsub;
// We really want to have equals here, not match
// we want to be very explicit about namespace here
if (elementQName.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), false);
} else {
xsub = parseElementList(elements, false);
}
xmap.merge(elementQName, 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).
*/
private ListXNode parseElementList(List<Element> elements, boolean storeElementNames) throws SchemaException {
ListXNode xlist = new ListXNode();
for (Element element: elements) {
XNode xnode = parseElementContent(element, storeElementNames);
xlist.add(xnode);
}
return xlist;
}
// changed from anonymous to be able to make it static (serializable independently of DomParser)
private static class PrimitiveValueParser<T> implements ValueParser<T>, Serializable {
private Element element;
private PrimitiveValueParser(Element element) {
this.element = element;
}
@Override
public T parse(QName typeName, XNodeProcessorEvaluationMode mode) throws SchemaException {
return parsePrimitiveElementValue(element, typeName, mode);
}
@Override
public boolean isEmpty() {
return DOMUtil.isEmpty(element);
}
@Override
public String getStringValue() {
return element.getTextContent();
}
@Override
public Map<String, String> getPotentiallyRelevantNamespaces() {
return DOMUtil.getAllVisibleNamespaceDeclarations(element);
}
@Override
public String toString() {
return "ValueParser(DOMe, "+PrettyPrinter.prettyPrint(DOMUtil.getQName(element))+": "+element.getTextContent()+")";
}
};
private static class PrimitiveAttributeParser<T> implements ValueParser<T>, Serializable {
private Attr attr;
public PrimitiveAttributeParser(Attr attr) {
this.attr = attr;
}
@Override
public T parse(QName typeName, XNodeProcessorEvaluationMode mode) throws SchemaException {
return parsePrimitiveAttrValue(attr, typeName, mode);
}
@Override
public boolean isEmpty() {
return DOMUtil.isEmpty(attr);
}
@Override
public String getStringValue() {
return attr.getValue();
}
@Override
public String toString() {
return "ValueParser(DOMa, " + PrettyPrinter.prettyPrint(DOMUtil.getQName(attr)) + ": "
+ attr.getTextContent() + ")";
}
@Override
public Map<String, String> getPotentiallyRelevantNamespaces() {
return DOMUtil.getAllVisibleNamespaceDeclarations(attr);
}
}
private <T> PrimitiveXNode<T> parsePrimitiveElement(final Element element) throws SchemaException {
PrimitiveXNode<T> xnode = new PrimitiveXNode<T>();
xnode.setValueParser(new PrimitiveValueParser<T>(element));
return xnode;
}
private static <T> T parsePrimitiveElementValue(Element element, QName typeName, XNodeProcessorEvaluationMode mode) throws SchemaException {
try {
if (ItemPathType.COMPLEX_TYPE.equals(typeName)) {
return (T) parsePath(element);
} else if (DOMUtil.XSD_QNAME.equals(typeName)) {
return (T) DOMUtil.getQNameValue(element);
} else if (XmlTypeConverter.canConvert(typeName)) {
return (T) XmlTypeConverter.toJavaValue(element, typeName);
} else if (DOMUtil.XSD_ANYTYPE.equals(typeName)) {
return (T) element.getTextContent(); // if parsing primitive as xsd:anyType, we can safely parse it as string
} else {
throw new SchemaException("Cannot convert element '" + element + "' to " + typeName);
}
} catch (IllegalArgumentException e) {
return processIllegalArgumentException(element.getTextContent(), typeName, e, mode); // primitive way of ensuring compatibility mode
}
}
private static <T> T processIllegalArgumentException(String value, QName typeName, IllegalArgumentException e, XNodeProcessorEvaluationMode mode) {
if (mode != XNodeProcessorEvaluationMode.COMPAT) {
throw e;
}
LOGGER.warn("Value of '{}' couldn't be parsed as '{}' -- interpreting as null because of COMPAT mode set", value, typeName, e);
return null;
}
private <T> PrimitiveXNode<T> parseAttributeValue(final Attr attr) {
PrimitiveXNode<T> xnode = new PrimitiveXNode<T>();
xnode.setValueParser(new PrimitiveAttributeParser<T>(attr));
xnode.setAttribute(true);
return xnode;
}
private static <T> T parsePrimitiveAttrValue(Attr attr, QName typeName, XNodeProcessorEvaluationMode mode) throws SchemaException {
if (DOMUtil.XSD_QNAME.equals(typeName)) {
try {
return (T) DOMUtil.getQNameValue(attr);
} catch (IllegalArgumentException e) {
return processIllegalArgumentException(attr.getTextContent(), typeName, e, mode); // primitive way of ensuring compatibility mode
}
}
if (XmlTypeConverter.canConvert(typeName)) {
String stringValue = attr.getTextContent();
try {
return XmlTypeConverter.toJavaValue(stringValue, typeName);
} catch (IllegalArgumentException e) {
return processIllegalArgumentException(attr.getTextContent(), typeName, e, mode); // primitive way of ensuring compatibility mode
}
} else {
throw new SchemaException("Cannot convert attribute '"+attr+"' to "+typeName);
}
}
@NotNull
private static ItemPathType parsePath(Element element) {
XPathHolder holder = new XPathHolder(element);
return new ItemPathType(holder.toItemPath());
}
private SchemaXNode parseSchemaElement(Element schemaElement) {
SchemaXNode xschema = new SchemaXNode();
xschema.setSchemaElement(schemaElement);
return xschema;
}
@Override
public boolean canRead(@NotNull File file) throws IOException {
return file.getName().endsWith(".xml");
}
@Override
public boolean canRead(@NotNull String dataString) {
if (dataString.startsWith("<?xml")) {
return true;
}
Pattern p = Pattern.compile("\\A\\s*?<\\w+");
Matcher m = p.matcher(dataString);
if (m.find()) {
return true;
}
return false;
}
@NotNull
@Override
public String write(@NotNull XNode xnode, @NotNull QName rootElementName, SerializationContext serializationContext) throws SchemaException {
DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry);
RootXNode xroot = LexicalUtils.createRootXNode(xnode, rootElementName);
Element element = serializer.serialize(xroot);
return DOMUtil.serializeDOMToString(element);
}
@NotNull
@Override
public String write(@NotNull RootXNode xnode, SerializationContext serializationContext) throws SchemaException {
DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry);
Element element = serializer.serialize(xnode);
return DOMUtil.serializeDOMToString(element);
}
public Element serializeUnderElement(XNode xnode, QName rootElementName, Element parentElement) throws SchemaException {
DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry);
RootXNode xroot = LexicalUtils.createRootXNode(xnode, rootElementName);
return serializer.serializeUnderElement(xroot, parentElement);
}
public Element serializeXMapToElement(MapXNode xmap, QName elementName) throws SchemaException {
DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry);
return serializer.serializeToElement(xmap, elementName);
}
private Element serializeXPrimitiveToElement(PrimitiveXNode<?> xprim, QName elementName) throws SchemaException {
DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry);
return serializer.serializeXPrimitiveToElement(xprim, elementName);
}
@NotNull
public Element writeXRootToElement(@NotNull RootXNode xroot) throws SchemaException {
DomLexicalWriter serializer = new DomLexicalWriter(schemaRegistry);
return serializer.serialize(xroot);
}
private Element serializeToElement(XNode xnode, QName elementName) throws SchemaException {
Validate.notNull(xnode);
Validate.notNull(elementName);
if (xnode instanceof MapXNode) {
return serializeXMapToElement((MapXNode) xnode, elementName);
} else if (xnode instanceof PrimitiveXNode<?>) {
return serializeXPrimitiveToElement((PrimitiveXNode<?>) xnode, elementName);
} else if (xnode instanceof RootXNode) {
return writeXRootToElement((RootXNode)xnode);
} else if (xnode instanceof ListXNode) {
ListXNode xlist = (ListXNode) 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 serialized "+xnode+" to element");
}
}
public Element serializeSingleElementMapToElement(MapXNode xmap) throws SchemaException {
if (xmap == null || xmap.isEmpty()) {
return null;
}
Entry<QName, XNode> subEntry = xmap.getSingleSubEntry(xmap.toString());
Element parent = serializeToElement(xmap, subEntry.getKey());
return DOMUtil.getFirstChildElement(parent);
}
}