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 peekLine() throws IOException { 64 if (mPreviousLine != null) { 65 String ret = mPreviousLine; 66 return ret; 67 } else { 68 return mReader.peekLine(); 69 } 70 } 71 72 @Override 73 protected String getLine() throws IOException { 74 if (mPreviousLine != null) { 75 String ret = mPreviousLine; 76 mPreviousLine = null; 77 return ret; 78 } else { 79 return mReader.readLine(); 80 } 81 } 82 83 /** 84 * vCard 3.0 requires that the line with space at the beginning of the line 85 * must be combined with previous line. 86 */ 87 @Override 88 protected String getNonEmptyLine() throws IOException, VCardException { 89 String line; 90 StringBuilder builder = null; 91 while ((line = mReader.readLine()) != null) { 92 // Skip empty lines in order to accomodate implementations that 93 // send line termination variations such as \r\r\n. 94 if (line.length() == 0) { 95 continue; 96 } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { 97 // RFC 2425 describes line continuation as \r\n followed by 98 // a single ' ' or '\t' whitespace character. 99 if (builder == null) { 100 builder = new StringBuilder(); 101 } 102 if (mPreviousLine != null) { 103 builder.append(mPreviousLine); 104 mPreviousLine = null; 105 } 106 builder.append(line.substring(1)); 107 } else { 108 if (builder != null || mPreviousLine != null) { 109 break; 110 } 111 mPreviousLine = line; 112 } 113 } 114 115 String ret = null; 116 if (builder != null) { 117 ret = builder.toString(); 118 } else if (mPreviousLine != null) { 119 ret = mPreviousLine; 120 } 121 mPreviousLine = line; 122 if (ret == null) { 123 throw new VCardException("Reached end of buffer."); 124 } 125 return ret; 126 } 127 128 /* 129 * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF 130 * 1 * (contentline) 131 * ;A vCard object MUST include the VERSION, FN and N types. 132 * [group "."] "END" ":" "VCARD" 1 * CRLF 133 */ 134 @Override 135 protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { 136 // TODO: vCard 3.0 supports group. 137 return super.readBeginVCard(allowGarbage); 138 } 139 140 /** 141 * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. 142 */ 143 @Override 144 protected void handleParams(VCardProperty propertyData, final String params) 145 throws VCardException { 146 try { 147 super.handleParams(propertyData, params); 148 } catch (VCardException e) { 149 // maybe IANA type 150 String[] strArray = params.split("=", 2); 151 if (strArray.length == 2) { 152 handleAnyParam(propertyData, strArray[0], strArray[1]); 153 } else { 154 // Must not come here in the current implementation. 155 throw new VCardException( 156 "Unknown params value: " + params); 157 } 158 } 159 } 160 161 @Override 162 protected void handleAnyParam( 163 VCardProperty propertyData, final String paramName, final String paramValue) { 164 splitAndPutParam(propertyData, paramName, paramValue); 165 } 166 167 @Override 168 protected void handleParamWithoutName(VCardProperty property, final String paramValue) { 169 handleType(property, paramValue); 170 } 171 172 /* 173 * vCard 3.0 defines 174 * 175 * param = param-name "=" param-value *("," param-value) 176 * param-name = iana-token / x-name 177 * param-value = ptext / quoted-string 178 * quoted-string = DQUOTE QSAFE-CHAR DQUOTE 179 * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII 180 * ; Any character except CTLs, DQUOTE 181 * 182 * QSAFE-CHAR must not contain DQUOTE, including escaped one (\"). 183 */ 184 @Override 185 protected void handleType(VCardProperty property, final String paramValue) { 186 splitAndPutParam(property, VCardConstants.PARAM_TYPE, paramValue); 187 } 188 189 /** 190 * Splits parameter values into pieces in accordance with vCard 3.0 specification and 191 * puts pieces into mInterpreter. 192 */ 193 /* 194 * param-value = ptext / quoted-string 195 * quoted-string = DQUOTE QSAFE-CHAR DQUOTE 196 * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII 197 * ; Any character except CTLs, DQUOTE 198 * 199 * QSAFE-CHAR must not contain DQUOTE, including escaped one (\") 200 */ 201 private void splitAndPutParam(VCardProperty property, String paramName, String paramValue) { 202 // "comma,separated:inside.dquote",pref 203 // --> 204 // - comma,separated:inside.dquote 205 // - pref 206 // 207 // Note: Though there's a code, we don't need to take much care of 208 // wrongly-added quotes like the example above, as they induce 209 // parse errors at the top level (when splitting a line into parts). 210 StringBuilder builder = null; // Delay initialization. 211 boolean insideDquote = false; 212 final int length = paramValue.length(); 213 for (int i = 0; i < length; i++) { 214 final char ch = paramValue.charAt(i); 215 if (ch == '"') { 216 if (insideDquote) { 217 // End of Dquote. 218 property.addParameter(paramName, encodeParamValue(builder.toString())); 219 builder = null; 220 insideDquote = false; 221 } else { 222 if (builder != null) { 223 if (builder.length() > 0) { 224 // e.g. 225 // pref"quoted" 226 Log.w(LOG_TAG, "Unexpected Dquote inside property."); 227 } else { 228 // e.g. 229 // pref,"quoted" 230 // "quoted",pref 231 property.addParameter(paramName, encodeParamValue(builder.toString())); 232 } 233 } 234 insideDquote = true; 235 } 236 } else if (ch == ',' && !insideDquote) { 237 if (builder == null) { 238 Log.w(LOG_TAG, "Comma is used before actual string comes. (" + 239 paramValue + ")"); 240 } else { 241 property.addParameter(paramName, encodeParamValue(builder.toString())); 242 builder = null; 243 } 244 } else { 245 // To stop creating empty StringBuffer at the end of parameter, 246 // we delay creating this object until this point. 247 if (builder == null) { 248 builder = new StringBuilder(); 249 } 250 builder.append(ch); 251 } 252 } 253 if (insideDquote) { 254 // e.g. 255 // "non-quote-at-end 256 Log.d(LOG_TAG, "Dangling Dquote."); 257 } 258 if (builder != null) { 259 if (builder.length() == 0) { 260 Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " + 261 "at the end of parameter value parsing."); 262 } else { 263 property.addParameter(paramName, encodeParamValue(builder.toString())); 264 } 265 } 266 } 267 268 /** 269 * Encode a param value using UTF-8. 270 */ 271 protected String encodeParamValue(String paramValue) { 272 return VCardUtils.convertStringCharset( 273 paramValue, VCardConfig.DEFAULT_INTERMEDIATE_CHARSET, "UTF-8"); 274 } 275 276 @Override 277 protected void handleAgent(VCardProperty property) { 278 // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. 279 // 280 // e.g. 281 // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n 282 // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n 283 // ET:jfriday (at) host.com\nEND:VCARD\n 284 // 285 // TODO: fix this. 286 // 287 // issue: 288 // vCard 3.0 also allows this as an example. 289 // 290 // AGENT;VALUE=uri: 291 // CID:JQPUBLIC.part3.960129T083020.xyzMail (at) host3.com 292 // 293 // This is not vCard. Should we support this? 294 // 295 // Just ignore the line for now, since we cannot know how to handle it... 296 if (!mEmittedAgentWarning) { 297 Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); 298 mEmittedAgentWarning = true; 299 } 300 } 301 302 /** 303 * This is only called from handlePropertyValue(), which has already 304 * read the first line of this property. With v3.0, the getNonEmptyLine() 305 * routine has already concatenated all following continuation lines. 306 * The routine is implemented in the V21 parser to concatenate v2.1 style 307 * data blocks, but is unnecessary here. 308 */ 309 @Override 310 protected String getBase64(final String firstString) 311 throws IOException, VCardException { 312 return firstString; 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(final String text) { 324 return unescapeText(text); 325 } 326 327 public static String unescapeText(final String text) { 328 StringBuilder builder = new StringBuilder(); 329 final 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 final 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(final char ch) { 348 return unescapeCharacter(ch); 349 } 350 351 public static String unescapeCharacter(final char ch) { 352 if (ch == 'n' || ch == 'N') { 353 return "\n"; 354 } else { 355 return String.valueOf(ch); 356 } 357 } 358 359 @Override 360 protected Set<String> getKnownPropertyNameSet() { 361 return VCardParser_V30.sKnownPropertyNameSet; 362 } 363 } 364