/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.sling.jcr.contentparser.impl; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.jcr.PropertyType; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.lang3.StringUtils; import org.apache.sling.jcr.contentparser.ContentHandler; import org.apache.sling.jcr.contentparser.ContentParser; import org.apache.sling.jcr.contentparser.ParseException; import org.apache.sling.jcr.contentparser.ParserOptions; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * Parses XML files that contains content fragments. * Instance of this class is thread-safe. */ public final class XmlContentParser implements ContentParser { private final ParserHelper helper; private final DocumentBuilderFactory documentBuilderFactory; public XmlContentParser(ParserOptions options) { this.helper = new ParserHelper(options); documentBuilderFactory = DocumentBuilderFactory.newInstance(); } @Override public void parse(ContentHandler handler, InputStream is) throws IOException, ParseException { try { DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); Document doc = documentBuilder.parse(is); parse(handler, doc.getDocumentElement(), null); } catch (ParserConfigurationException | SAXException ex) { throw new ParseException("Error parsing JCR XML content.", ex); } } private void parse(ContentHandler handler, Element element, String parentPath) { // build node path String path; if (parentPath == null) { path = "/"; } else { String name = getChildText(element, "name"); if (StringUtils.isEmpty(name)) { throw new ParseException("Child node without name detected below path " + parentPath); } path = helper.concatenatePath(parentPath, name); if (helper.ignoreResource(name)) { return; } } Map<String,Object> properties = new HashMap<>(); // primary node type and mixins String primaryType = getChildText(element, "primaryNodeType"); if (StringUtils.isNotBlank(primaryType) && !helper.ignoreProperty("jcr:primaryType")) { properties.put("jcr:primaryType", primaryType); } String[] mixins = getChildTextArray(element, "mixinNodeType"); if (mixins.length > 0 && !helper.ignoreProperty("jcr:mixinTypes")) { properties.put("jcr:mixinTypes", mixins); } // properties List<Element> propertyElements = getChildren(element, "property"); for (Element propertyElement : propertyElements) { // property name String name = getChildText(propertyElement, "name"); if (StringUtils.isBlank(name)) { throw new ParseException("Property without name detected at path " + path); } if (helper.ignoreProperty(name)) { continue; } // property type String typeString = getChildText(propertyElement, "type"); if (StringUtils.isBlank(typeString)) { throw new ParseException("Property '" + name + "' has no type at path " + path); } int type; try { type = PropertyType.valueFromName(typeString); } catch (IllegalArgumentException ex) { throw new ParseException("Property '" + name + "' has illegal type '" + typeString + "' at path " + path); } // property value Object value; List<Element> valuesElements = getChildren(propertyElement, "values"); if (!valuesElements.isEmpty()) { Element valuesElement = valuesElements.get(0); List<Element> valueElements = getChildren(valuesElement, "value"); String[] stringValues = new String[valueElements.size()]; for (int i=0; i<valueElements.size(); i++) { stringValues[i] = valueElements.get(i).getTextContent(); } value = convertMultiValue(stringValues, type); } else { String stringValue = getChildText(propertyElement, "value"); value = convertValue(stringValue, type); } properties.put(name, value); } // report current JSON object helper.ensureDefaultPrimaryType(properties); handler.resource(path, properties); // child nodes List<Element> nodeElements = getChildren(element, "node"); for (Element node : nodeElements) { parse(handler, node, path); } } private List<Element> getChildren(Element element, String childName) { List<Element> result = new ArrayList<>(); NodeList children = element.getChildNodes(); int len = children.getLength(); for (int i=0; i<len; i++) { Node child = children.item(i); if (child instanceof Element) { Element childElement = (Element)child; if (StringUtils.equals(childElement.getNodeName(), childName)) { result.add(childElement); } } } return result; } private String getChildText(Element element, String childName) { List<Element> children = getChildren(element, childName); if (children.isEmpty()) { return null; } else if (children.size() == 1) { return children.get(0).getTextContent(); } else { throw new ParseException("Found multiple elements with name '" + childName + "': " + children.size()); } } private String[] getChildTextArray(Element element, String childName) { List<Element> children = getChildren(element, childName); String[] result = new String[children.size()]; for (int i=0; i<children.size(); i++) { result[i] = children.get(i).getTextContent(); } return result; } private Object convertValue(String value, int type) { switch (type) { case PropertyType.STRING: case PropertyType.NAME: case PropertyType.PATH: case PropertyType.REFERENCE: case PropertyType.WEAKREFERENCE: case PropertyType.URI: return value; case PropertyType.LONG: return Long.valueOf(value); case PropertyType.DOUBLE: return Double.valueOf(value); case PropertyType.DATE: return helper.tryParseCalendar(value); case PropertyType.BOOLEAN: return Boolean.valueOf(value); case PropertyType.DECIMAL: return new BigDecimal(value); default: throw new ParseException("Unsupported property type: " + PropertyType.nameFromValue(type)); } } private Object convertMultiValue(String[] values, int type) { Object[] result = new Object[values.length]; for (int i=0; i<values.length; i++) { result[i] = convertValue(values[i], type); } return helper.convertSingleTypeArray(result); } }