Home | History | Annotate | Download | only in utility
      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