View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.exchange;
20  
21  import java.util.ArrayList;
22  import java.util.HashSet;
23  import java.util.List;
24  
25  /**
26   * VCard property
27   */
28  public class VProperty {
29      protected static enum State {
30          KEY, PARAM_NAME, PARAM_VALUE, QUOTED_PARAM_VALUE, VALUE, BACKSLASH
31      }
32  
33      protected static final HashSet<String> MULTIVALUED_PROPERTIES = new HashSet<String>();
34  
35      static {
36          MULTIVALUED_PROPERTIES.add("RESOURCES");
37          MULTIVALUED_PROPERTIES.add("LOCATION");
38      }
39  
40      protected static class Param {
41          String name;
42          List<String> values;
43  
44          public void addAll(List<String> paramValues) {
45              if (values == null) {
46                  values = new ArrayList<String>();
47              }
48              values.addAll(paramValues);
49          }
50  
51          protected String getValue() {
52              if (values != null && !values.isEmpty()) {
53                  return values.get(0);
54              } else {
55                  return null;
56              }
57          }
58      }
59  
60      protected String key;
61      protected List<Param> params;
62      protected List<String> values;
63  
64      /**
65       * Create VProperty for key and value.
66       *
67       * @param name  property name
68       * @param value property value
69       */
70      public VProperty(String name, String value) {
71          setKey(name);
72          setValue(value);
73      }
74  
75      /**
76       * Create VProperty from line.
77       *
78       * @param line card line
79       */
80      public VProperty(String line) {
81          if (line != null && !"END:VCARD".equals(line)) {
82              State state = State.KEY;
83              String paramName = null;
84              List<String> paramValues = null;
85              int startIndex = 0;
86              for (int i = 0; i < line.length(); i++) {
87                  char currentChar = line.charAt(i);
88                  if (state == State.KEY) {
89                      if (currentChar == ':') {
90                          setKey(line.substring(startIndex, i));
91                          state = State.VALUE;
92                          startIndex = i + 1;
93                      } else if (currentChar == ';') {
94                          setKey(line.substring(startIndex, i));
95                          state = State.PARAM_NAME;
96                          startIndex = i + 1;
97                      }
98                  } else if (state == State.PARAM_NAME) {
99                      if (currentChar == '=') {
100                         paramName = line.substring(startIndex, i).toUpperCase();
101                         state = State.PARAM_VALUE;
102                         paramValues = new ArrayList<String>();
103                         startIndex = i + 1;
104                     } else if (currentChar == ';') {
105                         // param with no value
106                         paramName = line.substring(startIndex, i).toUpperCase();
107                         addParam(paramName);
108                         state = State.PARAM_NAME;
109                         startIndex = i + 1;
110                     } else if (currentChar == ':') {
111                         // param with no value
112                         paramName = line.substring(startIndex, i).toUpperCase();
113                         addParam(paramName);
114                         state = State.VALUE;
115                         startIndex = i + 1;
116                     }
117                 } else if (state == State.PARAM_VALUE) {
118                     if (currentChar == '"') {
119                         state = State.QUOTED_PARAM_VALUE;
120                         startIndex = i + 1;
121                     } else if (currentChar == ':') {
122                         if (startIndex < i) {
123                             paramValues = addParamValue(paramValues, line.substring(startIndex, i));
124                         }
125                         addParam(paramName, paramValues);
126                         state = State.VALUE;
127                         startIndex = i + 1;
128                     } else if (currentChar == ';') {
129                         if (startIndex < i) {
130                             paramValues = addParamValue(paramValues, line.substring(startIndex, i));
131                         }
132                         addParam(paramName, paramValues);
133                         state = State.PARAM_NAME;
134                         startIndex = i + 1;
135                     } else if (currentChar == ',') {
136                         if (startIndex < i) {
137                             paramValues = addParamValue(paramValues, line.substring(startIndex, i));
138                         }
139                         startIndex = i + 1;
140                     }
141                 } else if (state == State.QUOTED_PARAM_VALUE) {
142                     if (currentChar == '"') {
143                         state = State.PARAM_VALUE;
144                         paramValues = addParamValue(paramValues, line.substring(startIndex, i));
145                         startIndex = i + 1;
146                     }
147                 } else if (state == State.VALUE) {
148                     if (currentChar == '\\') {
149                         state = State.BACKSLASH;
150                     } else if (currentChar == ';' || (MULTIVALUED_PROPERTIES.contains(key) && currentChar == ',')) {
151                         addValue(line.substring(startIndex, i));
152                         startIndex = i + 1;
153                     }
154                 } else if (state == State.BACKSLASH) {
155                     state = State.VALUE;
156                 }
157             }
158             if (state == State.VALUE) {
159                 addValue(line.substring(startIndex));
160             } else {
161                 throw new IllegalArgumentException("Invalid property line: " + line);
162             }
163         }
164     }
165 
166     /**
167      * Property key, without optional parameters (e.g. TEL).
168      *
169      * @return key
170      */
171     public String getKey() {
172         return key;
173     }
174 
175     /**
176      * Property value.
177      *
178      * @return value
179      */
180     public String getValue() {
181         if (values == null || values.isEmpty()) {
182             return null;
183         } else {
184             return values.get(0);
185         }
186     }
187 
188     /**
189      * Property values.
190      *
191      * @return values
192      */
193     public List<String> getValues() {
194         return values;
195     }
196 
197     /**
198      * Test if the property has a param named paramName with given value.
199      *
200      * @param paramName  param name
201      * @param paramValue param value
202      * @return true if property has param name and value
203      */
204     public boolean hasParam(String paramName, String paramValue) {
205         return params != null && getParam(paramName) != null && containsIgnoreCase(getParam(paramName).values, paramValue);
206     }
207 
208     /**
209      * Test if the property has a param named paramName.
210      *
211      * @param paramName param name
212      * @return true if property has param name
213      */
214     public boolean hasParam(String paramName) {
215         return params != null && getParam(paramName) != null;
216     }
217 
218     /**
219      * Remove param from property.
220      *
221      * @param paramName param name
222      */
223     public void removeParam(String paramName) {
224         if (params != null) {
225             Param param = getParam(paramName);
226             if (param != null) {
227                 params.remove(param);
228             }
229         }
230     }
231 
232     protected boolean containsIgnoreCase(List<String> stringCollection, String value) {
233         for (String collectionValue : stringCollection) {
234             if (value.equalsIgnoreCase(collectionValue)) {
235                 return true;
236             }
237         }
238         return false;
239     }
240 
241     /**
242      * Add value to paramValues and return list, create list if null.
243      *
244      * @param paramValues value list
245      * @param value new value
246      * @return updated value list
247      */
248     protected List<String> addParamValue(List<String> paramValues, String value) {
249         List<String> result = paramValues;
250         if (result == null) {
251             result = new ArrayList<String>();
252         }
253         result.add(value);
254         return result;
255     }
256 
257     protected void addParam(String paramName) {
258         addParam(paramName, (String) null);
259     }
260 
261     /**
262      * Set param value on property.
263      *
264      * @param paramName  param name
265      * @param paramValue param value
266      */
267     public void setParam(String paramName, String paramValue) {
268         Param currentParam = getParam(paramName);
269         if (currentParam != null) {
270             params.remove(currentParam);
271         }
272         addParam(paramName, paramValue);
273     }
274 
275     /**
276      * Add param value on property.
277      *
278      * @param paramName  param name
279      * @param paramValue param value
280      */
281     public void addParam(String paramName, String paramValue) {
282         if (paramValue != null) {
283             List<String> paramValues = new ArrayList<String>();
284             paramValues.add(paramValue);
285             addParam(paramName, paramValues);
286         }
287     }
288 
289     protected void addParam(String paramName, List<String> paramValues) {
290         if (params == null) {
291             params = new ArrayList<Param>();
292         }
293         Param currentParam = getParam(paramName);
294         if (currentParam == null) {
295             currentParam = new Param();
296             currentParam.name = paramName;
297             params.add(currentParam);
298         }
299         currentParam.addAll(paramValues);
300     }
301 
302     protected Param getParam(String paramName) {
303         if (params != null && paramName != null) {
304             for (Param param : params) {
305                 if (paramName.equals(param.name)) {
306                     return param;
307                 }
308             }
309         }
310         return null;
311     }
312 
313     /**
314      * Return param value.
315      * @param paramName param name
316      * @return value
317      */
318     public String getParamValue(String paramName) {
319         Param param = getParam(paramName);
320         if (param != null) {
321             return param.getValue();
322         } else {
323             return null;
324         }
325     }
326 
327     protected List<Param> getParams() {
328         return params;
329     }
330 
331     protected void setParams(List<Param> params) {
332         this.params = params;
333     }
334 
335     protected void setValue(String value) {
336         if (value == null) {
337             values = null;
338         } else {
339             if (values == null) {
340                 values = new ArrayList<String>();
341             } else {
342                 values.clear();
343             }
344             values.add(decodeValue(value));
345         }
346     }
347 
348     protected void addValue(String value) {
349         if (values == null) {
350             values = new ArrayList<String>();
351         }
352         values.add(decodeValue(value));
353     }
354 
355     protected String decodeValue(String value) {
356         if (value == null || (value.indexOf('\\') < 0 && value.indexOf(',') < 0)) {
357             return value;
358         } else {
359             // decode value
360             StringBuilder decodedValue = new StringBuilder();
361             for (int i = 0; i < value.length(); i++) {
362                 char c = value.charAt(i);
363                 if (c == '\\') {
364                     //noinspection AssignmentToForLoopParameter
365                     i++;
366                     if (i == value.length()) {
367                         break;
368                     }
369                     c = value.charAt(i);
370                     if (c == 'n' || c == 'N') {
371                         c = '\n';
372                     } else if (c == 'r') {
373                         c = '\r';
374                     }
375                 }
376                 // iPhone encodes category separator
377                 if (c == ',' &&
378                         // multivalued properties
379                         ("N".equals(key) ||
380                                 "CATEGORIES".equals(key) ||
381                                 "NICKNAME".equals(key)
382                         )) {
383                     // convert multiple values to multiline values (e.g. street)
384                     c = '\n';
385                 }
386                 decodedValue.append(c);
387             }
388             return decodedValue.toString();
389         }
390     }
391 
392     /**
393      * Set property key.
394      *
395      * @param key property key
396      */
397     public void setKey(String key) {
398         int dotIndex = key.indexOf('.');
399         if (dotIndex < 0) {
400             this.key = key;
401         } else {
402             this.key = key.substring(dotIndex + 1);
403         }
404     }
405 
406     public String toString() {
407         StringBuilder buffer = new StringBuilder();
408         buffer.append(key);
409         if (params != null) {
410             for (Param param : params) {
411                 buffer.append(';').append(param.name);
412                 appendParamValues(buffer, param);
413             }
414         }
415         buffer.append(':');
416         if (values != null) {
417             boolean firstValue = true;
418             for (String value : values) {
419                 if (firstValue) {
420                     firstValue = false;
421                 } else if (MULTIVALUED_PROPERTIES.contains(key)) {
422                     buffer.append(',');
423                 } else {
424                     buffer.append(';');
425                 }
426                 appendMultilineEncodedValue(buffer, value);
427             }
428         }
429         return buffer.toString();
430     }
431 
432     protected void appendParamValues(StringBuilder buffer, Param param) {
433         if (param.values != null) {
434             buffer.append('=');
435             boolean first = true;
436             for (String value : param.values) {
437                 if (first) {
438                     first = false;
439                 } else {
440                     buffer.append(',');
441                 }
442                 // always quote CN param
443                 if ("CN".equalsIgnoreCase(param.name)
444                         // quote param values with special characters
445                         || value.indexOf(';') >= 0 || value.indexOf(',') >= 0
446                         || value.indexOf('(') >= 0 || value.indexOf('/') >= 0
447                         || value.indexOf(':') >= 0) {
448                     buffer.append('"').append(value).append('"');
449                 } else {
450                     buffer.append(value);
451                 }
452             }
453         }
454     }
455 
456     /**
457      * Append and encode \n to \\n in value.
458      *
459      * @param buffer line buffer
460      * @param value  value
461      */
462     protected void appendMultilineEncodedValue(StringBuilder buffer, String value) {
463         for (int i = 0; i < value.length(); i++) {
464             char c = value.charAt(i);
465             if (c == '\n') {
466                 buffer.append("\\n");
467             } else if (MULTIVALUED_PROPERTIES.contains(key) && c == ',') {
468                 buffer.append('\\').append(',');
469             } else {
470                 buffer.append(value.charAt(i));
471             }
472         }
473     }
474 
475 }
476