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