// // Copyright 2009 Robin Komiwes, Bruno Verachten, Christophe Cordenier // // Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 // // 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 com.wooki.services.parsers; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.util.List; import java.util.Set; import org.apache.tapestry5.ioc.internal.util.CollectionFactory; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; import org.jdom.xpath.XPath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.wooki.domain.exception.PublicationXmlException; import com.wooki.domain.model.Comment; public class DOMManagerImpl implements DOMManager { private static final String COMMENTABLE_CLASS = "commentable"; private static final String BLOCKQUOTE = "blockquote"; private final Set<String> COMMENTABLE = CollectionFactory.newSet( "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "pre", "blockquote"); private static final String CHAPTER_ROOT_NODE = "chapter"; /** * Used to set the current id to start from when assigning ids to node. */ private static final String ID_START = "idStart"; private final Logger logger = LoggerFactory.getLogger(DOMManagerImpl.class); private String characterEncoding = "UTF-8"; /** * Used to allocate id on content. * * @author ccordenier */ class IdAllocator { private long startIdx; public IdAllocator(long startIdx) { this.startIdx = startIdx; } public long next() { return startIdx++; } public long current() { return startIdx; } } public String adaptContent(String content, Long prefix) { StringBuffer result = new StringBuffer(); result.append("<?xml version=\"1.0\" encoding=\"" + this.getCharacterEncoding() + "\"?>"); result.append("<").append(CHAPTER_ROOT_NODE).append(" ").append(ID_START).append("=\"0\">"); result.append(content); result.append("</").append(CHAPTER_ROOT_NODE).append(">"); return addIds(result.toString(), prefix); } private String addIds(String document, Long prefix) { // Parse document Document doc = parseContent(document); if (doc == null) { return null; } // Get the current number of node used to add unique id on node to // link with comments String nbNode = doc.getRootElement().getAttributeValue(ID_START); long idx = Long.parseLong(nbNode); IdAllocator allocator = new IdAllocator(idx); for (Element elt : (List<Element>) doc.getRootElement().getChildren()) { buildIds(allocator, elt, prefix); } // Set the new value for doc.getRootElement().setAttribute(ID_START, "" + allocator.current()); return serializeContent(doc, true); } public void reAssignComment(List<Comment> comments, String content, String newContent) { Document currentDoc = parseContent(content); Document newDoc = parseContent(newContent); if (currentDoc != null && newDoc != null) { for (Comment comment : comments) { String domId = comment.getDomId(); try { // Verify that the comment do no exist in the new document XPath path = XPath.newInstance("//*[@id=" + domId + "]"); Element elt = (Element) path.selectSingleNode(newDoc.getRootElement()); // Reassign if needed if (elt == null) { elt = (Element) path.selectSingleNode(currentDoc.getRootElement()); // Cannot find the element in the existing document if (elt == null) { comment.setDomId(null); break; } if (elt.getParent() != null) { int idx = elt.getParent().indexOf(elt); boolean found = false; for (int i = idx - 1; i >= 0; i--) { Element cont = (Element) elt.getParentElement().getContent(i); if (cont.getName().startsWith("h")) { XPath reaffect = XPath.newInstance("//*[@id=" + cont.getAttributeValue("id") + "]"); // Check that the new node still exist if (reaffect.selectSingleNode(newDoc) != null) { found = true; comment.setDomId(cont.getAttributeValue("id")); break; } } } if (!found) { if (CHAPTER_ROOT_NODE.equals(elt.getParentElement().getName())) { comment.setDomId(null); } else { comment .setDomId(elt.getParentElement() .getAttributeValue("id")); } } } else { } } } catch (JDOMException e) { logger.error(e.getMessage()); } } } else { logger.error("Document content cannot be parsed during comment reassignment."); } } /** * This method can be called to generate bookmarks for PDF. * * @param content * @return */ public String generatePdfBookmarks(String content, int startIdx, int level) { Document result = new Document(); Element root = new Element("bookmarks"); result.addContent(root); Document doc = parseContent(content); List<Element> children = doc.getRootElement().getChildren(); int currentIdx = 0; Element currentElt = root; if (children != null) { for (Element child : children) { if (child.getName().startsWith("h")) { int hIdx = this.readIndex(child.getName()); // Check if the header should be added to the bookmark list if (hIdx >= startIdx && (hIdx - startIdx) >= 0 && Math.abs(hIdx - currentIdx) <= level) { int navigate = hIdx - startIdx; if (navigate == currentIdx) { Element e = new Element("bookmark"); e.setAttribute("name", child.getValue()); e.setAttribute("href", "#" + child.getAttributeValue("id")); currentElt.addContent(e); } else if (navigate > currentIdx) { for (int i = currentIdx; i < navigate; i++) { if (currentElt.getChildren() != null && currentElt.getChildren().size() > 0) { currentElt = (Element) currentElt.getChildren().get( currentElt.getChildren().size() - 1); } else { Element e = new Element("bookmark"); e.setAttribute("name", "undefined"); currentElt.addContent(e); currentElt = e; } } Element toAdd = new Element("bookmark"); toAdd.setAttribute("name", child.getValue()); toAdd.setAttribute("href", "#" + child.getAttributeValue("id")); currentElt.addContent(toAdd); } else { for (int i = currentIdx; i > navigate; i--) { currentElt = currentElt.getParentElement(); } Element e = new Element("bookmark"); e.setAttribute("name", child.getValue()); e.setAttribute("href", "#" + child.getAttributeValue("id")); currentElt.addContent(e); } currentIdx = navigate; } } } } // Serialize result return serializeContent(result, false); } /** * Read header index. * * @param header * @return */ private int readIndex(String header) { String idx = header.substring(1); return Integer.parseInt(idx); } /** * Recursivily add ids to dom elements. * * @param elt */ private void buildIds(IdAllocator allocator, Element elt, Long prefix) { if (COMMENTABLE.contains(elt.getName().toLowerCase())) { if (prefix != null) { elt.setAttribute("id", "b" + prefix.toString() + allocator.next()); } else { elt.setAttribute("id", "b" + allocator.next()); } elt.setAttribute("class", COMMENTABLE_CLASS); } if (!BLOCKQUOTE.equalsIgnoreCase(elt.getName())) { for (Element child : (List<Element>) elt.getChildren()) { buildIds(allocator, child, prefix); } } else { for (Element child : (List<Element>) elt.getChildren()) { clearId(child); } } } /** * Clear all subsequent ids. * * @param elt */ private void clearId(Element elt) { if (elt != null) { elt.removeAttribute("id"); for (Element child : (List<Element>) elt.getChildren()) { clearId(child); } } } /** * Used to transform String into DOM element. * * @param content * @return */ private Document parseContent(String content) { try { SAXBuilder builder = new SAXBuilder(); builder.setValidation(false); builder.setIgnoringElementContentWhitespace(true); Document doc = builder.build(new StringReader(content)); return doc; } catch (JDOMException jdEx) { logger.error("Error during document parsing", jdEx); throw new PublicationXmlException( "An parsing error has occured during document analysis", jdEx); } catch (IOException ioEx) { logger.error("Error while reading parse document", ioEx); // Return null in case of errors throw new PublicationXmlException("An io error has occured during document analysis", ioEx); } } /** * Transform DOM into String representation. * * @param doc * @param prettyPrint * TODO * @return */ private String serializeContent(Document doc, boolean prettyPrint) { XMLOutputter output; if (prettyPrint) { output = new XMLOutputter(Format.getPrettyFormat().setEncoding( this.getCharacterEncoding())); } else { output = new XMLOutputter(Format.getCompactFormat().setEncoding( this.getCharacterEncoding())); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { if (doc != null) { List<Element> children = doc.getRootElement().getChildren(); if (children != null) { for (Element elt : children) { output.output(elt, bos); } } } bos.flush(); return new String(bos.toByteArray(), this.getCharacterEncoding()); } catch (IOException ioEx) { logger.error("Error during document serialization", ioEx); return ""; } finally { if (bos != null) { try { bos.close(); } catch (IOException ioEx) { // Cannot do anything } } } } public String getCharacterEncoding() { return characterEncoding; } public void setCharacterEncoding(String characterEncoding) { this.characterEncoding = characterEncoding; } }