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.email.Utility;
     19 
     20 import android.text.TextUtils;
     21 
     22 import java.io.ByteArrayOutputStream;
     23 import java.io.IOException;
     24 import java.io.UnsupportedEncodingException;
     25 
     26 /**
     27  * Class to generate iCalender object (*.ics) per RFC 5545.
     28  */
     29 public 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