/* This file is part of NanoXML. * * $Revision: 1.17 $ * $Date: 2000/09/04 20:26:55 $ * $Name: RELEASE_1_6_4 $ * * Copyright (C) 2000 Marc De Scheemaecker, All Rights Reserved. * * This software is provided 'as-is', without any express or implied warranty. * In no event will the authors be held liable for any damages arising from the * use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software in * a product, an acknowledgment in the product documentation would be * appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. */ package nanoxml; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; /** * kXMLElement is a representation of an XML object. The object is able to parse * XML code. *

* Note that NanoXML is not 100% XML 1.0 compliant: *

*

* You can opt to use a SAX compatible API, by including both * nanoxml.jar and nanoxml-sax.jar in your classpath * and setting the property org.xml.sax.parser to * nanoxml.sax.SAXParser *

* $Revision: 1.17 $
* $Date: 2000/09/04 20:26:55 $

* * @see nanoxml.XMLParseException * * @author Marc De Scheemaecker * <Marc.DeScheemaecker@advalvas.be> * @version 1.6 */ public class kXMLElement { /** * Major version of NanoXML. */ public static final int NANOXML_MAJOR_VERSION = 1; /** * Minor version of NanoXML. */ public static final int NANOXML_MINOR_VERSION = 6; /** * The attributes given to the object. */ private FakeProperties attributes; /** * Subobjects of the object. The subobjects are of class kXMLElement * themselves. */ private Vector children; /** * The class of the object (the name indicated in the tag). */ private String tagName; /** * The #PCDATA content of the object. If there is no such content, this * field is null. */ private String contents; /** * Conversion table for &...; tags. */ private FakeProperties conversionTable; /** * Whether to skip leading whitespace in CDATA. */ private boolean skipLeadingWhitespace; /** * The line number where the element starts. */ private int lineNr; /** * Whether the parsing is case sensitive. */ private boolean ignoreCase; /** * Creates a new XML element. The following settings are used: *

Conversion table
*
Minimal XML conversions: &amp; &lt; &gt; * &apos; &quot;
*
Skip whitespace in contents
*
false
*
Ignore Case
*
true
*
* * @see nanoxml.kXMLElement#kXMLElement(FakeProperties) * @see nanoxml.kXMLElement#kXMLElement(boolean) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties,boolean) */ public kXMLElement() { this(new FakeProperties(), false, true, true); } /** * Creates a new XML element. The following settings are used: *
Conversion table
*
conversionTable combined with the minimal XML * conversions: &amp; &lt; &gt; * &apos; &quot;
*
Skip whitespace in contents
*
false
*
Ignore Case
*
true
*
* * @see nanoxml.kXMLElement#kXMLElement() * @see nanoxml.kXMLElement#kXMLElement(boolean) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties,boolean) */ public kXMLElement(FakeProperties conversionTable) { this(conversionTable, false, true, true); } /** * Creates a new XML element. The following settings are used: *
Conversion table
*
Minimal XML conversions: &amp; &lt; &gt; * &apos; &quot;
*
Skip whitespace in contents
*
skipLeadingWhitespace
*
Ignore Case
*
true
*
* * @see nanoxml.kXMLElement#kXMLElement() * @see nanoxml.kXMLElement#kXMLElement(FakeProperties) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties,boolean) */ public kXMLElement(boolean skipLeadingWhitespace) { this(new FakeProperties(), skipLeadingWhitespace, true, true); } /** * Creates a new XML element. The following settings are used: *
Conversion table
*
conversionTable combined with the minimal XML * conversions: &amp; &lt; &gt; * &apos; &quot;
*
Skip whitespace in contents
*
skipLeadingWhitespace
*
Ignore Case
*
true
*
* * @see nanoxml.kXMLElement#kXMLElement() * @see nanoxml.kXMLElement#kXMLElement(boolean) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties) */ public kXMLElement(FakeProperties conversionTable, boolean skipLeadingWhitespace) { this(conversionTable, skipLeadingWhitespace, true, true); } /** * Creates a new XML element. The following settings are used: *
Conversion table
*
conversionTable, eventually combined with the minimal XML * conversions: &amp; &lt; &gt; * &apos; &quot; * (depending on fillBasicConversionTable)
*
Skip whitespace in contents
*
skipLeadingWhitespace
*
Ignore Case
*
ignoreCase
*
*

* This constructor should only be called from kXMLElement itself * to create child elements. * * @see nanoxml.kXMLElement#kXMLElement() * @see nanoxml.kXMLElement#kXMLElement(boolean) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties,boolean) */ public kXMLElement(FakeProperties conversionTable, boolean skipLeadingWhitespace, boolean ignoreCase) { this(conversionTable, skipLeadingWhitespace, true, ignoreCase); } /** * Creates a new XML element. The following settings are used: *

Conversion table
*
conversionTable, eventually combined with the minimal XML * conversions: &amp; &lt; &gt; * &apos; &quot; * (depending on fillBasicConversionTable)
*
Skip whitespace in contents
*
skipLeadingWhitespace
*
Ignore Case
*
ignoreCase
*
*

* This constructor should only be called from kXMLElement itself * to create child elements. * * @see nanoxml.kXMLElement#kXMLElement() * @see nanoxml.kXMLElement#kXMLElement(boolean) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties) * @see nanoxml.kXMLElement#kXMLElement(FakeProperties,boolean) */ protected kXMLElement(FakeProperties conversionTable, boolean skipLeadingWhitespace, boolean fillBasicConversionTable, boolean ignoreCase) { this.ignoreCase = ignoreCase; this.skipLeadingWhitespace = skipLeadingWhitespace; this.tagName = null; this.contents = ""; this.attributes = new FakeProperties(); this.children = new Vector(); this.conversionTable = conversionTable; this.lineNr = 0; if (fillBasicConversionTable) { this.conversionTable.put("lt", "<"); this.conversionTable.put("gt", ">"); this.conversionTable.put("quot", "\""); this.conversionTable.put("apos", "'"); this.conversionTable.put("amp", "&"); } } /** * Adds a subobject. */ public void addChild(kXMLElement child) { this.children.addElement(child); } /** * Adds a property. * If the element is case insensitive, the property name is capitalized. */ public void addProperty(String key, Object value) { if (this.ignoreCase) { key = key.toUpperCase(); } this.attributes.put(key, value.toString()); } /** * Adds a property. * If the element is case insensitive, the property name is capitalized. */ public void addProperty(String key, int value) { if (this.ignoreCase) { key = key.toUpperCase(); } this.attributes.put(key, Integer.toString(value)); } /** * Returns the number of subobjects of the object. */ public int countChildren() { return this.children.size(); } /** * Enumerates the attribute names. */ public Enumeration enumeratePropertyNames() { return this.attributes.keys(); } /** * Enumerates the subobjects of the object. */ public Enumeration enumerateChildren() { return this.children.elements(); } /** * Returns the subobjects of the object. */ public Vector getChildren() { return this.children; } /** * Returns the #PCDATA content of the object. If there is no such content, * null is returned. */ public String getContents() { return this.contents; } /** * Returns the line nr on which the element is found. */ public int getLineNr() { return this.lineNr; } /** * Returns a property by looking up a key in a hashtable. * If the property doesn't exist, the value corresponding to defaultValue * is returned. */ public int getIntProperty(String key, Hashtable valueSet, String defaultValue) { String val = this.attributes.getProperty(key); Integer result; if (this.ignoreCase) { key = key.toUpperCase(); } if (val == null) { val = defaultValue; } try { result = (Integer)(valueSet.get(val)); } catch (ClassCastException e) { throw this.invalidValueSet(key); } if (result == null) { throw this.invalidValue(key, val, this.lineNr); } return result.intValue(); } /** * Returns a property of the object. If there is no such property, this * method returns null. */ public String getProperty(String key) { if (this.ignoreCase) { key = key.toUpperCase(); } return this.attributes.getProperty(key); } /** * Returns a property of the object. * If the property doesn't exist, defaultValue is returned. */ public String getProperty(String key, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } return this.attributes.getProperty(key, defaultValue); } /** * Returns an integer property of the object. * If the property doesn't exist, defaultValue is returned. */ public int getProperty(String key, int defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); if (val == null) { return defaultValue; } else { try { return Integer.parseInt(val); } catch (NumberFormatException e) { throw this.invalidValue(key, val, this.lineNr); } } } /** * Returns a boolean property of the object. If the property is missing, * defaultValue is returned. */ public boolean getProperty(String key, String trueValue, String falseValue, boolean defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); if (val == null) { return defaultValue; } else if (val.equals(trueValue)) { return true; } else if (val.equals(falseValue)) { return false; } else { throw this.invalidValue(key, val, this.lineNr); } } /** * Returns a property by looking up a key in the hashtable valueSet * If the property doesn't exist, the value corresponding to * defaultValue is returned. */ public Object getProperty(String key, Hashtable valueSet, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); if (val == null) { val = defaultValue; } Object result = valueSet.get(val); if (result == null) { throw this.invalidValue(key, val, this.lineNr); } return result; } /** * Returns a property by looking up a key in the hashtable valueSet. * If the property doesn't exist, the value corresponding to * defaultValue is returned. */ public String getStringProperty(String key, Hashtable valueSet, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); String result; if (val == null) { val = defaultValue; } try { result = (String)(valueSet.get(val)); } catch (ClassCastException e) { throw this.invalidValueSet(key); } if (result == null) { throw this.invalidValue(key, val, this.lineNr); } return result; } /** * Returns a property by looking up a key in the hashtable valueSet. * If the value is not defined in the hashtable, the value is considered to * be an integer. * If the property doesn't exist, the value corresponding to * defaultValue is returned. */ public int getSpecialIntProperty(String key, Hashtable valueSet, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); Integer result; if (val == null) { val = defaultValue; } try { result = (Integer)(valueSet.get(val)); } catch (ClassCastException e) { throw this.invalidValueSet(key); } if (result == null) { try { return Integer.parseInt(val); } catch (NumberFormatException e) { throw this.invalidValue(key, val, this.lineNr); } } return result.intValue(); } /** * Returns the class (i.e. the name indicated in the tag) of the object. */ public String getTagName() { return this.tagName; } /** * Checks whether a character may be part of an identifier. */ private boolean isIdentifierChar(char ch) { return (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')) || ((ch >= '0') && (ch <= '9')) || (".-_:".indexOf(ch) >= 0)); } /** * Reads an XML definition from a java.io.Reader and parses it. * * @exception java.io.IOException * if an error occured while reading the input * @exception nanoxml.XMLParseException * if an error occured while parsing the read data */ public void parseFromReader(Reader reader) throws IOException, XMLParseException { this.parseFromReader(reader, 1); } /** * Reads an XML definition from a java.io.Reader and parses it. * * @exception java.io.IOException * if an error occured while reading the input * @exception nanoxml.XMLParseException * if an error occured while parsing the read data */ public void parseFromReader(Reader reader, int startingLineNr) throws IOException, XMLParseException { int blockSize = 4096; char[] input = null; int size = 0; for (;;) { if (input == null) { input = new char[blockSize]; } else { char[] oldInput = input; input = new char[input.length + blockSize]; System.arraycopy(oldInput, 0, input, 0, oldInput.length); } int charsRead = reader.read(input, size, blockSize); if (charsRead < 0) { break; } size += charsRead; } this.parseCharArray(input, 0, size, startingLineNr); } /** * Parses an XML definition. * * @exception nanoxml.XMLParseException * if an error occured while parsing the string */ public void parseString(String string) throws XMLParseException { this.parseCharArray(string.toCharArray(), 0, string.length(), 1); } /** * Parses an XML definition starting at offset. * * @return the offset of the string following the XML data * * @exception nanoxml.XMLParseException * if an error occured while parsing the string */ public int parseString(String string, int offset) throws XMLParseException { return this.parseCharArray(string.toCharArray(), offset, string.length(), 1); } /** * Parses an XML definition starting at offset. * * @return the offset of the string following the XML data (<= end) * * @exception nanoxml.XMLParseException * if an error occured while parsing the string */ public int parseString(String string, int offset, int end) throws XMLParseException { return this.parseCharArray(string.toCharArray(), offset, end, 1); } /** * Parses an XML definition starting at offset. * * @return the offset of the string following the XML data (<= end) * * @exception nanoxml.XMLParseException * if an error occured while parsing the string */ public int parseString(String string, int offset, int end, int startingLineNr) throws XMLParseException { return this.parseCharArray(string.toCharArray(), offset, end, startingLineNr); } /** * Parses an XML definition starting at offset. * * @return the offset of the array following the XML data (<= end) * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ public int parseCharArray(char[] input, int offset, int end) throws XMLParseException { return this.parseCharArray(input, offset, end, 1); } /** * Parses an XML definition starting at offset. * * @return the offset of the array following the XML data (<= end) * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ public int parseCharArray(char[] input, int offset, int end, int startingLineNr) throws XMLParseException { int[] lineNr = new int[1]; lineNr[0] = startingLineNr; return this.parseCharArray(input, offset, end, lineNr); } /** * Parses an XML definition starting at offset. * * @return the offset of the array following the XML data (<= end) * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private int parseCharArray(char[] input, int offset, int end, int[] currentLineNr) throws XMLParseException { this.lineNr = currentLineNr[0]; this.tagName = null; this.contents = ""; this.attributes = new FakeProperties(); this.children = new Vector(); try { offset = this.skipWhitespace(input, offset, end, currentLineNr); } catch (XMLParseException e) { return offset; } offset = this.skipPreamble(input, offset, end, currentLineNr); offset = this.scanTagName(input, offset, end, currentLineNr); this.lineNr = currentLineNr[0]; offset = this.scanAttributes(input, offset, end, currentLineNr); int[] contentOffset = new int[1]; int[] contentSize = new int[1]; int contentLineNr = currentLineNr[0]; offset = this.scanContent(input, offset, end, contentOffset, contentSize, currentLineNr); if (contentSize[0] > 0) { this.scanChildren(input, contentOffset[0], contentSize[0], contentLineNr); if (this.children.size() > 0) { this.contents = null; } else { this.processContents(input, contentOffset[0], contentSize[0], contentLineNr); } } return offset; } /** * Decodes the entities in the contents and, if skipLeadingWhitespace is * true, removes extraneous whitespaces after newlines and * convert those newlines into spaces. * * @see nanoxml.kXMLElement#decodeString * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private void processContents(char[] input, int contentOffset, int contentSize, int contentLineNr) throws XMLParseException { int[] lineNr = new int[1]; lineNr[0] = contentLineNr; if (! this.skipLeadingWhitespace) { String str = new String(input, contentOffset, contentSize); this.contents = this.decodeString(str, lineNr[0]); return; } StringBuffer result = new StringBuffer(contentSize); int end = contentSize + contentOffset; for (int i = contentOffset; i < end; i++) { char ch = input[i]; // The end of the contents is always a < character, so there's // no danger for bounds violation while ((ch == '\r') || (ch == '\n')) { lineNr[0]++; result.append(ch); i++; ch = input[i]; if (ch != '\n') { result.append(ch); } do { i++; ch = input[i]; } while ((ch == ' ') || (ch == '\t')); } if (i < end) { result.append(ch); } } this.contents = this.decodeString(result.toString(), lineNr[0]); } /** * Removes a child object. If the object is not a child, nothing happens. */ public void removeChild(kXMLElement child) { this.children.removeElement(child); } /** * Removes an attribute. */ public void removeChild(String key) { if (this.ignoreCase) { key = key.toUpperCase(); } this.attributes.remove(key); } /** * Scans the attributes of the object. * * @return the offset in the string following the attributes, so that * input[offset] in { '/', '>' } * * @see nanoxml.kXMLElement#scanOneAttribute * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanAttributes(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { String key, value; for (;;) { offset = this.skipWhitespace(input, offset, end, lineNr); char ch = input[offset]; if ((ch == '/') || (ch == '>')) { break; } offset = this.scanOneAttribute(input, offset, end, lineNr); } return offset; } /**!!! * Searches the content for child objects. If such objects exist, the * content is reduced to null. * * @see nanoxml.kXMLElement#parseCharArray * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ protected void scanChildren(char[] input, int contentOffset, int contentSize, int contentLineNr) throws XMLParseException { int end = contentOffset + contentSize; int offset = contentOffset; int lineNr[] = new int[1]; lineNr[0] = contentLineNr; while (offset < end) { try { offset = this.skipWhitespace(input, offset, end, lineNr); } catch (XMLParseException e) { return; } if ((input[offset] != '<') || ((input[offset + 1] == '!') && (input[offset + 2] == '['))) { return; } kXMLElement child = this.createAnotherElement(); offset = child.parseCharArray(input, offset, end, lineNr); this.children.addElement(child); } } /** * Creates a new XML element. */ protected kXMLElement createAnotherElement() { return new kXMLElement(this.conversionTable, this.skipLeadingWhitespace, false, this.ignoreCase); } /** * Scans the content of the object. * * @return the offset after the XML element; contentOffset points to the * start of the content section; contentSize is the size of the * content section * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanContent(char[] input, int offset, int end, int[] contentOffset, int[] contentSize, int[] lineNr) throws XMLParseException { if (input[offset] == '/') { contentSize[0] = 0; if (input[offset + 1] != '>') { throw this.expectedInput("'>'", lineNr[0]); } return offset + 2; } if (input[offset] != '>') { throw this.expectedInput("'>'", lineNr[0]); } if (this.skipLeadingWhitespace) { offset = this.skipWhitespace(input, offset + 1, end, lineNr); } else { offset++; } int begin = offset; contentOffset[0] = offset; int level = 0; char[] tag = this.tagName.toCharArray(); end -= (tag.length + 2); while ((offset < end) && (level >= 0)) { if (input[offset] == '<') { boolean ok = true; if ((offset < (end - 1)) && (input[offset + 1] == '!')) { offset++; continue; } for (int i = 0; ok && (i < tag.length); i++) { ok &= (input[offset + (i + 1)] == tag[i]); } ok &= ! this.isIdentifierChar(input[offset + tag.length + 1]); if (ok) { while ((offset < end) && (input[offset] != '>')) { offset++; } if (input[offset - 1] != '/') { level++; } continue; } else if (input[offset + 1] == '/') { ok = true; for (int i = 0; ok && (i < tag.length); i++) { ok &= (input[offset + (i + 2)] == tag[i]); } if (ok) { contentSize[0] = offset - contentOffset[0]; offset += tag.length + 2; offset = this.skipWhitespace(input, offset, end, lineNr); if (input[offset] == '>') { level--; offset++; } continue; } } } if (input[offset] == '\r') { lineNr[0]++; if ((offset != end) && (input[offset + 1] == '\n')) { offset++; } } else if (input[offset] == '\n') { lineNr[0]++; } offset++; } if (level >= 0) { throw this.unexpectedEndOfData(lineNr[0]); } if (this.skipLeadingWhitespace) { int i = contentOffset[0] + contentSize[0] - 1; while ((contentSize[0] >= 0) && (input[i] <= ' ')) { i--; contentSize[0]--; } } return offset; } /** * Scans an identifier. * * @return the identifier, or null if offset doesn't point * to an identifier */ private String scanIdentifier(char[] input, int offset, int end) { int begin = offset; while ((offset < end) && (this.isIdentifierChar(input[offset]))) { offset++; } if ((offset == end) || (offset == begin)) { return null; } else { return new String(input, begin, offset - begin); } } /** * Scans one attribute of an object. * * @return the offset after the attribute * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanOneAttribute(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { String key, value; key = this.scanIdentifier(input, offset, end); if (key == null) { throw this.syntaxError("an attribute key", lineNr[0]); } offset = this.skipWhitespace(input, offset + key.length(), end, lineNr); if (this.ignoreCase) { key = key.toUpperCase(); } if (input[offset] != '=') { throw this.valueMissingForAttribute(key, lineNr[0]); } offset = this.skipWhitespace(input, offset + 1, end, lineNr); value = this.scanString(input, offset, end, lineNr); if (value == null) { throw this.syntaxError("an attribute value", lineNr[0]); } if ((value.charAt(0) == '"') || (value.charAt(0) == '\'')) { value = value.substring(1, (value.length() - 1)); offset += 2; } this.attributes.put(key, this.decodeString(value, lineNr[0])); return offset + value.length(); } /** * Scans a string. Strings are either identifiers, or text delimited by * double quotes. * * @return the string found, without delimiting double quotes; or null * if offset didn't point to a valid string * * @see nanoxml.kXMLElement#scanIdentifier * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private String scanString(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { char delim = input[offset]; if ((delim == '"') || (delim == '\'')) { int begin = offset; offset++; while ((offset < end) && (input[offset] != delim)) { if (input[offset] == '\r') { lineNr[0]++; if ((offset != end) && (input[offset + 1] == '\n')) { offset++; } } else if (input[offset] == '\n') { lineNr[0]++; } offset++; } if (offset == end) { return null; } else { return new String(input, begin, offset - begin + 1); } } else { return this.scanIdentifier(input, offset, end); } } /** * Scans the class (tag) name of the object. * * @return the position after the tag name * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanTagName(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { this.tagName = this.scanIdentifier(input, offset, end); if (this.tagName == null) { throw this.syntaxError("a tag name", lineNr[0]); } return offset + this.tagName.length(); } /** * Changes the content string. * * @param content The new content string. */ public void setContent(String content) { this.contents = content; } /** * Changes the tag name. * * @param tagName The new tag name. */ public void setTagName(String tagName) { this.tagName = tagName; } /** * Skips a tag that don't contain any useful data: <?...?>, * <!...> and comments. * * @return the position after the tag * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ protected int skipBogusTag(char[] input, int offset, int end, int[] lineNr) { if ((input[offset + 1] == '-') && (input[offset + 2] == '-')) { while ((offset < end) && ((input[offset] != '-') || (input[offset + 1] != '-') || (input[offset + 2] != '>'))) { if (input[offset] == '\r') { lineNr[0]++; if ((offset != end) && (input[offset + 1] == '\n')) { offset++; } } else if (input[offset] == '\n') { lineNr[0]++; } offset++; } if (offset == end) { throw unexpectedEndOfData(lineNr[0]); } else { return offset + 3; } } int level = 1; while (offset < end) { char ch = input[offset++]; switch (ch) { case '\r': if ((offset < end) && (input[offset] == '\n')) { offset++; } lineNr[0]++; break; case '\n': lineNr[0]++; break; case '<': level++; break; case '>': level--; if (level == 0) { return offset; } break; default: } } throw this.unexpectedEndOfData(lineNr[0]); } /** * Skips a tag that don't contain any useful data: <?...?>, * <!...> and comments. * * @return the position after the tag * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private int skipPreamble(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { char ch; do { offset = this.skipWhitespace(input, offset, end, lineNr); if (input[offset] != '<') { this.expectedInput("'<'", lineNr[0]); } offset++; if (offset >= end) { throw this.unexpectedEndOfData(lineNr[0]); } ch = input[offset]; if ((ch == '!') || (ch == '?')) { offset = this.skipBogusTag(input, offset, end, lineNr); } } while (! isIdentifierChar(ch)); return offset; } /** * Skips whitespace characters. * * @return the position after the whitespace * * @exception nanoxml.XMLParseException * if an error occured while parsing the array */ private int skipWhitespace(char[] input, int offset, int end, int[] lineNr) { while ((offset < end) && (input[offset] <= ' ')) { if (input[offset] == '\r') { lineNr[0]++; if ((offset != end) && (input[offset + 1] == '\n')) { offset++; } } else if (input[offset] == '\n') { lineNr[0]++; } offset++; } if (offset == end) { throw this.unexpectedEndOfData(lineNr[0]); } return offset; } /** * Converts &...; sequences to "normal" chars. */ protected String decodeString(String s, int lineNr) { StringBuffer result = new StringBuffer(s.length()); int index = 0; while (index < s.length()) { int index2 = (s + '&').indexOf('&', index); int index3 = (s + "").indexOf("]]>", index3 + 9); result.append(s.substring(index, index3)); result.append(s.substring(index3 + 9, index4)); index = index4 + 2; } index++; } return result.toString(); } /** * Writes the XML element to a string. */ public String toString() { FakeStringWriter writer = new FakeStringWriter(); this.write(writer); return writer.toString(); } /** * Writes the XML element to a writer. */ public void write(Writer writer) { this.write(writer, 0); } /** * Writes the XML element to a writer. */ public void write(Writer writer, int indent) { FakePrintWriter out = new FakePrintWriter(writer); for (int i = 0; i < indent; i++) { out.print(' '); } if (this.tagName == null) { this.writeEncoded(out, this.contents); return; } out.print('<'); out.print(this.tagName); if (! this.attributes.isEmpty()) { Enumeration enum = this.attributes.keys(); while (enum.hasMoreElements()) { out.print(' '); String key = (String)(enum.nextElement()); String value = (String)(this.attributes.get(key)); out.print(key); out.print("=\""); this.writeEncoded(out, value); out.print('"'); } } if ((this.contents != null) && (this.contents.length() > 0)) { if (this.skipLeadingWhitespace) { out.println('>'); for (int i = 0; i < indent + 4; i++) { out.print(' '); } out.println(this.contents); for (int i = 0; i < indent; i++) { out.print(' '); } } else { out.print('>'); this.writeEncoded(out, this.contents); } out.print("'); } else if (this.children.isEmpty()) { out.println("/>"); } else { out.println('>'); Enumeration enum = this.enumerateChildren(); while (enum.hasMoreElements()) { kXMLElement child = (kXMLElement)(enum.nextElement()); child.write(writer, indent + 4); } for (int i = 0; i < indent; i++) { out.print(' '); } out.print("'); } } /** * Writes a string encoded to a writer. */ protected void writeEncoded(FakePrintWriter out, String str) { for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); switch (ch) { case '<': out.write("<"); break; case '>': out.write(">"); break; case '&': out.write("&"); break; case '"': out.write("""); break; case '\'': out.write("&"); break; case '\r': case '\n': out.write(ch); break; default: if ((int)ch < 16) { out.write("�"); out.write(Integer.toString((int)ch, 16)); out.write(';'); } else if (((int)ch < 32) || (((int)ch > 126) && ((int)ch < 256))) { out.write("&#x"); out.write(Integer.toString((int)ch, 16)); out.write(';'); } else { out.write(ch); } } } } /** * Creates a parse exception for when an invalid valueset is given to * a method. */ private XMLParseException invalidValueSet(String key) { String msg = "Invalid value set (key = \"" + key + "\")"; return new XMLParseException(this.getTagName(), msg); } /** * Creates a parse exception for when an invalid value is given to a * method. */ private XMLParseException invalidValue(String key, String value, int lineNr) { String msg = "Attribute \"" + key + "\" does not contain a valid " + "value (\"" + value + "\")"; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * The end of the data input has been reached. */ private XMLParseException unexpectedEndOfData(int lineNr) { String msg = "Unexpected end of data reached"; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * A syntax error occured. */ private XMLParseException syntaxError(String context, int lineNr) { String msg = "Syntax error while parsing " + context; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * A character has been expected. */ private XMLParseException expectedInput(String charSet, int lineNr) { String msg = "Expected: " + charSet; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * A value is missing for an attribute. */ private XMLParseException valueMissingForAttribute(String key, int lineNr) { String msg = "Value missing for attribute with key \"" + key + "\""; return new XMLParseException(this.getTagName(), lineNr, msg); } //------------------------------------------------------------------ public static class FakeProperties extends Hashtable { /** */ public FakeProperties() { } /** */ public String getProperty( String key ) { return (String) super.get( key ); } /** */ public String getProperty( String key, String defstr ) { String val = getProperty( key ); return( ( val != null ) ? val : defstr ); } } //------------------------------------------------------------------ /** * A simple replacement for StringWriter. */ private static class FakeStringWriter extends Writer { private StringBuffer buf; /** */ public FakeStringWriter() { buf = new StringBuffer(); } /** */ public void close() { } /** */ public void flush() { } /** */ public void write( int c ) { buf.append( (char) c ); } /** */ public void write( char b[], int off, int len ) { buf.append( b, off, len ); } /** */ public void write( String str ) { buf.append( str ); } /** */ public String toString() { return buf.toString(); } } //------------------------------------------------------------------ /** * A simple replacement for PrintWriter. */ private static class FakePrintWriter extends Writer { private Writer out; /** */ public FakePrintWriter( Writer out ) { super( out ); this.out = out; } public void close() { try { out.close(); } catch( IOException e ){ } } public void flush() { try { out.flush(); } catch( IOException e ){ } } public void write( int c ) { try { out.write( c ); } catch( IOException e ){ } } public void write( char buf[], int off, int len ) { try { out.write( buf, off, len ); } catch( IOException e ){ } } public void write( String s ) { try { out.write( s, 0, s.length() ); } catch( IOException e ){ } } /** */ public void print( char ch ) { write( String.valueOf( ch ) ); } /** */ private void newLine() { write( '\n' ); } /** */ public void print( String str ) { if( str == null ){ str = "null"; } write( str ); } /** */ public void println( char ch ) { print( ch ); newLine(); } /** */ public void println( String str ) { print( str ); newLine(); } } }