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