package com.jbidwatcher.util.xml;
/* This file was part of NanoXML.
*
* $Revision: 1.32 $
* $Date: 2006/08/25 08:34:00 $
* $Name: $
*
* 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.
*/
/* Un-'packaged' from the NanoXML package, so it can be easily
* integrated w/ JBidwatcher. -- mrs: 07-August-2000 21:24
* Then HEAVILY modified, to support writing XML as well as reading it.
* Plus reformatted, for readability and to fit with the rest of the
* project that it's being integrated into.
*/
import java.io.IOException;
import java.io.Reader;
import java.util.*;
/**
* XMLElement is a representation of an XML object. The object is able to parse
* XML code.
* <P>
* Note that NanoXML is not 100% XML 1.0 compliant:
* <UL><LI>The parser is non-validating.
* <LI>The DTD is fully ignored, including <CODE><!ENTITY...></CODE>.
* <LI>There is no support for mixed content (elements containing both
* subelements and CDATA elements)
* <LI>There is no support for <CODE><![CDATA[...]]></CODE>
* </UL>
* <P>
* You can opt to use a SAX compatible API, by including both
* <CODE>nanoxml.jar</CODE> and <CODE>nanoxml-sax.jar</CODE> in your classpath
* and setting the property <CODE>org.xml.sax.parser</CODE> to
* <CODE>nanoxml.sax.SAXParser</CODE>
* <P>
* $Revision: 1.32 $<BR>
* $Date: 2006/08/25 08:34:00 $<P>
*
* @see XMLParseException
*
* @author Marc De Scheemaecker
* <<A HREF="mailto:Marc.DeScheemaecker@advalvas.be"
* >Marc.DeScheemaecker@advalvas.be</A>>
* @version 1.5
*/
@SuppressWarnings({"JavaDoc", "ThrowableInstanceNeverThrown"})
public class XMLElement implements XMLSerialize, XMLInterface {
/**
* Major version of NanoXML.
*/
public static final int NANOXML_MAJOR_VERSION = 1;
/**
* Minor version of NanoXML.
*/
public static final int NANOXML_MINOR_VERSION = 7;
private static boolean mRejectBadHTML = false;
private class Scanner {
private char[] mInput;
private int mEnd;
private int[] mLineNr;
private int mOffset;
private String mKey;
private String mValue;
public Scanner(char[] input, int end, int[] lineNr, int offset, String key) {
mInput = input;
mEnd = end;
mLineNr = lineNr;
mOffset = offset;
mKey = key;
}
public int getOffset() {
return mOffset;
}
public String getValue() {
return mValue;
}
public Scanner invoke() {
if (mInput[mOffset] == '=') {
mOffset = skipWhitespace(mInput, mOffset + 1, mEnd, mLineNr);
mValue = scanString(mInput, mOffset, mEnd, mLineNr);
if (mValue == null) {
throw syntaxError("an attribute value (" + new String(mInput) + ")", mLineNr[0]);
}
// If some idiot forgot to put quotes around a value, and it has an '=' in it, try and recover.
if (mInput[mOffset + mValue.length()] == '=') {
Scanner scanner = new Scanner(mInput, mEnd, mLineNr, mOffset+mValue.length(), mKey).invoke();
mValue = mValue + "=" + scanner.getValue();
}
if (mValue.charAt(0) == '"') {
mValue = mValue.substring(1, mValue.length() - 1);
mOffset += 2;
}
} else {
mValue = "";
if(!mKey.equals("disabled") && !mKey.equals("checked") && !mKey.equals("/")) {
if(mRejectBadHTML) {
throw valueMissingForAttribute(mKey, mLineNr[0]);
}
}
}
return this;
}
}
public static void rejectBadHTML(boolean rejectBadHtml) {
mRejectBadHTML = rejectBadHtml;
}
public static boolean rejectingBadHTML() { return mRejectBadHTML; }
/**
* The attributes given to the object.
*/
protected HashMap<String, String> _attributes;
/**
* Subobjects of the object. The subobjects are of class XMLElement
* themselves.
*/
protected List<XMLInterface> mChildren;
/**
* The class of the object (the name indicated in the tag).
*/
protected String _tagName;
/**
* Whether or not this is a clean 'empty' tag.
*/
protected boolean _empty;
/**
* The #PCDATA content of the object. If there is no such content, this
* field is null.
*/
protected String _contents;
/**
* Conversion table for &...; tags.
*/
protected static HashMap<String, String> _conversionTable = new HashMap<String, String>(10);
static {
_conversionTable.put("lt", "<");
_conversionTable.put("gt", ">");
_conversionTable.put("quot", "\"");
_conversionTable.put("apos", "'");
_conversionTable.put("amp", "&");
_conversionTable.put("pound", "#");
}
/**
* Whether to skip leading whitespace in CDATA.
*/
protected boolean _skipLeadingWhitespace;
/**
* The line number where the element starts.
*/
protected int _lineNr;
private static final int INITIAL_BLOCKSIZE = 20480;
/**
* Creates a new XML element. The following settings are used:
* <DL><DT>Conversion table</DT>
* <DD>Minimal XML conversions: <CODE>& < >
* ' "</CODE></DD>
* <DT>Skip whitespace in contents</DT>
* <DD><CODE>false</CODE></DD>
* </DL>
*
* @see XMLElement#XMLElement(boolean)
*/
public XMLElement() {
this(false);
}
/**
* Creates a new XML element. The following settings are used:
* <DL><DT>Conversion table</DT>
* <DD><I>conversionTable</I>, eventually combined with the minimal XML
* conversions: <CODE>& < >
* ' "</CODE>
* (depending on <I>fillBasicConversionTable</I>)</DD>
* <DT>Skip whitespace in contents</DT>
* <DD><I>skipLeadingWhitespace</I></DD>
* </DL>
* <P>
* This constructor should <I>only</I> be called from XMLElement itself
* to create child elements.
*
* @see XMLElement#XMLElement()
* @see XMLElement#XMLElement(boolean)
* @param skipLeadingWhitespace - Ignore white space that starts the tag content.
*/
public XMLElement(boolean skipLeadingWhitespace) {
_skipLeadingWhitespace = skipLeadingWhitespace;
_tagName = null;
_contents = null;
_attributes = new HashMap<String, String>(10);
mChildren = new ArrayList<XMLInterface>(10);
_lineNr = 0;
}
public XMLElement(String elementName) {
this(false);
_tagName = elementName;
}
/**
* Returns the number of subobjects of the object.
* @return - The number of children of this element.
*/
public int countChildren() {
return mChildren.size();
}
/**
* Enumerates the attribute names.
* @return - The list of attribute names.
*/
public Iterator<String> getAttributes() {
return _attributes.keySet().iterator();
}
/**
* Enumerates the subobjects of the object.
* @return - An iterator over the children of this element.
*/
public Iterator<XMLInterface> getChildren() {
return mChildren.iterator();
}
/**
* Returns the specifically named child.
* @param tagName - The child tag to get.
* @return - The element identified by the named child tag.
*/
public XMLElement getChild(String tagName) {
Iterator<XMLInterface> step = getChildren();
while(step.hasNext()) {
XMLElement child = (XMLElement) step.next();
if(child.getTagName().equals(tagName)) {
return child;
}
}
return null;
}
/**
* Returns the #PCDATA content of the object. If there is no such content,
* <CODE>null</CODE> is returned.
*/
public String getContents() {
return _contents;
}
/**
* Returns the line nr on which the element is found.
*/
public int getLineNr() {
return _lineNr;
}
/**
* Returns a property of the object. The property has to be specified in
* capital letters. If there is no such property, this method returns
* <CODE>null</CODE>.
*/
public String getProperty(String key) {
return _attributes.get(key.toLowerCase());
}
/**
* Returns a property of the object. The property has to be specified in
* capital letters.
* If the property doesn't exist, <I>defaultValue</I> is returned.
*/
public String getProperty(String key, String defaultValue) {
String result = _attributes.get(key.toLowerCase());
if(result == null) result = defaultValue;
return result;
}
/**
* Sets a property of the object.
*/
public void setProperty(String key, String newValue) {
String lowerKey = key.toLowerCase();
_attributes.put(lowerKey, newValue);
}
/**
* Returns the class (i.e. the name indicated in the tag) of the object.
*/
public String getTagName() {
return _tagName;
}
/**
* Identify this tag as empty.
*/
public void setEmpty() {
_empty = true;
}
/**
* Identify this tag as a normal tag.
*/
public void setNonEmpty() {
_empty = false;
}
/**
* Returns the class (i.e. the name indicated in the tag) of the object.
*/
public void setTagName(String newName) {
_tagName = newName;
}
/**
* Sets the content of this XMLElement to be a particular value. Does
* not need to be quoted. This cannot be done if a child has already
* been added.
*/
public void setContents(String newContents) {
if(mChildren.size() == 0) {
_contents = newContents;
} else {
throw new XMLParseException(_tagName, _lineNr, "Cannot add contents to an XML element that already has children.");
}
}
/**
* Adds another XMLElement as a child of this one. This can't be done if
* contents is already set.
*/
public void addChild(XMLInterface newChild) {
if(_contents == null) {
mChildren.add(newChild);
} else {
throw new XMLParseException(_tagName, _lineNr, "Cannot add children to an XML element that already has contents.");
}
}
/**
* Appends to a StringBuffer all the attributes of this element, in a form clean
* enough to append into a tag to recreate the original tag. Returns the provided
* stringbuffer, untouched, if there are no attributes.
*/
private StringBuffer allAttribs(StringBuffer attrList) {
Iterator<String> attrStep = getAttributes();
while(attrStep.hasNext()) {
String attrName = attrStep.next();
attrList.append(' ');
attrList.append(attrName);
attrList.append("=\"");
String prop = getProperty(attrName);
if(prop != null) {
attrList.append(encodeString(prop));
}
attrList.append('\"');
}
return attrList;
}
/**
* Convert the entire XMLElement tree from this element down into a string.
*/
public String toString() {
return toStringBuffer().toString();
}
/**
* Convert the entire XMLElement tree from this element down into a string, with
* prepended spacing dependant on the tree depth.
*/
public String toString(int depth) {
return toStringBuffer(new StringBuffer(), depth).toString();
}
public StringBuffer toStringBuffer() {
return toStringBuffer(new StringBuffer(), 0);
}
public StringBuffer toStringBuffer(StringBuffer wholeXML) {
return toStringBuffer(wholeXML, 0);
}
public StringBuffer toStringBuffer(StringBuffer wholeXML, int depth) {
prependSpaces(wholeXML, depth);
wholeXML.append('<').append(_tagName);
allAttribs(wholeXML);
if (_empty) {
wholeXML.append("/>");
} else {
wholeXML.append('>');
if (_contents == null) {
Iterator<XMLInterface> xmlStep = getChildren();
wholeXML.append('\n');
while (xmlStep.hasNext()) {
(xmlStep.next()).toStringBuffer(wholeXML, depth+1);
}
prependSpaces(wholeXML, depth);
} else {
wholeXML.append(encodeString(_contents));
}
wholeXML.append("</").append(_tagName).append('>');
}
wholeXML.append('\n');
return wholeXML;
}
private static void prependSpaces(StringBuffer wholeXML, int depth) {
for(int i=0; i<depth; i++) {
wholeXML.append(" ");
}
}
/**
* Checks whether a character may be part of an identifier.
*/
protected static 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 IOException
* if an error occured while reading the input
* @exception XMLParseException
* if an error occured while parsing the read data
*/
public void parseFromReader(Reader reader) throws IOException, XMLParseException {
parseFromReader(reader, 1);
}
/**
* Reads an XML definition from a java.io.Reader and parses it.
*
* @exception IOException
* if an error occured while reading the input
* @exception XMLParseException
* if an error occured while parsing the read data
*/
public void parseFromReader(Reader reader, int startingLineNr) throws IOException, XMLParseException {
int blockSize = INITIAL_BLOCKSIZE;
char[] input = null;
int size = 0;
while (reader.ready()) {
if (input == null) {
input = new char[blockSize];
} else {
char[] oldInput = input;
input = new char[input.length*3];
blockSize = oldInput.length * 2;
System.arraycopy(oldInput, 0, input, 0, oldInput.length);
}
int charsRead = reader.read(input, size, blockSize);
if (charsRead < 0) {
break;
}
size += charsRead;
}
parseCharArray(input, 0, size, startingLineNr);
}
/**
* Parses an XML definition.
*
* @exception XMLParseException
* if an error occured while parsing the string
*/
public void parseString(String string) throws XMLParseException {
parseCharArray(string.toCharArray(), 0, string.length(), 1);
}
/**
* Parses an XML definition starting at <I>offset</I>.
*
* @return the offset of the string following the XML data
*
* @exception XMLParseException
* if an error occured while parsing the string
*/
public int parseString(String string, int offset) throws XMLParseException {
return parseCharArray(string.toCharArray(), offset,
string.length(), 1);
}
/**
* Parses an XML definition starting at <I>offset</I>.
*
* @return the offset of the string following the XML data (<= end)
*
* @exception XMLParseException
* if an error occured while parsing the string
*/
public int parseString(String string, int offset, int end) throws XMLParseException {
return parseCharArray(string.toCharArray(), offset, end, 1);
}
/**
* Parses an XML definition starting at <I>offset</I>.
*
* @return the offset of the string following the XML data (<= end)
*
* @exception XMLParseException
* if an error occured while parsing the string
*/
public int parseString(String string, int offset, int end, int startingLineNr) throws XMLParseException {
return parseCharArray(string.toCharArray(), offset, end, startingLineNr);
}
/**
* Parses an XML definition starting at <I>offset</I>.
*
* @return the offset of the array following the XML data (<= end)
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
public int parseCharArray(char[] input, int offset, int end) throws XMLParseException {
return parseCharArray(input, offset, end, 1);
}
/**
* Parses an XML definition starting at <I>offset</I>.
*
* @return the offset of the array following the XML data (<= end)
*
* @exception 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 parseCharArray(input, offset, end, lineNr);
}
/**
* Parses an XML definition starting at <I>offset</I>.
*
* @return the offset of the array following the XML data (<= end)
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int parseCharArray(char[] input, int startLoc, int end, int[] currentLineNr) throws XMLParseException {
int offset = startLoc;
offset = skipWhitespace(input, offset, end, currentLineNr);
offset = skipPreamble(input, offset, end, currentLineNr);
offset = scanTagName(input, offset, end, currentLineNr);
_lineNr = currentLineNr[0];
offset = scanAttributes(input, offset, end, currentLineNr);
int[] contentOffset = new int[1];
int[] contentSize = new int[1];
int contentLineNr = currentLineNr[0];
offset = scanContent(input, offset, end, contentOffset, contentSize, currentLineNr);
// If there was any data (not including whitespace) between
// this tag and the next, then process that.
if (contentSize[0] > 0) {
scanChildren(input, contentOffset[0], contentSize[0], contentLineNr);
// If child-elements were found, then contents is set to null...
if (mChildren.size() > 0) {
_contents = null;
} else {
// If there are no child elements, but there is data, then
// it's contents to be processed.
processContents(input, contentOffset[0], contentSize[0], contentLineNr);
}
}
return offset;
}
/**
* Decodes the entities in the contents and, if skipLeadingWhitespace is
* <CODE>true</CODE>, removes extraneous whitespaces after newlines and
* convert those newlines into spaces.
*
* @see XMLElement#decodeString
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected void processContents(char[] input, int contentOffset,
int contentSize, int contentLineNr) throws XMLParseException {
int[] lineNr = new int[1];
lineNr[0] = contentLineNr;
if (!_skipLeadingWhitespace) {
String str = new String(input, contentOffset, contentSize);
handleCData(str);
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 { ch = input[++i]; } while ((ch == ' ') || (ch == '\t'));
}
if (i < end) result.append(ch);
}
handleCData(result.toString());
}
private void handleCData(String content) {
if(content.startsWith("<![CDATA[")) {
_contents = content.substring(9, content.length()-3);
} else {
_contents = decodeString(content);
}
}
/**
* Scans the attributes of the object.
*
* @return the offset in the string following the attributes, so that
* input[offset] in { '/', '>' }
*
* @see XMLElement#scanOneAttribute
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int scanAttributes(char[] input, int startLoc, int end, int[] lineNr) throws XMLParseException {
int offset = startLoc;
boolean done=false;
while(!done) {
offset = skipWhitespace(input, offset, end, lineNr);
char ch = input[offset];
if ((offset + 1 < end && ch == '/' && input[offset + 1] == '>') || ch == '>') done = true;
else offset = scanOneAttribute(input, offset, end, lineNr);
}
return offset;
}
/**
* Searches the content for child objects. If such objects exist, the
* content is reduced to <CODE>null</CODE>.
*
* @see XMLElement#parseCharArray
*
* @exception 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;
//noinspection PointlessArithmeticExpression
if(contentSize > 11 &&
input[offset+0] == '<' &&
input[offset+1] == '!' &&
input[offset+2] == '[' &&
input[offset+3] == 'C' &&
input[offset+4] == 'D' &&
input[offset+5] == 'A' &&
input[offset+6] == 'T' &&
input[offset+7] == 'A' &&
input[offset+8] == '[' &&
input[offset+contentSize-3] == ']' &&
input[offset+contentSize-2] == ']' &&
input[offset+contentSize-1] == '>') {
return;
}
while (offset < end) {
try {
offset = skipWhitespace(input, offset, end, lineNr);
} catch (XMLParseException e) {
return;
}
if (input[offset] != '<') {
return;
}
XMLElement child = new XMLElement(_skipLeadingWhitespace);
offset = child.parseCharArray(input, offset, end, lineNr);
mChildren.add(child);
}
}
/**
* 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. 'offset' points to the first byte past the
* tag and attributes. (I.e. in '<zarf/>', offset would point to
* the '/'.) If multiple content sections were found, it SHOULD
* point each of the contentOffset entries to them. That's not
* really possible with this design.
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int scanContent(char[] input, int startLoc, int expectedEnd,
int[] contentOffset, int[] contentSize, int[] lineNr) throws XMLParseException {
int offset=startLoc;
int end = expectedEnd;
char ch = input[offset];
// If this is an 'EMPTY' tag, then the content size is zero. Ensure that the
// next character is a '>' and punt.
if (ch == '/') {
_empty = true;
contentSize[0] = 0;
// Return an offset pointing to right after the tag.
if (input[offset + 1] != '>') throw expectedInput("'>'", lineNr[0]);
return offset + 2;
}
// Otherwise this REALLY should be a '>', to end the open-tag we've just
// processed.
if (ch != '>') throw expectedInput("'>'", lineNr[0]);
// Everything's okay so far, now skip all whitespace between the backside of the
// open tag, and the first non-whitespace character.
if (_skipLeadingWhitespace) {
offset = skipWhitespace(input, offset + 1, end, lineNr);
} else {
offset++;
}
// 'begin' is the beginning of non-whitespace content.
// int begin = offset;
// Write out the start location of the content.
contentOffset[0] = offset;
// 'tag' is used to search for the end-tag for this item.
char[] tag = _tagName.toCharArray();
// Since there 'must' be </'tag'> at the end, it pulls back the end of
// the possible content.
end -= (tag.length + 2);
// Level is, in some fashion, the number of elements deep we are.
int level = 0;
while ((offset < end) && (level >= 0)) {
ch = input[offset];
// Is it the start of another tag, either close or open?
if (ch == '<') {
boolean ok = true;
// Loop over the tag, just comparing the tag with the 'current' tag,
// to see if it's identical.
for (int i = 0; ok && (i < tag.length); i++) {
ok &= (input[offset + (i + 1)] == tag[i]);
}
// If the tag is identical... (Nodes can be embedded inside themselves?!?)
if (ok) {
// off2 is the content starting just past the end of the duplicate tag.
int off2 = offset + tag.length + 1;
// Skip all whitespace after the duplicate tag, and return
// the offset of that location.
offset = skipWhitespace(input, off2, end, lineNr);
ch = input[offset];
// Open level if /<BLAH[ \t]+[^\/]/ or /<BLAH[ \t]*>/ Allow
// another level deep, if it's the same node as the outer
// one. I.e. nodes can be embedded w/in themselves.
// Prolly wrong.
if ((ch == '>') || ((off2 != offset) && (ch != '/'))) {
level++;
}
continue;
// On the other hand, if it's not identical, let's see if
// it's a close-tag.
} else if (input[offset + 1] == '/') {
ok = true;
// It is a close tag, let's see if it's for the current tag.
for (int i = 0; ok && (i < tag.length); i++) {
ok &= (input[offset + (i + 2)] == tag[i]);
}
// Great! It's a close tag for the current tag! We're set!
if (ok) {
// Set the content size that gets returned.
contentSize[0] = offset - contentOffset[0];
// Increment the offset to just past the open tag.
offset += tag.length + 2;
// plus any whitespace.
offset = skipWhitespace(input, offset, end, lineNr);
// If there's an end-tag marker, consider it ending the
// current depth, since it ends the current tag too...
if (input[offset] == '>') {
level--;
offset++;
}
} else {
offset++;
}
continue;
}
}
// Is it the end of the line?
if (ch == '\r') {
lineNr[0]++;
// (On the off chance it's DOS formatted, clear out the
// extra character too...)
if ((offset != end) && (input[offset + 1] == '\n')) {
offset++;
}
// In case it's Mac formatted...
} else if (ch == '\n') {
lineNr[0]++;
}
offset++;
}
// If there weren't an equal amount of tag starts as tag ends,
// then punt.
if (level >= 0) throw unexpectedEndOfData(lineNr[0]);
// If we're supposed to skip whitespace then go ahead and do
// that. NOTE: skipLeadingWhitespace is a misnomer! This skips
// leading AND trailing whitespace!
if (_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 <CODE>null</CODE> if offset doesn't point
* to an identifier
*/
protected String scanIdentifier(char[] input, int offset, int end) {
int begin = offset;
while ((offset < end) && (isIdentifierChar(input[offset]) || (input[offset] == '/' && input[offset+1] != '>'))) {
offset++;
}
return offset == begin ? null : new String(input, begin, offset - begin);
}
/**
* Scans one attribute of an object.
*
* @return the offset after the attribute
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int scanOneAttribute(char[] input, int startLoc, int end, int[] lineNr) throws XMLParseException {
int offset = startLoc;
String key = scanIdentifier(input, offset, end);
if (key == null) {
throw syntaxError("an attribute key (" + new String(input) + ")", lineNr[0]);
}
offset = skipWhitespace(input, offset + key.length(), end, lineNr);
key = key.toLowerCase(); // toUpperCase();
Scanner scanner = new Scanner(input, end, lineNr, offset, key).invoke();
String value = scanner.getValue();
offset = scanner.getOffset();
if(!key.equals("/")) _attributes.put(key, decodeString(value));
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 XMLElement#scanIdentifier
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected String scanString(char[] input,
int startLoc,
int end,
int[] lineNr)
throws XMLParseException
{
int offset = startLoc;
char delim = input[offset];
if (delim == '"' || delim == '\'') {
int begin = offset;
offset++;
while ((offset < end) && (input[offset] != delim)) {
offset += nextCharCountSkipEOL(input, offset, end, lineNr);
}
if (offset == end) {
return null;
} else {
return new String(input, begin, offset - begin + 1);
}
} else {
return scanIdentifier(input, offset, end);
}
}
/**
* Scans the class (tag) name of the object.
*
* @return the position after the tag name
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int scanTagName(char[] input, int offset, int end, int[] lineNr) throws XMLParseException {
_tagName = scanIdentifier(input, offset, end);
if (_tagName == null) {
throw syntaxError("a tag name", lineNr[0]);
}
return offset + _tagName.length();
}
/**
* Skips a tag that don't contain any useful data: <?...?>,
* <!...> and comments.
*
* @return the position after the tag
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int skipBogusTag(char[] input, int startLoc, int end, int[] lineNr) {
if(isComment(input, startLoc)) {
return skipComment(input, end, lineNr, startLoc);
} else {
return findTagEnd(input, end, lineNr, startLoc);
}
}
private int findTagEnd(char[] input, int end, int[] lineNr, int offset) {
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 unexpectedEndOfData(lineNr[0]);
}
private boolean isComment(char[] input, int offset) {return (input[offset + 1] == '-') && (input[offset + 2] == '-');}
private int skipComment(char[] input, int end, int[] lineNr, int offset) {
while ((offset < end) && ((input[offset] != '-')
|| (input[offset + 1] != '-')
|| (input[offset + 2] != '>'))) {
offset += nextCharCountSkipEOL(input, offset, end, lineNr);
}
if (offset == end) {
throw unexpectedEndOfData(lineNr[0]);
} else {
return offset + 3;
}
}
/**
* Skips a tag that don't contain any useful data: <?...?>,
* <!...> and comments.
*
* @return the position after the tag
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int skipPreamble(char[] input,
int startLoc,
int end,
int[] lineNr)
throws XMLParseException
{
int offset = startLoc;
char ch;
do {
offset = skipWhitespace(input, offset, end, lineNr);
if (input[offset] != '<') {
expectedInput("'<'", lineNr[0]);
}
offset++;
if (offset >= end) throw unexpectedEndOfData(lineNr[0]);
ch = input[offset];
if ((ch == '!') || (ch == '?')) {
offset = skipBogusTag(input, offset, end, lineNr);
}
} while (ch == '?' || !isIdentifierChar(ch));
return offset;
}
protected static int nextCharCountSkipEOL(char[] input, int offset, int end, int[] lineNr) {
int initial_offset = offset;
if (input[offset] == '\r') {
lineNr[0]++;
if ((offset != end) && (input[offset + 1] == '\n')) {
offset++;
}
} else if (input[offset] == '\n') {
lineNr[0]++;
}
if(offset == initial_offset) {
return 1;
} else {
return offset-initial_offset;
}
}
/**
* Skips whitespace characters.
*
* @return the position after the whitespace
*
* @exception XMLParseException
* if an error occured while parsing the array
*/
protected int skipWhitespace(char[] input, int startLoc, int end, int[] lineNr) {
int offset = startLoc;
while ((offset < end) && (input[offset] <= ' ')) {
offset += nextCharCountSkipEOL(input, offset, end, lineNr);
}
if (offset == end) {
throw unexpectedEndOfData(lineNr[0]);
}
return offset;
}
static Map<Character, String> sEncodeMap = new HashMap<Character, String>(4);
static {
sEncodeMap.put('<', "<");
sEncodeMap.put('>', ">");
sEncodeMap.put('"', """);
sEncodeMap.put('&', "&");
}
/**
* Converts 'normal' characters into escaped '&...;' sequences.
* BUG -- This does not use the same conversion table as the main
* decode routine.
*/
public static String encodeString(String s) {
char raw[] = new char[s.length()];
s.getChars(0, s.length(), raw, 0);
int lastIndex = 0;
String insertString = null;
StringBuffer outBuffer = null;
for(int i = 0; i<raw.length; i++) {
char ch = raw[i];
insertString = null;
if(ch == '#') {
if (i == 0 || (i > 0 && raw[i - 1] != '&')) insertString = "£";
} else {
insertString = sEncodeMap.get(ch);
}
if(ch > 0x80) insertString = "" + (int)ch + ";";
if(insertString != null) {
if(outBuffer == null) outBuffer = new StringBuffer();
outBuffer.append(s.substring(lastIndex, i));
outBuffer.append(insertString);
lastIndex = i + 1;
}
}
if (insertString == null && lastIndex != 0) {
outBuffer.append(s.substring(lastIndex));
}
return lastIndex == 0 ? s : outBuffer.toString();
}
/**
* Converts &...; sequences to "normal" chars.
*/
public static String decodeString(String s) {
if(s == null) return "";
StringBuffer result = new StringBuffer(s.length());
int index = 0;
for (;;) {
int index2 = (s + '&').indexOf('&', index);
result.append(s.substring(index, index2));
if (index2 == s.length()) break;
index = s.indexOf(';', index2);
int space = s.indexOf(' ', index2);
int amp = s.indexOf('&', index2+1);
if(space == -1) space = s.length();
if(amp == -1) amp = s.length();
if(index < 0) {
result.append(s.substring(index2));
break;
} else if (index > space || index > amp) {
result.append(s.substring(index2, Math.min(space, amp)));
index = index2+1;
continue;
}
String key = s.substring(index2 + 1, index);
if (key.charAt(0) == '#') {
if (key.charAt(1) == 'x') {
result.append((char) Integer.parseInt(key.substring(2), 16));
} else {
result.append((char) Integer.parseInt(key.substring(1), 10));
}
} else {
String cvt = _conversionTable.get(key);
if(cvt == null) {
result.append('&').append(key).append(';');
} else {
result.append(cvt);
}
}
index++;
}
return result.toString();
}
/**
* Creates a parse exception for when an invalid valueset is given to
* a method.
*/
protected XMLParseException invalidValueSet(String key) {
String msg = "Invalid value set (key = \"" + key + "\")";
return new XMLParseException(getTagName(), msg);
}
/**
* Creates a parse exception for when an invalid value is given to a
* method.
*/
protected XMLParseException invalidValue(String key,
String value,
int lineNr) {
String msg = "Attribute \"" + key + "\" does not contain a valid "
+ "value (\"" + value + "\")";
return new XMLParseException(getTagName(), lineNr, msg);
}
/**
* The end of the data input has been reached.
*/
protected XMLParseException unexpectedEndOfData(int lineNr) {
String msg = "Unexpected end of data reached";
return new XMLParseException(getTagName(), lineNr, msg);
}
/**
* A syntax error occured.
*/
protected XMLParseException syntaxError(String context, int lineNr) {
String msg = "Syntax error while parsing " + context;
return new XMLParseException(getTagName(), lineNr, msg);
}
/**
* A character has been expected.
*/
protected XMLParseException expectedInput(String charSet, int lineNr) {
String msg = "Expected: " + charSet;
return new XMLParseException(getTagName(), lineNr, msg);
}
/**
* A value is missing for an attribute.
*/
protected XMLParseException valueMissingForAttribute(String key, int lineNr) {
String msg = "Value missing for attribute with key \"" + key + '\"';
return new XMLParseException(getTagName(), lineNr, msg);
}
public XMLElement toXML() {
return this;
}
public void fromXML(XMLInterface inXMLIFace) {
XMLElement inXML = (XMLElement) inXMLIFace;
_attributes = inXML._attributes;
mChildren = inXML.mChildren;
_tagName = inXML._tagName;
_empty = inXML._empty;
_contents = inXML._contents;
}
public void reset() {
_tagName = null;
_contents = null;
_attributes.clear();
mChildren.clear();
_lineNr = 0;
}
}