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