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.*;
22  
23  /**
24   * VCard property
25   */
26  public class VProperty {
27  
28      protected enum State {
29          KEY, PARAM_NAME, PARAM_VALUE, QUOTED_PARAM_VALUE, QUOTED_PARAM_VALUE_BACKSLASH, VALUE, BACKSLASH
30      }
31  
32      protected static final HashSet<String> MULTIVALUED_PROPERTIES = new HashSet<>();
33  
34      static {
35          MULTIVALUED_PROPERTIES.add("RESOURCES");
36          MULTIVALUED_PROPERTIES.add("LOCATION");
37      }
38  
39      protected static class Param {
40          String name;
41          List<String> values;
42  
43          public void addAll(List<String> paramValues) {
44              if (values == null) {
45                  values = new ArrayList<>();
46              }
47              values.addAll(paramValues);
48          }
49  
50          protected String getValue() {
51              if (values != null && !values.isEmpty()) {
52                  return values.get(0);
53              } else {
54                  return null;
55              }
56          }
57      }
58  
59      protected String key;
60      protected List<Param> params;
61      protected List<String> values;
62  
63      /**
64       * Create VProperty for key and value.
65       *
66       * @param name  property name
67       * @param value property value
68       */
69      public VProperty(String name, String value) {
70          setKey(name);
71          setValue(value);
72      }
73  
74      /**
75       * Create VProperty from line.
76       *
77       * @param line card line
78       */
79      public VProperty(String line) {
80          if (line != null && !"END:VCARD".equals(line)) {
81              State state = State.KEY;
82              String paramName = null;
83              List<String> paramValues = null;
84              int startIndex = 0;
85              for (int i = 0; i < line.length(); i++) {
86                  char currentChar = line.charAt(i);
87                  if (state == State.KEY) {
88                      if (currentChar == ':') {
89                          setKey(line.substring(startIndex, i));
90                          state = State.VALUE;
91                          startIndex = i + 1;
92                      } else if (currentChar == ';') {
93                          setKey(line.substring(startIndex, i));
94                          state = State.PARAM_NAME;
95                          startIndex = i + 1;
96                      }
97                  } else if (state == State.PARAM_NAME) {
98                      if (currentChar == '=') {
99                          paramName = line.substring(startIndex, i).toUpperCase();
100                         state = State.PARAM_VALUE;
101                         paramValues = new ArrayList<>();
102                         startIndex = i + 1;
103                     } else if (currentChar == ';') {
104                         // param with no value
105                         paramName = line.substring(startIndex, i).toUpperCase();
106                         addParam(paramName);
107                         state = State.PARAM_NAME;
108                         startIndex = i + 1;
109                     } else if (currentChar == ':') {
110                         // param with no value
111                         paramName = line.substring(startIndex, i).toUpperCase();
112                         addParam(paramName);
113                         state = State.VALUE;
114                         startIndex = i + 1;
115                     }
116                 } else if (state == State.PARAM_VALUE) {
117                     if (currentChar == '"') {
118                         state = State.QUOTED_PARAM_VALUE;
119                         startIndex = i + 1;
120                     } else if (currentChar == ':') {
121                         if (startIndex < i) {
122                             paramValues = addParamValue(paramValues, line.substring(startIndex, i));
123                         }
124                         addParam(paramName, paramValues);
125                         state = State.VALUE;
126                         startIndex = i + 1;
127                     } else if (currentChar == ';') {
128                         if (startIndex < i) {
129                             paramValues = addParamValue(paramValues, line.substring(startIndex, i));
130                         }
131                         addParam(paramName, paramValues);
132                         state = State.PARAM_NAME;
133                         startIndex = i + 1;
134                     } else if (currentChar == ',') {
135                         if (startIndex < i) {
136                             paramValues = addParamValue(paramValues, line.substring(startIndex, i));
137                         }
138                         startIndex = i + 1;
139                     }
140                 } else if (state == State.QUOTED_PARAM_VALUE) {
141                     if (currentChar == '\\') {
142                         state = State.QUOTED_PARAM_VALUE_BACKSLASH;
143                     } else if (currentChar == '"') {
144                         state = State.PARAM_VALUE;
145                         paramValues = addParamValue(paramValues, line.substring(startIndex, i));
146                         startIndex = i + 1;
147                     }
148                 } else if (state == State.QUOTED_PARAM_VALUE_BACKSLASH){
149                     state = State.QUOTED_PARAM_VALUE;
150                 } else if (state == State.VALUE) {
151                     if (currentChar == '\\') {
152                         state = State.BACKSLASH;
153                     } else if (currentChar == ';' || (MULTIVALUED_PROPERTIES.contains(key) && currentChar == ',')) {
154                         addValue(line.substring(startIndex, i));
155                         startIndex = i + 1;
156                     }
157                 } else if (state == State.BACKSLASH) {
158                     state = State.VALUE;
159                 }
160             }
161             if (state == State.VALUE) {
162                 addValue(line.substring(startIndex));
163             } else {
164                 throw new IllegalArgumentException("Invalid property line: " + line);
165             }
166         }
167     }
168 
169     /**
170      * Property key, without optional parameters (e.g. TEL).
171      *
172      * @return key
173      */
174     public String getKey() {
175         return key;
176     }
177 
178     /**
179      * Property value.
180      *
181      * @return value
182      */
183     public String getValue() {
184         if (values == null || values.isEmpty()) {
185             return null;
186         } else {
187             return values.get(0);
188         }
189     }
190 
191     /**
192      * Property values.
193      *
194      * @return values
195      */
196     public List<String> getValues() {
197         return values;
198     }
199 
200     /**
201      * Return property values as a map.
202      * Typical use for RRULE content
203      * @return values as map
204      */
205     public Map<String, String> getValuesAsMap() {
206         HashMap<String, String> valuesMap = new HashMap<>();
207 
208         if (values != null) {
209             for (String value:values) {
210                 if (value.contains("=")) {
211                     int index = value.indexOf("=");
212                     valuesMap.put(value.substring(0, index), value.substring(index+1));
213                 }
214             }
215         }
216         return valuesMap;
217     }
218 
219 
220     /**
221      * Test if the property has a param named paramName with given value.
222      *
223      * @param paramName  param name
224      * @param paramValue param value
225      * @return true if property has param name and value
226      */
227     public boolean hasParam(String paramName, String paramValue) {
228         return params != null && getParam(paramName) != null && containsIgnoreCase(getParam(paramName).values, paramValue);
229     }
230 
231     /**
232      * Test if the property has a param named paramName.
233      *
234      * @param paramName param name
235      * @return true if property has param name
236      */
237     public boolean hasParam(String paramName) {
238         return params != null && getParam(paramName) != null;
239     }
240 
241     /**
242      * Remove param from property.
243      *
244      * @param paramName param name
245      */
246     public void removeParam(String paramName) {
247         if (params != null) {
248             Param param = getParam(paramName);
249             if (param != null) {
250                 params.remove(param);
251             }
252         }
253     }
254 
255     protected boolean containsIgnoreCase(List<String> stringCollection, String value) {
256         for (String collectionValue : stringCollection) {
257             if (value.equalsIgnoreCase(collectionValue)) {
258                 return true;
259             }
260         }
261         return false;
262     }
263 
264     /**
265      * Add value to paramValues and return list, create list if null.
266      *
267      * @param paramValues value list
268      * @param value new value
269      * @return updated value list
270      */
271     protected List<String> addParamValue(List<String> paramValues, String value) {
272         List<String> result = paramValues;
273         if (result == null) {
274             result = new ArrayList<>();
275         }
276         result.add(value);
277         return result;
278     }
279 
280     protected void addParam(String paramName) {
281         addParam(paramName, (String) null);
282     }
283 
284     /**
285      * Set param value on property.
286      *
287      * @param paramName  param name
288      * @param paramValue param value
289      */
290     public void setParam(String paramName, String paramValue) {
291         Param currentParam = getParam(paramName);
292         if (currentParam != null) {
293             params.remove(currentParam);
294         }
295         addParam(paramName, paramValue);
296     }
297 
298     /**
299      * Add param value on property.
300      *
301      * @param paramName  param name
302      * @param paramValue param value
303      */
304     public void addParam(String paramName, String paramValue) {
305         if (paramValue != null) {
306             List<String> paramValues = new ArrayList<>();
307             paramValues.add(paramValue);
308             addParam(paramName, paramValues);
309         }
310     }
311 
312     protected void addParam(String paramName, List<String> paramValues) {
313         if (params == null) {
314             params = new ArrayList<>();
315         }
316         Param currentParam = getParam(paramName);
317         if (currentParam == null) {
318             currentParam = new Param();
319             currentParam.name = paramName;
320             params.add(currentParam);
321         }
322         currentParam.addAll(paramValues);
323     }
324 
325     protected Param getParam(String paramName) {
326         if (params != null && paramName != null) {
327             for (Param param : params) {
328                 if (paramName.equals(param.name)) {
329                     return param;
330                 }
331             }
332         }
333         return null;
334     }
335 
336     /**
337      * Return param value.
338      * @param paramName param name
339      * @return value
340      */
341     public String getParamValue(String paramName) {
342         Param param = getParam(paramName);
343         if (param != null) {
344             return param.getValue();
345         } else {
346             return null;
347         }
348     }
349 
350     protected List<Param> getParams() {
351         return params;
352     }
353 
354     protected void setParams(List<Param> params) {
355         this.params = params;
356     }
357 
358     protected void setValue(String value) {
359         if (value == null) {
360             values = null;
361         } else {
362             if (values == null) {
363                 values = new ArrayList<>();
364             } else {
365                 values.clear();
366             }
367             values.add(decodeValue(value));
368         }
369     }
370 
371     protected void addValue(String value) {
372         if (values == null) {
373             values = new ArrayList<>();
374         }
375         values.add(decodeValue(value));
376     }
377 
378     public void removeValue(String value) {
379         if (values != null) {
380             int index = -1;
381             for (int i=0;i<values.size();i++) {
382                 if (value.equals(values.get(i))) {
383                     index = i;
384                 }
385             }
386             if (index >= 0) {
387                 values.remove(index);
388             }
389         }
390     }
391 
392 
393     protected String decodeValue(String value) {
394         if (value == null || (value.indexOf('\\') < 0 && value.indexOf(',') < 0)) {
395             return value;
396         } else {
397             // decode value
398             StringBuilder decodedValue = new StringBuilder();
399             for (int i = 0; i < value.length(); i++) {
400                 char c = value.charAt(i);
401                 if (c == '\\') {
402                     //noinspection AssignmentToForLoopParameter
403                     i++;
404                     if (i == value.length()) {
405                         break;
406                     }
407                     c = value.charAt(i);
408                     if (c == 'n' || c == 'N') {
409                         c = '\n';
410                     } else if (c == 'r') {
411                         c = '\r';
412                     }
413                 }
414                 // iPhone encodes category separator
415                 if (c == ',' &&
416                         // multivalued properties
417                         ("N".equals(key) ||
418                                 //"CATEGORIES".equals(key) ||
419                                 "NICKNAME".equals(key)
420                         )) {
421                     // convert multiple values to multiline values (e.g. street)
422                     c = '\n';
423                 }
424                 decodedValue.append(c);
425             }
426             return decodedValue.toString();
427         }
428     }
429 
430     /**
431      * Set property key.
432      *
433      * @param key property key
434      */
435     public void setKey(String key) {
436         int dotIndex = key.indexOf('.');
437         if (dotIndex < 0) {
438             this.key = key;
439         } else {
440             this.key = key.substring(dotIndex + 1);
441         }
442     }
443 
444     public String toString() {
445         StringBuilder buffer = new StringBuilder();
446         buffer.append(key);
447         if (params != null) {
448             for (Param param : params) {
449                 buffer.append(';').append(param.name);
450                 appendParamValues(buffer, param);
451             }
452         }
453         buffer.append(':');
454         if (values != null) {
455             boolean firstValue = true;
456             for (String value : values) {
457                 if (firstValue) {
458                     firstValue = false;
459                 } else if (MULTIVALUED_PROPERTIES.contains(key)) {
460                     buffer.append(',');
461                 } else {
462                     buffer.append(';');
463                 }
464                 appendMultilineEncodedValue(buffer, value);
465             }
466         }
467         return buffer.toString();
468     }
469 
470     protected void appendParamValues(StringBuilder buffer, Param param) {
471         if (param.values != null) {
472             buffer.append('=');
473             boolean first = true;
474             for (String value : param.values) {
475                 if (first) {
476                     first = false;
477                 } else {
478                     buffer.append(',');
479                 }
480                 // always quote CN param
481                 if ("CN".equalsIgnoreCase(param.name)
482                         // quote param values with special characters
483                         || value.indexOf(';') >= 0 || value.indexOf(',') >= 0
484                         || value.indexOf('(') >= 0 || value.indexOf('/') >= 0
485                         || value.indexOf(':') >= 0) {
486                     buffer.append('"').append(value).append('"');
487                 } else {
488                     buffer.append(value);
489                 }
490             }
491         }
492     }
493 
494     /**
495      * Append and encode \n to \\n in value.
496      *
497      * @param buffer line buffer
498      * @param value  value
499      */
500     protected void appendMultilineEncodedValue(StringBuilder buffer, String value) {
501         for (int i = 0; i < value.length(); i++) {
502             char c = value.charAt(i);
503             if (c == '\n') {
504                 buffer.append("\\n");
505             } else if (MULTIVALUED_PROPERTIES.contains(key) && c == ',') {
506                 buffer.append('\\').append(',');
507             // skip carriage return
508             } else if (c != '\r') {
509                 buffer.append(value.charAt(i));
510             }
511         }
512     }
513 
514 }
515