001: /*
002: * Copyright 2007 Volantis Systems Ltd., All Rights Reserved.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package net.n3.nanoxml;
017:
018: import java.io.IOException;
019: import java.io.InputStream;
020: import java.io.InputStreamReader;
021: import java.net.HttpURLConnection;
022: import java.net.MalformedURLException;
023: import java.net.URL;
024: import java.net.URLConnection;
025: import java.util.Iterator;
026: import java.util.Stack;
027: import java.util.Vector;
028:
029: /**
030: * Extend the XMLBuilder to add XInclude functionality
031: */
032: public class XIncludeXMLBuilder extends StdXMLBuilder {
033: /**
034: * Namespace for XInclude (NOTE that this is not used
035: * at the moment). The specification can be found
036: * <a href="http://www.w3.org/TR/xinclude/">here</a>.
037: */
038: public static final String INCLUDE_NS = "http://www.w3.org/2001/XInclude";
039: /**
040: * The name of the include element (this should be "include" using the
041: * {@link #INCLUDE_NS} but namespaces are not supported
042: */
043: public static final String INCLUDE_ELEMENT = "xinclude";
044: /**
045: * The location of the included data
046: */
047: public static final String HREF_ATTRIB = "href";
048:
049: /**
050: * The xpointer attribute. This must not be used when "parse='text'"
051: */
052: public static final String XPOINTER_ATTRIB = "xpointer";
053:
054: /**
055: * The attribute to decribe the encoding of the text include (no effect when
056: * parse='xml')
057: */
058: public static final String ENCODING_ATTRIB = "encoding";
059:
060: /**
061: * The attribute describing the accept header that will be used with
062: * http based includes.
063: */
064: public static final String ACCEPT_ENCODING = "accept";
065:
066: /**
067: * The element for handling fallbacks. This should be called "fallback" and
068: * be in the {@link #INCLUDE_NS} but namespaces are not supported
069: */
070: public static final String FALLBACK_ELEMENT = "xfallback";
071:
072: /**
073: * Parse attribute. If missing this implies "xml" its other valid value
074: * is "text"
075: */
076: public static final String PARSE_ATTRIB = "parse";
077:
078: /**
079: * Namespace for the "fragment" element used to include xml documents with
080: * no explicit root node.
081: */
082: public static final String FRAGMENT_NS = "http://izpack.org/izpack/fragment";
083:
084: /**
085: * The fragment element is a root node element that can be
086: * used to wrap xml fragments for inclusion. It is removed during the
087: * include operation. This should be called "fragment" and be in the
088: * {@link #FRAGMENT_NS} but namespaces are not supported.
089: */
090: public static final String FRAGMENT = "xfragment";
091:
092: // Javadoc inherited
093: public void endElement(String name, String nsPrefix,
094: String nsSystemID) {
095: // get the current element before it gets popped from the stack
096: XMLElement element = getCurrentElement();
097: // let normal processing occur
098: super .endElement(name, nsPrefix, nsSystemID);
099: // now process the "include" element
100: processXInclude(element);
101: }
102:
103: /**
104: * This method handles XInclude elements in the code
105: *
106: * @param element the node currently being procesed. In this case it should
107: * be the {@link #INCLUDE_ELEMENT}
108: */
109: private void processXInclude(final XMLElement element) {
110: if (INCLUDE_ELEMENT.equals(element.getName())) {
111:
112: Vector<XMLElement> fallbackChildren = element
113: .getChildrenNamed(FALLBACK_ELEMENT);
114: if (element.getChildrenCount() != fallbackChildren.size()
115: || fallbackChildren.size() > 1) {
116: throw new RuntimeException(new XMLParseException(
117: element.getSystemID(), element.getLineNr(),
118: INCLUDE_ELEMENT
119: + " can optionally have a single "
120: + FRAGMENT + " as a child"));
121: }
122: boolean usingFallback = false;
123:
124: String href = element.getAttribute(HREF_ATTRIB, "");
125: if (!href.equals("")) { // including an external file.
126:
127: IXMLReader reader = null;
128: try {
129: reader = getReader(element);
130: } catch (Exception e) { // yes really catch all exceptions
131: // ok failed to read from the location for some reason.
132: // see if we have a fallback
133: reader = handleFallback(element);
134: usingFallback = true;
135: }
136: String parse = element
137: .getAttribute(PARSE_ATTRIB, "xml");
138: // process as text if we are not using our fallback and the parse
139: // type is "text"
140: if ("text".equals(parse) && !usingFallback) {
141: includeText(element, reader);
142: } else if ("xml".equals(parse)) {
143: includeXML(element, reader);
144: } else {
145: throw new RuntimeException(
146: new XMLParseException(
147: element.getSystemID(),
148: element.getLineNr(),
149: PARSE_ATTRIB
150: + " attribute of "
151: + INCLUDE_ELEMENT
152: + " must be \"xml\" or \"text\" but was "
153: + parse));
154: }
155: } else { // including part of this file rather then an external one
156: if (!element.hasAttribute(XPOINTER_ATTRIB)) {
157: throw new RuntimeException(new XMLParseException(
158: element.getSystemID(), element.getLineNr(),
159: XPOINTER_ATTRIB
160: + "must be specified if href is "
161: + "empty or missing"));
162: }
163: }
164: }
165: }
166:
167: /**
168: * Handle the fallback if one exists. If one does not exist then throw
169: * a runtime exception as this is a fatal error
170: *
171: * @param include the include element
172: * @return a reader for the fallback
173: */
174: private IXMLReader handleFallback(XMLElement include) {
175: Vector<XMLElement> fallbackChildren = include
176: .getChildrenNamed(FALLBACK_ELEMENT);
177: if (fallbackChildren.size() == 1) {
178: // process fallback
179:
180: XMLElement fallback = fallbackChildren.get(0);
181: // fallback element can only contain a CDATA so it will not have
182: // its content in un-named children
183: String content = fallback.getContent();
184: if (content != null) {
185: content = content.trim();
186: }
187:
188: if ("".equals(content) || content == null) {
189: // an empty fragment requires us to just remove the "include"
190: // element. A nasty hack follows:
191: // a "fragment" with no children will just be removed along with
192: // the "include" element.
193: content = "<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"yes\" ?><"
194: + FRAGMENT + "/>";
195: }
196: return StdXMLReader.stringReader(content);
197: } else {
198: throw new RuntimeException(new XMLParseException(include
199: .getSystemID(), include.getLineNr(),
200: "could not load content"));
201: }
202: }
203:
204: /**
205: * Include the xml contained in the specified reader. This content will be
206: * parsed and attached to the parent of the <param>element</param> node
207: *
208: * @param element the include element
209: * @param reader the reader containing the xml to parse and include.
210: */
211: private void includeXML(final XMLElement element, IXMLReader reader) {
212:
213: try {
214: Stack<XMLElement> stack = getStack();
215: // set up a new parser to parse the include file.
216: StdXMLParser parser = new StdXMLParser();
217: parser.setBuilder(XMLBuilderFactory.createXMLBuilder());
218: parser.setReader(reader);
219: parser.setValidator(new NonValidator());
220:
221: XMLElement childroot = (XMLElement) parser.parse();
222: // if the include element was the root element in the original
223: // document then keep the element as-is (i.e.
224: // don't remove the "fragment" element from the included content
225: if (stack.isEmpty()) {
226: setRootElement(childroot);
227: } else {
228: XMLElement parent = stack.peek();
229: // remove the include element from its parent
230: parent.removeChild(element);
231:
232: // if there was a "fragment" included remove the fragment
233: // element and attach its children in place of this include
234: // element.
235: if (FRAGMENT.equals(childroot.getName())) {
236: Vector grandchildren = childroot.getChildren();
237: Iterator it = grandchildren.iterator();
238: while (it.hasNext()) {
239: XMLElement grandchild = (XMLElement) it.next();
240: parent.addChild(grandchild);
241: }
242: } else {
243: // if it was a complete document included then
244: // just add it in place of the include element
245: parent.addChild(childroot);
246: }
247: }
248: } catch (XMLException e) {
249: throw new RuntimeException(
250: new XMLParseException(element.getSystemID(),
251: element.getLineNr(), e.getMessage()));
252: }
253: }
254:
255: /**
256: * Include plain text. The reader contains the content in the appropriate
257: * encoding as determined by the {@link #ENCODING_ATTRIB} if one was
258: * present.
259: *
260: * @param element the include element
261: * @param reader the reader containing the include text
262: */
263: private void includeText(XMLElement element, IXMLReader reader) {
264:
265: if (element.getAttribute("xpointer") != null) {
266: throw new RuntimeException(new XMLParseException(
267: "xpointer cannot be used with parse='text'"));
268: }
269:
270: Stack<XMLElement> stack = getStack();
271: if (stack.isEmpty()) {
272: throw new RuntimeException(new XMLParseException(element
273: .getSystemID(), element.getLineNr(),
274: "cannot include text as the root node"));
275: }
276:
277: // remove the include element from the parent
278: XMLElement parent = stack.peek();
279: parent.removeChild(element);
280: StringBuffer buffer = new StringBuffer();
281: try {
282: while (!reader.atEOF()) {
283: buffer.append(reader.read());
284: }
285: } catch (IOException e) {
286: throw new RuntimeException(
287: new XMLParseException(element.getSystemID(),
288: element.getLineNr(), e.getMessage()));
289: }
290:
291: if (parent.getChildrenCount() == 0) {
292: // no children so just set the content as there cannot have been
293: // any content there already
294: parent.setContent(buffer.toString());
295: } else {
296: // nanoxml also claims to store #PCDATA in unnamed children
297: // if there was a combination of #PCDATA and child elements.
298: // This should put it in the correct place as we haven't finihshed
299: // parsing the children of includes parent yet.
300: XMLElement content = new XMLElement();
301: content.setContent(buffer.toString());
302: parent.addChild(content);
303: }
304: }
305:
306: /**
307: * Return a reader for the specified {@link #INCLUDE_ELEMENT}. The caller
308: * is responsible for closing the reader produced.
309: *
310: * @param element the include element to obtain a reader for
311: * @return a reader for the include element
312: * @throws XMLParseException if a problem occurs parsing the
313: * {@link #INCLUDE_ELEMENT}
314: * @throws IOException if the href cannot be read
315: */
316: private IXMLReader getReader(XMLElement element)
317: throws XMLParseException, IOException {
318: String href = element.getAttribute(HREF_ATTRIB);
319: // This is a bit nasty but is a simple way of handling files that are
320: // not fully qualified urls
321: URL url = null;
322: try {
323: // standard URL
324: url = new URL(href);
325: } catch (MalformedURLException e) {
326: try {
327: // absolute file without a protocol
328: if (href.charAt(0) == '/') {
329: url = new URL("file://" + href);
330: } else {
331: // relative file
332: url = new URL(new URL(element.getSystemID()), href);
333: }
334: } catch (MalformedURLException e1) {
335: new XMLParseException(element.getSystemID(), element
336: .getLineNr(), "malformed url '" + href + "'");
337: }
338: }
339:
340: URLConnection connection = url.openConnection();
341: // special handling for http and https
342: if (connection instanceof HttpURLConnection
343: && element.hasAttribute(ENCODING_ATTRIB)) {
344: connection.setRequestProperty("accept", element
345: .getAttribute(ENCODING_ATTRIB));
346: }
347:
348: InputStream is = connection.getInputStream();
349:
350: InputStreamReader reader = null;
351: // Only pay attention to the {@link #ENCODING_ATTRIB} if parse='text'
352: if (element.getAttribute(PARSE_ATTRIB, "xml").equals("text")
353: && element.hasAttribute(ENCODING_ATTRIB)) {
354: reader = new InputStreamReader(is, element.getAttribute(
355: ENCODING_ATTRIB, ""));
356: } else {
357: reader = new InputStreamReader(is);
358: }
359:
360: IXMLReader ireader = new StdXMLReader(reader);
361: ireader.setSystemID(url.toExternalForm());
362: return ireader;
363: }
364:
365: /**
366: * used to record the system id for this document.
367: *
368: * @param systemID the system id of the document being built
369: * @param lineNr the line number
370: */
371: public void startBuilding(String systemID, int lineNr) {
372: super.startBuilding(systemID, lineNr);
373: }
374: }
|