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