001package components.xmltree;
002
003import java.io.File;
004import java.io.IOException;
005import java.io.InputStreamReader;
006import java.io.Reader;
007import java.net.MalformedURLException;
008import java.net.URI;
009import java.net.URISyntaxException;
010import java.net.URL;
011import java.nio.charset.Charset;
012
013import javax.swing.JFrame;
014import javax.xml.parsers.DocumentBuilder;
015import javax.xml.parsers.DocumentBuilderFactory;
016import javax.xml.parsers.ParserConfigurationException;
017
018import org.w3c.dom.Document;
019import org.w3c.dom.NamedNodeMap;
020import org.w3c.dom.Node;
021import org.w3c.dom.NodeList;
022import org.xml.sax.InputSource;
023import org.xml.sax.SAXException;
024
025import components.map.Map;
026import components.map.Map1L;
027import components.sequence.Sequence;
028import components.set.Set;
029import components.set.Set1L;
030import components.tree.Tree;
031import components.tree.Tree1;
032
033/**
034 * {@code XMLTree} represented as a {@link Tree}, with implementations of all
035 * methods.
036 */
037public class XMLTree2 extends XMLTreeSecondary {
038
039    /*
040     * Private members --------------------------------------------------------
041     */
042
043    /**
044     * Node for the tree representation.
045     */
046    private static final class TreeNode {
047
048        /**
049         * The label (a tag or plain text).
050         */
051        private String label;
052
053        /**
054         * Flag to record whether the label is a tag.
055         */
056        private boolean isTag;
057
058        /**
059         * Map from attribute name to attribute value for the tag.
060         */
061        private Map<String, String> attributes;
062
063    }
064
065    /**
066     * Tree representation for the {@code XMLTree}.
067     */
068    private final Tree<TreeNode> rep;
069
070    /**
071     * The window to display the {@code XMLTree}.
072     */
073    private JFrame frame;
074
075    /**
076     * Parses the XML input and constructs (and returns) the corresponding DOM
077     * tree.
078     *
079     * @param url
080     *            the XML source (either a file or web URL)
081     * @return the XML DOM tree
082     */
083    private Node getXML(URL url) {
084        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
085        DocumentBuilder db = null;
086        Document doc = null;
087        try {
088            db = dbf.newDocumentBuilder();
089            InputSource is = new InputSource(url.toString());
090            doc = db.parse(is);
091        } catch (ParserConfigurationException e) {
092            throw new AssertionError("Violation of: input is valid XML");
093        } catch (SAXException e) {
094            throw new AssertionError("Violation of: input is valid XML");
095        } catch (IOException e) {
096            // problem reading input: it may be an encoding issue
097            try {
098                String encoding = "UTF-8";
099                if (Charset.defaultCharset().toString().equals(encoding)) {
100                    encoding = "windows-1252";
101                }
102                Reader reader = new InputStreamReader(url.openStream(), encoding);
103                InputSource is = new InputSource(reader);
104                doc = db.parse(is);
105                try {
106                    reader.close();
107                } catch (IOException e1) {
108                    throw new AssertionError(
109                            "Violation of: " + url.getFile() + " can be closed");
110                }
111            } catch (MalformedURLException e1) {
112                throw new AssertionError("Violation of: input is valid XML");
113            } catch (SAXException e1) {
114                throw new AssertionError("Violation of: input is valid XML");
115            } catch (IOException e1) {
116                throw new AssertionError("Violation of: input is valid XML");
117            } catch (NullPointerException e1) { // could occur on line 103?
118                throw new AssertionError("Violation of: input is valid XML");
119            }
120        }
121        return doc.getDocumentElement();
122    }
123
124    /**
125     * Constructs the XMLTree representation from the given {@code Node}.
126     *
127     * @param root
128     *            the DOM tree to construct tree
129     * @param trimWhitespace
130     *            flag to indicate whether leading and trailing whitespace
131     *            should be trimmed from text leaves
132     * @return the new tree representation
133     */
134    private static Tree<TreeNode> constructTree(Node root, boolean trimWhitespace) {
135        Tree<TreeNode> tree = new Tree1<TreeNode>();
136        TreeNode tNode = new TreeNode();
137        Sequence<Tree<TreeNode>> children = tree.newSequenceOfTree();
138        if ((root.getNodeType() == Node.TEXT_NODE)
139                || (root.getNodeType() == Node.CDATA_SECTION_NODE)) {
140            String text = root.getTextContent();
141            if (trimWhitespace) {
142                text = text.trim();
143            }
144            tNode.label = text;
145        } else {
146            tNode.label = root.getNodeName();
147            tNode.isTag = true;
148            tNode.attributes = constructAttributes(root);
149
150            NodeList nl = root.getChildNodes();
151            for (int pos = 0; pos < nl.getLength(); pos++) {
152                Node child = nl.item(pos);
153                Tree<TreeNode> subtree = constructTree(child, trimWhitespace);
154                if (subtree.root().isTag) {
155                    children.add(children.length(), subtree);
156                } else {
157                    if (!subtree.root().label.equals("")) {
158                        children.add(children.length(), subtree);
159                    }
160                }
161            }
162        }
163        tree.assemble(tNode, children);
164        return tree;
165    }
166
167    /**
168     * Returns attribute map constructed from the given {@code Node}.
169     *
170     * @param root
171     *            the DOM tree whose root's attributes will be returned in the
172     *            attribute map
173     * @return the map of attribute (name, value) pairs
174     */
175    private static Map<String, String> constructAttributes(Node root) {
176        Map<String, String> map = new Map1L<String, String>();
177        NamedNodeMap nnm = root.getAttributes();
178        if (nnm != null) {
179            for (int i = 0; i < nnm.getLength(); i++) {
180                Node a = nnm.item(i);
181                map.add(a.getNodeName(), a.getNodeValue());
182            }
183        }
184        return map;
185    }
186
187    /**
188     * Constructor to be used by {@code child}.
189     *
190     * @param rep
191     *            the representation for the new {@code XMLTree}
192     */
193    private XMLTree2(Tree<TreeNode> rep) {
194        this.rep = rep;
195    }
196
197    /*
198     * Constructors -----------------------------------------------------------
199     */
200
201    /**
202     * Constructs an {@code XMLTree} from input {@code source} (could be a file
203     * or a URL). Leading and trailing whitespace is trimmed from text nodes.
204     *
205     * @param source
206     *            XML input
207     * @requires source is the name of a file or a URL
208     * @ensures <pre>
209     * this = [the XMLTree corresponding to the given input source,
210     *         with whitespace trimmed]
211     * </pre>
212     */
213    public XMLTree2(String source) {
214        this(source, true);
215    }
216
217    /**
218     * Constructs an {@code XMLTree} from input {@code source} (could be a file
219     * or a URL).
220     *
221     * @param source
222     *            XML input
223     * @param trimWhitespace
224     *            flag to indicate whether leading and trailing whitespace
225     *            should be trimmed from text nodes
226     * @requires source is the name of a file or a URL
227     * @ensures <pre>
228     * this = [the XMLTree corresponding to the given input source,
229     *         with whitespace trimmed only if trimWhitespace]
230     * </pre>
231     */
232    public XMLTree2(String source, boolean trimWhitespace) {
233        assert source != null : "Violation of: source is not null";
234        URL url = null;
235        try {
236            url = new URI(source).toURL();
237        } catch (URISyntaxException | MalformedURLException e) {
238            if (source.indexOf("://") != -1) {
239                // assume it was a URL
240                throw new AssertionError("Violation of: " + source + " is a valid URL");
241            }
242            try {
243                File file = new File(source);
244                if (!file.exists()) {
245                    throw new AssertionError("Violation of: " + source + " exists");
246                }
247                if (!file.isFile()) {
248                    throw new AssertionError("Violation of: " + source + " is a file");
249                }
250                if (!file.canRead()) {
251                    throw new AssertionError("Violation of: " + source + " is readable");
252                }
253                url = file.toURI().toURL();
254            } catch (MalformedURLException e1) {
255                throw new AssertionError(
256                        "Violation of: " + source + " is a valid XML source");
257            }
258        }
259
260        Node root = this.getXML(url);
261        this.rep = constructTree(root, trimWhitespace);
262    }
263
264    /*
265     * Kernel methods ---------------------------------------------------------
266     */
267
268    @Override
269    public final String label() {
270        return this.rep.root().label;
271    }
272
273    @Override
274    public final boolean isTag() {
275        return this.rep.root().isTag;
276    }
277
278    @Override
279    public final boolean hasAttribute(String name) {
280        assert name != null : "Violation of: name is not null";
281        assert this.rep.root().isTag
282                : "Violation of: the label of the root of this is a tag";
283        return this.rep.root().attributes.hasKey(name);
284    }
285
286    @Override
287    public final String attributeValue(String name) {
288        assert name != null : "Violation of: name is not null";
289        assert this.rep.root().isTag
290                : "Violation of: the label of the root of this is a tag";
291        assert this.rep.root().attributes.hasKey(name)
292                : "Violation of: the root of this has an attribute called " + name;
293        return this.rep.root().attributes.value(name);
294    }
295
296    @Override
297    public final int numberOfChildren() {
298        assert this.rep.root().isTag
299                : "Violation of: the label of the root of this is a tag";
300        Sequence<Tree<TreeNode>> children = this.rep.newSequenceOfTree();
301        TreeNode root = this.rep.disassemble(children);
302        int length = children.length();
303        this.rep.assemble(root, children);
304        return length;
305    }
306
307    @Override
308    public final XMLTree child(int k) {
309        assert this.rep.root().isTag
310                : "Violation of: the label of the root of this is a tag";
311        assert k >= 0 : "Violation of: 0 <= k";
312        assert k < this.rep.numberOfSubtrees()
313                : "Violation of: k < the number of subtrees of the root of this";
314        Sequence<Tree<TreeNode>> children = this.rep.newSequenceOfTree();
315        TreeNode root = this.rep.disassemble(children);
316        Tree<TreeNode> child = children.entry(k);
317        this.rep.assemble(root, children);
318        return new XMLTree2(child);
319    }
320
321    @Override
322    public final Iterable<String> attributeNames() {
323        assert this.rep.root().isTag
324                : "Violation of: the label of the root of this is a tag";
325        Set<String> set = new Set1L<>();
326        for (Map.Pair<String, String> pair : this.rep.root().attributes) {
327            set.add(pair.key());
328        }
329        return set;
330    }
331
332    @Override
333    public final void display() {
334        this.display(XMLTreeFrame.DEFAULT_TITLE);
335    }
336
337    @Override
338    public final void display(String title) {
339        if (this.frame == null) {
340            this.frame = new XMLTreeFrame(this, title);
341        }
342        this.frame.setVisible(true);
343    }
344
345}