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