/* 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; } }