blob: ca99a77050c0eed1d68addeaf3f70c5b9afc5515 [file] [log] [blame]
/*
* Copyright (C) 2008-2009 Marc Blank
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange.adapter;
import com.android.exchange.Eas;
import com.android.exchange.EasException;
import com.android.exchange.utility.FileLogger;
import android.content.Context;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
/**
* Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that
* EAS uses (as defined in the EAS specification)
*
*/
public abstract class Parser {
private static final String TAG = "EasParser";
// The following constants are Wbxml standard
public static final int START_DOCUMENT = 0;
public static final int DONE = 1;
public static final int START = 2;
public static final int END = 3;
public static final int TEXT = 4;
public static final int END_DOCUMENT = 3;
private static final int NOT_FETCHED = Integer.MIN_VALUE;
private static final int NOT_ENDED = Integer.MIN_VALUE;
private static final int EOF_BYTE = -1;
private boolean logging = false;
private boolean capture = false;
private ArrayList<Integer> captureArray;
// The input stream for this parser
private InputStream in;
// The current tag depth
private int depth;
// The upcoming (saved) id from the stream
private int nextId = NOT_FETCHED;
// The current tag table (i.e. the tag table for the current page)
private String[] tagTable;
// An array of tag tables, as defined in EasTags
static private String[][] tagTables = new String[24][];
// The stack of names of tags being processed; used when debug = true
private String[] nameArray = new String[32];
// The stack of tags being processed
private int[] startTagArray = new int[32];
// The following vars are available to all to avoid method calls that represent the state of
// the parser at any given time
public int endTag = NOT_ENDED;
public int startTag;
// The type of the last token read
public int type;
// The current page
public int page;
// The current tag
public int tag;
// The name of the current tag
public String name;
// Whether the current tag is associated with content (a value)
private boolean noContent;
// The value read, as a String. Only one of text or num will be valid, depending on whether the
// value was requested as a String or an int (to avoid wasted effort in parsing)
public String text;
// The value read, as an int
public int num;
public class EofException extends IOException {
private static final long serialVersionUID = 1L;
}
public class EodException extends IOException {
private static final long serialVersionUID = 1L;
}
public class EasParserException extends IOException {
private static final long serialVersionUID = 1L;
EasParserException() {
super("WBXML format error");
}
EasParserException(String reason) {
super(reason);
}
}
public boolean parse() throws IOException, EasException {
return false;
}
/**
* Initialize the tag tables; they are constant
*
*/
{
String[][] pages = Tags.pages;
for (int i = 0; i < pages.length; i++) {
String[] page = pages[i];
if (page.length > 0) {
tagTables[i] = page;
}
}
}
public Parser(InputStream in) throws IOException {
setInput(in);
logging = Eas.PARSER_LOG;
}
/**
* Set the debug state of the parser. When debugging is on, every token is logged (Log.v) to
* the console.
*
* @param val the desired state for debug output
*/
public void setDebug(boolean val) {
logging = val;
}
/**
* Turns on data capture; this is used to create test streams that represent "live" data and
* can be used against the various parsers.
*/
public void captureOn() {
capture = true;
captureArray = new ArrayList<Integer>();
}
/**
* Turns off data capture; writes the captured data to a specified file.
*/
public void captureOff(Context context, String file) {
try {
FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE);
out.write(captureArray.toString().getBytes());
out.close();
} catch (FileNotFoundException e) {
// This is debug code; exceptions aren't interesting.
} catch (IOException e) {
// This is debug code; exceptions aren't interesting.
}
}
/**
* Return the value of the current tag, as a String
*
* @return the String value of the current tag
* @throws IOException
*/
public String getValue() throws IOException {
// The false argument tells getNext to return the value as a String
getNext(false);
// Save the value
String val = text;
// Read the next token; it had better be the end of the current tag
getNext(false);
// If not, throw an exception
if (type != END) {
throw new IOException("No END found!");
}
endTag = startTag;
return val;
}
/**
* Return the value of the current tag, as an integer
*
* @return the integer value of the current tag
* @throws IOException
*/
public int getValueInt() throws IOException {
// The true argument to getNext indicates the desire for an integer return value
getNext(true);
// Save the value
int val = num;
// Read the next token; it had better be the end of the current tag
getNext(false);
// If not, throw an exception
if (type != END) {
throw new IOException("No END found!");
}
endTag = startTag;
return val;
}
/**
* Return the next tag found in the stream; special tags END and END_DOCUMENT are used to
* mark the end of the current tag and end of document. If we hit end of document without
* looking for it, generate an EodException. The tag returned consists of the page number
* shifted PAGE_SHIFT bits OR'd with the tag retrieved from the stream. Thus, all tags returned
* are unique.
*
* @param endingTag the tag that would represent the end of the tag we're processing
* @return the next tag found
* @throws IOException
*/
public int nextTag(int endingTag) throws IOException {
// Lose the page information
endTag = endingTag &= Tags.PAGE_MASK;
while (getNext(false) != DONE) {
// If we're a start, set tag to include the page and return it
if (type == START) {
tag = page | startTag;
return tag;
// If we're at the ending tag we're looking for, return the END signal
} else if (type == END && startTag == endTag) {
return END;
}
}
// We're at end of document here. If we're looking for it, return END_DOCUMENT
if (endTag == START_DOCUMENT) {
return END_DOCUMENT;
}
// Otherwise, we've prematurely hit end of document, so exception out
// EodException is a subclass of IOException; this will be treated as an IO error by
// SyncManager.
throw new EodException();
}
/**
* Skip anything found in the stream until the end of the current tag is reached. This can be
* used to ignore stretches of xml that aren't needed by the parser.
*
* @throws IOException
*/
public void skipTag() throws IOException {
int thisTag = startTag;
// Just loop until we hit the end of the current tag
while (getNext(false) != DONE) {
if (type == END && startTag == thisTag) {
return;
}
}
// If we're at end of document, that's bad
throw new EofException();
}
/**
* Retrieve the next token from the input stream
*
* @return the token found
* @throws IOException
*/
public int nextToken() throws IOException {
getNext(false);
return type;
}
/**
* Initializes the parser with an input stream; reads the first 4 bytes (which are always the
* same in EAS, and then sets the tag table to point to page 0 (by definition, the starting
* page).
*
* @param in the InputStream associated with this parser
* @throws IOException
*/
public void setInput(InputStream in) throws IOException {
this.in = in;
readByte(); // version
readInt(); // ?
readInt(); // 106 (UTF-8)
readInt(); // string table length
tagTable = tagTables[0];
}
void log(String str) {
int cr = str.indexOf('\n');
if (cr > 0) {
str = str.substring(0, cr);
}
Log.v(TAG, str);
if (Eas.FILE_LOG) {
FileLogger.log(TAG, str);
}
}
/**
* Return the next piece of data from the stream. The return value indicates the type of data
* that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or
* TEXT (the value of a tag)
*
* @param asInt whether a TEXT value should be parsed as a String or an int.
* @return the type of data retrieved
* @throws IOException
*/
private final int getNext(boolean asInt) throws IOException {
int savedEndTag = endTag;
if (type == END) {
depth--;
} else {
endTag = NOT_ENDED;
}
if (noContent) {
type = END;
noContent = false;
endTag = savedEndTag;
return type;
}
text = null;
name = null;
int id = nextId ();
while (id == Wbxml.SWITCH_PAGE) {
nextId = NOT_FETCHED;
// Get the new page number
int pg = readByte();
// Save the shifted page to add into the startTag in nextTag
page = pg << Tags.PAGE_SHIFT;
// Retrieve the current tag table
tagTable = tagTables[pg];
id = nextId();
}
nextId = NOT_FETCHED;
switch (id) {
case EOF_BYTE:
// End of document
type = DONE;
break;
case Wbxml.END:
// End of tag
type = END;
if (logging) {
name = nameArray[depth];
//log("</" + name + '>');
}
// Retrieve the now-current startTag from our stack
startTag = endTag = startTagArray[depth];
break;
case Wbxml.STR_I:
// Inline string
type = TEXT;
if (asInt) {
num = readInlineInt();
} else {
text = readInlineString();
}
if (logging) {
name = tagTable[startTag - 5];
log(name + ": " + (asInt ? Integer.toString(num) : text));
}
break;
default:
// Start of tag
type = START;
// The tag is in the low 6 bits
startTag = id & 0x3F;
// If the high bit is set, there is content (a value) to be read
noContent = (id & 0x40) == 0;
depth++;
if (logging) {
name = tagTable[startTag - 5];
//log('<' + name + '>');
nameArray[depth] = name;
}
// Save the startTag to our stack
startTagArray[depth] = startTag;
}
// Return the type of data we're dealing with
return type;
}
/**
* Read an int from the input stream, and capture it if necessary for debugging. Seems a small
* price to pay...
*
* @return the int read
* @throws IOException
*/
private int read() throws IOException {
int i;
i = in.read();
if (capture) {
captureArray.add(i);
}
return i;
}
private int nextId() throws IOException {
if (nextId == NOT_FETCHED) {
nextId = read();
}
return nextId;
}
private int readByte() throws IOException {
int i = read();
if (i == EOF_BYTE) {
throw new EofException();
}
return i;
}
/**
* Read an integer from the stream; this is called when the parser knows that what follows is
* an inline string representing an integer (e.g. the Read tag in Email has a value known to
* be either "0" or "1")
*
* @return the integer as parsed from the stream
* @throws IOException
*/
private int readInlineInt() throws IOException {
int result = 0;
while (true) {
int i = readByte();
// Inline strings are always terminated with a zero byte
if (i == 0) {
return result;
}
if (i >= '0' && i <= '9') {
result = (result * 10) + (i - '0');
} else {
throw new IOException("Non integer");
}
}
}
private int readInt() throws IOException {
int result = 0;
int i;
do {
i = readByte();
result = (result << 7) | (i & 0x7f);
} while ((i & 0x80) != 0);
return result;
}
/**
* Read an inline string from the stream
*
* @return the String as parsed from the stream
* @throws IOException
*/
private String readInlineString() throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
while (true) {
int i = read();
if (i == 0) {
break;
} else if (i == EOF_BYTE) {
throw new EofException();
}
outputStream.write(i);
}
outputStream.flush();
String res = outputStream.toString("UTF-8");
outputStream.close();
return res;
}
}