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 
24 package com.android.exchange.adapter;
25 
26 import android.content.ContentValues;
27 import android.text.TextUtils;
28 
29 import com.android.exchange.Eas;
30 import com.android.exchange.service.EasService;
31 import com.android.exchange.utility.FileLogger;
32 import com.android.mail.utils.LogUtils;
33 import com.google.common.annotations.VisibleForTesting;
34 
35 import java.io.ByteArrayOutputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.OutputStream;
39 import java.util.ArrayDeque;
40 import java.util.Arrays;
41 import java.util.Deque;
42 
43 public 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 
Serializer()53     public Serializer() throws IOException {
54         this(new ByteArrayOutputStream(), true);
55     }
56 
Serializer(OutputStream os)57     public Serializer(OutputStream os) throws IOException {
58         this(os, true);
59     }
60 
61     @VisibleForTesting
Serializer(boolean startDocument)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      */
Serializer(final OutputStream outputStream, final boolean startDocument)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 
log(final String str)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 
done()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 
startDocument()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 
checkPendingTag(final boolean degenerated)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 
start(final int tag)146     public Serializer start(final int tag) throws IOException {
147         checkPendingTag(false);
148         mPendingTag = tag;
149         return this;
150     }
151 
end()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 
tag(final int tag)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      */
data(final int tag, final String value)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      */
text(final String text)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      */
opaque(final InputStream is, final int length)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      */
writeOpaqueHeader(final int length)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
writeInteger(final OutputStream out, int i)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 
writeInlineString(final OutputStream out, final String s)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      */
writeStringValue(final ContentValues cv, final String key, final int tag)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
toString()273     public String toString() {
274         if (mOutput instanceof ByteArrayOutputStream) {
275             return ((ByteArrayOutputStream)mOutput).toString();
276         }
277         throw new IllegalStateException();
278     }
279 
toByteArray()280     public byte[] toByteArray() {
281         if (mOutput instanceof ByteArrayOutputStream) {
282             return ((ByteArrayOutputStream)mOutput).toByteArray();
283         }
284         throw new IllegalStateException();
285     }
286 
287 }
288