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