1 /* 2 * Copyright (C) 2010 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.Set; 23 24 /** 25 * <p> 26 * Basic implementation achieving vCard 3.0 parsing. 27 * </p> 28 * <p> 29 * This class inherits vCard 2.1 implementation since technically they are similar, 30 * while specifically there's logical no relevance between them. 31 * So that developers are not confused with the inheritance, 32 * {@link VCardParser_V30} does not inherit {@link VCardParser_V21}, while 33 * {@link VCardParserImpl_V30} inherits {@link VCardParserImpl_V21}. 34 * </p> 35 * @hide 36 */ 37 /* package */ class VCardParserImpl_V30 extends VCardParserImpl_V21 { 38 private static final String LOG_TAG = "VCardParserImpl_V30"; 39 40 private String mPreviousLine; 41 private boolean mEmittedAgentWarning = false; 42 43 public VCardParserImpl_V30() { 44 super(); 45 } 46 47 public VCardParserImpl_V30(int vcardType) { 48 super(vcardType); 49 } 50 51 @Override 52 protected int getVersion() { 53 return VCardConfig.VERSION_30; 54 } 55 56 @Override 57 protected String getVersionString() { 58 return VCardConstants.VERSION_V30; 59 } 60 61 @Override 62 protected String getLine() throws IOException { 63 if (mPreviousLine != null) { 64 String ret = mPreviousLine; 65 mPreviousLine = null; 66 return ret; 67 } else { 68 return mReader.readLine(); 69 } 70 } 71 72 /** 73 * vCard 3.0 requires that the line with space at the beginning of the line 74 * must be combined with previous line. 75 */ 76 @Override 77 protected String getNonEmptyLine() throws IOException, VCardException { 78 String line; 79 StringBuilder builder = null; 80 while (true) { 81 line = mReader.readLine(); 82 if (line == null) { 83 if (builder != null) { 84 return builder.toString(); 85 } else if (mPreviousLine != null) { 86 String ret = mPreviousLine; 87 mPreviousLine = null; 88 return ret; 89 } 90 throw new VCardException("Reached end of buffer."); 91 } else if (line.length() == 0) { 92 if (builder != null) { 93 return builder.toString(); 94 } else if (mPreviousLine != null) { 95 String ret = mPreviousLine; 96 mPreviousLine = null; 97 return ret; 98 } 99 } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { 100 if (builder != null) { 101 // See Section 5.8.1 of RFC 2425 (MIME-DIR document). 102 // Following is the excerpts from it. 103 // 104 // DESCRIPTION:This is a long description that exists on a long line. 105 // 106 // Can be represented as: 107 // 108 // DESCRIPTION:This is a long description 109 // that exists on a long line. 110 // 111 // It could also be represented as: 112 // 113 // DESCRIPTION:This is a long descrip 114 // tion that exists o 115 // n a long line. 116 builder.append(line.substring(1)); 117 } else if (mPreviousLine != null) { 118 builder = new StringBuilder(); 119 builder.append(mPreviousLine); 120 mPreviousLine = null; 121 builder.append(line.substring(1)); 122 } else { 123 throw new VCardException("Space exists at the beginning of the line"); 124 } 125 } else { 126 if (mPreviousLine == null) { 127 mPreviousLine = line; 128 if (builder != null) { 129 return builder.toString(); 130 } 131 } else { 132 String ret = mPreviousLine; 133 mPreviousLine = line; 134 return ret; 135 } 136 } 137 } 138 } 139 140 /* 141 * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF 142 * 1 * (contentline) 143 * ;A vCard object MUST include the VERSION, FN and N types. 144 * [group "."] "END" ":" "VCARD" 1 * CRLF 145 */ 146 @Override 147 protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { 148 // TODO: vCard 3.0 supports group. 149 return super.readBeginVCard(allowGarbage); 150 } 151 152 @Override 153 protected void readEndVCard(boolean useCache, boolean allowGarbage) 154 throws IOException, VCardException { 155 // TODO: vCard 3.0 supports group. 156 super.readEndVCard(useCache, allowGarbage); 157 } 158 159 /** 160 * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. 161 */ 162 @Override 163 protected void handleParams(final String params) throws VCardException { 164 try { 165 super.handleParams(params); 166 } catch (VCardException e) { 167 // maybe IANA type 168 String[] strArray = params.split("=", 2); 169 if (strArray.length == 2) { 170 handleAnyParam(strArray[0], strArray[1]); 171 } else { 172 // Must not come here in the current implementation. 173 throw new VCardException( 174 "Unknown params value: " + params); 175 } 176 } 177 } 178 179 @Override 180 protected void handleAnyParam(final String paramName, final String paramValue) { 181 mInterpreter.propertyParamType(paramName); 182 splitAndPutParamValue(paramValue); 183 } 184 185 @Override 186 protected void handleParamWithoutName(final String paramValue) { 187 handleType(paramValue); 188 } 189 190 /* 191 * vCard 3.0 defines 192 * 193 * param = param-name "=" param-value *("," param-value) 194 * param-name = iana-token / x-name 195 * param-value = ptext / quoted-string 196 * quoted-string = DQUOTE QSAFE-CHAR DQUOTE 197 * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII 198 * ; Any character except CTLs, DQUOTE 199 * 200 * QSAFE-CHAR must not contain DQUOTE, including escaped one (\"). 201 */ 202 @Override 203 protected void handleType(final String paramValue) { 204 mInterpreter.propertyParamType("TYPE"); 205 splitAndPutParamValue(paramValue); 206 } 207 208 /** 209 * Splits parameter values into pieces in accordance with vCard 3.0 specification and 210 * puts pieces into mInterpreter. 211 */ 212 /* 213 * param-value = ptext / quoted-string 214 * quoted-string = DQUOTE QSAFE-CHAR DQUOTE 215 * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII 216 * ; Any character except CTLs, DQUOTE 217 * 218 * QSAFE-CHAR must not contain DQUOTE, including escaped one (\") 219 */ 220 private void splitAndPutParamValue(String paramValue) { 221 // "comma,separated:inside.dquote",pref 222 // --> 223 // - comma,separated:inside.dquote 224 // - pref 225 // 226 // Note: Though there's a code, we don't need to take much care of 227 // wrongly-added quotes like the example above, as they induce 228 // parse errors at the top level (when splitting a line into parts). 229 StringBuilder builder = null; // Delay initialization. 230 boolean insideDquote = false; 231 final int length = paramValue.length(); 232 for (int i = 0; i < length; i++) { 233 final char ch = paramValue.charAt(i); 234 if (ch == '"') { 235 if (insideDquote) { 236 // End of Dquote. 237 mInterpreter.propertyParamValue(builder.toString()); 238 builder = null; 239 insideDquote = false; 240 } else { 241 if (builder != null) { 242 if (builder.length() > 0) { 243 // e.g. 244 // pref"quoted" 245 Log.w(LOG_TAG, "Unexpected Dquote inside property."); 246 } else { 247 // e.g. 248 // pref,"quoted" 249 // "quoted",pref 250 mInterpreter.propertyParamValue(builder.toString()); 251 } 252 } 253 insideDquote = true; 254 } 255 } else if (ch == ',' && !insideDquote) { 256 if (builder == null) { 257 Log.w(LOG_TAG, "Comma is used before actual string comes. (" + 258 paramValue + ")"); 259 } else { 260 mInterpreter.propertyParamValue(builder.toString()); 261 builder = null; 262 } 263 } else { 264 // To stop creating empty StringBuffer at the end of parameter, 265 // we delay creating this object until this point. 266 if (builder == null) { 267 builder = new StringBuilder(); 268 } 269 builder.append(ch); 270 } 271 } 272 if (insideDquote) { 273 // e.g. 274 // "non-quote-at-end 275 Log.d(LOG_TAG, "Dangling Dquote."); 276 } 277 if (builder != null) { 278 if (builder.length() == 0) { 279 Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " + 280 "at the end of parameter value parsing."); 281 } else { 282 mInterpreter.propertyParamValue(builder.toString()); 283 } 284 } 285 } 286 287 @Override 288 protected void handleAgent(final String propertyValue) { 289 // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. 290 // 291 // e.g. 292 // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n 293 // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n 294 // ET:jfriday (at) host.com\nEND:VCARD\n 295 // 296 // TODO: fix this. 297 // 298 // issue: 299 // vCard 3.0 also allows this as an example. 300 // 301 // AGENT;VALUE=uri: 302 // CID:JQPUBLIC.part3.960129T083020.xyzMail (at) host3.com 303 // 304 // This is not vCard. Should we support this? 305 // 306 // Just ignore the line for now, since we cannot know how to handle it... 307 if (!mEmittedAgentWarning) { 308 Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); 309 mEmittedAgentWarning = true; 310 } 311 } 312 313 /** 314 * vCard 3.0 does not require two CRLF at the last of BASE64 data. 315 * It only requires that data should be MIME-encoded. 316 */ 317 @Override 318 protected String getBase64(final String firstString) 319 throws IOException, VCardException { 320 final StringBuilder builder = new StringBuilder(); 321 builder.append(firstString); 322 323 while (true) { 324 final String line = getLine(); 325 if (line == null) { 326 throw new VCardException("File ended during parsing BASE64 binary"); 327 } 328 if (line.length() == 0) { 329 break; 330 } else if (!line.startsWith(" ") && !line.startsWith("\t")) { 331 mPreviousLine = line; 332 break; 333 } 334 builder.append(line); 335 } 336 337 return builder.toString(); 338 } 339 340 /** 341 * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") 342 * ; \\ encodes \, \n or \N encodes newline 343 * ; \; encodes ;, \, encodes , 344 * 345 * Note: Apple escapes ':' into '\:' while does not escape '\' 346 */ 347 @Override 348 protected String maybeUnescapeText(final String text) { 349 return unescapeText(text); 350 } 351 352 public static String unescapeText(final String text) { 353 StringBuilder builder = new StringBuilder(); 354 final int length = text.length(); 355 for (int i = 0; i < length; i++) { 356 char ch = text.charAt(i); 357 if (ch == '\\' && i < length - 1) { 358 final char next_ch = text.charAt(++i); 359 if (next_ch == 'n' || next_ch == 'N') { 360 builder.append("\n"); 361 } else { 362 builder.append(next_ch); 363 } 364 } else { 365 builder.append(ch); 366 } 367 } 368 return builder.toString(); 369 } 370 371 @Override 372 protected String maybeUnescapeCharacter(final char ch) { 373 return unescapeCharacter(ch); 374 } 375 376 public static String unescapeCharacter(final char ch) { 377 if (ch == 'n' || ch == 'N') { 378 return "\n"; 379 } else { 380 return String.valueOf(ch); 381 } 382 } 383 384 @Override 385 protected Set<String> getKnownPropertyNameSet() { 386 return VCardParser_V30.sKnownPropertyNameSet; 387 } 388 } 389