Home | History | Annotate | Download | only in adapter
      1 /*
      2  * Copyright (C) 2008-2009 Marc Blank
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.exchange.adapter;
     19 
     20 import com.android.exchange.Eas;
     21 import com.android.exchange.EasException;
     22 import com.android.exchange.utility.FileLogger;
     23 
     24 import android.content.Context;
     25 import android.util.Log;
     26 
     27 import java.io.ByteArrayOutputStream;
     28 import java.io.FileNotFoundException;
     29 import java.io.FileOutputStream;
     30 import java.io.IOException;
     31 import java.io.InputStream;
     32 import java.util.ArrayList;
     33 
     34 /**
     35  * Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that
     36  * EAS uses (as defined in the EAS specification)
     37  *
     38  */
     39 public abstract class Parser {
     40 
     41     // The following constants are Wbxml standard
     42     public static final int START_DOCUMENT = 0;
     43     public static final int DONE = 1;
     44     public static final int START = 2;
     45     public static final int END = 3;
     46     public static final int TEXT = 4;
     47     public static final int END_DOCUMENT = 3;
     48     private static final int NOT_FETCHED = Integer.MIN_VALUE;
     49     private static final int NOT_ENDED = Integer.MIN_VALUE;
     50     private static final int EOF_BYTE = -1;
     51     private boolean logging = false;
     52     private boolean capture = false;
     53     private String logTag = "EAS Parser";
     54 
     55     // Where tags start in a page
     56     private static final int TAG_BASE = 5;
     57 
     58     private ArrayList<Integer> captureArray;
     59 
     60     // The input stream for this parser
     61     private InputStream in;
     62 
     63     // The current tag depth
     64     private int depth;
     65 
     66     // The upcoming (saved) id from the stream
     67     private int nextId = NOT_FETCHED;
     68 
     69     // The current tag table (i.e. the tag table for the current page)
     70     private String[] tagTable;
     71 
     72     // An array of tag tables, as defined in EasTags
     73     static private String[][] tagTables = new String[24][];
     74 
     75     // The stack of names of tags being processed; used when debug = true
     76     private String[] nameArray = new String[32];
     77 
     78     // The stack of tags being processed
     79     private int[] startTagArray = new int[32];
     80 
     81     // The following vars are available to all to avoid method calls that represent the state of
     82     // the parser at any given time
     83     public int endTag = NOT_ENDED;
     84 
     85     public int startTag;
     86 
     87     // The type of the last token read
     88     public int type;
     89 
     90     // The current page
     91     public int page;
     92 
     93     // The current tag
     94     public int tag;
     95 
     96     // The name of the current tag
     97     public String name;
     98 
     99     // Whether the current tag is associated with content (a value)
    100     private boolean noContent;
    101 
    102     // The value read, as a String.  Only one of text or num will be valid, depending on whether the
    103     // value was requested as a String or an int (to avoid wasted effort in parsing)
    104     public String text;
    105 
    106     // The value read, as an int
    107     public int num;
    108 
    109     public class EofException extends IOException {
    110         private static final long serialVersionUID = 1L;
    111     }
    112 
    113     public class EodException extends IOException {
    114         private static final long serialVersionUID = 1L;
    115     }
    116 
    117     public class EasParserException extends IOException {
    118         private static final long serialVersionUID = 1L;
    119 
    120         EasParserException() {
    121             super("WBXML format error");
    122         }
    123 
    124         EasParserException(String reason) {
    125             super(reason);
    126         }
    127     }
    128 
    129     public boolean parse() throws IOException, EasException {
    130         return false;
    131     }
    132 
    133     /**
    134      * Initialize the tag tables; they are constant
    135      *
    136      */
    137     {
    138         String[][] pages = Tags.pages;
    139         for (int i = 0; i < pages.length; i++) {
    140             String[] page = pages[i];
    141             if (page.length > 0) {
    142                 tagTables[i] = page;
    143             }
    144         }
    145     }
    146 
    147     public Parser(InputStream in) throws IOException {
    148         setInput(in);
    149         logging = Eas.PARSER_LOG;
    150     }
    151 
    152     /**
    153      * Set the debug state of the parser.  When debugging is on, every token is logged (Log.v) to
    154      * the console.
    155      *
    156      * @param val the desired state for debug output
    157      */
    158     public void setDebug(boolean val) {
    159         logging = val;
    160     }
    161 
    162     /**
    163      * Set the tag used for logging.  When debugging is on, every token is logged (Log.v) to
    164      * the console.
    165      *
    166      * @param val the logging tag
    167      */
    168     public void setLoggingTag(String val) {
    169         logTag = val;
    170     }
    171 
    172     /**
    173      * Turns on data capture; this is used to create test streams that represent "live" data and
    174      * can be used against the various parsers.
    175      */
    176     public void captureOn() {
    177         capture = true;
    178         captureArray = new ArrayList<Integer>();
    179     }
    180 
    181     /**
    182      * Turns off data capture; writes the captured data to a specified file.
    183      */
    184     public void captureOff(Context context, String file) {
    185         try {
    186             FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE);
    187             out.write(captureArray.toString().getBytes());
    188             out.close();
    189         } catch (FileNotFoundException e) {
    190             // This is debug code; exceptions aren't interesting.
    191         } catch (IOException e) {
    192             // This is debug code; exceptions aren't interesting.
    193         }
    194     }
    195 
    196     /**
    197      * Return the value of the current tag, as a String
    198      *
    199      * @return the String value of the current tag
    200      * @throws IOException
    201      */
    202     public String getValue() throws IOException {
    203         // The false argument tells getNext to return the value as a String
    204         getNext(false);
    205         // This means there was no value given, just <Foo/>; we'll return empty string for now
    206         if (type == END) {
    207             if (logging) {
    208                 log("No value for tag: " + tagTable[startTag - TAG_BASE]);
    209             }
    210             return "";
    211         }
    212         // Save the value
    213         String val = text;
    214         // Read the next token; it had better be the end of the current tag
    215         getNext(false);
    216         // If not, throw an exception
    217         if (type != END) {
    218             throw new IOException("No END found!");
    219         }
    220         endTag = startTag;
    221         return val;
    222     }
    223 
    224     /**
    225      * Return the value of the current tag, as an integer
    226      *
    227      * @return the integer value of the current tag
    228      * @throws IOException
    229      */
    230    public int getValueInt() throws IOException {
    231         // The true argument to getNext indicates the desire for an integer return value
    232         getNext(true);
    233         if (type == END) {
    234             return 0;
    235         }
    236         // Save the value
    237         int val = num;
    238         // Read the next token; it had better be the end of the current tag
    239         getNext(false);
    240         // If not, throw an exception
    241         if (type != END) {
    242             throw new IOException("No END found!");
    243         }
    244         endTag = startTag;
    245         return val;
    246     }
    247 
    248     /**
    249      * Return the next tag found in the stream; special tags END and END_DOCUMENT are used to
    250      * mark the end of the current tag and end of document.  If we hit end of document without
    251      * looking for it, generate an EodException.  The tag returned consists of the page number
    252      * shifted PAGE_SHIFT bits OR'd with the tag retrieved from the stream.  Thus, all tags returned
    253      * are unique.
    254      *
    255      * @param endingTag the tag that would represent the end of the tag we're processing
    256      * @return the next tag found
    257      * @throws IOException
    258      */
    259     public int nextTag(int endingTag) throws IOException {
    260         // Lose the page information
    261         endTag = endingTag &= Tags.PAGE_MASK;
    262         while (getNext(false) != DONE) {
    263             // If we're a start, set tag to include the page and return it
    264             if (type == START) {
    265                 tag = page | startTag;
    266                 return tag;
    267             // If we're at the ending tag we're looking for, return the END signal
    268             } else if (type == END && startTag == endTag) {
    269                 return END;
    270             }
    271         }
    272         // We're at end of document here.  If we're looking for it, return END_DOCUMENT
    273         if (endTag == START_DOCUMENT) {
    274             return END_DOCUMENT;
    275         }
    276         // Otherwise, we've prematurely hit end of document, so exception out
    277         // EodException is a subclass of IOException; this will be treated as an IO error by
    278         // SyncManager.
    279         throw new EodException();
    280     }
    281 
    282     /**
    283      * Skip anything found in the stream until the end of the current tag is reached.  This can be
    284      * used to ignore stretches of xml that aren't needed by the parser.
    285      *
    286      * @throws IOException
    287      */
    288     public void skipTag() throws IOException {
    289         int thisTag = startTag;
    290         // Just loop until we hit the end of the current tag
    291         while (getNext(false) != DONE) {
    292             if (type == END && startTag == thisTag) {
    293                 return;
    294             }
    295         }
    296 
    297         // If we're at end of document, that's bad
    298         throw new EofException();
    299     }
    300 
    301     /**
    302      * Retrieve the next token from the input stream
    303      *
    304      * @return the token found
    305      * @throws IOException
    306      */
    307     public int nextToken() throws IOException {
    308         getNext(false);
    309         return type;
    310     }
    311 
    312     /**
    313      * Initializes the parser with an input stream; reads the first 4 bytes (which are always the
    314      * same in EAS, and then sets the tag table to point to page 0 (by definition, the starting
    315      * page).
    316      *
    317      * @param in the InputStream associated with this parser
    318      * @throws IOException
    319      */
    320     public void setInput(InputStream in) throws IOException {
    321         this.in = in;
    322         readByte(); // version
    323         readInt();  // ?
    324         readInt();  // 106 (UTF-8)
    325         readInt();  // string table length
    326         tagTable = tagTables[0];
    327     }
    328 
    329     /*package*/ void resetInput(InputStream in) {
    330         this.in = in;
    331     }
    332 
    333     void log(String str) {
    334         int cr = str.indexOf('\n');
    335         if (cr > 0) {
    336             str = str.substring(0, cr);
    337         }
    338         Log.v(logTag, str);
    339         if (Eas.FILE_LOG) {
    340             FileLogger.log(logTag, str);
    341         }
    342     }
    343 
    344     /**
    345      * Return the next piece of data from the stream.  The return value indicates the type of data
    346      * that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or
    347      * TEXT (the value of a tag)
    348      *
    349      * @param asInt whether a TEXT value should be parsed as a String or an int.
    350      * @return the type of data retrieved
    351      * @throws IOException
    352      */
    353     private final int getNext(boolean asInt) throws IOException {
    354         int savedEndTag = endTag;
    355         if (type == END) {
    356             depth--;
    357         } else {
    358             endTag = NOT_ENDED;
    359         }
    360 
    361         if (noContent) {
    362             type = END;
    363             noContent = false;
    364             endTag = savedEndTag;
    365             return type;
    366         }
    367 
    368         text = null;
    369         name = null;
    370 
    371         int id = nextId ();
    372         while (id == Wbxml.SWITCH_PAGE) {
    373             nextId = NOT_FETCHED;
    374             // Get the new page number
    375             int pg = readByte();
    376             // Save the shifted page to add into the startTag in nextTag
    377             page = pg << Tags.PAGE_SHIFT;
    378             // Retrieve the current tag table
    379             tagTable = tagTables[pg];
    380             id = nextId();
    381         }
    382         nextId = NOT_FETCHED;
    383 
    384         switch (id) {
    385             case EOF_BYTE:
    386                 // End of document
    387                 type = DONE;
    388                 break;
    389 
    390             case Wbxml.END:
    391                 // End of tag
    392                 type = END;
    393                 if (logging) {
    394                     name = nameArray[depth];
    395                     //log("</" + name + '>');
    396                 }
    397                 // Retrieve the now-current startTag from our stack
    398                 startTag = endTag = startTagArray[depth];
    399                 break;
    400 
    401             case Wbxml.STR_I:
    402                 // Inline string
    403                 type = TEXT;
    404                 if (asInt) {
    405                     num = readInlineInt();
    406                 } else {
    407                     text = readInlineString();
    408                 }
    409                 if (logging) {
    410                     name = tagTable[startTag - TAG_BASE];
    411                     log(name + ": " + (asInt ? Integer.toString(num) : text));
    412                 }
    413                 break;
    414 
    415             default:
    416                 // Start of tag
    417                 type = START;
    418                 // The tag is in the low 6 bits
    419                 startTag = id & 0x3F;
    420                 // If the high bit is set, there is content (a value) to be read
    421                 noContent = (id & 0x40) == 0;
    422                 depth++;
    423                 if (logging) {
    424                     name = tagTable[startTag - TAG_BASE];
    425                     //log('<' + name + '>');
    426                     nameArray[depth] = name;
    427                 }
    428                 // Save the startTag to our stack
    429                 startTagArray[depth] = startTag;
    430         }
    431 
    432         // Return the type of data we're dealing with
    433         return type;
    434     }
    435 
    436     /**
    437      * Read an int from the input stream, and capture it if necessary for debugging.  Seems a small
    438      * price to pay...
    439      *
    440      * @return the int read
    441      * @throws IOException
    442      */
    443     private int read() throws IOException {
    444         int i;
    445         i = in.read();
    446         if (capture) {
    447             captureArray.add(i);
    448         }
    449         return i;
    450     }
    451 
    452     private int nextId() throws IOException {
    453         if (nextId == NOT_FETCHED) {
    454             nextId = read();
    455         }
    456         return nextId;
    457     }
    458 
    459     private int readByte() throws IOException {
    460         int i = read();
    461         if (i == EOF_BYTE) {
    462             throw new EofException();
    463         }
    464         return i;
    465     }
    466 
    467     /**
    468      * Read an integer from the stream; this is called when the parser knows that what follows is
    469      * an inline string representing an integer (e.g. the Read tag in Email has a value known to
    470      * be either "0" or "1")
    471      *
    472      * @return the integer as parsed from the stream
    473      * @throws IOException
    474      */
    475     private int readInlineInt() throws IOException {
    476         int result = 0;
    477 
    478         while (true) {
    479             int i = readByte();
    480             // Inline strings are always terminated with a zero byte
    481             if (i == 0) {
    482                 return result;
    483             }
    484             if (i >= '0' && i <= '9') {
    485                 result = (result * 10) + (i - '0');
    486             } else {
    487                 throw new IOException("Non integer");
    488             }
    489         }
    490     }
    491 
    492     private int readInt() throws IOException {
    493         int result = 0;
    494         int i;
    495 
    496         do {
    497             i = readByte();
    498             result = (result << 7) | (i & 0x7f);
    499         } while ((i & 0x80) != 0);
    500 
    501         return result;
    502     }
    503 
    504     /**
    505      * Read an inline string from the stream
    506      *
    507      * @return the String as parsed from the stream
    508      * @throws IOException
    509      */
    510     private String readInlineString() throws IOException {
    511         ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
    512         while (true) {
    513             int i = read();
    514             if (i == 0) {
    515                 break;
    516             } else if (i == EOF_BYTE) {
    517                 throw new EofException();
    518             }
    519             outputStream.write(i);
    520         }
    521         outputStream.flush();
    522         String res = outputStream.toString("UTF-8");
    523         outputStream.close();
    524         return res;
    525     }
    526 }
    527