jDoc, possible leak on crash, new depth option
[jvcard.git] / src / be / nikiroo / jvcard / Contact.java
1 package be.nikiroo.jvcard;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.HashMap;
7 import java.util.LinkedList;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.UUID;
11
12 import be.nikiroo.jvcard.parsers.Format;
13 import be.nikiroo.jvcard.parsers.Parser;
14 import be.nikiroo.utils.StringUtils;
15
16 /**
17 * A contact is the information that represent a contact person or organisation.
18 * <p>
19 * Each {@link Data} inside can be binary encoded or not: if it is binary
20 * encoded, it has an active BKey number (not -1) associated to it (of value 0
21 * if still not sorted, or unique for the whole {@link Contact} if already
22 * processed).
23 *
24 * @author niki
25 */
26 public class Contact extends BaseClass<Data> {
27 private int nextBKey = 1;
28 private Map<Integer, Data> binaries;
29
30 /**
31 * Create a new Contact from the given information. Note that the BKeys data
32 * will be reset.
33 *
34 * @param content
35 * the information about the contact
36 */
37 public Contact(List<Data> content) {
38 super(load(content));
39 updateBKeys(true);
40 }
41
42 /**
43 * Return the preferred Data field with the given name, the first one if
44 * none is preferred, or NULL if none at all.
45 *
46 * @param name
47 * the name to look for
48 *
49 * @return the {@link Data} field, or NULL
50 */
51 public Data getPreferredData(String name) {
52 Data pref = null;
53 int ipref = Integer.MAX_VALUE;
54 for (Data data : getData(name)) {
55 if (pref == null)
56 pref = data;
57
58 if (data.getPreferred() < ipref)
59 pref = data;
60 }
61
62 return pref;
63 }
64
65 /**
66 * Return the value of the preferred data field with this name, or NULL if
67 * none (you cannot differentiate a NULL value and no value with this method
68 * -- for that, check {@link Contact#getPreferredData(String)}).
69 *
70 * @param name
71 * the name to look for
72 * @return the value (which can be NULL), or NULL
73 */
74 public String getPreferredDataValue(String name) {
75 Data data = getPreferredData(name);
76 if (data != null && data.getValue() != null)
77 return data.getValue().trim();
78 return null;
79 }
80
81 /**
82 * Get the Data fields that share the given name.
83 *
84 * @param name
85 * the name to look for
86 * @return a list of Data fields with this name
87 */
88 public List<Data> getData(String name) {
89 List<Data> found = new LinkedList<Data>();
90
91 for (Data data : this) {
92 if (data.getName().equals(name))
93 found.add(data);
94 }
95
96 return found;
97 }
98
99 /**
100 * Return a {@link String} representation of this contact formated
101 * accordingly to the given format.
102 *
103 * <p>
104 * The format is basically a list of field names separated by a pipe and
105 * optionally parametrised with the 'at' (@) symbol. The parameters allows
106 * you to:
107 * <ul>
108 * <li>@x: show only a present/not present info</li>
109 * <li>@n: limit the size to a fixed value 'n'</li>
110 * <li>@+: expand the size of this field as much as possible</li>
111 * </ul>
112 * </p>
113 *
114 * <p>
115 * In case of lists or multiple-fields values, you can select a specific
116 * list or field with:
117 * <ul>
118 * <li>FIELD@(0): select the first value in a list</li>
119 * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
120 * </ul>
121 * </p>
122 *
123 * <p>
124 * You can also add a fixed text if it starts with a simple-quote (').
125 * </p>
126 *
127 * <p>
128 * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
129 * </p>
130 *
131 * @param format
132 * the format to use
133 * @param separator
134 * the separator {@link String} to use between fields
135 *
136 * @return the {@link String} representation
137 */
138 public String toString(String format, String separator) {
139 return toString(format, separator, null, -1, true);
140 }
141
142 /**
143 * Return a {@link String} representation of this contact formated
144 * accordingly to the given format.
145 *
146 * <p>
147 * The format is basically a list of field names separated by a pipe and
148 * optionally parametrised. The parameters allows you to:
149 * <ul>
150 * <li>@x: show only a present/not present info</li>
151 * <li>@n: limit the size to a fixed value 'n'</li>
152 * <li>@+: expand the size of this field as much as possible</li>
153 * </ul>
154 * </p>
155 *
156 * <p>
157 * In case of lists or multiple-fields values, you can select a specific
158 * list or field with:
159 * <ul>
160 * <li>FIELD@(0): select the first value in a list</li>
161 * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
162 * </ul>
163 * </p>
164 *
165 * <p>
166 * You can also add a fixed text if it starts with a simple-quote (').
167 * </p>
168 *
169 * <p>
170 * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
171 * </p>
172 *
173 * @param format
174 * the format to use
175 * @param separator
176 * the separator {@link String} to use between fields
177 * @param padding
178 * the {@link String} to use for left and right padding
179 * @param width
180 * a fixed width or -1 for "as long as needed"
181 * @param unicode
182 * allow Unicode or only ASCII characters
183 *
184 * @return the {@link String} representation
185 */
186 public String toString(String format, String separator, String padding,
187 int width, boolean unicode) {
188 StringBuilder builder = new StringBuilder();
189
190 for (String str : toStringArray(format, separator, padding, width,
191 unicode)) {
192 builder.append(str);
193 }
194
195 return builder.toString();
196 }
197
198 /**
199 * Return a {@link String} representation of this contact formated
200 * accordingly to the given format, part by part.
201 *
202 * <p>
203 * The format is basically a list of field names separated by a pipe and
204 * optionally parametrised. The parameters allows you to:
205 * <ul>
206 * <li>@x: show only a present/not present info</li>
207 * <li>@n: limit the size to a fixed value 'n'</li>
208 * <li>@+: expand the size of this field as much as possible</li>
209 * </ul>
210 * </p>
211 *
212 * <p>
213 * In case of lists or multiple-fields values, you can select a specific
214 * list or field with:
215 * <ul>
216 * <li>FIELD@(0): select the first value in a list</li>
217 * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
218 * </ul>
219 * </p>
220 *
221 * <p>
222 * You can also add a fixed text if it starts with a simple-quote (').
223 * </p>
224 *
225 * <p>
226 * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
227 * </p>
228 *
229 * @param format
230 * the format to use
231 * @param separator
232 * the separator {@link String} to use between fields
233 * @param padding
234 * the {@link String} to use for left and right padding
235 * @param width
236 * a fixed width or -1 for "as long as needed"
237 *
238 * @param unicode
239 * allow Uniode or only ASCII characters
240 *
241 * @return the {@link String} representation
242 */
243 public String[] toStringArray(String format, String separator,
244 String padding, int width, boolean unicode) {
245 if (width > -1) {
246 int numOfFields = format.split("\\|").length;
247 if (separator != null)
248 width -= (numOfFields - 1) * separator.length();
249 if (padding != null)
250 width -= (numOfFields) * (2 * padding.length());
251
252 if (width < 0)
253 width = 0;
254 }
255
256 List<String> str = new LinkedList<String>();
257
258 boolean first = true;
259 for (String s : toStringArray(format, width, unicode)) {
260 if (!first) {
261 str.add(separator);
262 }
263
264 if (padding != null)
265 str.add(padding + s + padding);
266 else
267 str.add(s);
268
269 first = false;
270 }
271
272 return str.toArray(new String[] {});
273 }
274
275 /**
276 * Return a {@link String} representation of this contact formated
277 * accordingly to the given format, part by part.
278 *
279 * <p>
280 * The format is basically a list of field names separated by a pipe and
281 * optionally parametrised. The parameters allows you to:
282 * <ul>
283 * <li>@x: show only a present/not present info</li>
284 * <li>@n: limit the size to a fixed value 'n'</li>
285 * <li>@+: expand the size of this field as much as possible</li>
286 * </ul>
287 * </p>
288 *
289 * <p>
290 * In case of lists or multiple-fields values, you can select a specific
291 * list or field with:
292 * <ul>
293 * <li>FIELD@(0): select the first value in a list</li>
294 * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
295 * </ul>
296 * </p>
297 *
298 * <p>
299 * You can also add a fixed text if it starts with a simple-quote (').
300 * </p>
301 *
302 * <p>
303 * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
304 * </p>
305 *
306 * @param format
307 * the format to use
308 * @param width
309 * a fixed width or -1 for "as long as needed"
310 * @param unicode
311 * allow Uniode or only ASCII characters
312 *
313 * @return the {@link String} representation
314 */
315 public String[] toStringArray(String format, int width, boolean unicode) {
316 List<String> str = new LinkedList<String>();
317
318 String[] formatFields = format.split("\\|");
319 String[] values = new String[formatFields.length];
320 Boolean[] expandedFields = new Boolean[formatFields.length];
321 Boolean[] fixedsizeFields = new Boolean[formatFields.length];
322 int numOfFieldsToExpand = 0;
323 int totalSize = 0;
324
325 if (width == 0) {
326 for (int i = 0; i < formatFields.length; i++) {
327 str.add("");
328 }
329
330 return str.toArray(new String[] {});
331 }
332
333 for (int i = 0; i < formatFields.length; i++) {
334 String field = formatFields[i];
335
336 int size = -1;
337 boolean binary = false;
338 boolean expand = false;
339 int fieldNum = -1;
340 int valueNum = -1;
341
342 if (field.length() > 0 && field.charAt(0) != '\''
343 && field.contains("@")) {
344 String[] opts = field.split("@");
345 if (opts.length > 0)
346 field = opts[0];
347 for (int io = 1; io < opts.length; io++) {
348 String opt = opts[io];
349 if (opt.equals("x")) {
350 binary = true;
351 } else if (opt.equals("+")) {
352 expand = true;
353 numOfFieldsToExpand++;
354 } else if (opt.length() > 0 && opt.charAt(0) == '(') {
355 try {
356 opt = opt.substring(1, opt.length() - 1);
357 valueNum = Integer.parseInt(opt);
358 } catch (Exception e) {
359 }
360 } else if (opt.length() > 0 && opt.charAt(0) == '[') {
361 try {
362 opt = opt.substring(1, opt.length() - 1);
363 fieldNum = Integer.parseInt(opt);
364 } catch (Exception e) {
365 }
366 } else {
367 try {
368 size = Integer.parseInt(opt);
369 } catch (NumberFormatException e) {
370 }
371 }
372 }
373 }
374
375 String value = null;
376 if (field.length() > 0 && field.charAt(0) == '\'') {
377 value = field.substring(1);
378 } else if (valueNum >= 0) {
379 List<String> vv = getPreferredData(field).getValues();
380 if (valueNum < vv.size()) {
381 value = vv.get(valueNum);
382 }
383 } else if (fieldNum >= 0) {
384 List<String> ff = getPreferredData(field).getFields();
385 if (fieldNum < ff.size()) {
386 value = ff.get(fieldNum);
387 }
388 } else {
389 // we don't need the *data* in binary mode...
390 if (binary)
391 value = getData(field).size() > 0 ? "x" : null;
392 else
393 value = getPreferredDataValue(field);
394 }
395
396 if (value == null) {
397 value = "";
398 } else {
399 value = StringUtils.sanitize(value, unicode);
400 }
401
402 if (size > -1) {
403 value = StringUtils.padString(value, size);
404 }
405
406 expandedFields[i] = expand;
407 fixedsizeFields[i] = (size > -1);
408
409 if (binary) {
410 if (value != null && !value.equals(""))
411 values[i] = "x";
412 else
413 values[i] = " ";
414 totalSize++;
415 } else {
416 values[i] = value;
417 totalSize += value.length();
418 }
419 }
420
421 if (width > -1 && totalSize > width) {
422 int toDo = totalSize - width;
423 for (int i = fixedsizeFields.length - 1; toDo > 0 && i >= 0; i--) {
424 if (!fixedsizeFields[i]) {
425 int valueLength = values[i].length();
426 if (valueLength > 0) {
427 if (valueLength >= toDo) {
428 values[i] = values[i].substring(0, valueLength
429 - toDo);
430 toDo = 0;
431 } else {
432 values[i] = "";
433 toDo -= valueLength;
434 }
435 }
436 }
437 }
438
439 totalSize = width + toDo;
440 }
441
442 if (width > -1 && numOfFieldsToExpand > 0) {
443 int availablePadding = width - totalSize;
444
445 if (availablePadding > 0) {
446 int padPerItem = availablePadding / numOfFieldsToExpand;
447 int remainder = availablePadding % numOfFieldsToExpand;
448
449 for (int i = 0; i < values.length; i++) {
450 if (expandedFields[i]) {
451 if (remainder > 0) {
452 values[i] = values[i]
453 + StringUtils.padString("", remainder);
454 remainder = 0;
455 }
456 if (padPerItem > 0) {
457 values[i] = values[i]
458 + StringUtils.padString("", padPerItem);
459 }
460 }
461 }
462
463 totalSize = width;
464 }
465 }
466
467 int currentSize = 0;
468 for (int i = 0; i < values.length; i++) {
469 currentSize += addToList(str, values[i], currentSize, width);
470 }
471
472 return str.toArray(new String[] {});
473 }
474
475 /**
476 * Update the information from this contact with the information in the
477 * given contact. Non present fields will be removed, new fields will be
478 * added, BKey'ed fields will be completed with the binary information known
479 * by this contact.
480 *
481 * @param vc
482 * the contact with the newer information and optional BKeys
483 */
484 public void updateFrom(Contact vc) {
485 updateBKeys(false);
486
487 List<Data> newDatas = new LinkedList<Data>(vc);
488 for (int i = 0; i < newDatas.size(); i++) {
489 Data data = newDatas.get(i);
490 int bkey = Parser.getBKey(data);
491 if (bkey >= 0) {
492 if (binaries.containsKey(bkey)) {
493 newDatas.set(i, binaries.get(bkey));
494 }
495 }
496 }
497
498 replaceListContent(newDatas);
499 this.nextBKey = vc.nextBKey;
500 }
501
502 @Override
503 public String getId() {
504 return "" + getPreferredDataValue("UID");
505 }
506
507 @Override
508 public String getState() {
509 return getId();
510 }
511
512 /**
513 * Return a simple {@link String} representation of this contact without
514 * BKeys.
515 *
516 * @return the {@link String} representation
517 */
518 @Override
519 public String toString() {
520 return "[Contact: " + getPreferredDataValue("FN") + "]";
521 }
522
523 /**
524 * Mark all the binary fields with a BKey number.
525 *
526 * @param force
527 * force the marking, and reset all the numbers.
528 */
529 protected void updateBKeys(boolean force) {
530 if (force) {
531 binaries = new HashMap<Integer, Data>();
532 nextBKey = 1;
533 }
534
535 if (binaries == null) {
536 binaries = new HashMap<Integer, Data>();
537 }
538
539 for (Data data : this) {
540 if (data.isBinary() && (data.getB64Key() <= 0 || force)) {
541 binaries.put(nextBKey, data);
542 data.resetB64Key(nextBKey++);
543 }
544 }
545 }
546
547 /**
548 * Load the data from the given {@link File} under the given {@link Format}.
549 *
550 * @param file
551 * the {@link File} to load from
552 * @param format
553 * the {@link Format} to load as
554 *
555 * @return the list of elements
556 * @throws IOException
557 * in case of IO error
558 */
559 static private List<Data> load(List<Data> content) {
560 List<Data> datas = new ArrayList<Data>();
561
562 boolean fn = false;
563 boolean n = false;
564 boolean uid = false;
565 if (content != null) {
566 for (Data data : content) {
567 if (data.getName().equals("N")) {
568 n = true;
569 } else if (data.getName().equals("FN")) {
570 fn = true;
571 } else if (data.getName().equals("UID")) {
572 uid = true;
573 }
574
575 if (!data.getName().equals("VERSION")) {
576 datas.add(data);
577 }
578 }
579 }
580
581 // required fields:
582 if (!n) // required since vCard 3.0, supported in 2.1
583 datas.add(new Data(null, "N", "", null));
584 if (!fn) // not required anymore but still supported in 4.0
585 datas.add(new Data(null, "FN", "", null));
586 if (!uid) // supported by vCard, required by this program
587 datas.add(new Data(null, "UID", UUID.randomUUID().toString(), null));
588
589 return datas;
590 }
591
592 /**
593 * Add a {@link String} to the given {@link List}, but make sure it does not
594 * exceed the maximum size, and truncate it if needed to fit.
595 * <p>
596 * Will always add one and only one {@link String} (potentially empty) at
597 * the end of <tt>list</tt>.
598 *
599 * @param list
600 * the list to add to
601 * @param add
602 * the {@link String} to (either fully or partially) add
603 * @param currentSize
604 * the current total size (managed outside of this method)
605 * @param maxSize
606 * the maximum size that cannot be exceeded (or -1 for
607 * "no maximum") -- if the maximum size would be exceeded by
608 * adding this {@link String}, only a part of it will be added;
609 * if the maximum size is already reached or exceeded (should not
610 * happen because of this method), an empty {@link String} will
611 * be added
612 *
613 * @return the number of characters added (the size of the last
614 * {@link String} in <tt>list</tt>)
615 */
616 static private int addToList(List<String> list, String add,
617 int currentSize, int maxSize) {
618 if (add == null || add.length() == 0) {
619 if (add != null)
620 list.add(add);
621 return 0;
622 }
623
624 if (maxSize > -1) {
625 if (currentSize < maxSize) {
626 if (currentSize + add.length() >= maxSize) {
627 add = add.substring(0, maxSize - currentSize);
628 }
629 } else {
630 add = "";
631 }
632 }
633
634 list.add(add);
635 return add.length();
636 }
637 }