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