View Javadoc

1   /*
2    * Created on Aug 26, 2004
3    */
4   package DTDDoc;
5   
6   import java.io.PrintWriter;
7   import java.text.Collator;
8   import java.util.*;
9   
10  import com.wutka.dtd.*;
11  
12  /**
13   * Used to build the HTML/Javascript code necessary to set up the d-tree.
14   *
15   * <p>
16   * Please note we used d-tree code as a <em>basis</em> for our. d-tree was not
17   * our original work. Nothing on the original copyright notice stated that we
18   * can't use it the way we do. We contacted the author about that but had no
19   * answer so far. Therefore, we conclude that we can use his code (and the
20   * JavaScript library as well). If some disagree, we'll be able to explain and,
21   * if needed, comply...
22   * </p>
23   *
24   * <p>
25   * This class traverses a tree defined as follows:
26   * <ul>
27   * <li>The root is a DTD file.
28   * <li>The children of the root are all the elements in the file.
29   * <li>The children of each element are the content model and attribute list
30   * for that tree.
31   * </ul>
32   * Emits a tree node for each node in this tree.
33   * </p>
34   *
35   * @author Michael Koehrsen
36   * @author Stefan Champailler
37   */
38  public class ElementTreeBuilder {
39  	/**
40  	 * ATTENTION ! Not used right now. Was prototyped in the hope to clean the
41  	 * code some more
42  	 */
43  
44  	private static class TreeRunState {
45  		/** Maps DTDelement to their numerical Id. */
46  		private final Map elementNodeIds = new HashMap();
47  
48  		/** The Id that the next element (in the map) will get. */
49  		private int nextNodeId = 0;
50  
51  		/**
52  		 * Allocates an id for an element of the tree. Please note that id
53  		 * allocated this way will not be tied to a particular DTDElement. So
54  		 * please use this function for things that are not DTDElement.
55  		 *
56  		 * @return A new id.
57  		 */
58  		int allocateIdGeneric() {
59  			int nid = nextNodeId;
60  			nextNodeId++;
61  			return nid;
62  
63  		}
64  
65  		/**
66  		 * Adds a element to the mapping. If the element is already in the
67  		 * mapping, doesn't do anything.
68  		 *
69  		 * @param element
70  		 *            Element to add.
71  		 * @return The id associted to the node.
72  		 */
73  
74  		int addIdElement(DTDElement element) throws Exception {
75  			String key = element.getName();
76  
77  			if (!isNodeVisited(element)) {
78  				int nid = allocateIdGeneric();
79  				elementNodeIds.put(key, new Integer(nid));
80  				return nid;
81  			} else
82  				return getElementId(element);
83  		}
84  
85  		boolean isNodeVisited(DTDElement element) {
86  			return elementNodeIds.containsKey(element.getName());
87  		}
88  
89  		int getElementId(DTDElement element) throws Exception {
90  			String key = element.getName();
91  
92  			if (isNodeVisited(element))
93  				return ((Integer) elementNodeIds.get(key)).intValue();
94  			else
95  				throw new Exception("Requested the id of " + key + ", which"
96  						+ " doesn't exist !");
97  
98  		}
99  	}
100 
101 	private Logger log;
102 	
103 	// modifier bitmasks:
104 	private static final int UNMODIFIED = 0;
105 
106 	private static final int BOLD = 1;
107 
108 	private static final int ITALIC = 2;
109 
110 	private static final int ALWAYS_OPEN = 4;
111 
112 	private static final int INITIALLY_OPEN = 8;
113 
114 	private final PrintWriter out;
115 
116 	private final DTDCommenter commenter;
117 
118 	private final Set dtds;
119 
120 	private ExtendedDTD currentDTD;
121 
122 	private int nextNodeId = 0;
123 
124 	private Map elementNodeIds = new HashMap(); // Maps element names to node
125 												// ids for currentDTD
126 
127 	public ElementTreeBuilder(PrintWriter out, DTDCommenter commenter,
128 			Set dtds, Logger log) {
129 		this.log = log;
130 		this.out = out;
131 		this.commenter = commenter;
132 		this.dtds = dtds;
133 	}
134 
135 	/**
136 	 * Builds the HTML/Javascript code necessary to the invocation of d-tree.
137 	 */
138 
139 	public void generateTree() {
140 		out.println("<div class='dtree'>");
141 		out.println("<script type='text/javascript'>");
142 		out.println("//<!--"); // comment script body
143 
144 		out.println("eltTree = new CCTree('detail');");
145 
146 		// The nodes will be identified according to the following ID.
147 		// therefore, we make sure it is suitable before going any further.
148 		nextNodeId = 0;
149 
150 		// Note that the top node as a parent with id of -1.
151 		//addTreeNode(-1, "Files", null,ALWAYS_OPEN);
152 		
153 		SortedMap dtdsByTitle = new TreeMap(Collator.getInstance());
154 
155 		for (Iterator iter = dtds.iterator(); iter.hasNext(); ) {
156 			ExtendedDTD dtd = (ExtendedDTD)iter.next();
157 			dtdsByTitle.put(dtd.getTitle(), dtd);
158 		}
159 
160 		for (Iterator iter = dtdsByTitle.entrySet().iterator(); iter.hasNext(); ) {
161 			Map.Entry entry = (Map.Entry)iter.next();
162 			ExtendedDTD dtd = (ExtendedDTD)entry.getValue();
163 			visitCurrentDTD(dtd);
164 		}
165 
166 		out.println("document.write(eltTree);");
167 		out.println("//-->");
168 		out.println("</script>");
169 		out.println("</div>");
170 	}
171 
172 	private void visitCurrentDTD(ExtendedDTD dtd) {
173 		// Some initialisations...
174 		currentDTD = dtd;
175 		elementNodeIds = new HashMap();
176 
177 		// The DTD's name is a root node.
178 		int nodeId = generateTreeNode(-1, dtd.getTitle(), commenter
179 				.getDTDBaseURI(dtd), UNMODIFIED);
180 
181 		// The first level of this DTD children is populated with
182 		// the elements definitions. We sort them to make the read
183 		// easier.
184 
185 		Collection c = Tools.sort(dtd.getElementsCollection(),
186 				new Comparator() {
187 					public int compare(Object oa, Object ob) {
188 						DTDElement a = (DTDElement) oa;
189 						DTDElement b = (DTDElement) ob;
190 						return a.getName().compareToIgnoreCase(b.getName());
191 					}
192 				});
193 
194 		// Build the tree.
195 
196 		for (Iterator i = c.iterator(); i.hasNext();)
197 			visitElement(nodeId, (DTDElement) i.next(), null, true);
198 	}
199 
200 	/**
201 	 * Visit an element and generate the Javascript d-tree code.
202 	 *
203 	 * @param parentNodeId
204 	 *            Id of the parent of the element
205 	 * @param dtdElement
206 	 *            The element
207 	 * @param cardinality
208 	 *            The cardinality <em>applied</em> to the element (this is
209 	 *            obviously given in the parent definition).
210 	 * @throws Exception
211 	 */
212 	private void visitElement(int parentNodeId, DTDElement dtdElement,
213 			DTDCardinal cardinality, boolean firstLevel) {
214 		// We check if we have already visited the element. This avoids
215 		// all sorts of infinite loop.
216 
217 		if (elementNodeIds.containsKey(dtdElement)) {
218 			// Since it's the case, we don't need to generate the full
219 			// node again. A simple link-node will suffice.
220 
221 			generateTreeLink(parentNodeId, ((Integer) elementNodeIds
222 					.get(dtdElement)).intValue(), dtdElement.getName()
223 					+ getCardinalitySymbol(cardinality),
224 					getCardinalityModifier(cardinality));
225 			return;
226 		}
227 
228 		// It's the first time we visit the element. Therefore, a corresponding
229 		// node must be created in the tree.
230 
231 		String name = dtdElement.getName();
232 
233 		// Make empty element clear.
234 		if (dtdElement.getContent() instanceof DTDEmpty)
235 			name = "&lt;" + name + "/&gt;";
236 		else if (dtdElement.getContent() instanceof DTDAny)
237 			name = name + " (any)";
238 
239 		int typefaceModifier = getCardinalityModifier(cardinality);
240 
241 		// Make the root appear clearly. Root is emphasized only when
242 		// displayed on the first level of children of the DTD node.
243 
244 		if (currentDTD.isRoot(dtdElement) && firstLevel) {
245 			name = name + " (root)";
246 			typefaceModifier |= BOLD;
247 		}
248 
249 		int nodeId = generateTreeNode(parentNodeId, name
250 				+ getCardinalitySymbol(cardinality), commenter.newURITo(
251 				currentDTD, dtdElement).toString(), typefaceModifier);
252 
253 		// Remeber that we visited the current element.
254 		elementNodeIds.put(dtdElement, new Integer(nodeId));
255 
256 		// Show attributes, if any
257 		generateAttributesNodes(nodeId, dtdElement);
258 
259 		// Now we look for recursion ddeper in the tree
260 
261 		if (dtdElement.getContent() instanceof DTDContainer)
262 
263 			visitContainer(nodeId, (DTDContainer) dtdElement.getContent());
264 
265 		else if (dtdElement.getContent() instanceof DTDAny) {
266 
267 			// See section 3 of the XML spec for info about this ANY thing.
268 
269 			Iterator i = currentDTD.getElementsCollection().iterator();
270 			while (i.hasNext())
271 				// This won't loop infitenly because we shortcut
272 				// it at the beginning of this method.
273 				visitElement(nodeId, (DTDElement) i.next(), null, true);
274 		}
275 
276 		// FIXME what about DTDName,
277 		// DTDPCData : must appear in parenthieseis, that is, in a container.
278 	}
279 
280 	/**
281 	 * Visit a container and generate the Javascript d-tree code. The children
282 	 * of the container are visited (recursively) as well.
283 	 *
284 	 * @param parentId
285 	 *            The parent id of the parent of the container (-1 if none).
286 	 * @param container
287 	 *            The container to visit.
288 	 * @throws Exception
289 	 *             If anything goes wrong.
290 	 */
291 
292 	public void visitContainer(final int parentId, DTDContainer container) {
293 		// We simplify the display of elements defined like this :
294 		// <!ELEMENT alpha (#PCDATA)>
295 		// is shown as a simple element (i.e. containing just text
296 		// with no apparent structure, although it's a container).
297 
298 		if (container instanceof DTDMixed && container.getItems() != null
299 				&& container.getItems().length == 1
300 				&& container.getItems()[0] instanceof DTDPCData)
301 			return;
302 
303 		int newNodeId = parentId;
304 
305 		// A sequence without cardinality (for example
306 		// <!ELEMENT alpha (beta,gamma,etha)>) is not shown
307 		// explicitely, to ease reading.
308 
309 		if (!(container instanceof DTDSequence && container.getCardinal() == DTDCardinal.NONE))
310 			newNodeId = generateContainerNode(container, parentId);
311 
312 		// Now we recurse...
313 
314 		//DTDItem[] childItems = container.getItems();
315 		Iterator i = container.getItemsVec().iterator();
316 
317 		while (i.hasNext()) {
318 			DTDItem childItem = (DTDItem) i.next();
319 
320 			if (childItem instanceof DTDName) {
321 
322 				DTDElement childElement = (DTDElement) currentDTD
323 						.getElementByName((DTDName) childItem);
324 
325 				if (childElement != null)
326 					visitElement(newNodeId, childElement, childItem
327 							.getCardinal(), true);
328                                 // SC 3/8/2006 Removed this log 'cos too verbose
329 				//else
330 				//	log.warn("Malformed DTD ! Unknown element: "
331 				//			+ ((DTDName) childItem).getValue() + " in "
332 				//			+ currentDTD.getSystemPath());
333 			} else if (childItem instanceof DTDContainer)
334 				visitContainer(newNodeId, (DTDContainer) childItem);
335 
336 			// Note : EMPTY and ANY can't appear here because we're inside
337 			// a container
338 		}
339 	}
340 
341 	/**
342 	 * Generate the d-tree code for a container node. Does not recurse over the
343 	 * children of the container.
344 	 *
345 	 * @param container
346 	 *            The container node.
347 	 * @param parentId
348 	 *            The id of the parent of the container (-1 if none).
349 	 * @return The id associated to the container node.
350 	 */
351 
352 	private int generateContainerNode(DTDContainer container, int parentId) {
353 
354 		String s = null;
355 
356 		if (container instanceof DTDMixed)
357 			s = "mixed";
358 		else if (container instanceof DTDChoice)
359 			s = "choice";
360 		else if (container instanceof DTDSequence)
361 			s = "sequence";
362 		else
363 			log.error("Unsupported type of container: " + container.getClass());
364 
365 		return generateTreeNode(parentId, "&lt;" + s + "&gt;"
366 				+ getCardinalitySymbol(container.getCardinal()), null, ITALIC
367 				| getCardinalityModifier(container.getCardinal()) | ALWAYS_OPEN);
368 	}
369 
370 	private String getCardinalitySymbol(DTDCardinal card) {
371 		if (card == DTDCardinal.OPTIONAL)
372 			return "?";
373 		if (card == DTDCardinal.ZEROMANY)
374 			return "*";
375 		if (card == DTDCardinal.ONEMANY)
376 			return "+";
377 		return "";
378 	}
379 
380 	private int getCardinalityModifier(DTDCardinal card) {
381 		if ((card == DTDCardinal.NONE) || (card == DTDCardinal.ONEMANY))
382 			return BOLD;
383 		else
384 			return UNMODIFIED;
385 	}
386 
387 	/**
388 	 * Generates the Javascript code for representing the attributes of a given
389 	 * element. Once displayed in the tree, the attributes will be sorted
390 	 * according to the natural order of their names (e.g. alphabetical).
391 	 *
392 	 * @param parentNodeId
393 	 *            Parent id of the element.
394 	 * @param dtdElement
395 	 *            Element to get the attributes from.
396 	 * @throws Exception
397 	 */
398 
399 	private void generateAttributesNodes(int parentNodeId, DTDElement dtdElement) {
400 		// Make sure there's actually something to show
401 		if (dtdElement.attributes == null || dtdElement.attributes.size() <= 0)
402 			return;
403 
404 		SortedMap sortedAttributes = new TreeMap(dtdElement.attributes);
405 
406 		// Build the Javascript code for each attribute
407 
408 		for (Iterator iter = sortedAttributes.entrySet().iterator(); iter.hasNext(); ) {
409 			Map.Entry entry = (Map.Entry)iter.next();
410 			DTDAttribute attr = (DTDAttribute) entry.getValue();
411 			int modifier = UNMODIFIED;
412 			if (attr.decl == DTDDecl.REQUIRED)
413 				modifier = BOLD;
414 
415 			generateTreeNode(parentNodeId, "@" + attr.getName(),
416 					commenter.newURITo(currentDTD, dtdElement, attr).toString(),
417 					modifier);
418 		}
419 	}
420 
421 	/**
422 	 * Generates the Javascript code for one node of the d-tree.
423 	 *
424 	 * @param parentNodeId
425 	 *            The id of the parent node. If the node is the root node, then
426 	 *            this must be -1.
427 	 * @param label
428 	 *            The label that will represent the node.
429 	 * @param url
430 	 *            The URL that will be attached to the node.
431 	 * @param modifiers
432 	 *            Modifiers to change the presentation of the node. Accepted
433 	 *            values are ITALIC, BOLD, ALWAYS_OPEN, INITIALLY_OPEN.
434 	 *
435 	 * @return the nodeId that was allocated to the added node.
436 	 */
437 	private int generateTreeNode(int parentNodeId, String label, String url,
438 			int modifiers) {
439 		int nodeId = nextNodeId++;
440 		label = Tools.escapeHTMLUnicode(label, true);
441 
442 		if ((modifiers & ITALIC) == ITALIC)
443 			label = "<i>" + label + "</i>";
444 		if ((modifiers & BOLD) == BOLD)
445 			label = "<b>" + label + "</b>";
446 
447 		// FIXME url should be in the class URL, not a string !
448 
449 		if (url != null)
450 			url = "'" + url + "'";
451 		else
452 			url = "null";
453 
454 		boolean alwaysOpen = (modifiers & ALWAYS_OPEN) == ALWAYS_OPEN;
455 		boolean initiallyOpen = (modifiers & INITIALLY_OPEN) == INITIALLY_OPEN;
456 
457 		String addNodeFunc = (parentNodeId >= 0 ? "addNode" : "addRootNode");
458 
459 		out.println("eltTree." + addNodeFunc + "('" + nodeId + "','" + label
460 				+ "'," + url + "," + alwaysOpen + "," + initiallyOpen + ")");
461 
462 		if (parentNodeId >= 0)
463 			out.println("eltTree.linkNodes('" + parentNodeId + "','" + nodeId
464 					+ "')");
465 
466 		return nodeId;
467 	}
468 
469 	/**
470 	 * Generates the Javascript code for one link-node of the d-tree. A link is
471 	 * a node of the tree that represents another node (non-link) of the tree.
472 	 *
473 	 * @param parentNodeId
474 	 *            Id of the parent node of the link-node.
475 	 * @param nodeId
476 	 *            Id of the node the link refers to.
477 	 * @param label
478 	 *            Label to be displayed for the link.
479 	 * @param modifiers
480 	 *            Modifiers to change the presentation of the node. Accepted
481 	 *            values are ITALIC, BOLD, ALWAYS_OPEN, INITIALLY_OPEN.
482 	 */
483 
484 	private void generateTreeLink(int parentNodeId, int nodeId, String label,
485 			int modifiers) {
486 		if ((modifiers & ITALIC) == ITALIC)
487 			label = "<i>" + label + "</i>";
488 		if ((modifiers & BOLD) == BOLD)
489 			label = "<b>" + label + "</b>";
490 
491 		out.println("eltTree.linkNodes('" + parentNodeId + "','" + nodeId
492 				+ "','" + label + "')");
493 	}
494 
495 	/**
496 	 * Generates preliminary Javascript code needed to load the d-tree library.
497 	 *
498 	 * @param out
499 	 *            Where to generate the code.
500 	 */
501 
502 	public static void generateJavascriptSetup(PrintWriter out) {
503 		out.println("<script type='text/javascript' src='cctree.js'></script>");
504 	}
505 
506 }