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