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