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, 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.PARAM_VALUE;
143                         paramValues = addParamValue(paramValues, line.substring(startIndex, i));
144                         startIndex = i + 1;
145                     }
146                 } else if (state == State.VALUE) {
147                     if (currentChar == '\\') {
148                         state = State.BACKSLASH;
149                     } else if (currentChar == ';' || (MULTIVALUED_PROPERTIES.contains(key) && currentChar == ',')) {
150                         addValue(line.substring(startIndex, i));
151                         startIndex = i + 1;
152                     }
153                 } else if (state == State.BACKSLASH) {
154                     state = State.VALUE;
155                 }
156             }
157             if (state == State.VALUE) {
158                 addValue(line.substring(startIndex));
159             } else {
160                 throw new IllegalArgumentException("Invalid property line: " + line);
161             }
162         }
163     }
164 
165     /**
166      * Property key, without optional parameters (e.g. TEL).
167      *
168      * @return key
169      */
170     public String getKey() {
171         return key;
172     }
173 
174     /**
175      * Property value.
176      *
177      * @return value
178      */
179     public String getValue() {
180         if (values == null || values.isEmpty()) {
181             return null;
182         } else {
183             return values.get(0);
184         }
185     }
186 
187     /**
188      * Property values.
189      *
190      * @return values
191      */
192     public List<String> getValues() {
193         return values;
194     }
195 
196     /**
197      * Return property values as a map.
198      * Typical use for RRULE content
199      * @return values as map
200      */
201     public Map<String, String> getValuesAsMap() {
202         HashMap<String, String> valuesMap = new HashMap<>();
203 
204         if (values != null) {
205             for (String value:values) {
206                 if (value.contains("=")) {
207                     int index = value.indexOf("=");
208                     valuesMap.put(value.substring(0, index), value.substring(index+1));
209                 }
210             }
211         }
212         return valuesMap;
213     }
214 
215 
216     /**
217      * Test if the property has a param named paramName with given value.
218      *
219      * @param paramName  param name
220      * @param paramValue param value
221      * @return true if property has param name and value
222      */
223     public boolean hasParam(String paramName, String paramValue) {
224         return params != null && getParam(paramName) != null && containsIgnoreCase(getParam(paramName).values, paramValue);
225     }
226 
227     /**
228      * Test if the property has a param named paramName.
229      *
230      * @param paramName param name
231      * @return true if property has param name
232      */
233     public boolean hasParam(String paramName) {
234         return params != null && getParam(paramName) != null;
235     }
236 
237     /**
238      * Remove param from property.
239      *
240      * @param paramName param name
241      */
242     public void removeParam(String paramName) {
243         if (params != null) {
244             Param param = getParam(paramName);
245             if (param != null) {
246                 params.remove(param);
247             }
248         }
249     }
250 
251     protected boolean containsIgnoreCase(List<String> stringCollection, String value) {
252         for (String collectionValue : stringCollection) {
253             if (value.equalsIgnoreCase(collectionValue)) {
254                 return true;
255             }
256         }
257         return false;
258     }
259 
260     /**
261      * Add value to paramValues and return list, create list if null.
262      *
263      * @param paramValues value list
264      * @param value new value
265      * @return updated value list
266      */
267     protected List<String> addParamValue(List<String> paramValues, String value) {
268         List<String> result = paramValues;
269         if (result == null) {
270             result = new ArrayList<>();
271         }
272         result.add(value);
273         return result;
274     }
275 
276     protected void addParam(String paramName) {
277         addParam(paramName, (String) null);
278     }
279 
280     /**
281      * Set param value on property.
282      *
283      * @param paramName  param name
284      * @param paramValue param value
285      */
286     public void setParam(String paramName, String paramValue) {
287         Param currentParam = getParam(paramName);
288         if (currentParam != null) {
289             params.remove(currentParam);
290         }
291         addParam(paramName, paramValue);
292     }
293 
294     /**
295      * Add param value on property.
296      *
297      * @param paramName  param name
298      * @param paramValue param value
299      */
300     public void addParam(String paramName, String paramValue) {
301         if (paramValue != null) {
302             List<String> paramValues = new ArrayList<>();
303             paramValues.add(paramValue);
304             addParam(paramName, paramValues);
305         }
306     }
307 
308     protected void addParam(String paramName, List<String> paramValues) {
309         if (params == null) {
310             params = new ArrayList<>();
311         }
312         Param currentParam = getParam(paramName);
313         if (currentParam == null) {
314             currentParam = new Param();
315             currentParam.name = paramName;
316             params.add(currentParam);
317         }
318         currentParam.addAll(paramValues);
319     }
320 
321     protected Param getParam(String paramName) {
322         if (params != null && paramName != null) {
323             for (Param param : params) {
324                 if (paramName.equals(param.name)) {
325                     return param;
326                 }
327             }
328         }
329         return null;
330     }
331 
332     /**
333      * Return param value.
334      * @param paramName param name
335      * @return value
336      */
337     public String getParamValue(String paramName) {
338         Param param = getParam(paramName);
339         if (param != null) {
340             return param.getValue();
341         } else {
342             return null;
343         }
344     }
345 
346     protected List<Param> getParams() {
347         return params;
348     }
349 
350     protected void setParams(List<Param> params) {
351         this.params = params;
352     }
353 
354     protected void setValue(String value) {
355         if (value == null) {
356             values = null;
357         } else {
358             if (values == null) {
359                 values = new ArrayList<>();
360             } else {
361                 values.clear();
362             }
363             values.add(decodeValue(value));
364         }
365     }
366 
367     protected void addValue(String value) {
368         if (values == null) {
369             values = new ArrayList<>();
370         }
371         values.add(decodeValue(value));
372     }
373 
374     public void removeValue(String value) {
375         if (values != null) {
376             int index = -1;
377             for (int i=0;i<values.size();i++) {
378                 if (value.equals(values.get(i))) {
379                     index = i;
380                 }
381             }
382             if (index >= 0) {
383                 values.remove(index);
384             }
385         }
386     }
387 
388 
389     protected String decodeValue(String value) {
390         if (value == null || (value.indexOf('\\') < 0 && value.indexOf(',') < 0)) {
391             return value;
392         } else {
393             // decode value
394             StringBuilder decodedValue = new StringBuilder();
395             for (int i = 0; i < value.length(); i++) {
396                 char c = value.charAt(i);
397                 if (c == '\\') {
398                     //noinspection AssignmentToForLoopParameter
399                     i++;
400                     if (i == value.length()) {
401                         break;
402                     }
403                     c = value.charAt(i);
404                     if (c == 'n' || c == 'N') {
405                         c = '\n';
406                     } else if (c == 'r') {
407                         c = '\r';
408                     }
409                 }
410                 // iPhone encodes category separator
411                 if (c == ',' &&
412                         // multivalued properties
413                         ("N".equals(key) ||
414                                 //"CATEGORIES".equals(key) ||
415                                 "NICKNAME".equals(key)
416                         )) {
417                     // convert multiple values to multiline values (e.g. street)
418                     c = '\n';
419                 }
420                 decodedValue.append(c);
421             }
422             return decodedValue.toString();
423         }
424     }
425 
426     /**
427      * Set property key.
428      *
429      * @param key property key
430      */
431     public void setKey(String key) {
432         int dotIndex = key.indexOf('.');
433         if (dotIndex < 0) {
434             this.key = key;
435         } else {
436             this.key = key.substring(dotIndex + 1);
437         }
438     }
439 
440     public String toString() {
441         StringBuilder buffer = new StringBuilder();
442         buffer.append(key);
443         if (params != null) {
444             for (Param param : params) {
445                 buffer.append(';').append(param.name);
446                 appendParamValues(buffer, param);
447             }
448         }
449         buffer.append(':');
450         if (values != null) {
451             boolean firstValue = true;
452             for (String value : values) {
453                 if (firstValue) {
454                     firstValue = false;
455                 } else if (MULTIVALUED_PROPERTIES.contains(key)) {
456                     buffer.append(',');
457                 } else {
458                     buffer.append(';');
459                 }
460                 appendMultilineEncodedValue(buffer, value);
461             }
462         }
463         return buffer.toString();
464     }
465 
466     protected void appendParamValues(StringBuilder buffer, Param param) {
467         if (param.values != null) {
468             buffer.append('=');
469             boolean first = true;
470             for (String value : param.values) {
471                 if (first) {
472                     first = false;
473                 } else {
474                     buffer.append(',');
475                 }
476                 // always quote CN param
477                 if ("CN".equalsIgnoreCase(param.name)
478                         // quote param values with special characters
479                         || value.indexOf(';') >= 0 || value.indexOf(',') >= 0
480                         || value.indexOf('(') >= 0 || value.indexOf('/') >= 0
481                         || value.indexOf(':') >= 0) {
482                     buffer.append('"').append(value).append('"');
483                 } else {
484                     buffer.append(value);
485                 }
486             }
487         }
488     }
489 
490     /**
491      * Append and encode \n to \\n in value.
492      *
493      * @param buffer line buffer
494      * @param value  value
495      */
496     protected void appendMultilineEncodedValue(StringBuilder buffer, String value) {
497         for (int i = 0; i < value.length(); i++) {
498             char c = value.charAt(i);
499             if (c == '\n') {
500                 buffer.append("\\n");
501             } else if (MULTIVALUED_PROPERTIES.contains(key) && c == ',') {
502                 buffer.append('\\').append(',');
503             // skip carriage return
504             } else if (c != '\r') {
505                 buffer.append(value.charAt(i));
506             }
507         }
508     }
509 
510 }
511