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