1/* Copyright 2010, The Android Open Source Project
2 **
3 ** Licensed under the Apache License, Version 2.0 (the "License");
4 ** you may not use this file except in compliance with the License.
5 ** You may obtain a copy of the License at
6 **
7 **     http://www.apache.org/licenses/LICENSE-2.0
8 **
9 ** Unless required by applicable law or agreed to in writing, software
10 ** distributed under the License is distributed on an "AS IS" BASIS,
11 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 ** See the License for the specific language governing permissions and
13 ** limitations under the License.
14 */
15
16package com.android.exchange.utility;
17
18import com.android.emailcommon.utility.Utility;
19
20import android.text.TextUtils;
21
22import java.io.ByteArrayOutputStream;
23import java.io.IOException;
24import java.io.UnsupportedEncodingException;
25
26/**
27 * Class to generate iCalender object (*.ics) per RFC 5545.
28 */
29public class SimpleIcsWriter {
30    private static final int MAX_LINE_LENGTH = 75; // In bytes, excluding CRLF
31    private static final int CHAR_MAX_BYTES_IN_UTF8 = 4;  // Used to be 6, but RFC3629 limited it.
32    private final ByteArrayOutputStream mOut = new ByteArrayOutputStream();
33
34    public SimpleIcsWriter() {
35    }
36
37    /**
38     * Low level method to write a line, performing line-folding if necessary.
39     */
40    /* package for testing */ void writeLine(String string) {
41        int numBytes = 0;
42        for (byte b : Utility.toUtf8(string)) {
43            // Fold it when necessary.
44            // To make it simple, we assume all chars are 4 bytes.
45            // If not (and usually it's not), we end up wrapping earlier than necessary, but that's
46            // completely fine.
47            if (numBytes > (MAX_LINE_LENGTH - CHAR_MAX_BYTES_IN_UTF8)
48                    && Utility.isFirstUtf8Byte(b)) { // Only wrappable if it's before the first byte
49                mOut.write((byte) '\r');
50                mOut.write((byte) '\n');
51                mOut.write((byte) '\t');
52                numBytes = 1; // for TAB
53            }
54            mOut.write(b);
55            numBytes++;
56        }
57        mOut.write((byte) '\r');
58        mOut.write((byte) '\n');
59    }
60
61    /**
62     * Write a tag with a value.
63     */
64    public void writeTag(String name, String value) {
65        // Belt and suspenders here; don't crash on null value; just return
66        if (TextUtils.isEmpty(value)) {
67            return;
68        }
69
70        // The following properties take a TEXT value, which need to be escaped.
71        // (These property names should be all interned, so using equals() should be faster than
72        // using a hash table.)
73
74        // TODO make constants for these literals
75        if ("CALSCALE".equals(name)
76                || "METHOD".equals(name)
77                || "PRODID".equals(name)
78                || "VERSION".equals(name)
79                || "CATEGORIES".equals(name)
80                || "CLASS".equals(name)
81                || "COMMENT".equals(name)
82                || "DESCRIPTION".equals(name)
83                || "LOCATION".equals(name)
84                || "RESOURCES".equals(name)
85                || "STATUS".equals(name)
86                || "SUMMARY".equals(name)
87                || "TRANSP".equals(name)
88                || "TZID".equals(name)
89                || "TZNAME".equals(name)
90                || "CONTACT".equals(name)
91                || "RELATED-TO".equals(name)
92                || "UID".equals(name)
93                || "ACTION".equals(name)
94                || "REQUEST-STATUS".equals(name)
95                || "X-LIC-LOCATION".equals(name)
96                ) {
97            value = escapeTextValue(value);
98        }
99        writeLine(name + ":" + value);
100    }
101
102    /**
103     * For debugging
104     */
105    @Override
106    public String toString() {
107        return Utility.fromUtf8(getBytes());
108    }
109
110    /**
111     * @return the entire iCalendar invitation object.
112     */
113    public byte[] getBytes() {
114        try {
115            mOut.flush();
116        } catch (IOException wonthappen) {
117        }
118        return mOut.toByteArray();
119    }
120
121    /**
122     * Quote a param-value string, according to RFC 5545, section 3.1
123     */
124    public static String quoteParamValue(String paramValue) {
125        if (paramValue == null) {
126            return null;
127        }
128        // Wrap with double quotes.
129        // The spec doesn't allow putting double-quotes in a param value, so let's use single quotes
130        // as a substitute.
131        // It's not the smartest implementation.  e.g. we don't have to wrap an empty string with
132        // double quotes.  But it works.
133        return "\"" + paramValue.replace("\"", "'") + "\"";
134    }
135
136    /**
137     * Escape a TEXT value per RFC 5545 section 3.3.11
138     */
139    /* package for testing */ static String escapeTextValue(String s) {
140        StringBuilder sb = new StringBuilder(s.length());
141        for (int i = 0; i < s.length(); i++) {
142            char ch = s.charAt(i);
143            if (ch == '\n') {
144                sb.append("\\n");
145            } else if (ch == '\r') {
146                // Remove CR
147            } else if (ch == ',' || ch == ';' || ch == '\\') {
148                sb.append('\\');
149                sb.append(ch);
150            } else {
151                sb.append(ch);
152            }
153        }
154        return sb.toString();
155    }
156}
157