/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community 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://opensource.org/licenses/ecl2.txt * * 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.opencastproject.oaipmh.util; import static com.entwinemedia.fn.Stream.$; import static org.opencastproject.fun.juc.Immutables.list; import static org.opencastproject.util.IoSupport.withResource; import static org.opencastproject.util.data.Option.some; import static org.opencastproject.util.data.functions.Misc.chuck; import org.opencastproject.metadata.dublincore.DublinCore; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Function0; import org.opencastproject.util.data.Option; import com.entwinemedia.fn.Fn; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.OutputStream; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; /** * DOM based XML generation environment. Implement {@link #create()} to create the XML. Serialize to an output stream * with {@link #generate(java.io.OutputStream)}. * * todo document the node creator functions */ public abstract class XmlGen { private final Document document; private final Option<String> defaultNamespace; /** * Create a new environment. */ public XmlGen(Option<String> defaultNamespace) { document = createDocument(); this.defaultNamespace = defaultNamespace; } private Document createDocument() { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); return builder.newDocument(); } catch (ParserConfigurationException e) { return chuck(e); } } private void write(OutputStream out) { try { TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); DOMSource source = new DOMSource(document); StreamResult result = new StreamResult(out); transformer.transform(source, result); } catch (TransformerException e) { throw new RuntimeException(e); } } /** * Generate the XML and write it to <code>out</code>. */ public void generate(OutputStream out) { generate(); write(out); } /** * Generate the document. */ public Document generate() { final Node node = document.importNode(create(), true); final Element docElem = document.getDocumentElement(); if (docElem != null) { document.removeChild(docElem); } document.appendChild(node); return document; } /** Generate the document as a string. */ public String generateAsString() { return withResource(new ByteArrayOutputStream(), new Function<ByteArrayOutputStream, String>() { @Override public String apply(ByteArrayOutputStream out) { generate(out); return out.toString(); } }); } /** * Implement this method to create the DOM. Use the various node creation functions for this purpose. */ public abstract Element create(); // -- protected Namespace ns(String prefix, String namespace) { return new Namespace(prefix, namespace); } protected Node schemaLocation(String location) { return $a("xsi:schemaLocation", location); } // CHECKSTYLE:OFF protected Node $langNode(String language) { if (StringUtils.isBlank(language) || DublinCore.LANGUAGE_UNDEFINED.equals(language) || DublinCore.LANGUAGE_ANY.equals(language)) return nodeZero(); Attr a = document.createAttributeNS(XMLConstants.XML_NS_URI, "xml:lang"); a.setValue(language); return a; } protected Node $a(String name, String value) { Attr a = document.createAttribute(name); a.setValue(value); return a; } protected Node $aBlank(String name, String value) { if (StringUtils.isNotBlank(value)) { Attr a = document.createAttribute(name); a.setValue(value); return a; } else { return nodeZero(); } } protected Node $aSome(final String name, final Option<String> value) { return value.fold(new Option.Match<String, Node>() { @Override public Node some(String value) { Attr a = document.createAttribute(name); a.setValue(value); return a; } @Override public Node none() { return nodeZero(); } }); } protected Element $e(String qname, Option<String> namespace, List<Node> nodes) { return appendTo(createElemNs(namespace, qname), nodes); } /** * Create an element with the qualified name <code>qname</code> -- i.e. <code>prefix:tagname</code> -- in the * namespace <code>namespace</code> with children <code>nodes</code>. */ protected Element $e(String qname, Option<String> namespace, NodeList nodes) { return appendTo(createElemNs(namespace, qname), nodes); } protected Element $e(String qname, Option<String> namespace, Node... nodes) { return $e(qname, namespace, list(nodes)); } protected Element $e(String name, Node... nodes) { return $e(name, defaultNamespace, list(nodes)); } protected Element $e(String name, List<Node> nodes) { return $e(name, defaultNamespace, list(nodes)); } /** * Create an element with the qualified name <code>qname</code> -- i.e. <code>prefix:tagname</code> -- in the * namespace <code>namespace</code> with children <code>nodes</code>. */ protected Element $e(String qname, String namespace, Node... nodes) { return $e(qname, some(namespace), list(nodes)); } protected Element $e(String qname, String namespace, List<Node> nodes) { return $e(qname, some(namespace), nodes); } protected Node $eTxtBlank(final String name, String text) { return $txtBlank(text).map(new Function<Node, Node>() { @Override public Node apply(Node text) { final Element e = createElemDefaultNs(name); e.appendChild(text); return e; } }).getOrElse(nodeZero); } protected Node $eTxt(final String name, String text) { final Element e = createElemDefaultNs(name); e.appendChild($txt(text)); return e; } protected Node $eTxt(final String qname, final String namespace, String text) { final Element e = createElemNs(namespace, qname); e.appendChild($txt(text)); return e; } protected Element $e(String name, List<Namespace> namespaces, Node... nodes) { return appendTo(appendNs(createElemDefaultNs(name), namespaces), list(nodes)); } protected Element $e(String name, List<Namespace> namespaces, NodeList nodes) { return appendTo(appendNs(createElemDefaultNs(name), namespaces), nodes); } protected Element $e(String name, List<Namespace> namespaces, List<Node> nodes) { return appendTo(appendNs(createElemDefaultNs(name), namespaces), nodes); } protected Element $e(String qname, String namespace, List<Namespace> namespaces, Node... nodes) { return appendTo(appendNs(createElemNs(namespace, qname), namespaces), list(nodes)); } private Element createElemDefaultNs(String name) { return createElemNs(defaultNamespace, name); } private Element createElemNs(Option<String> namespace, String qname) { return createElemNs(namespace.getOrElseNull(), qname); } /** * @param namespace * may be null. */ private Element createElemNs(String namespace, String qname) { return document.createElementNS(namespace, qname); } /** * Create a new DOM element. * * @param qname * fully qualified tag name, e.g. "name" or "dc:title" * @param namespace * namespace to which this tag belongs to * @param namespaces * additional namespace declarations * @param nodes * child nodes */ protected Element $e(String qname, String namespace, List<Namespace> namespaces, List<Node> nodes) { return appendTo(appendNs(createElemNs(namespace, qname), namespaces), nodes); } /** * Conditional element. Only created if at least one subnode is present. Subnodes may be attributes, elements, text * nodes, etc. */ protected Node $e(String name, Option<Node>... nodes) { final List<Node> existing = filter(list(nodes)); if (!existing.isEmpty()) { return $e(name, existing); } else { return nodeZero(); } } protected Node $txt(String text) { return document.createTextNode(text); } protected Node $cdata(String text) { return document.createCDATASection(text); } /** * Text blank. */ protected Option<Node> $txtBlank(String text) { return StringUtils.isNotBlank(text) ? some($txt(text)) : Option.<Node>none(); } // -- // CHECKSTYLE:ON private List<Node> filter(List<Option<Node>> nodes) { return $(nodes).bind(new Fn<Option<Node>, Collection<Node>>() { @Override public Collection<Node> apply(Option<Node> nodeOption) { return nodeOption.fold(new Option.Match<Node, Collection<Node>>() { @Override public Collection<Node> some(Node node) { return list(node); } @Override public Collection<Node> none() { return Collections.EMPTY_LIST; } }); } }).toList(); } private Element appendNs(Element e, List<Namespace> namespaces) { for (Namespace n : namespaces) { e.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLConstants.XMLNS_ATTRIBUTE + ":" + n.getPrefix(), n.getNamespace()); } return e; } /** * Append <code>nodes</code> to element <code>e</code>. Respects different node types like attributes and elements. */ private Element appendTo(Element e, List<Node> nodes) { for (Node node : nodes) appendTo(e, node); return e; } /** * Like {@link #appendTo(org.w3c.dom.Element, java.util.List)} but with a different signature. */ private Element appendTo(Element e, NodeList nodes) { for (int i = 0; i < nodes.getLength(); i++) appendTo(e, nodes.item(i)); return e; } /** * Append node <code>n</code> to element <code>e</code> respecting different node types like attributes and elements. */ private void appendTo(Element e, Node n) { Node toAppend = ObjectUtils.equals(n.getOwnerDocument(), document) ? n : document.importNode(n, true); if (toAppend instanceof Attr) { e.setAttributeNode((Attr) toAppend); } else { e.appendChild(toAppend); } } /** * The neutral element. */ protected Node nodeZero() { return document.createTextNode(""); } /** * Lazy version of {@link #nodeZero()}. */ protected Function0<Node> nodeZero = new Function0<Node>() { @Override public Node apply() { return nodeZero(); } }; /** * Create a text node from a string. */ protected Function<String, Node> mkText = new Function<String, Node>() { @Override public Node apply(String token) { return $txt(token); } }; protected class Namespace { private final String prefix; private final String namespace; Namespace(String prefix, String namespace) { this.prefix = prefix; this.namespace = namespace; } public String getPrefix() { return prefix; } public String getNamespace() { return namespace; } } }