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}