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