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