/* * This file is part of the DiffX library. * * For licensing information please see the file license.txt included in the release. * A copy of this licence can also be found at * http://www.opensource.org/licenses/artistic-license-2.0.php */ package com.topologi.diffx.load; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.xml.parsers.DocumentBuilder; import org.docx4j.XmlUtils; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; import org.xml.sax.InputSource; import com.topologi.diffx.config.DiffXConfig; import com.topologi.diffx.event.AttributeEvent; import com.topologi.diffx.event.CloseElementEvent; import com.topologi.diffx.event.OpenElementEvent; import com.topologi.diffx.event.TextEvent; import com.topologi.diffx.event.impl.EventFactory; import com.topologi.diffx.event.impl.ProcessingInstructionEvent; import com.topologi.diffx.load.text.TextTokenizer; import com.topologi.diffx.load.text.TokenizerFactory; import com.topologi.diffx.sequence.EventSequence; import com.topologi.diffx.sequence.PrefixMapping; /** * Loads a DOM documents as a sequence of events. * * <p>This class implements the methods {@link Recorder#process(File)} and * {@link Recorder#process(String)} for convenience, but is it much more efficient * to feed this recorder directly with a DOM. * * <p>This class is not synchronised. * * @author Christophe Lauret * @version 10 May 2010 * * @since 0.7 */ public final class DOMRecorder implements XMLRecorder { // class attributes ------------------------------------------------------------------------------- /** * The DiffX configuration to use */ private DiffXConfig config = new DiffXConfig(); // state variables -------------------------------------------------------------------------------- /** * The factory that will produce events according to the configuration. */ private transient EventFactory efactory; /** * The text tokenizer used by this recorder. */ private transient TextTokenizer tokenizer; /** * The sequence of event for this recorder. */ private transient EventSequence sequence; /** * The sequence of event for this recorder. */ private transient PrefixMapping mapping; /** * The weight of the current element. */ private transient int currentWeight = -1; /** * The stack of events' weight, should only contain <code>Integer</code>. */ private transient List<Integer> weights = new ArrayList<Integer>(); /** * Indicates whether the given document is a fragment. * * <p>An fragment is a portion of XML that is not necessarily well-formed by itself, because the * namespace has been declared higher in the hierarchy, in which if the DOM tree was serialised * it would not produce well-formed XML. * * <p>This option indicates that the recorder should try to generate the prefix mapping without * the declaration. */ private transient boolean isFragment = true; // methods ---------------------------------------------------------------------------------------- /** * Returns the configuration used by this recorder. * * @return the configuration used by this recorder. */ public DiffXConfig getConfig() { return this.config; } /** * Sets the configuration used by this recorder. * * @param config The configuration used by this recorder. */ public void setConfig(DiffXConfig config) { this.config = config; } /** * {@inheritDoc} */ public EventSequence process(File file) throws LoadingException, IOException { InputStream in = new BufferedInputStream(new FileInputStream(file)); return process(new InputSource(in)); } /** * {@inheritDoc} */ public EventSequence process(String xml) throws LoadingException { return process(new InputSource(new StringReader(xml))); } /** * Runs the recorder on the specified input source. * * @param is The input source. * * @return The recorded sequence of events. * * @throws LoadingException If thrown while parsing. */ public EventSequence process(InputSource is) throws LoadingException { this.isFragment = false; // input source is not a fragment // DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); // dbFactory.setNamespaceAware(this.config.isNamespaceAware()); // dbFactory.setExpandEntityReferences(true); // dbFactory.setValidating(false); try { DocumentBuilder builder = XmlUtils.getNewDocumentBuilder(); Document document = builder.parse(is); return this.process(document); } catch (Exception ex) { throw new LoadingException(ex); } } /** * Processes the given node and returns the corresponding event sequence. * * @param node The W3C DOM node to be processed. * * @return The recorded sequence of events. * * @throws LoadingException If thrown while parsing. */ public EventSequence process(Node node) throws LoadingException { // initialise the state variables. this.efactory = new EventFactory(this.config.isNamespaceAware()); this.tokenizer = TokenizerFactory.get(this.config); this.sequence = new EventSequence(); this.mapping = this.sequence.getPrefixMapping(); // start processing the nodes loadNode(node); this.isFragment = true; return this.sequence; } /** * Processes the given node list and returns the corresponding event sequence. * * <p>This method only returns the event sequence from the first node in the node list, if the * node list is empty, this method returns an empty sequence. * * @param node The W3C DOM node to be processed. * * @return The recorded sequence of events. * * @throws LoadingException If thrown while parsing. */ public EventSequence process(NodeList node) throws LoadingException { if (node.getLength() == 0) return new EventSequence(); return process(node.item(0)); } // specific loaders --------------------------------------------------------------------- /** * Loads the given node in the current sequence. * * @param node The W3C DOM node to load. * * @throws LoadingException If thrown while parsing. */ private void loadNode(Node node) throws LoadingException { // dispatch to the correct loader performance: order by occurrence if (node instanceof Element) { load((Element)node); } if (node instanceof Text) { load((Text)node); } else if (node instanceof Attr) { load((Attr)node); } else if (node instanceof Document) { load((Document)node); } else if (node instanceof ProcessingInstruction) { load((ProcessingInstruction)node); // all other node types are ignored } } /** * Loads the given document in the current sequence. * * @param document The W3C DOM document node to load. * * @throws LoadingException If thrown while parsing. */ private void load(Document document) throws LoadingException { load(document.getDocumentElement()); } /** * Loads the given element in the current sequence. * * @param element The W3C DOM element node to load. * * @throws LoadingException If thrown while parsing. */ private void load(Element element) throws LoadingException { if (this.currentWeight > 0) { this.weights.add(Integer.valueOf(this.currentWeight)); } this.currentWeight = 1; // namespace handling OpenElementEvent open = null; // namespace aware configuration if (this.config.isNamespaceAware()) { String uri = element.getNamespaceURI() == null? "" : element.getNamespaceURI(); String name = element.getLocalName(); handlePrefixMapping(uri, element.getPrefix()); open = this.efactory.makeOpenElement(uri, name); // not namespace aware } else { open = this.efactory.makeOpenElement(null, element.getNodeName()); } this.sequence.addEvent(open); NamedNodeMap atts = element.getAttributes(); // only 1 attribute, just load it if (atts.getLength() == 1) { load((Attr)atts.item(0)); // several attributes sort them in alphabetical order // TODO: also use URI } else if (atts.getLength() > 1) { String[] names = new String[atts.getLength()]; for (int i = 0; i < atts.getLength(); i++) { Attr attr = (Attr)atts.item(i); names[i] = attr.getName(); } Arrays.sort(names); for (String name : names) { load((Attr)atts.getNamedItem(name)); } } // load all the child nodes NodeList list = element.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { loadNode(list.item(i)); } CloseElementEvent close = this.efactory.makeCloseElement(open); this.sequence.addEvent(close); // handle the weights close.setWeight(this.currentWeight); open.setWeight(this.currentWeight); this.currentWeight += popWeight(); } /** * Loads the given text in the current sequence depending on the configuration. * * @param text The W3C DOM text node to load. * * @throws LoadingException If thrown while parsing. */ private void load(Text text) throws LoadingException { List<TextEvent> events = this.tokenizer.tokenize(text.getData()); for (TextEvent e : events) { this.sequence.addEvent(e); } this.currentWeight += events.size(); } /** * Loads the given processing instruction in the current sequence. * * @param pi The W3C DOM PI node to load. * * @throws LoadingException If thrown while parsing. */ private void load(ProcessingInstruction pi) throws LoadingException { this.sequence.addEvent(new ProcessingInstructionEvent(pi.getTarget(), pi.getData())); this.currentWeight++; } /** * Returns the last weight and remove it from the stack. * * @return The weight on top of the stack. */ private int popWeight() { if (this.weights.size() > 0) return ((Integer)this.weights.remove(this.weights.size() - 1)).intValue(); else return 0; } /** * Loads the given attribute in the current sequence. * * @param attr The W3C DOM attribute node to load. */ private void load(Attr attr) { handlePrefixMapping(attr.getNamespaceURI(), attr.getPrefix()); load(this.efactory.makeAttribute(attr.getNamespaceURI(), attr.getLocalName(), attr.getNodeName(), attr.getValue())); } /** * Loads the given attribute in the current sequence. * * @param e An attribute event. */ private void load(AttributeEvent e) { // a namespace declaration, translate the event into a prefix mapping if ("http://www.w3.org/2000/xmlns/".equals(e.getURI())) { this.sequence.mapPrefix(e.getValue(), e.getName()); // a regular attribute } else { e.setWeight(2); this.currentWeight += 2; this.sequence.addEvent(e); } } /** * Handles the prefix mapping. * * If the current process is working on a fragment, * * @param uri The namespace URI. * @param prefix The prefix used for the namespace. */ private void handlePrefixMapping(String uri, String prefix) { if (this.isFragment) { if (this.mapping.getPrefix(uri) != null) return; if (prefix == null && !"".equals(uri)) { this.mapping.add(uri, ""); } else if (prefix != null && !"xmlns".equals(prefix)) { this.mapping.add(uri, prefix); } } } }