View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  package org.opencastproject.oaipmh.util;
22  
23  import static org.opencastproject.util.IoSupport.withResource;
24  import static org.opencastproject.util.data.functions.Misc.chuck;
25  
26  import org.opencastproject.metadata.dublincore.DublinCore;
27  import org.opencastproject.util.XmlSafeParser;
28  
29  
30  import org.apache.commons.io.output.ByteArrayOutputStream;
31  import org.apache.commons.lang3.ObjectUtils;
32  import org.apache.commons.lang3.StringUtils;
33  import org.w3c.dom.Attr;
34  import org.w3c.dom.Document;
35  import org.w3c.dom.Element;
36  import org.w3c.dom.Node;
37  import org.w3c.dom.NodeList;
38  
39  import java.io.OutputStream;
40  import java.util.ArrayList;
41  import java.util.Arrays;
42  import java.util.Collections;
43  import java.util.List;
44  import java.util.Optional;
45  
46  import javax.xml.XMLConstants;
47  import javax.xml.parsers.DocumentBuilder;
48  import javax.xml.parsers.DocumentBuilderFactory;
49  import javax.xml.parsers.ParserConfigurationException;
50  import javax.xml.transform.OutputKeys;
51  import javax.xml.transform.Transformer;
52  import javax.xml.transform.TransformerException;
53  import javax.xml.transform.dom.DOMSource;
54  import javax.xml.transform.stream.StreamResult;
55  
56  /**
57   * DOM based XML generation environment. Implement {@link #create()} to create the XML. Serialize to an output stream
58   * with {@link #generate(java.io.OutputStream)}.
59   *
60   * todo document the node creator functions
61   */
62  public abstract class XmlGen {
63    private final Document document;
64    private final Optional<String> defaultNamespace;
65  
66    /**
67     * Create a new environment.
68     */
69    public XmlGen(Optional<String> defaultNamespace) {
70      document = createDocument();
71      this.defaultNamespace = defaultNamespace;
72    }
73  
74    private Document createDocument() {
75      try {
76        DocumentBuilderFactory factory = XmlSafeParser.newDocumentBuilderFactory();
77        factory.setNamespaceAware(true);
78        DocumentBuilder builder = factory.newDocumentBuilder();
79        return builder.newDocument();
80      } catch (ParserConfigurationException e) {
81        return chuck(e);
82      }
83    }
84  
85    private void write(OutputStream out) {
86      try {
87        Transformer transformer = XmlSafeParser.newTransformerFactory().newTransformer();
88        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
89        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
90        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
91        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
92        DOMSource source = new DOMSource(document);
93        StreamResult result = new StreamResult(out);
94        transformer.transform(source, result);
95      } catch (TransformerException e) {
96        throw new RuntimeException(e);
97      }
98    }
99  
100   /**
101    * Generate the XML and write it to <code>out</code>.
102    */
103   public void generate(OutputStream out) {
104     generate();
105     write(out);
106   }
107 
108   /**
109    * Generate the document.
110    */
111   public Document generate() {
112     final Node node = document.importNode(create(), true);
113     final Element docElem = document.getDocumentElement();
114     if (docElem != null) {
115       document.removeChild(docElem);
116     }
117     document.appendChild(node);
118     return document;
119   }
120 
121   /** Generate the document as a string. */
122   public String generateAsString() {
123     return withResource(new ByteArrayOutputStream(), out -> {
124       generate(out);
125       return out.toString();
126     });
127   }
128 
129   /**
130    * Implement this method to create the DOM. Use the various node creation functions for this purpose.
131    */
132   public abstract Element create();
133 
134   // --
135 
136   protected Namespace ns(String prefix, String namespace) {
137     return new Namespace(prefix, namespace);
138   }
139 
140   protected Node schemaLocation(String location) {
141     return $a("xsi:schemaLocation", location);
142   }
143 
144   // CHECKSTYLE:OFF
145 
146   protected Node $langNode(String language) {
147     if (StringUtils.isBlank(language) || DublinCore.LANGUAGE_UNDEFINED.equals(language)
148             || DublinCore.LANGUAGE_ANY.equals(language))
149       return nodeZero();
150 
151     Attr a = document.createAttributeNS(XMLConstants.XML_NS_URI, "xml:lang");
152     a.setValue(language);
153     return a;
154   }
155 
156   protected Node $a(String name, String value) {
157     Attr a = document.createAttribute(name);
158     a.setValue(value);
159     return a;
160   }
161 
162   protected Node $aBlank(String name, String value) {
163     if (StringUtils.isNotBlank(value)) {
164       Attr a = document.createAttribute(name);
165       a.setValue(value);
166       return a;
167     } else {
168       return nodeZero();
169     }
170   }
171 
172   protected Node $aSome(final String name, final Optional<String> value) {
173     return value
174         .map(v -> {
175           Attr a = document.createAttribute(name);
176           a.setValue(v);
177           return (Node) a;
178         })
179         .orElseGet(this::nodeZero);
180   }
181 
182   protected Element $e(String qname, Optional<String> namespace, List<Node> nodes) {
183     return appendTo(createElemNs(namespace, qname), nodes);
184   }
185 
186   /**
187    * Create an element with the qualified name <code>qname</code> -- i.e. <code>prefix:tagname</code> -- in the
188    * namespace <code>namespace</code> with children <code>nodes</code>.
189    */
190   protected Element $e(String qname, Optional<String> namespace, NodeList nodes) {
191     return appendTo(createElemNs(namespace, qname), nodes);
192   }
193 
194   protected Element $e(String qname, Optional<String> namespace, Node... nodes) {
195     return $e(qname, namespace, Arrays.asList(nodes));
196   }
197 
198   protected Element $e(String name, Node... nodes) {
199     return $e(name, defaultNamespace, Arrays.asList(nodes));
200   }
201 
202   protected Element $e(String name, List<Node> nodes) {
203     return $e(name, defaultNamespace, Collections.unmodifiableList(nodes));
204   }
205 
206   /**
207    * Create an element with the qualified name <code>qname</code> -- i.e. <code>prefix:tagname</code> -- in the
208    * namespace <code>namespace</code> with children <code>nodes</code>.
209    */
210   protected Element $e(String qname, String namespace, Node... nodes) {
211     return $e(qname, Optional.of(namespace), Arrays.asList(nodes));
212   }
213 
214   protected Element $e(String qname, String namespace, List<Node> nodes) {
215     return $e(qname, Optional.of(namespace), nodes);
216   }
217 
218   protected Node $eTxtBlank(final String name, String text) {
219     Optional<Node> txtNodeOpt = $txtBlank(text);
220     if (txtNodeOpt.isPresent()) {
221       Node txtNode = txtNodeOpt.get();
222       final Element e = createElemDefaultNs(name);
223       e.appendChild(txtNode);
224       return e;
225     } else {
226       return nodeZero();
227     }
228   }
229 
230   protected Node $eTxt(final String name, String text) {
231     final Element e = createElemDefaultNs(name);
232     e.appendChild($txt(text));
233     return e;
234   }
235 
236   protected Node $eTxt(final String qname, final String namespace, String text) {
237     final Element e = createElemNs(namespace, qname);
238     e.appendChild($txt(text));
239     return e;
240   }
241 
242   protected Element $e(String name, List<Namespace> namespaces, Node... nodes) {
243     return appendTo(appendNs(createElemDefaultNs(name), namespaces), Arrays.asList(nodes));
244   }
245 
246   protected Element $e(String name, List<Namespace> namespaces, NodeList nodes) {
247     return appendTo(appendNs(createElemDefaultNs(name), namespaces), nodes);
248   }
249 
250   protected Element $e(String name, List<Namespace> namespaces, List<Node> nodes) {
251     return appendTo(appendNs(createElemDefaultNs(name), namespaces), nodes);
252   }
253 
254   protected Element $e(String qname, String namespace, List<Namespace> namespaces, Node... nodes) {
255     return appendTo(appendNs(createElemNs(namespace, qname), namespaces), Arrays.asList(nodes));
256   }
257 
258   private Element createElemDefaultNs(String name) {
259     return createElemNs(defaultNamespace, name);
260   }
261 
262   private Element createElemNs(Optional<String> namespace, String qname) {
263     return createElemNs(namespace.orElse(null), qname);
264   }
265 
266   /**
267    * @param namespace
268    *         may be null.
269    */
270   private Element createElemNs(String namespace, String qname) {
271     return document.createElementNS(namespace, qname);
272   }
273 
274   /**
275    * Create a new DOM element.
276    *
277    * @param qname
278    *         fully qualified tag name, e.g. "name" or "dc:title"
279    * @param namespace
280    *         namespace to which this tag belongs to
281    * @param namespaces
282    *         additional namespace declarations
283    * @param nodes
284    *         child nodes
285    */
286   protected Element $e(String qname, String namespace, List<Namespace> namespaces, List<Node> nodes) {
287     return appendTo(appendNs(createElemNs(namespace, qname), namespaces), nodes);
288   }
289 
290   /**
291    * Conditional element. Only created if at least one subnode is present. Subnodes may be attributes, elements, text
292    * nodes, etc.
293    */
294   protected Node $e(String name, Optional<Node>... nodes) {
295     final List<Node> existing = filter(Arrays.asList(nodes));
296     if (!existing.isEmpty()) {
297       return $e(name, existing);
298     } else {
299       return nodeZero();
300     }
301   }
302 
303   protected Node $txt(String text) {
304     return document.createTextNode(text);
305   }
306 
307   protected Node $cdata(String text) {
308     return document.createCDATASection(text);
309   }
310 
311   /**
312    * Text blank.
313    */
314   protected Optional<Node> $txtBlank(String text) {
315     return StringUtils.isNotBlank(text) ? Optional.of($txt(text)) : Optional.<Node>empty();
316   }
317 
318   // --
319 
320   // CHECKSTYLE:ON
321 
322   private List<Node> filter(List<Optional<Node>> nodes) {
323     List<Node> result = new ArrayList<>();
324     for (Optional<Node> opt : nodes) {
325       opt.ifPresent(result::add);
326     }
327     return result;
328   }
329 
330 
331   private Element appendNs(Element e, List<Namespace> namespaces) {
332     for (Namespace n : namespaces) {
333       e.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLConstants.XMLNS_ATTRIBUTE + ":" + n.getPrefix(),
334                        n.getNamespace());
335     }
336     return e;
337   }
338 
339   /**
340    * Append <code>nodes</code> to element <code>e</code>. Respects different node types like attributes and elements.
341    */
342   private Element appendTo(Element e, List<Node> nodes) {
343     for (Node node : nodes)
344       appendTo(e, node);
345     return e;
346   }
347 
348   /**
349    * Like {@link #appendTo(org.w3c.dom.Element, java.util.List)} but with a different signature.
350    */
351   private Element appendTo(Element e, NodeList nodes) {
352     for (int i = 0; i < nodes.getLength(); i++)
353       appendTo(e, nodes.item(i));
354     return e;
355   }
356 
357   /**
358    * Append node <code>n</code> to element <code>e</code> respecting different node types like attributes and elements.
359    */
360   private void appendTo(Element e, Node n) {
361     Node toAppend = ObjectUtils.equals(n.getOwnerDocument(), document) ? n : document.importNode(n, true);
362     if (toAppend instanceof Attr) {
363       e.setAttributeNode((Attr) toAppend);
364     } else {
365       e.appendChild(toAppend);
366     }
367   }
368 
369   /**
370    * The neutral element.
371    */
372   protected Node nodeZero() {
373     return document.createTextNode("");
374   }
375 
376   protected class Namespace {
377     private final String prefix;
378     private final String namespace;
379 
380     Namespace(String prefix, String namespace) {
381       this.prefix = prefix;
382       this.namespace = namespace;
383     }
384 
385     public String getPrefix() {
386       return prefix;
387     }
388 
389     public String getNamespace() {
390       return namespace;
391     }
392   }
393 }