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