001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.tools.ant.util;
020:
021: import java.io.IOException;
022: import java.io.OutputStream;
023: import java.io.OutputStreamWriter;
024: import java.io.Writer;
025: import java.util.ArrayList;
026: import java.util.HashMap;
027: import java.util.Iterator;
028: import org.w3c.dom.Attr;
029: import org.w3c.dom.Element;
030: import org.w3c.dom.NamedNodeMap;
031: import org.w3c.dom.Node;
032: import org.w3c.dom.NodeList;
033: import org.w3c.dom.Text;
034:
035: /**
036: * Writes a DOM tree to a given Writer.
037: * warning: this utility currently does not declare XML Namespaces.
038: * <p>Utility class used by {@link org.apache.tools.ant.XmlLogger
039: * XmlLogger} and
040: * org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter
041: * XMLJUnitResultFormatter}.</p>
042: *
043: */
044: public class DOMElementWriter {
045:
046: /** prefix for genefrated prefixes */
047: private static final String NS = "ns";
048:
049: /** xml declaration is on by default */
050: private boolean xmlDeclaration = true;
051:
052: /**
053: * XML Namespaces are ignored by default.
054: */
055: private XmlNamespacePolicy namespacePolicy = XmlNamespacePolicy.IGNORE;
056:
057: /**
058: * Map (URI to prefix) of known namespaces.
059: */
060: private HashMap nsPrefixMap = new HashMap();
061:
062: /**
063: * Number of generated prefix to use next.
064: */
065: private int nextPrefix = 0;
066:
067: /**
068: * Map (Element to URI) of namespaces defined on a given element.
069: */
070: private HashMap nsURIByElement = new HashMap();
071:
072: /**
073: * Whether namespaces should be ignored for elements and attributes.
074: *
075: * @since Ant 1.7
076: */
077: public static class XmlNamespacePolicy {
078: private boolean qualifyElements;
079: private boolean qualifyAttributes;
080:
081: /**
082: * Ignores namespaces for elements and attributes, the default.
083: */
084: public static final XmlNamespacePolicy IGNORE = new XmlNamespacePolicy(
085: false, false);
086:
087: /**
088: * Ignores namespaces for attributes.
089: */
090: public static final XmlNamespacePolicy ONLY_QUALIFY_ELEMENTS = new XmlNamespacePolicy(
091: true, false);
092:
093: /**
094: * Qualifies namespaces for elements and attributes.
095: */
096: public static final XmlNamespacePolicy QUALIFY_ALL = new XmlNamespacePolicy(
097: true, true);
098:
099: /**
100: * @param qualifyElements whether to qualify elements
101: * @param qualifyAttributes whether to qualify elements
102: */
103: public XmlNamespacePolicy(boolean qualifyElements,
104: boolean qualifyAttributes) {
105: this .qualifyElements = qualifyElements;
106: this .qualifyAttributes = qualifyAttributes;
107: }
108: }
109:
110: /**
111: * Create an element writer.
112: * The ?xml? declaration will be included, namespaces ignored.
113: */
114: public DOMElementWriter() {
115: }
116:
117: /**
118: * Create an element writer
119: * XML namespaces will be ignored.
120: * @param xmlDeclaration flag to indicate whether the ?xml? declaration
121: * should be included.
122: * @since Ant1.7
123: */
124: public DOMElementWriter(boolean xmlDeclaration) {
125: this (xmlDeclaration, XmlNamespacePolicy.IGNORE);
126: }
127:
128: /**
129: * Create an element writer
130: * XML namespaces will be ignored.
131: * @param xmlDeclaration flag to indicate whether the ?xml? declaration
132: * should be included.
133: * @param namespacePolicy the policy to use.
134: * @since Ant1.7
135: */
136: public DOMElementWriter(boolean xmlDeclaration,
137: XmlNamespacePolicy namespacePolicy) {
138: this .xmlDeclaration = xmlDeclaration;
139: this .namespacePolicy = namespacePolicy;
140: }
141:
142: private static String lSep = System.getProperty("line.separator");
143:
144: // CheckStyle:VisibilityModifier OFF - bc
145: /**
146: * Don't try to be too smart but at least recognize the predefined
147: * entities.
148: */
149: protected String[] knownEntities = { "gt", "amp", "lt", "apos",
150: "quot" };
151:
152: // CheckStyle:VisibilityModifier ON
153:
154: /**
155: * Writes a DOM tree to a stream in UTF8 encoding. Note that
156: * it prepends the <?xml version='1.0' encoding='UTF-8'?> if
157: * the xmlDeclaration field is true.
158: * The indent number is set to 0 and a 2-space indent.
159: * @param root the root element of the DOM tree.
160: * @param out the outputstream to write to.
161: * @throws IOException if an error happens while writing to the stream.
162: */
163: public void write(Element root, OutputStream out)
164: throws IOException {
165: Writer wri = new OutputStreamWriter(out, "UTF8");
166: writeXMLDeclaration(wri);
167: write(root, wri, 0, " ");
168: wri.flush();
169: }
170:
171: /**
172: * Writes the XML declaration if xmlDeclaration is true.
173: * @param wri the writer to write to.
174: * @throws IOException if there is an error.
175: * @since Ant 1.7.0
176: */
177: public void writeXMLDeclaration(Writer wri) throws IOException {
178: if (xmlDeclaration) {
179: wri.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
180: }
181: }
182:
183: /**
184: * Writes a DOM tree to a stream.
185: *
186: * @param element the Root DOM element of the tree
187: * @param out where to send the output
188: * @param indent number of
189: * @param indentWith string that should be used to indent the
190: * corresponding tag.
191: * @throws IOException if an error happens while writing to the stream.
192: */
193: public void write(Element element, Writer out, int indent,
194: String indentWith) throws IOException {
195:
196: // Write child elements and text
197: NodeList children = element.getChildNodes();
198: boolean hasChildren = (children.getLength() > 0);
199: boolean hasChildElements = false;
200: openElement(element, out, indent, indentWith, hasChildren);
201:
202: if (hasChildren) {
203: for (int i = 0; i < children.getLength(); i++) {
204: Node child = children.item(i);
205:
206: switch (child.getNodeType()) {
207:
208: case Node.ELEMENT_NODE:
209: hasChildElements = true;
210: if (i == 0) {
211: out.write(lSep);
212: }
213: write((Element) child, out, indent + 1, indentWith);
214: break;
215:
216: case Node.TEXT_NODE:
217: out.write(encode(child.getNodeValue()));
218: break;
219:
220: case Node.COMMENT_NODE:
221: out.write("<!--");
222: out.write(encode(child.getNodeValue()));
223: out.write("-->");
224: break;
225:
226: case Node.CDATA_SECTION_NODE:
227: out.write("<![CDATA[");
228: out.write(encodedata(((Text) child).getData()));
229: out.write("]]>");
230: break;
231:
232: case Node.ENTITY_REFERENCE_NODE:
233: out.write('&');
234: out.write(child.getNodeName());
235: out.write(';');
236: break;
237:
238: case Node.PROCESSING_INSTRUCTION_NODE:
239: out.write("<?");
240: out.write(child.getNodeName());
241: String data = child.getNodeValue();
242: if (data != null && data.length() > 0) {
243: out.write(' ');
244: out.write(data);
245: }
246: out.write("?>");
247: break;
248: default:
249: // Do nothing
250: }
251: }
252: closeElement(element, out, indent, indentWith,
253: hasChildElements);
254: }
255: }
256:
257: /**
258: * Writes the opening tag - including all attributes -
259: * corresponding to a DOM element.
260: *
261: * @param element the DOM element to write
262: * @param out where to send the output
263: * @param indent number of
264: * @param indentWith string that should be used to indent the
265: * corresponding tag.
266: * @throws IOException if an error happens while writing to the stream.
267: */
268: public void openElement(Element element, Writer out, int indent,
269: String indentWith) throws IOException {
270: openElement(element, out, indent, indentWith, true);
271: }
272:
273: /**
274: * Writes the opening tag - including all attributes -
275: * corresponding to a DOM element.
276: *
277: * @param element the DOM element to write
278: * @param out where to send the output
279: * @param indent number of
280: * @param indentWith string that should be used to indent the
281: * corresponding tag.
282: * @param hasChildren whether this element has children.
283: * @throws IOException if an error happens while writing to the stream.
284: * @since Ant 1.7
285: */
286: public void openElement(Element element, Writer out, int indent,
287: String indentWith, boolean hasChildren) throws IOException {
288: // Write indent characters
289: for (int i = 0; i < indent; i++) {
290: out.write(indentWith);
291: }
292:
293: // Write element
294: out.write("<");
295: if (namespacePolicy.qualifyElements) {
296: String uri = getNamespaceURI(element);
297: String prefix = (String) nsPrefixMap.get(uri);
298: if (prefix == null) {
299: if (nsPrefixMap.isEmpty()) {
300: // steal default namespace
301: prefix = "";
302: } else {
303: prefix = NS + (nextPrefix++);
304: }
305: nsPrefixMap.put(uri, prefix);
306: addNSDefinition(element, uri);
307: }
308: if (!"".equals(prefix)) {
309: out.write(prefix);
310: out.write(":");
311: }
312: }
313: out.write(element.getTagName());
314:
315: // Write attributes
316: NamedNodeMap attrs = element.getAttributes();
317: for (int i = 0; i < attrs.getLength(); i++) {
318: Attr attr = (Attr) attrs.item(i);
319: out.write(" ");
320: if (namespacePolicy.qualifyAttributes) {
321: String uri = getNamespaceURI(attr);
322: String prefix = (String) nsPrefixMap.get(uri);
323: if (prefix == null) {
324: prefix = NS + (nextPrefix++);
325: nsPrefixMap.put(uri, prefix);
326: addNSDefinition(element, uri);
327: }
328: out.write(prefix);
329: out.write(":");
330: }
331: out.write(attr.getName());
332: out.write("=\"");
333: out.write(encode(attr.getValue()));
334: out.write("\"");
335: }
336:
337: // write namespace declarations
338: ArrayList al = (ArrayList) nsURIByElement.get(element);
339: if (al != null) {
340: Iterator iter = al.iterator();
341: while (iter.hasNext()) {
342: String uri = (String) iter.next();
343: String prefix = (String) nsPrefixMap.get(uri);
344: out.write(" xmlns");
345: if (!"".equals(prefix)) {
346: out.write(":");
347: out.write(prefix);
348: }
349: out.write("=\"");
350: out.write(uri);
351: out.write("\"");
352: }
353: }
354:
355: if (hasChildren) {
356: out.write(">");
357: } else {
358: removeNSDefinitions(element);
359: out.write(" />");
360: out.write(lSep);
361: out.flush();
362: }
363: }
364:
365: /**
366: * Writes a DOM tree to a stream.
367: *
368: * @param element the Root DOM element of the tree
369: * @param out where to send the output
370: * @param indent number of
371: * @param indentWith string that should be used to indent the
372: * corresponding tag.
373: * @param hasChildren if true indent.
374: * @throws IOException if an error happens while writing to the stream.
375: */
376: public void closeElement(Element element, Writer out, int indent,
377: String indentWith, boolean hasChildren) throws IOException {
378: // If we had child elements, we need to indent before we close
379: // the element, otherwise we're on the same line and don't need
380: // to indent
381: if (hasChildren) {
382: for (int i = 0; i < indent; i++) {
383: out.write(indentWith);
384: }
385: }
386:
387: // Write element close
388: out.write("</");
389: if (namespacePolicy.qualifyElements) {
390: String uri = getNamespaceURI(element);
391: String prefix = (String) nsPrefixMap.get(uri);
392: if (prefix != null && !"".equals(prefix)) {
393: out.write(prefix);
394: out.write(":");
395: }
396: removeNSDefinitions(element);
397: }
398: out.write(element.getTagName());
399: out.write(">");
400: out.write(lSep);
401: out.flush();
402: }
403:
404: /**
405: * Escape <, > & ', " as their entities and
406: * drop characters that are illegal in XML documents.
407: * @param value the string to encode.
408: * @return the encoded string.
409: */
410: public String encode(String value) {
411: StringBuffer sb = new StringBuffer();
412: int len = value.length();
413: for (int i = 0; i < len; i++) {
414: char c = value.charAt(i);
415: switch (c) {
416: case '<':
417: sb.append("<");
418: break;
419: case '>':
420: sb.append(">");
421: break;
422: case '\'':
423: sb.append("'");
424: break;
425: case '\"':
426: sb.append(""");
427: break;
428: case '&':
429: int nextSemi = value.indexOf(";", i);
430: if (nextSemi < 0
431: || !isReference(value
432: .substring(i, nextSemi + 1))) {
433: sb.append("&");
434: } else {
435: sb.append('&');
436: }
437: break;
438: default:
439: if (isLegalCharacter(c)) {
440: sb.append(c);
441: }
442: break;
443: }
444: }
445: return sb.substring(0);
446: }
447:
448: /**
449: * Drop characters that are illegal in XML documents.
450: *
451: * <p>Also ensure that we are not including an <code>]]></code>
452: * marker by replacing that sequence with
453: * <code>&#x5d;&#x5d;&gt;</code>.</p>
454: *
455: * <p>See XML 1.0 2.2 <a
456: * href="http://www.w3.org/TR/1998/REC-xml-19980210#charsets">
457: * http://www.w3.org/TR/1998/REC-xml-19980210#charsets</a> and
458: * 2.7 <a
459: * href="http://www.w3.org/TR/1998/REC-xml-19980210#sec-cdata-sect">http://www.w3.org/TR/1998/REC-xml-19980210#sec-cdata-sect</a>.</p>
460: * @param value the value to be encoded.
461: * @return the encoded value.
462:
463: */
464: public String encodedata(final String value) {
465: StringBuffer sb = new StringBuffer();
466: int len = value.length();
467: for (int i = 0; i < len; ++i) {
468: char c = value.charAt(i);
469: if (isLegalCharacter(c)) {
470: sb.append(c);
471: }
472: }
473:
474: String result = sb.substring(0);
475: int cdEnd = result.indexOf("]]>");
476: while (cdEnd != -1) {
477: sb.setLength(cdEnd);
478: sb.append("]]>").append(
479: result.substring(cdEnd + 3));
480: result = sb.substring(0);
481: cdEnd = result.indexOf("]]>");
482: }
483:
484: return result;
485: }
486:
487: /**
488: * Is the given argument a character or entity reference?
489: * @param ent the value to be checked.
490: * @return true if it is an entity.
491: */
492: public boolean isReference(String ent) {
493: if (!(ent.charAt(0) == '&') || !ent.endsWith(";")) {
494: return false;
495: }
496:
497: if (ent.charAt(1) == '#') {
498: if (ent.charAt(2) == 'x') {
499: try {
500: Integer.parseInt(
501: ent.substring(3, ent.length() - 1), 16);
502: return true;
503: } catch (NumberFormatException nfe) {
504: return false;
505: }
506: } else {
507: try {
508: Integer
509: .parseInt(ent
510: .substring(2, ent.length() - 1));
511: return true;
512: } catch (NumberFormatException nfe) {
513: return false;
514: }
515: }
516: }
517:
518: String name = ent.substring(1, ent.length() - 1);
519: for (int i = 0; i < knownEntities.length; i++) {
520: if (name.equals(knownEntities[i])) {
521: return true;
522: }
523: }
524: return false;
525: }
526:
527: /**
528: * Is the given character allowed inside an XML document?
529: *
530: * <p>See XML 1.0 2.2 <a
531: * href="http://www.w3.org/TR/1998/REC-xml-19980210#charsets">
532: * http://www.w3.org/TR/1998/REC-xml-19980210#charsets</a>.</p>
533: * @param c the character to test.
534: * @return true if the character is allowed.
535: * @since 1.10, Ant 1.5
536: */
537: public boolean isLegalCharacter(char c) {
538: if (c == 0x9 || c == 0xA || c == 0xD) {
539: return true;
540: } else if (c < 0x20) {
541: return false;
542: } else if (c <= 0xD7FF) {
543: return true;
544: } else if (c < 0xE000) {
545: return false;
546: } else if (c <= 0xFFFD) {
547: return true;
548: }
549: return false;
550: }
551:
552: private void removeNSDefinitions(Element element) {
553: ArrayList al = (ArrayList) nsURIByElement.get(element);
554: if (al != null) {
555: Iterator iter = al.iterator();
556: while (iter.hasNext()) {
557: nsPrefixMap.remove(iter.next());
558: }
559: nsURIByElement.remove(element);
560: }
561: }
562:
563: private void addNSDefinition(Element element, String uri) {
564: ArrayList al = (ArrayList) nsURIByElement.get(element);
565: if (al == null) {
566: al = new ArrayList();
567: nsURIByElement.put(element, al);
568: }
569: al.add(uri);
570: }
571:
572: private static String getNamespaceURI(Node n) {
573: String uri = n.getNamespaceURI();
574: if (uri == null) {
575: // FIXME: Is "No Namespace is Empty Namespace" really OK?
576: uri = "";
577: }
578: return uri;
579: }
580: }
|