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