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