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