1/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany 2 * 3 * Permission is hereby granted, free of charge, to any person obtaining a copy 4 * of this software and associated documentation files (the "Software"), to deal 5 * in the Software without restriction, including without limitation the rights 6 * to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 * sell copies of the Software, and to permit persons to whom the Software is 8 * furnished to do so, subject to the following conditions: 9 * 10 * The above copyright notice and this permission notice shall be included in 11 * all copies or substantial portions of the Software. 12 * 13 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 * IN THE SOFTWARE. */ 20 21//Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian 22// Greatly simplified for Google, Inc. by Marc Blank 23 24package com.android.exchange.adapter; 25 26import android.content.ContentValues; 27import android.text.TextUtils; 28 29import com.android.exchange.Eas; 30import com.android.exchange.service.EasService; 31import com.android.exchange.utility.FileLogger; 32import com.android.mail.utils.LogUtils; 33import com.google.common.annotations.VisibleForTesting; 34 35import java.io.ByteArrayOutputStream; 36import java.io.IOException; 37import java.io.InputStream; 38import java.io.OutputStream; 39import java.util.ArrayDeque; 40import java.util.Arrays; 41import java.util.Deque; 42 43public class Serializer { 44 private static final String TAG = Eas.LOG_TAG; 45 private static final int BUFFER_SIZE = 16*1024; 46 private static final int NOT_PENDING = -1; 47 48 private final OutputStream mOutput; 49 private int mPendingTag = NOT_PENDING; 50 private final Deque<String> mNameStack = new ArrayDeque<String>(); 51 private int mTagPage = 0; 52 53 public Serializer() throws IOException { 54 this(new ByteArrayOutputStream(), true); 55 } 56 57 public Serializer(OutputStream os) throws IOException { 58 this(os, true); 59 } 60 61 @VisibleForTesting 62 public Serializer(boolean startDocument) throws IOException { 63 this(new ByteArrayOutputStream(), startDocument); 64 } 65 66 /** 67 * Base constructor 68 * @param outputStream the stream we're serializing to 69 * @param startDocument whether or not to start a document 70 * @throws IOException 71 */ 72 public Serializer(final OutputStream outputStream, final boolean startDocument) 73 throws IOException { 74 super(); 75 mOutput = outputStream; 76 if (startDocument) { 77 startDocument(); 78 } else { 79 mOutput.write(0); 80 } 81 } 82 83 void log(final String str) { 84 if (!EasService.getProtocolLogging()) { 85 return; 86 } 87 final String logStr; 88 final int cr = str.indexOf('\n'); 89 if (cr > 0) { 90 logStr = str.substring(0, cr); 91 } else { 92 logStr = str; 93 } 94 final char [] charArray = new char[mNameStack.size() * 2]; 95 Arrays.fill(charArray, ' '); 96 final String indent = new String(charArray); 97 LogUtils.d(TAG, "%s%s", indent, logStr); 98 if (EasService.getFileLogging()) { 99 FileLogger.log(TAG, logStr); 100 } 101 } 102 103 public void done() throws IOException { 104 if (mNameStack.size() != 0 || mPendingTag != NOT_PENDING) { 105 throw new IOException("Done received with unclosed tags"); 106 } 107 mOutput.flush(); 108 } 109 110 public void startDocument() throws IOException { 111 mOutput.write(0x03); // version 1.3 112 mOutput.write(0x01); // unknown or missing public identifier 113 mOutput.write(106); // UTF-8 114 mOutput.write(0); // 0 length string array 115 } 116 117 private void checkPendingTag(final boolean degenerated) throws IOException { 118 if (mPendingTag == NOT_PENDING) { 119 return; 120 } 121 122 final int page = mPendingTag >> Tags.PAGE_SHIFT; 123 final int tag = mPendingTag & Tags.PAGE_MASK; 124 if (page != mTagPage) { 125 mTagPage = page; 126 mOutput.write(Wbxml.SWITCH_PAGE); 127 mOutput.write(page); 128 } 129 130 mOutput.write(degenerated ? tag : tag | Wbxml.WITH_CONTENT); 131 String name = "unknown"; 132 if (!Tags.isValidPage(page)) { 133 log("Unrecognized page " + page); 134 } else if (!Tags.isValidTag(page, tag)) { 135 log("Unknown tag " + tag + " on page " + page); 136 } else { 137 name = Tags.getTagName(page, tag); 138 } 139 log("<" + name + (degenerated ? "/>" : ">")); 140 if (!degenerated) { 141 mNameStack.addFirst(name); 142 } 143 mPendingTag = NOT_PENDING; 144 } 145 146 public Serializer start(final int tag) throws IOException { 147 checkPendingTag(false); 148 mPendingTag = tag; 149 return this; 150 } 151 152 public Serializer end() throws IOException { 153 if (mPendingTag >= 0) { 154 checkPendingTag(true); 155 } else { 156 mOutput.write(Wbxml.END); 157 final String tagName = mNameStack.removeFirst(); 158 log("</" + tagName + '>'); 159 } 160 return this; 161 } 162 163 public Serializer tag(final int tag) throws IOException { 164 start(tag); 165 end(); 166 return this; 167 } 168 169 /** 170 * Writes <tag>value</tag>. Throws IOException for null strings. 171 */ 172 public Serializer data(final int tag, final String value) throws IOException { 173 start(tag); 174 text(value); 175 end(); 176 return this; 177 } 178 179 /** 180 * Writes out inline string. Throws IOException for null strings. 181 */ 182 public Serializer text(final String text) throws IOException { 183 if (text == null) { 184 throw new IOException("Null text write for pending tag: " + mPendingTag); 185 } 186 checkPendingTag(false); 187 writeInlineString(mOutput, text); 188 log(text); 189 return this; 190 } 191 192 /** 193 * Writes out opaque data blocks. Throws IOException for negative buffer 194 * sizes or if is unable to read sufficient bytes from input stream. 195 */ 196 public Serializer opaque(final InputStream is, final int length) throws IOException { 197 writeOpaqueHeader(length); 198 log("opaque: " + length); 199 // Now write out the opaque data in batches 200 final byte[] buffer = new byte[BUFFER_SIZE]; 201 int totalBytesRead = 0; 202 while (totalBytesRead < length) { 203 final int bytesRead = is.read(buffer, 0, Math.min(BUFFER_SIZE, length)); 204 if (bytesRead == -1) { 205 throw new IOException("Invalid opaque data block; read " 206 + totalBytesRead + " bytes but expected " + length); 207 } 208 mOutput.write(buffer, 0, bytesRead); 209 totalBytesRead += bytesRead; 210 } 211 return this; 212 } 213 214 /** 215 * Writes out opaque data header, without the actual opaque data bytes. 216 * Used internally by opaque(), and externally to calculate content length 217 * without having to allocate the memory for the data copy. 218 * Throws IOException if length is negative; is a no-op for length 0. 219 */ 220 public Serializer writeOpaqueHeader(final int length) throws IOException { 221 if (length < 0) { 222 throw new IOException("Invalid negative opaque data length " + length); 223 } 224 if (length == 0) { 225 return this; 226 } 227 checkPendingTag(false); 228 mOutput.write(Wbxml.OPAQUE); 229 writeInteger(mOutput, length); 230 return this; 231 } 232 233 @VisibleForTesting 234 static void writeInteger(final OutputStream out, int i) throws IOException { 235 final byte[] buf = new byte[5]; 236 int idx = 0; 237 238 do { 239 buf[idx++] = (byte) (i & 0x7f); 240 // Use >>> to shift in 0s so loop terminates 241 i = i >>> 7; 242 } while (i != 0); 243 244 while (idx > 1) { 245 out.write(buf[--idx] | 0x80); 246 } 247 out.write(buf[0]); 248 } 249 250 private static void writeInlineString(final OutputStream out, final String s) 251 throws IOException { 252 out.write(Wbxml.STR_I); 253 final byte[] data = s.getBytes("UTF-8"); 254 out.write(data); 255 out.write(0); 256 } 257 258 /** 259 * Looks up key in cv; if absent or empty writes out <tag/> otherwise 260 * writes out <tag>value</tag>. 261 */ 262 public void writeStringValue (final ContentValues cv, final String key, 263 final int tag) throws IOException { 264 final String value = cv.getAsString(key); 265 if (!TextUtils.isEmpty(value)) { 266 data(tag, value); 267 } else { 268 tag(tag); 269 } 270 } 271 272 @Override 273 public String toString() { 274 if (mOutput instanceof ByteArrayOutputStream) { 275 return ((ByteArrayOutputStream)mOutput).toString(); 276 } 277 throw new IllegalStateException(); 278 } 279 280 public byte[] toByteArray() { 281 if (mOutput instanceof ByteArrayOutputStream) { 282 return ((ByteArrayOutputStream)mOutput).toByteArray(); 283 } 284 throw new IllegalStateException(); 285 } 286 287} 288