Commit | Line | Data |
---|---|---|
a3b510ab NR |
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; | |
296a0b75 | 10 | import be.nikiroo.jvcard.tui.StringUtils; |
a3b510ab NR |
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; | |
176a8327 NR |
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 | } | |
a3b510ab | 44 | |
176a8327 NR |
45 | if (!data.getName().equals("VERSION")) { |
46 | datas.add(data); | |
47 | } | |
a3b510ab NR |
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 | /** | |
78e4af97 | 63 | * Return the number of {@link Data} present in this {@link Contact}. |
a3b510ab | 64 | * |
78e4af97 | 65 | * @return the number of {@link Data}s |
a3b510ab | 66 | */ |
78e4af97 NR |
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); | |
a3b510ab NR |
84 | } |
85 | ||
176a8327 NR |
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 | ||
a3b510ab NR |
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; | |
78e4af97 NR |
124 | for (int index = 0; index < data.size(); index++) { |
125 | TypeInfo type = data.get(index); | |
a3b510ab NR |
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 | ||
0b0b2b0f NR |
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) { | |
296a0b75 | 203 | return toString(format, "|", null, -1, true, false); |
0b0b2b0f NR |
204 | } |
205 | ||
a3b510ab NR |
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> | |
d56a0ad4 | 213 | * <li>@x: (the 'x' is the letter 'x') show only a present/not present info</li> |
a3b510ab NR |
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 | |
0b0b2b0f NR |
224 | * @param padding |
225 | * the {@link String} to use for left and right padding | |
a3b510ab NR |
226 | * @param width |
227 | * a fixed width or -1 for "as long as needed" | |
228 | * | |
296a0b75 NR |
229 | * @param unicode |
230 | * allow Uniode or only ASCII characters | |
231 | * | |
a3b510ab NR |
232 | * @return the {@link String} representation |
233 | */ | |
0b0b2b0f | 234 | public String toString(String format, String separator, String padding, |
296a0b75 | 235 | int width, boolean unicode, boolean removeAccents) { |
9c8baf0c | 236 | StringBuilder builder = new StringBuilder(); |
a3b510ab | 237 | |
296a0b75 NR |
238 | for (String str : toStringArray(format, separator, padding, width, |
239 | unicode)) { | |
0b0b2b0f NR |
240 | builder.append(str); |
241 | } | |
a3b510ab | 242 | |
0b0b2b0f NR |
243 | return builder.toString(); |
244 | } | |
a3b510ab | 245 | |
0b0b2b0f NR |
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 | * | |
296a0b75 NR |
269 | * @param unicode |
270 | * allow Uniode or only ASCII characters | |
271 | * | |
0b0b2b0f NR |
272 | * @return the {@link String} representation |
273 | */ | |
274 | public String[] toStringArray(String format, String separator, | |
296a0b75 | 275 | String padding, int width, boolean unicode) { |
0b0b2b0f NR |
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; | |
a3b510ab NR |
285 | } |
286 | ||
0b0b2b0f NR |
287 | List<String> str = new LinkedList<String>(); |
288 | ||
289 | boolean first = true; | |
296a0b75 | 290 | for (String s : toStringArray(format, width, unicode)) { |
0b0b2b0f NR |
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; | |
9c8baf0c NR |
301 | } |
302 | ||
0b0b2b0f | 303 | return str.toArray(new String[] {}); |
9c8baf0c NR |
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 | |
0b0b2b0f NR |
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" | |
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; | |
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); | |
296a0b75 | 375 | if (value == null) { |
a3b510ab | 376 | value = ""; |
296a0b75 NR |
377 | } else { |
378 | value = StringUtils.sanitize(value, unicode); | |
379 | } | |
a3b510ab NR |
380 | |
381 | if (size > -1) { | |
296a0b75 | 382 | value = StringUtils.padString(value, size); |
a3b510ab NR |
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 | } | |
9c8baf0c | 399 | |
a3b510ab NR |
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 | } | |
9c8baf0c | 420 | |
a3b510ab NR |
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) { | |
296a0b75 NR |
431 | values[i] = values[i] |
432 | + StringUtils.padString("", remainder); | |
a3b510ab NR |
433 | remainder = 0; |
434 | } | |
435 | if (padPerItem > 0) { | |
296a0b75 NR |
436 | values[i] = values[i] |
437 | + StringUtils.padString("", padPerItem); | |
a3b510ab NR |
438 | } |
439 | } | |
440 | } | |
441 | ||
442 | totalSize = width; | |
443 | } | |
444 | } | |
a3b510ab | 445 | |
9c8baf0c NR |
446 | int currentSize = 0; |
447 | for (int i = 0; i < values.length; i++) { | |
448 | currentSize += addToList(str, values[i], currentSize, width); | |
a3b510ab NR |
449 | } |
450 | ||
9c8baf0c | 451 | return str.toArray(new String[] {}); |
a3b510ab NR |
452 | } |
453 | ||
a3b510ab NR |
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 | ||
78e4af97 NR |
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) { | |
176a8327 | 491 | return parent.remove(this); |
78e4af97 NR |
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 | ||
a3b510ab NR |
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 | ||
a3b510ab NR |
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 | ||
78e4af97 NR |
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 | */ | |
bcb54330 | 568 | void setParent(Card parent) { |
a3b510ab NR |
569 | this.parent = parent; |
570 | for (Data data : datas) { | |
571 | data.setParent(this); | |
572 | } | |
573 | } | |
296a0b75 | 574 | |
bcb54330 | 575 | /** |
78e4af97 NR |
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. | |
bcb54330 | 578 | * |
78e4af97 NR |
579 | * @param list |
580 | * @param add | |
581 | * @param currentSize | |
582 | * @param maxSize | |
583 | * @return | |
bcb54330 | 584 | */ |
78e4af97 NR |
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); | |
bcb54330 | 597 | } |
78e4af97 NR |
598 | } else { |
599 | add = ""; | |
bcb54330 NR |
600 | } |
601 | } | |
602 | ||
78e4af97 NR |
603 | list.add(add); |
604 | return add.length(); | |
bcb54330 NR |
605 | } |
606 | ||
a3b510ab | 607 | } |