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  
22  package org.opencastproject.event.comment;
23  
24  import static javax.xml.xpath.XPathConstants.STRING;
25  import static org.opencastproject.util.data.functions.Misc.chuck;
26  
27  import org.opencastproject.mediapackage.UnsupportedElementException;
28  import org.opencastproject.security.api.User;
29  import org.opencastproject.security.api.UserDirectoryService;
30  import org.opencastproject.util.DateTimeSupport;
31  import org.opencastproject.util.XmlSafeParser;
32  import org.opencastproject.util.data.Option;
33  
34  import org.apache.commons.io.IOUtils;
35  import org.apache.commons.lang3.BooleanUtils;
36  import org.apache.commons.lang3.StringUtils;
37  import org.w3c.dom.Document;
38  import org.w3c.dom.Element;
39  import org.w3c.dom.Node;
40  import org.w3c.dom.NodeList;
41  
42  import java.io.StringWriter;
43  import java.util.Collection;
44  import java.util.Date;
45  
46  import javax.xml.parsers.DocumentBuilderFactory;
47  import javax.xml.parsers.ParserConfigurationException;
48  import javax.xml.transform.Transformer;
49  import javax.xml.transform.dom.DOMSource;
50  import javax.xml.transform.stream.StreamResult;
51  import javax.xml.xpath.XPath;
52  import javax.xml.xpath.XPathConstants;
53  import javax.xml.xpath.XPathExpressionException;
54  import javax.xml.xpath.XPathFactory;
55  
56  /**
57   * Convenience implementation that supports serializing and deserializing comment catalogs.
58   */
59  public final class EventCommentParser {
60  
61    private static final String NAMESPACE = "http://comment.opencastproject.org";
62  
63    private EventCommentParser() {
64    }
65  
66    /** The xpath facility */
67    private static final XPath xpath = XPathFactory.newInstance().newXPath();
68  
69    /**
70     * Parses the comment catalog and returns its object representation.
71     * 
72     * @param xml
73     *          the serialized comment
74     * @param userDirectoryService
75     *          the user directory service
76     * @return the comment instance
77     * @throws EventCommentException
78     *           unable to parse comment from XML
79     */
80    public static EventComment getCommentFromXml(String xml, UserDirectoryService userDirectoryService)
81            throws EventCommentException {
82      try {
83        Document doc = XmlSafeParser.newDocumentBuilderFactory().newDocumentBuilder()
84                .parse(IOUtils.toInputStream(xml, "UTF-8"));
85        return commentFromManifest(doc.getDocumentElement(), userDirectoryService);
86      } catch (Exception e) {
87        throw new EventCommentException(e);
88      }
89    }
90  
91    private static EventComment commentFromManifest(Node commentNode, UserDirectoryService userDirectoryService)
92            throws UnsupportedElementException {
93      try {
94        // id
95        Long id = null;
96        Double idAsDouble = ((Number) xpath.evaluate("@id", commentNode, XPathConstants.NUMBER)).doubleValue();
97        if (!idAsDouble.isNaN()) {
98          id = idAsDouble.longValue();
99        }
100 
101       final String eventId = (String) xpath.evaluate("@eventId", commentNode, STRING);
102       final String organization = (String) xpath.evaluate("@organization", commentNode, STRING);
103 
104       // text
105       String text = (String) xpath.evaluate("text/text()", commentNode, XPathConstants.STRING);
106 
107       // Author
108       Node authorNode = (Node) xpath.evaluate("author", commentNode, XPathConstants.NODE);
109       User author = userFromManifest(authorNode, userDirectoryService);
110 
111       // ResolvedStatus
112       Boolean resolved = BooleanUtils
113               .toBoolean((Boolean) xpath.evaluate("@resolved", commentNode, XPathConstants.BOOLEAN));
114 
115       // Reason
116       String reason = (String) xpath.evaluate("reason/text()", commentNode, XPathConstants.STRING);
117       if (StringUtils.isNotBlank(reason)) {
118         reason = reason.trim();
119       }
120 
121       // CreationDate
122       String creationDateString = (String) xpath.evaluate("creationDate/text()", commentNode, XPathConstants.STRING);
123       Date creationDate = new Date(DateTimeSupport.fromUTC(creationDateString));
124 
125       // ModificationDate
126       String modificationDateString = (String) xpath.evaluate("modificationDate/text()", commentNode,
127               XPathConstants.STRING);
128       Date modificationDate = new Date(DateTimeSupport.fromUTC(modificationDateString));
129 
130       // Create comment
131       EventComment comment = EventComment.create(Option.option(id), eventId, organization,
132           text.trim(), author, reason, resolved, creationDate, modificationDate);
133 
134       // Replies
135       NodeList replyNodes = (NodeList) xpath.evaluate("replies/reply", commentNode, XPathConstants.NODESET);
136       for (int i = 0; i < replyNodes.getLength(); i++) {
137         comment.addReply(replyFromManifest(replyNodes.item(i), userDirectoryService));
138       }
139 
140       return comment;
141     } catch (XPathExpressionException e) {
142       throw new UnsupportedElementException("Error while reading comment information from manifest", e);
143     } catch (Exception e) {
144       if (e instanceof UnsupportedElementException) {
145         throw (UnsupportedElementException) e;
146       }
147       throw new UnsupportedElementException(
148               "Error while reading comment creation or modification date information from manifest", e);
149     }
150   }
151 
152   private static EventCommentReply replyFromManifest(Node commentReplyNode, UserDirectoryService userDirectoryService)
153           throws UnsupportedElementException {
154     try {
155       // id
156       Long id = null;
157       Double idAsDouble = ((Number) xpath.evaluate("@id", commentReplyNode, XPathConstants.NUMBER)).doubleValue();
158       if (!idAsDouble.isNaN()) {
159         id = idAsDouble.longValue();
160       }
161 
162       // text
163       String text = (String) xpath.evaluate("text/text()", commentReplyNode, XPathConstants.STRING);
164 
165       // Author
166       Node authorNode = (Node) xpath.evaluate("author", commentReplyNode, XPathConstants.NODE);
167       User author = userFromManifest(authorNode, userDirectoryService);
168 
169       // CreationDate
170       String creationDateString = (String) xpath.evaluate("creationDate/text()", commentReplyNode,
171               XPathConstants.STRING);
172       Date creationDate = new Date(DateTimeSupport.fromUTC(creationDateString));
173 
174       // ModificationDate
175       String modificationDateString = (String) xpath.evaluate("modificationDate/text()", commentReplyNode,
176               XPathConstants.STRING);
177       Date modificationDate = new Date(DateTimeSupport.fromUTC(modificationDateString));
178 
179       // Create reply
180       return EventCommentReply.create(Option.option(id), text.trim(), author, creationDate, modificationDate);
181     } catch (XPathExpressionException e) {
182       throw new UnsupportedElementException("Error while reading comment reply information from manifest", e);
183     } catch (Exception e) {
184       if (e instanceof UnsupportedElementException) {
185         throw (UnsupportedElementException) e;
186       }
187       throw new UnsupportedElementException(
188               "Error while reading comment reply creation or modification date information from manifest", e);
189     }
190   }
191 
192   private static User userFromManifest(Node authorNode, UserDirectoryService userDirectoryService) {
193     try {
194       // Username
195       String userName = (String) xpath.evaluate("username/text()", authorNode, XPathConstants.STRING);
196       return userDirectoryService.loadUser(userName);
197     } catch (XPathExpressionException e) {
198       throw new UnsupportedElementException("Error while reading comment author information from manifest", e);
199     }
200   }
201 
202   /**
203    * Serializes a comment collection.
204    * 
205    * @param comments
206    *          the comment collection
207    * @return the serialized comment collection
208    * @throws EventCommentException
209    *           unable to serialize comment collection to XML
210    */
211   public static String getAsXml(Collection<EventComment> comments) throws EventCommentException {
212     Document doc = newDocument();
213     // Root element "comments"
214     Element commentsXml = doc.createElementNS(NAMESPACE, "comments");
215     commentsXml.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", NAMESPACE);
216     doc.appendChild(commentsXml);
217 
218     for (EventComment c : comments) {
219       commentsXml.appendChild(getAsXmlDocument(c, doc));
220     }
221 
222     return serializeNode(commentsXml.getOwnerDocument());
223   }
224 
225   /**
226    * Serializes the comment.
227    * 
228    * @param comment
229    *          the comment
230    * @return the serialized comment
231    * @throws EventCommentException
232    *           unable to serialize comment to XML
233    */
234   public static String getAsXml(EventComment comment) throws EventCommentException {
235     Document doc = newDocument();
236 
237     Element commentNode = getAsXmlDocument(comment, doc);
238     return serializeNode(doc.appendChild(commentNode).getOwnerDocument());
239   }
240 
241   /**
242    * Serializes the comment reply.
243    * 
244    * @param reply
245    *          the comment reply
246    * @return the serialized comment reply
247    * @throws EventCommentException
248    *           unable to serialize comment reply to XML
249    */
250   public static String getAsXml(EventCommentReply reply) throws EventCommentException {
251     Document xmlDocument = getAsXmlDocument(reply);
252     return serializeNode(xmlDocument);
253   }
254 
255   /**
256    * Serializes the comment to a {@link org.w3c.dom.Document}.
257    * 
258    * @param comment
259    *          the comment
260    * @return the serialized comment
261    */
262   private static Element getAsXmlDocument(EventComment comment, Document doc) {
263     // Root element "comment"
264     Element commentXml = doc.createElementNS(NAMESPACE, "comment");
265     commentXml.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", NAMESPACE);
266 
267     // Identifier
268     if (comment.getId().isSome()) {
269       commentXml.setAttribute("id", comment.getId().get().toString());
270     }
271 
272     commentXml.setAttribute("eventId", comment.getEventId());
273     commentXml.setAttribute("organization", comment.getOrganization());
274 
275     // Resolved status
276     commentXml.setAttribute("resolved", Boolean.toString(comment.isResolvedStatus()));
277 
278     // Author
279     Element authorNode = getAuthorNode(comment.getAuthor(), doc);
280     commentXml.appendChild(authorNode);
281 
282     // Creation date
283     Element creationDate = doc.createElement("creationDate");
284     creationDate.setTextContent(DateTimeSupport.toUTC(comment.getCreationDate().getTime()));
285     commentXml.appendChild(creationDate);
286 
287     // Modification date
288     Element modificationDate = doc.createElement("modificationDate");
289     modificationDate.setTextContent(DateTimeSupport.toUTC(comment.getModificationDate().getTime()));
290     commentXml.appendChild(modificationDate);
291 
292     // Text
293     Element text = doc.createElement("text");
294     if (StringUtils.isNotBlank(comment.getText())) {
295       text.appendChild(doc.createCDATASection(comment.getText()));
296     }
297     commentXml.appendChild(text);
298 
299     // Reason
300     Element reason = doc.createElement("reason");
301     if (StringUtils.isNotBlank(comment.getReason())) {
302       reason.setTextContent(comment.getReason());
303     }
304     commentXml.appendChild(reason);
305 
306     // Replies
307     Element repliesNode = doc.createElement("replies");
308     for (EventCommentReply r : comment.getReplies()) {
309       repliesNode.appendChild(getAsXml(r, doc));
310     }
311     commentXml.appendChild(repliesNode);
312 
313     return commentXml;
314   }
315 
316   /**
317    * Serializes the comment reply to a {@link org.w3c.dom.Document}.
318    * 
319    * @param reply
320    *          the comment reply
321    * @return the serialized comment reply
322    */
323   private static Document getAsXmlDocument(EventCommentReply reply) {
324     Document doc = newDocument();
325 
326     Element replyNode = getAsXml(reply, doc);
327     return doc.appendChild(replyNode).getOwnerDocument();
328   }
329 
330   private static Element getAsXml(EventCommentReply reply, Document doc) {
331     // Root element "comment"
332     Element replyXml = doc.createElementNS(NAMESPACE, "reply");
333     replyXml.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", NAMESPACE);
334 
335     // Identifier
336     if (reply.getId().isSome()) {
337       replyXml.setAttribute("id", reply.getId().get().toString());
338     }
339 
340     // Author
341     Element authorNode = getAuthorNode(reply.getAuthor(), doc);
342     replyXml.appendChild(authorNode);
343 
344     // Creation date
345     Element creationDate = doc.createElement("creationDate");
346     creationDate.setTextContent(DateTimeSupport.toUTC(reply.getCreationDate().getTime()));
347     replyXml.appendChild(creationDate);
348 
349     // Modification date
350     Element modificationDate = doc.createElement("modificationDate");
351     modificationDate.setTextContent(DateTimeSupport.toUTC(reply.getModificationDate().getTime()));
352     replyXml.appendChild(modificationDate);
353 
354     // Text
355     Element text = doc.createElement("text");
356     if (StringUtils.isNotBlank(reply.getText())) {
357       text.appendChild(doc.createCDATASection(reply.getText()));
358     }
359 
360     replyXml.appendChild(text);
361 
362     return replyXml;
363   }
364 
365   private static Element getAuthorNode(User author, Document doc) {
366     Element authorNode = doc.createElement("author");
367     Element username = doc.createElement("username");
368     username.setTextContent(author.getUsername());
369     authorNode.appendChild(username);
370     Element email = doc.createElement("email");
371     email.setTextContent(author.getEmail());
372     authorNode.appendChild(email);
373     Element name = doc.createElement("name");
374     if (StringUtils.isNotBlank(author.getName())) {
375       name.appendChild(doc.createCDATASection(author.getName()));
376     }
377     authorNode.appendChild(name);
378     return authorNode;
379   }
380 
381   private static String serializeNode(Document xmlDocument) throws EventCommentException {
382     // Serialize the document
383     try {
384       Transformer transformer = XmlSafeParser.newTransformerFactory()
385               .newTransformer();
386       // transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
387       StringWriter writer = new StringWriter();
388       transformer.transform(new DOMSource(xmlDocument), new StreamResult(writer));
389       return writer.getBuffer().toString().trim();
390     } catch (Exception e) {
391       throw new EventCommentException(e);
392     }
393   }
394 
395   /** Create a new DOM document. */
396   private static Document newDocument() {
397     final DocumentBuilderFactory docBuilderFactory = XmlSafeParser.newDocumentBuilderFactory();
398     docBuilderFactory.setNamespaceAware(true);
399     try {
400       return docBuilderFactory.newDocumentBuilder().newDocument();
401     } catch (ParserConfigurationException e) {
402       return chuck(e);
403     }
404   }
405 
406 }