blob: 617e1d4931bb4a975d16fad0f6783a6eb037a864 [file] [log] [blame]
/* Copyright (c) 2002,2003,2004 Stefan Haustein, Oberhausen, Rhld., Germany
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE. */
// Contributors: Bjorn Aadland, Chris Bartley, Nicola Fankhauser,
// Victor Havin, Christian Kurzke, Bogdan Onoiu,
// Elias Ross, Jain Sanjay, David Santoro.
package org.kxml2.wap;
import java.io.*;
import java.util.Vector;
import java.util.Hashtable;
import org.xmlpull.v1.*;
public class WbxmlParser implements XmlPullParser {
static final String HEX_DIGITS = "0123456789abcdef";
/** Parser event type for Wbxml-specific events. The Wbxml event code can be
* accessed with getWapCode() */
public static final int WAP_EXTENSION = 64;
static final private String UNEXPECTED_EOF =
"Unexpected EOF";
static final private String ILLEGAL_TYPE =
"Wrong event type";
private InputStream in;
private int TAG_TABLE = 0;
private int ATTR_START_TABLE = 1;
private int ATTR_VALUE_TABLE = 2;
private String[] attrStartTable;
private String[] attrValueTable;
private String[] tagTable;
private byte[] stringTable;
private Hashtable cacheStringTable = null;
private boolean processNsp;
private int depth;
private String[] elementStack = new String[16];
private String[] nspStack = new String[8];
private int[] nspCounts = new int[4];
private int attributeCount;
private String[] attributes = new String[16];
private int nextId = -2;
private Vector tables = new Vector();
private int version;
private int publicIdentifierId;
// StartTag current;
// ParseEvent next;
private String prefix;
private String namespace;
private String name;
private String text;
private Object wapExtensionData;
private int wapCode;
private int type;
private boolean degenerated;
private boolean isWhitespace;
private String encoding;
public boolean getFeature(String feature) {
if (XmlPullParser
.FEATURE_PROCESS_NAMESPACES
.equals(feature))
return processNsp;
else
return false;
}
public String getInputEncoding() {
return encoding;
}
public void defineEntityReplacementText(
String entity,
String value)
throws XmlPullParserException {
// just ignore, has no effect
}
public Object getProperty(String property) {
return null;
}
public int getNamespaceCount(int depth) {
if (depth > this.depth)
throw new IndexOutOfBoundsException();
return nspCounts[depth];
}
public String getNamespacePrefix(int pos) {
return nspStack[pos << 1];
}
public String getNamespaceUri(int pos) {
return nspStack[(pos << 1) + 1];
}
public String getNamespace(String prefix) {
if ("xml".equals(prefix))
return "http://www.w3.org/XML/1998/namespace";
if ("xmlns".equals(prefix))
return "http://www.w3.org/2000/xmlns/";
for (int i = (getNamespaceCount(depth) << 1) - 2;
i >= 0;
i -= 2) {
if (prefix == null) {
if (nspStack[i] == null)
return nspStack[i + 1];
}
else if (prefix.equals(nspStack[i]))
return nspStack[i + 1];
}
return null;
}
public int getDepth() {
return depth;
}
public String getPositionDescription() {
StringBuffer buf =
new StringBuffer(
type < TYPES.length ? TYPES[type] : "unknown");
buf.append(' ');
if (type == START_TAG || type == END_TAG) {
if (degenerated)
buf.append("(empty) ");
buf.append('<');
if (type == END_TAG)
buf.append('/');
if (prefix != null)
buf.append("{" + namespace + "}" + prefix + ":");
buf.append(name);
int cnt = attributeCount << 2;
for (int i = 0; i < cnt; i += 4) {
buf.append(' ');
if (attributes[i + 1] != null)
buf.append(
"{"
+ attributes[i]
+ "}"
+ attributes[i
+ 1]
+ ":");
buf.append(
attributes[i
+ 2]
+ "='"
+ attributes[i
+ 3]
+ "'");
}
buf.append('>');
}
else if (type == IGNORABLE_WHITESPACE);
else if (type != TEXT)
buf.append(getText());
else if (isWhitespace)
buf.append("(whitespace)");
else {
String text = getText();
if (text.length() > 16)
text = text.substring(0, 16) + "...";
buf.append(text);
}
return buf.toString();
}
public int getLineNumber() {
return -1;
}
public int getColumnNumber() {
return -1;
}
public boolean isWhitespace()
throws XmlPullParserException {
if (type != TEXT
&& type != IGNORABLE_WHITESPACE
&& type != CDSECT)
exception(ILLEGAL_TYPE);
return isWhitespace;
}
public String getText() {
return text;
}
public char[] getTextCharacters(int[] poslen) {
if (type >= TEXT) {
poslen[0] = 0;
poslen[1] = text.length();
char[] buf = new char[text.length()];
text.getChars(0, text.length(), buf, 0);
return buf;
}
poslen[0] = -1;
poslen[1] = -1;
return null;
}
public String getNamespace() {
return namespace;
}
public String getName() {
return name;
}
public String getPrefix() {
return prefix;
}
public boolean isEmptyElementTag()
throws XmlPullParserException {
if (type != START_TAG)
exception(ILLEGAL_TYPE);
return degenerated;
}
public int getAttributeCount() {
return attributeCount;
}
public String getAttributeType(int index) {
return "CDATA";
}
public boolean isAttributeDefault(int index) {
return false;
}
public String getAttributeNamespace(int index) {
if (index >= attributeCount)
throw new IndexOutOfBoundsException();
return attributes[index << 2];
}
public String getAttributeName(int index) {
if (index >= attributeCount)
throw new IndexOutOfBoundsException();
return attributes[(index << 2) + 2];
}
public String getAttributePrefix(int index) {
if (index >= attributeCount)
throw new IndexOutOfBoundsException();
return attributes[(index << 2) + 1];
}
public String getAttributeValue(int index) {
if (index >= attributeCount)
throw new IndexOutOfBoundsException();
return attributes[(index << 2) + 3];
}
public String getAttributeValue(
String namespace,
String name) {
for (int i = (attributeCount << 2) - 4;
i >= 0;
i -= 4) {
if (attributes[i + 2].equals(name)
&& (namespace == null
|| attributes[i].equals(namespace)))
return attributes[i + 3];
}
return null;
}
public int getEventType() throws XmlPullParserException {
return type;
}
// TODO: Reuse resolveWapExtension here? Raw Wap extensions would still be accessible
// via nextToken(); ....?
public int next() throws XmlPullParserException, IOException {
isWhitespace = true;
int minType = 9999;
while (true) {
String save = text;
nextImpl();
if (type < minType)
minType = type;
if (minType > CDSECT) continue; // no "real" event so far
if (minType >= TEXT) { // text, see if accumulate
if (save != null) text = text == null ? save : save + text;
switch(peekId()) {
case Wbxml.ENTITY:
case Wbxml.STR_I:
case Wbxml.STR_T:
case Wbxml.LITERAL:
case Wbxml.LITERAL_C:
case Wbxml.LITERAL_A:
case Wbxml.LITERAL_AC: continue;
}
}
break;
}
type = minType;
if (type > TEXT)
type = TEXT;
return type;
}
public int nextToken() throws XmlPullParserException, IOException {
isWhitespace = true;
nextImpl();
return type;
}
public int nextTag() throws XmlPullParserException, IOException {
next();
if (type == TEXT && isWhitespace)
next();
if (type != END_TAG && type != START_TAG)
exception("unexpected type");
return type;
}
public String nextText() throws XmlPullParserException, IOException {
if (type != START_TAG)
exception("precondition: START_TAG");
next();
String result;
if (type == TEXT) {
result = getText();
next();
}
else
result = "";
if (type != END_TAG)
exception("END_TAG expected");
return result;
}
public void require(int type, String namespace, String name)
throws XmlPullParserException, IOException {
if (type != this.type
|| (namespace != null && !namespace.equals(getNamespace()))
|| (name != null && !name.equals(getName())))
exception(
"expected: " + (type == WAP_EXTENSION ? "WAP Ext." : (TYPES[type] + " {" + namespace + "}" + name)));
}
public void setInput(Reader reader) throws XmlPullParserException {
exception("InputStream required");
}
public void setInput(InputStream in, String enc)
throws XmlPullParserException {
this.in = in;
try {
version = readByte();
publicIdentifierId = readInt();
if (publicIdentifierId == 0)
readInt();
int charset = readInt(); // skip charset
if (null == enc){
switch (charset){
case 4: encoding = "ISO-8859-1"; break;
case 106: encoding = "UTF-8"; break;
// add more if you need them
// http://www.iana.org/assignments/character-sets
// case MIBenum: encoding = Name break;
default: throw new UnsupportedEncodingException(""+charset);
}
}else{
encoding = enc;
}
int strTabSize = readInt();
stringTable = new byte[strTabSize];
int ok = 0;
while(ok < strTabSize){
int cnt = in.read(stringTable, ok, strTabSize - ok);
if(cnt <= 0) break;
ok += cnt;
}
selectPage(0, true);
selectPage(0, false);
}
catch (IOException e) {
exception("Illegal input format");
}
}
public void setFeature(String feature, boolean value)
throws XmlPullParserException {
if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature))
processNsp = value;
else
exception("unsupported feature: " + feature);
}
public void setProperty(String property, Object value)
throws XmlPullParserException {
throw new XmlPullParserException("unsupported property: " + property);
}
// ---------------------- private / internal methods
private final boolean adjustNsp()
throws XmlPullParserException {
boolean any = false;
for (int i = 0; i < attributeCount << 2; i += 4) {
// * 4 - 4; i >= 0; i -= 4) {
String attrName = attributes[i + 2];
int cut = attrName.indexOf(':');
String prefix;
if (cut != -1) {
prefix = attrName.substring(0, cut);
attrName = attrName.substring(cut + 1);
}
else if (attrName.equals("xmlns")) {
prefix = attrName;
attrName = null;
}
else
continue;
if (!prefix.equals("xmlns")) {
any = true;
}
else {
int j = (nspCounts[depth]++) << 1;
nspStack = ensureCapacity(nspStack, j + 2);
nspStack[j] = attrName;
nspStack[j + 1] = attributes[i + 3];
if (attrName != null
&& attributes[i + 3].equals(""))
exception("illegal empty namespace");
// prefixMap = new PrefixMap (prefixMap, attrName, attr.getValue ());
//System.out.println (prefixMap);
System.arraycopy(
attributes,
i + 4,
attributes,
i,
((--attributeCount) << 2) - i);
i -= 4;
}
}
if (any) {
for (int i = (attributeCount << 2) - 4;
i >= 0;
i -= 4) {
String attrName = attributes[i + 2];
int cut = attrName.indexOf(':');
if (cut == 0)
throw new RuntimeException(
"illegal attribute name: "
+ attrName
+ " at "
+ this);
else if (cut != -1) {
String attrPrefix =
attrName.substring(0, cut);
attrName = attrName.substring(cut + 1);
String attrNs = getNamespace(attrPrefix);
if (attrNs == null)
throw new RuntimeException(
"Undefined Prefix: "
+ attrPrefix
+ " in "
+ this);
attributes[i] = attrNs;
attributes[i + 1] = attrPrefix;
attributes[i + 2] = attrName;
for (int j = (attributeCount << 2) - 4;
j > i;
j -= 4)
if (attrName.equals(attributes[j + 2])
&& attrNs.equals(attributes[j]))
exception(
"Duplicate Attribute: {"
+ attrNs
+ "}"
+ attrName);
}
}
}
int cut = name.indexOf(':');
if (cut == 0)
exception("illegal tag name: " + name);
else if (cut != -1) {
prefix = name.substring(0, cut);
name = name.substring(cut + 1);
}
this.namespace = getNamespace(prefix);
if (this.namespace == null) {
if (prefix != null)
exception("undefined prefix: " + prefix);
this.namespace = NO_NAMESPACE;
}
return any;
}
private final void setTable(int page, int type, String[] table) {
if(stringTable != null){
throw new RuntimeException("setXxxTable must be called before setInput!");
}
while(tables.size() < 3*page +3){
tables.addElement(null);
}
tables.setElementAt(table, page*3+type);
}
private final void exception(String desc)
throws XmlPullParserException {
throw new XmlPullParserException(desc, this, null);
}
private void selectPage(int nr, boolean tags) throws XmlPullParserException{
if(tables.size() == 0 && nr == 0) return;
if(nr*3 > tables.size())
exception("Code Page "+nr+" undefined!");
if(tags)
tagTable = (String[]) tables.elementAt(nr * 3 + TAG_TABLE);
else {
attrStartTable = (String[]) tables.elementAt(nr * 3 + ATTR_START_TABLE);
attrValueTable = (String[]) tables.elementAt(nr * 3 + ATTR_VALUE_TABLE);
}
}
private final void nextImpl()
throws IOException, XmlPullParserException {
String s;
if (type == END_TAG) {
depth--;
}
if (degenerated) {
type = XmlPullParser.END_TAG;
degenerated = false;
return;
}
text = null;
prefix = null;
name = null;
int id = peekId ();
while(id == Wbxml.SWITCH_PAGE){
nextId = -2;
selectPage(readByte(), true);
id = peekId();
}
nextId = -2;
switch (id) {
case -1 :
type = XmlPullParser.END_DOCUMENT;
break;
case Wbxml.END :
{
int sp = (depth - 1) << 2;
type = END_TAG;
namespace = elementStack[sp];
prefix = elementStack[sp + 1];
name = elementStack[sp + 2];
}
break;
case Wbxml.ENTITY :
{
type = ENTITY_REF;
char c = (char) readInt();
text = "" + c;
name = "#" + ((int) c);
}
break;
case Wbxml.STR_I :
type = TEXT;
text = readStrI();
break;
case Wbxml.EXT_I_0 :
case Wbxml.EXT_I_1 :
case Wbxml.EXT_I_2 :
case Wbxml.EXT_T_0 :
case Wbxml.EXT_T_1 :
case Wbxml.EXT_T_2 :
case Wbxml.EXT_0 :
case Wbxml.EXT_1 :
case Wbxml.EXT_2 :
case Wbxml.OPAQUE :
type = WAP_EXTENSION;
wapCode = id;
wapExtensionData = parseWapExtension(id);
break;
case Wbxml.PI :
throw new RuntimeException("PI curr. not supp.");
// readPI;
// break;
case Wbxml.STR_T :
{
type = TEXT;
text = readStrT();
}
break;
default :
parseElement(id);
}
// }
// while (next == null);
// return next;
}
/** Overwrite this method to intercept all wap events */
public Object parseWapExtension(int id) throws IOException, XmlPullParserException {
switch (id) {
case Wbxml.EXT_I_0 :
case Wbxml.EXT_I_1 :
case Wbxml.EXT_I_2 :
return readStrI();
case Wbxml.EXT_T_0 :
case Wbxml.EXT_T_1 :
case Wbxml.EXT_T_2 :
return new Integer(readInt());
case Wbxml.EXT_0 :
case Wbxml.EXT_1 :
case Wbxml.EXT_2 :
return null;
case Wbxml.OPAQUE :
{
int count = readInt();
byte[] buf = new byte[count];
while(count > 0){
count -= in.read(buf, buf.length-count, count);
}
return buf;
} // case OPAQUE
default:
exception("illegal id: "+id);
return null; // dead code
} // SWITCH
}
public void readAttr() throws IOException, XmlPullParserException {
int id = readByte();
int i = 0;
while (id != 1) {
while(id == Wbxml.SWITCH_PAGE){
selectPage(readByte(), false);
id = readByte();
}
String name = resolveId(attrStartTable, id);
StringBuffer value;
int cut = name.indexOf('=');
if (cut == -1)
value = new StringBuffer();
else {
value =
new StringBuffer(name.substring(cut + 1));
name = name.substring(0, cut);
}
id = readByte();
while (id > 128
|| id == Wbxml.SWITCH_PAGE
|| id == Wbxml.ENTITY
|| id == Wbxml.STR_I
|| id == Wbxml.STR_T
|| (id >= Wbxml.EXT_I_0 && id <= Wbxml.EXT_I_2)
|| (id >= Wbxml.EXT_T_0 && id <= Wbxml.EXT_T_2)) {
switch (id) {
case Wbxml.SWITCH_PAGE :
selectPage(readByte(), false);
break;
case Wbxml.ENTITY :
value.append((char) readInt());
break;
case Wbxml.STR_I :
value.append(readStrI());
break;
case Wbxml.EXT_I_0 :
case Wbxml.EXT_I_1 :
case Wbxml.EXT_I_2 :
case Wbxml.EXT_T_0 :
case Wbxml.EXT_T_1 :
case Wbxml.EXT_T_2 :
case Wbxml.EXT_0 :
case Wbxml.EXT_1 :
case Wbxml.EXT_2 :
case Wbxml.OPAQUE :
value.append(resolveWapExtension(id, parseWapExtension(id)));
break;
case Wbxml.STR_T :
value.append(readStrT());
break;
default :
value.append(
resolveId(attrValueTable, id));
}
id = readByte();
}
attributes = ensureCapacity(attributes, i + 4);
attributes[i++] = "";
attributes[i++] = null;
attributes[i++] = name;
attributes[i++] = value.toString();
attributeCount++;
}
}
private int peekId () throws IOException {
if (nextId == -2) {
nextId = in.read ();
}
return nextId;
}
/** overwrite for own WAP extension handling in attributes and high level parsing
* (above nextToken() level) */
protected String resolveWapExtension(int id, Object data){
if(data instanceof byte[]){
StringBuffer sb = new StringBuffer();
byte[] b = (byte[]) data;
for (int i = 0; i < b.length; i++) {
sb.append(HEX_DIGITS.charAt((b[i] >> 4) & 0x0f));
sb.append(HEX_DIGITS.charAt(b[i] & 0x0f));
}
return sb.toString();
}
return "$("+data+")";
}
String resolveId(String[] tab, int id) throws IOException {
int idx = (id & 0x07f) - 5;
if (idx == -1){
wapCode = -1;
return readStrT();
}
if (idx < 0
|| tab == null
|| idx >= tab.length
|| tab[idx] == null)
throw new IOException("id " + id + " undef.");
wapCode = idx+5;
return tab[idx];
}
void parseElement(int id)
throws IOException, XmlPullParserException {
type = START_TAG;
name = resolveId(tagTable, id & 0x03f);
attributeCount = 0;
if ((id & 128) != 0) {
readAttr();
}
degenerated = (id & 64) == 0;
int sp = depth++ << 2;
// transfer to element stack
elementStack = ensureCapacity(elementStack, sp + 4);
elementStack[sp + 3] = name;
if (depth >= nspCounts.length) {
int[] bigger = new int[depth + 4];
System.arraycopy(nspCounts, 0, bigger, 0, nspCounts.length);
nspCounts = bigger;
}
nspCounts[depth] = nspCounts[depth - 1];
for (int i = attributeCount - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (getAttributeName(i)
.equals(getAttributeName(j)))
exception(
"Duplicate Attribute: "
+ getAttributeName(i));
}
}
if (processNsp)
adjustNsp();
else
namespace = "";
elementStack[sp] = namespace;
elementStack[sp + 1] = prefix;
elementStack[sp + 2] = name;
}
private final String[] ensureCapacity(
String[] arr,
int required) {
if (arr.length >= required)
return arr;
String[] bigger = new String[required + 16];
System.arraycopy(arr, 0, bigger, 0, arr.length);
return bigger;
}
int readByte() throws IOException {
int i = in.read();
if (i == -1)
throw new IOException("Unexpected EOF");
return i;
}
int readInt() throws IOException {
int result = 0;
int i;
do {
i = readByte();
result = (result << 7) | (i & 0x7f);
}
while ((i & 0x80) != 0);
return result;
}
String readStrI() throws IOException {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
boolean wsp = true;
while (true){
int i = in.read();
if (i == 0){
break;
}
if (i == -1){
throw new IOException(UNEXPECTED_EOF);
}
if (i > 32){
wsp = false;
}
buf.write(i);
}
isWhitespace = wsp;
String result = new String(buf.toByteArray(), encoding);
buf.close();
return result;
}
String readStrT() throws IOException {
int pos = readInt();
// As the main reason of stringTable is compression we build a cache of Strings
// stringTable is supposed to help create Strings from parts which means some cache hit rate
// This will help to minimize the Strings created when invoking readStrT() repeatedly
if (cacheStringTable == null){
//Lazy init if device is not using StringTable but inline 0x03 strings
cacheStringTable = new Hashtable();
}
String forReturn = (String) cacheStringTable.get(new Integer(pos));
if (forReturn == null){
int end = pos;
while(end < stringTable.length && stringTable[end] != '\0'){
end++;
}
forReturn = new String(stringTable, pos, end-pos, encoding);
cacheStringTable.put(new Integer(pos), forReturn);
}
return forReturn;
}
/**
* Sets the tag table for a given page.
* The first string in the array defines tag 5, the second tag 6 etc.
*/
public void setTagTable(int page, String[] table) {
setTable(page, TAG_TABLE, table);
// this.tagTable = tagTable;
// if (page != 0)
// throw new RuntimeException("code pages curr. not supp.");
}
/** Sets the attribute start Table for a given page.
* The first string in the array defines attribute
* 5, the second attribute 6 etc. Please use the
* character '=' (without quote!) as delimiter
* between the attribute name and the (start of the) value
*/
public void setAttrStartTable(
int page,
String[] table) {
setTable(page, ATTR_START_TABLE, table);
}
/** Sets the attribute value Table for a given page.
* The first string in the array defines attribute value 0x85,
* the second attribute value 0x86 etc.
*/
public void setAttrValueTable(
int page,
String[] table) {
setTable(page, ATTR_VALUE_TABLE, table);
}
/** Returns the token ID for start tags or the event type for wap proprietary events
* such as OPAQUE.
*/
public int getWapCode(){
return wapCode;
}
public Object getWapExtensionData(){
return wapExtensionData;
}
}