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