Add some comparisons methods, fix the existing checks
[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;
296a0b75 14import be.nikiroo.jvcard.tui.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);
106 return Parser.toString(this, format, startingBKey);
107 }
108
0b0b2b0f
NR
109 /**
110 * Return a {@link String} representation of this contact formated
111 * accordingly to the given format.
112 *
113 * The format is basically a list of field names separated by a pipe and
114 * optionally parametrised. The parameters allows you to:
115 * <ul>
116 * <li>@x: show only a present/not present info</li>
117 * <li>@n: limit the size to a fixed value 'n'</li>
118 * <li>@+: expand the size of this field as much as possible</li>
119 * </ul>
120 *
121 * Example: "N@10|FN@20|NICK@+|PHOTO@x"
122 *
123 * @param format
124 * the format to use
125 *
126 * @return the {@link String} representation
127 */
128 public String toString(String format) {
296a0b75 129 return toString(format, "|", null, -1, true, false);
0b0b2b0f
NR
130 }
131
a3b510ab
NR
132 /**
133 * Return a {@link String} representation of this contact formated
134 * accordingly to the given format.
135 *
136 * The format is basically a list of field names separated by a pipe and
137 * optionally parametrised. The parameters allows you to:
138 * <ul>
d56a0ad4 139 * <li>@x: (the 'x' is the letter 'x') show only a present/not present info</li>
a3b510ab
NR
140 * <li>@n: limit the size to a fixed value 'n'</li>
141 * <li>@+: expand the size of this field as much as possible</li>
142 * </ul>
143 *
144 * Example: "N@10|FN@20|NICK@+|PHOTO@x"
145 *
146 * @param format
147 * the format to use
148 * @param separator
149 * the separator {@link String} to use between fields
0b0b2b0f
NR
150 * @param padding
151 * the {@link String} to use for left and right padding
a3b510ab
NR
152 * @param width
153 * a fixed width or -1 for "as long as needed"
154 *
296a0b75
NR
155 * @param unicode
156 * allow Uniode or only ASCII characters
157 *
a3b510ab
NR
158 * @return the {@link String} representation
159 */
0b0b2b0f 160 public String toString(String format, String separator, String padding,
296a0b75 161 int width, boolean unicode, boolean removeAccents) {
9c8baf0c 162 StringBuilder builder = new StringBuilder();
a3b510ab 163
296a0b75
NR
164 for (String str : toStringArray(format, separator, padding, width,
165 unicode)) {
0b0b2b0f
NR
166 builder.append(str);
167 }
a3b510ab 168
0b0b2b0f
NR
169 return builder.toString();
170 }
a3b510ab 171
0b0b2b0f
NR
172 /**
173 * Return a {@link String} representation of this contact formated
174 * accordingly to the given format, part by part.
175 *
176 * The format is basically a list of field names separated by a pipe and
177 * optionally parametrised. The parameters allows you to:
178 * <ul>
179 * <li>@x: show only a present/not present info</li>
180 * <li>@n: limit the size to a fixed value 'n'</li>
181 * <li>@+: expand the size of this field as much as possible</li>
182 * </ul>
183 *
184 * Example: "N@10|FN@20|NICK@+|PHOTO@x"
185 *
186 * @param format
187 * the format to use
188 * @param separator
189 * the separator {@link String} to use between fields
190 * @param padding
191 * the {@link String} to use for left and right padding
192 * @param width
193 * a fixed width or -1 for "as long as needed"
194 *
296a0b75
NR
195 * @param unicode
196 * allow Uniode or only ASCII characters
197 *
0b0b2b0f
NR
198 * @return the {@link String} representation
199 */
200 public String[] toStringArray(String format, String separator,
296a0b75 201 String padding, int width, boolean unicode) {
0b0b2b0f
NR
202 if (width > -1) {
203 int numOfFields = format.split("\\|").length;
204 if (separator != null)
205 width -= (numOfFields - 1) * separator.length();
206 if (padding != null)
207 width -= (numOfFields) * (2 * padding.length());
208
209 if (width < 0)
210 width = 0;
a3b510ab
NR
211 }
212
0b0b2b0f
NR
213 List<String> str = new LinkedList<String>();
214
215 boolean first = true;
296a0b75 216 for (String s : toStringArray(format, width, unicode)) {
0b0b2b0f
NR
217 if (!first) {
218 str.add(separator);
219 }
220
221 if (padding != null)
222 str.add(padding + s + padding);
223 else
224 str.add(s);
225
226 first = false;
9c8baf0c
NR
227 }
228
0b0b2b0f 229 return str.toArray(new String[] {});
9c8baf0c
NR
230 }
231
232 /**
233 * Return a {@link String} representation of this contact formated
234 * accordingly to the given format, part by part.
235 *
236 * The format is basically a list of field names separated by a pipe and
0b0b2b0f
NR
237 * optionally parametrised. The parameters allows you to:
238 * <ul>
239 * <li>@x: show only a present/not present info</li>
240 * <li>@n: limit the size to a fixed value 'n'</li>
241 * <li>@+: expand the size of this field as much as possible</li>
242 * </ul>
243 *
244 * Example: "N@10|FN@20|NICK@+|PHOTO@x"
9c8baf0c
NR
245 *
246 * @param format
247 * the format to use
248 * @param width
249 * a fixed width or -1 for "as long as needed"
296a0b75
NR
250 * @param unicode
251 * allow Uniode or only ASCII characters
252 *
9c8baf0c
NR
253 * @return the {@link String} representation
254 */
296a0b75 255 public String[] toStringArray(String format, int width, boolean unicode) {
9c8baf0c
NR
256 List<String> str = new LinkedList<String>();
257
258 String[] formatFields = format.split("\\|");
259 String[] values = new String[formatFields.length];
260 Boolean[] expandedFields = new Boolean[formatFields.length];
261 Boolean[] fixedsizeFields = new Boolean[formatFields.length];
262 int numOfFieldsToExpand = 0;
263 int totalSize = 0;
264
265 if (width == 0) {
0b0b2b0f
NR
266 for (int i = 0; i < formatFields.length; i++) {
267 str.add("");
268 }
bcb54330 269
9c8baf0c
NR
270 return str.toArray(new String[] {});
271 }
272
a3b510ab
NR
273 for (int i = 0; i < formatFields.length; i++) {
274 String field = formatFields[i];
275
276 int size = -1;
277 boolean binary = false;
278 boolean expand = false;
279
280 if (field.contains("@")) {
281 String[] opts = field.split("@");
282 if (opts.length > 0)
283 field = opts[0];
284 for (int io = 1; io < opts.length; io++) {
285 String opt = opts[io];
286 if (opt.equals("x")) {
287 binary = true;
288 } else if (opt.equals("+")) {
289 expand = true;
290 numOfFieldsToExpand++;
291 } else {
292 try {
293 size = Integer.parseInt(opt);
294 } catch (Exception e) {
295 }
296 }
297 }
298 }
299
300 String value = getPreferredDataValue(field);
296a0b75 301 if (value == null) {
a3b510ab 302 value = "";
296a0b75
NR
303 } else {
304 value = StringUtils.sanitize(value, unicode);
305 }
a3b510ab
NR
306
307 if (size > -1) {
296a0b75 308 value = StringUtils.padString(value, size);
a3b510ab
NR
309 }
310
311 expandedFields[i] = expand;
312 fixedsizeFields[i] = (size > -1);
313
314 if (binary) {
315 if (value != null && !value.equals(""))
316 values[i] = "x";
317 else
318 values[i] = " ";
319 totalSize++;
320 } else {
321 values[i] = value;
322 totalSize += value.length();
323 }
324 }
9c8baf0c 325
a3b510ab
NR
326 if (width > -1 && totalSize > width) {
327 int toDo = totalSize - width;
328 for (int i = fixedsizeFields.length - 1; toDo > 0 && i >= 0; i--) {
329 if (!fixedsizeFields[i]) {
330 int valueLength = values[i].length();
331 if (valueLength > 0) {
332 if (valueLength >= toDo) {
333 values[i] = values[i].substring(0, valueLength
334 - toDo);
335 toDo = 0;
336 } else {
337 values[i] = "";
338 toDo -= valueLength;
339 }
340 }
341 }
342 }
343
344 totalSize = width + toDo;
345 }
9c8baf0c 346
a3b510ab
NR
347 if (width > -1 && numOfFieldsToExpand > 0) {
348 int availablePadding = width - totalSize;
349
350 if (availablePadding > 0) {
351 int padPerItem = availablePadding / numOfFieldsToExpand;
352 int remainder = availablePadding % numOfFieldsToExpand;
353
354 for (int i = 0; i < values.length; i++) {
355 if (expandedFields[i]) {
356 if (remainder > 0) {
296a0b75
NR
357 values[i] = values[i]
358 + StringUtils.padString("", remainder);
a3b510ab
NR
359 remainder = 0;
360 }
361 if (padPerItem > 0) {
296a0b75
NR
362 values[i] = values[i]
363 + StringUtils.padString("", padPerItem);
a3b510ab
NR
364 }
365 }
366 }
367
368 totalSize = width;
369 }
370 }
a3b510ab 371
9c8baf0c
NR
372 int currentSize = 0;
373 for (int i = 0; i < values.length; i++) {
374 currentSize += addToList(str, values[i], currentSize, width);
a3b510ab
NR
375 }
376
9c8baf0c 377 return str.toArray(new String[] {});
a3b510ab
NR
378 }
379
a3b510ab
NR
380 /**
381 * Update the information from this contact with the information in the
382 * given contact. Non present fields will be removed, new fields will be
383 * added, BKey'ed fields will be completed with the binary information known
384 * by this contact.
385 *
386 * @param vc
387 * the contact with the newer information and optional BKeys
388 */
389 public void updateFrom(Contact vc) {
390 updateBKeys(false);
391
26d2bd05 392 List<Data> newDatas = new LinkedList<Data>(vc);
a3b510ab
NR
393 for (int i = 0; i < newDatas.size(); i++) {
394 Data data = newDatas.get(i);
395 int bkey = Parser.getBKey(data);
396 if (bkey >= 0) {
397 if (binaries.containsKey(bkey)) {
398 newDatas.set(i, binaries.get(bkey));
399 }
400 }
401 }
402
26d2bd05 403 replaceListContent(newDatas);
a3b510ab 404 this.nextBKey = vc.nextBKey;
78e4af97
NR
405 }
406
e253bd50
NR
407 @Override
408 public String getId() {
409 return "" + getPreferredDataValue("UID");
410 }
411
412 @Override
413 public String getState() {
414 return "" + getPreferredDataValue("UID");
415 }
416
78e4af97
NR
417 /**
418 * Return a {@link String} representation of this contact, in vCard 2.1,
419 * without BKeys.
420 *
421 * @return the {@link String} representation
422 */
423 @Override
424 public String toString() {
425 return toString(Format.VCard21, -1);
426 }
427
a3b510ab
NR
428 /**
429 * Mark all the binary fields with a BKey number.
430 *
431 * @param force
432 * force the marking, and reset all the numbers.
433 */
434 protected void updateBKeys(boolean force) {
435 if (force) {
436 binaries = new HashMap<Integer, Data>();
437 nextBKey = 1;
438 }
439
440 if (binaries == null) {
441 binaries = new HashMap<Integer, Data>();
442 }
443
26d2bd05 444 for (Data data : this) {
a3b510ab
NR
445 if (data.isBinary() && (data.getB64Key() <= 0 || force)) {
446 binaries.put(nextBKey, data);
447 data.resetB64Key(nextBKey++);
448 }
449 }
450 }
451
a3b510ab 452 /**
26d2bd05
NR
453 * Load the data from the given {@link File} under the given {@link Format}.
454 *
455 * @param file
456 * the {@link File} to load from
457 * @param format
458 * the {@link Format} to load as
459 *
460 * @return the list of elements
461 * @throws IOException
462 * in case of IO error
a3b510ab 463 */
26d2bd05
NR
464 static private List<Data> load(List<Data> content) {
465 List<Data> datas = new ArrayList<Data>();
a3b510ab 466
26d2bd05
NR
467 boolean fn = false;
468 boolean n = false;
e253bd50 469 boolean uid = false;
26d2bd05
NR
470 if (content != null) {
471 for (Data data : content) {
472 if (data.getName().equals("N")) {
473 n = true;
474 } else if (data.getName().equals("FN")) {
475 fn = true;
e253bd50
NR
476 } else if (data.getName().equals("UID")) {
477 uid = true;
26d2bd05
NR
478 }
479
480 if (!data.getName().equals("VERSION")) {
481 datas.add(data);
482 }
483 }
78e4af97 484 }
78e4af97 485
26d2bd05 486 // required fields:
e253bd50 487 if (!n) // required since vCard 3.0, supported in 2.1
26d2bd05 488 datas.add(new Data(null, "N", "", null));
e253bd50 489 if (!fn) // not required anymore but still supported in 4.0
26d2bd05 490 datas.add(new Data(null, "FN", "", null));
e253bd50
NR
491 if (!uid) // supported by vCard, required by this program
492 datas.add(new Data(null, "UID", UUID.randomUUID().toString(), null));
26d2bd05
NR
493
494 return datas;
a3b510ab 495 }
296a0b75 496
bcb54330 497 /**
78e4af97
NR
498 * Add a {@link String} to the given {@link List}, but make sure it does not
499 * exceed the maximum size, and truncate it if needed to fit.
bcb54330 500 *
78e4af97
NR
501 * @param list
502 * @param add
503 * @param currentSize
504 * @param maxSize
505 * @return
bcb54330 506 */
78e4af97
NR
507 static private int addToList(List<String> list, String add,
508 int currentSize, int maxSize) {
509 if (add == null || add.length() == 0) {
510 if (add != null)
511 list.add(add);
512 return 0;
513 }
514
515 if (maxSize > -1) {
516 if (currentSize < maxSize) {
517 if (currentSize + add.length() >= maxSize) {
518 add = add.substring(0, maxSize - currentSize);
bcb54330 519 }
78e4af97
NR
520 } else {
521 add = "";
bcb54330
NR
522 }
523 }
524
78e4af97
NR
525 list.add(add);
526 return add.length();
bcb54330 527 }
a3b510ab 528}