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