1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.pim.vcard; 17 18 import android.pim.vcard.exception.VCardException; 19 import android.util.Log; 20 21 import java.io.IOException; 22 import java.util.Arrays; 23 import java.util.HashSet; 24 25 /** 26 * The class used to parse vCard 3.0. 27 * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426). 28 */ 29 public class VCardParser_V30 extends VCardParser_V21 { 30 private static final String LOG_TAG = "VCardParser_V30"; 31 32 private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( 33 Arrays.asList( 34 "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", 35 "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", 36 "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 37 "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", 38 "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0 39 40 // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety. 41 private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>( 42 Arrays.asList("7BIT", "8BIT", "BASE64", "B")); 43 44 // Although RFC 2426 specifies some property must not have parameters, we allow it, 45 // since there may be some careers which violates the RFC... 46 private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); 47 48 private String mPreviousLine; 49 50 private boolean mEmittedAgentWarning = false; 51 52 /** 53 * True when the caller wants the parser to be strict about the input. 54 * Currently this is only for testing. 55 */ 56 private final boolean mStrictParsing; 57 58 public VCardParser_V30() { 59 super(); 60 mStrictParsing = false; 61 } 62 63 /** 64 * @param strictParsing when true, this object throws VCardException when the vcard is not 65 * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class 66 * is not fully yet for being used with this flag and may not notice invalid line(s). 67 * 68 * @hide currently only for testing! 69 */ 70 public VCardParser_V30(boolean strictParsing) { 71 super(); 72 mStrictParsing = strictParsing; 73 } 74 75 public VCardParser_V30(int parseMode) { 76 super(parseMode); 77 mStrictParsing = false; 78 } 79 80 @Override 81 protected int getVersion() { 82 return VCardConfig.FLAG_V30; 83 } 84 85 @Override 86 protected String getVersionString() { 87 return VCardConstants.VERSION_V30; 88 } 89 90 @Override 91 protected boolean isValidPropertyName(String propertyName) { 92 if (!(sAcceptablePropsWithParam.contains(propertyName) || 93 acceptablePropsWithoutParam.contains(propertyName) || 94 propertyName.startsWith("X-")) && 95 !mUnknownTypeMap.contains(propertyName)) { 96 mUnknownTypeMap.add(propertyName); 97 Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); 98 } 99 return true; 100 } 101 102 @Override 103 protected boolean isValidEncoding(String encoding) { 104 return sAcceptableEncodingV30.contains(encoding.toUpperCase()); 105 } 106 107 @Override 108 protected String getLine() throws IOException { 109 if (mPreviousLine != null) { 110 String ret = mPreviousLine; 111 mPreviousLine = null; 112 return ret; 113 } else { 114 return mReader.readLine(); 115 } 116 } 117 118 /** 119 * vCard 3.0 requires that the line with space at the beginning of the line 120 * must be combined with previous line. 121 */ 122 @Override 123 protected String getNonEmptyLine() throws IOException, VCardException { 124 String line; 125 StringBuilder builder = null; 126 while (true) { 127 line = mReader.readLine(); 128 if (line == null) { 129 if (builder != null) { 130 return builder.toString(); 131 } else if (mPreviousLine != null) { 132 String ret = mPreviousLine; 133 mPreviousLine = null; 134 return ret; 135 } 136 throw new VCardException("Reached end of buffer."); 137 } else if (line.length() == 0) { 138 if (builder != null) { 139 return builder.toString(); 140 } else if (mPreviousLine != null) { 141 String ret = mPreviousLine; 142 mPreviousLine = null; 143 return ret; 144 } 145 } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { 146 if (builder != null) { 147 // See Section 5.8.1 of RFC 2425 (MIME-DIR document). 148 // Following is the excerpts from it. 149 // 150 // DESCRIPTION:This is a long description that exists on a long line. 151 // 152 // Can be represented as: 153 // 154 // DESCRIPTION:This is a long description 155 // that exists on a long line. 156 // 157 // It could also be represented as: 158 // 159 // DESCRIPTION:This is a long descrip 160 // tion that exists o 161 // n a long line. 162 builder.append(line.substring(1)); 163 } else if (mPreviousLine != null) { 164 builder = new StringBuilder(); 165 builder.append(mPreviousLine); 166 mPreviousLine = null; 167 builder.append(line.substring(1)); 168 } else { 169 throw new VCardException("Space exists at the beginning of the line"); 170 } 171 } else { 172 if (mPreviousLine == null) { 173 mPreviousLine = line; 174 if (builder != null) { 175 return builder.toString(); 176 } 177 } else { 178 String ret = mPreviousLine; 179 mPreviousLine = line; 180 return ret; 181 } 182 } 183 } 184 } 185 186 187 /** 188 * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF 189 * 1 * (contentline) 190 * ;A vCard object MUST include the VERSION, FN and N types. 191 * [group "."] "END" ":" "VCARD" 1 * CRLF 192 */ 193 @Override 194 protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { 195 // TODO: vCard 3.0 supports group. 196 return super.readBeginVCard(allowGarbage); 197 } 198 199 @Override 200 protected void readEndVCard(boolean useCache, boolean allowGarbage) 201 throws IOException, VCardException { 202 // TODO: vCard 3.0 supports group. 203 super.readEndVCard(useCache, allowGarbage); 204 } 205 206 /** 207 * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. 208 */ 209 @Override 210 protected void handleParams(String params) throws VCardException { 211 try { 212 super.handleParams(params); 213 } catch (VCardException e) { 214 // maybe IANA type 215 String[] strArray = params.split("=", 2); 216 if (strArray.length == 2) { 217 handleAnyParam(strArray[0], strArray[1]); 218 } else { 219 // Must not come here in the current implementation. 220 throw new VCardException( 221 "Unknown params value: " + params); 222 } 223 } 224 } 225 226 @Override 227 protected void handleAnyParam(String paramName, String paramValue) { 228 super.handleAnyParam(paramName, paramValue); 229 } 230 231 @Override 232 protected void handleParamWithoutName(final String paramValue) throws VCardException { 233 if (mStrictParsing) { 234 throw new VCardException("Parameter without name is not acceptable in vCard 3.0"); 235 } else { 236 super.handleParamWithoutName(paramValue); 237 } 238 } 239 240 /** 241 * vCard 3.0 defines 242 * 243 * param = param-name "=" param-value *("," param-value) 244 * param-name = iana-token / x-name 245 * param-value = ptext / quoted-string 246 * quoted-string = DQUOTE QSAFE-CHAR DQUOTE 247 */ 248 @Override 249 protected void handleType(String ptypevalues) { 250 String[] ptypeArray = ptypevalues.split(","); 251 mBuilder.propertyParamType("TYPE"); 252 for (String value : ptypeArray) { 253 int length = value.length(); 254 if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { 255 mBuilder.propertyParamValue(value.substring(1, value.length() - 1)); 256 } else { 257 mBuilder.propertyParamValue(value); 258 } 259 } 260 } 261 262 @Override 263 protected void handleAgent(String propertyValue) { 264 // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. 265 // 266 // e.g. 267 // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n 268 // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n 269 // ET:jfriday (at) host.com\nEND:VCARD\n 270 // 271 // TODO: fix this. 272 // 273 // issue: 274 // vCard 3.0 also allows this as an example. 275 // 276 // AGENT;VALUE=uri: 277 // CID:JQPUBLIC.part3.960129T083020.xyzMail (at) host3.com 278 // 279 // This is not vCard. Should we support this? 280 // 281 // Just ignore the line for now, since we cannot know how to handle it... 282 if (!mEmittedAgentWarning) { 283 Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); 284 mEmittedAgentWarning = true; 285 } 286 } 287 288 /** 289 * vCard 3.0 does not require two CRLF at the last of BASE64 data. 290 * It only requires that data should be MIME-encoded. 291 */ 292 @Override 293 protected String getBase64(String firstString) throws IOException, VCardException { 294 StringBuilder builder = new StringBuilder(); 295 builder.append(firstString); 296 297 while (true) { 298 String line = getLine(); 299 if (line == null) { 300 throw new VCardException( 301 "File ended during parsing BASE64 binary"); 302 } 303 if (line.length() == 0) { 304 break; 305 } else if (!line.startsWith(" ") && !line.startsWith("\t")) { 306 mPreviousLine = line; 307 break; 308 } 309 builder.append(line); 310 } 311 312 return builder.toString(); 313 } 314 315 /** 316 * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") 317 * ; \\ encodes \, \n or \N encodes newline 318 * ; \; encodes ;, \, encodes , 319 * 320 * Note: Apple escapes ':' into '\:' while does not escape '\' 321 */ 322 @Override 323 protected String maybeUnescapeText(String text) { 324 return unescapeText(text); 325 } 326 327 public static String unescapeText(String text) { 328 StringBuilder builder = new StringBuilder(); 329 int length = text.length(); 330 for (int i = 0; i < length; i++) { 331 char ch = text.charAt(i); 332 if (ch == '\\' && i < length - 1) { 333 char next_ch = text.charAt(++i); 334 if (next_ch == 'n' || next_ch == 'N') { 335 builder.append("\n"); 336 } else { 337 builder.append(next_ch); 338 } 339 } else { 340 builder.append(ch); 341 } 342 } 343 return builder.toString(); 344 } 345 346 @Override 347 protected String maybeUnescapeCharacter(char ch) { 348 return unescapeCharacter(ch); 349 } 350 351 public static String unescapeCharacter(char ch) { 352 if (ch == 'n' || ch == 'N') { 353 return "\n"; 354 } else { 355 return String.valueOf(ch); 356 } 357 } 358 } 359