Some changes to support Files
[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;
10
11/**
12 * A contact is the information that represent a contact person or organisation.
13 *
14 * @author niki
15 *
16 */
17public 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 * @param separator
155 * the separator {@link String} to use between fields
156 * @param width
157 * a fixed width or -1 for "as long as needed"
158 *
159 * @return the {@link String} representation
160 */
161 public String toString(String format, String separator, int width) {
9c8baf0c 162 StringBuilder builder = new StringBuilder();
a3b510ab
NR
163
164 String[] formatFields = format.split("\\|");
a3b510ab
NR
165 if (width > -1 && separator != null && separator.length() > 0
166 && formatFields.length > 1) {
167 int swidth = (formatFields.length - 1) * separator.length();
168 if (swidth >= width) {
9c8baf0c
NR
169 int num = width / separator.length();
170 int remainder = width % separator.length();
171
172 if (remainder > 0)
173 num++;
174
175 while (builder.length() < width) {
176 if (builder.length() + separator.length() <= width)
177 builder.append(separator);
178 else
179 builder.append(separator
180 .substring(0, (builder.length() + separator
181 .length())
182 - width));
a3b510ab
NR
183 }
184
9c8baf0c 185 return builder.toString();
a3b510ab
NR
186 }
187
188 width -= swidth;
189 }
190
9c8baf0c
NR
191 for (String str : toStringArray(format, width)) {
192 builder.append(str);
193 }
194
195 return builder.toString();
196 }
197
198 /**
199 * Return a {@link String} representation of this contact formated
200 * accordingly to the given format, part by part.
201 *
202 * The format is basically a list of field names separated by a pipe and
203 * optionally parametrised. See {@link Contact#toString} for more
204 * information about the format.
205 *
206 * @param format
207 * the format to use
208 * @param width
209 * a fixed width or -1 for "as long as needed"
210 *
211 * @return the {@link String} representation
212 */
213 public String[] toStringArray(String format, int width) {
214 List<String> str = new LinkedList<String>();
215
216 String[] formatFields = format.split("\\|");
217 String[] values = new String[formatFields.length];
218 Boolean[] expandedFields = new Boolean[formatFields.length];
219 Boolean[] fixedsizeFields = new Boolean[formatFields.length];
220 int numOfFieldsToExpand = 0;
221 int totalSize = 0;
222
223 if (width == 0) {
224 return str.toArray(new String[] {});
225 }
226
a3b510ab
NR
227 for (int i = 0; i < formatFields.length; i++) {
228 String field = formatFields[i];
229
230 int size = -1;
231 boolean binary = false;
232 boolean expand = false;
233
234 if (field.contains("@")) {
235 String[] opts = field.split("@");
236 if (opts.length > 0)
237 field = opts[0];
238 for (int io = 1; io < opts.length; io++) {
239 String opt = opts[io];
240 if (opt.equals("x")) {
241 binary = true;
242 } else if (opt.equals("+")) {
243 expand = true;
244 numOfFieldsToExpand++;
245 } else {
246 try {
247 size = Integer.parseInt(opt);
248 } catch (Exception e) {
249 }
250 }
251 }
252 }
253
254 String value = getPreferredDataValue(field);
255 if (value == null)
256 value = "";
257
258 if (size > -1) {
259 value = fixedString(value, size);
260 }
261
262 expandedFields[i] = expand;
263 fixedsizeFields[i] = (size > -1);
264
265 if (binary) {
266 if (value != null && !value.equals(""))
267 values[i] = "x";
268 else
269 values[i] = " ";
270 totalSize++;
271 } else {
272 values[i] = value;
273 totalSize += value.length();
274 }
275 }
9c8baf0c 276
a3b510ab
NR
277 if (width > -1 && totalSize > width) {
278 int toDo = totalSize - width;
279 for (int i = fixedsizeFields.length - 1; toDo > 0 && i >= 0; i--) {
280 if (!fixedsizeFields[i]) {
281 int valueLength = values[i].length();
282 if (valueLength > 0) {
283 if (valueLength >= toDo) {
284 values[i] = values[i].substring(0, valueLength
285 - toDo);
286 toDo = 0;
287 } else {
288 values[i] = "";
289 toDo -= valueLength;
290 }
291 }
292 }
293 }
294
295 totalSize = width + toDo;
296 }
9c8baf0c 297
a3b510ab
NR
298 if (width > -1 && numOfFieldsToExpand > 0) {
299 int availablePadding = width - totalSize;
300
301 if (availablePadding > 0) {
302 int padPerItem = availablePadding / numOfFieldsToExpand;
303 int remainder = availablePadding % numOfFieldsToExpand;
304
305 for (int i = 0; i < values.length; i++) {
306 if (expandedFields[i]) {
307 if (remainder > 0) {
308 values[i] = values[i]
309 + new String(new char[remainder]).replace(
310 '\0', ' ');
311 remainder = 0;
312 }
313 if (padPerItem > 0) {
314 values[i] = values[i]
315 + new String(new char[padPerItem]).replace(
316 '\0', ' ');
317 }
318 }
319 }
320
321 totalSize = width;
322 }
323 }
a3b510ab 324
9c8baf0c
NR
325 int currentSize = 0;
326 for (int i = 0; i < values.length; i++) {
327 currentSize += addToList(str, values[i], currentSize, width);
a3b510ab
NR
328 }
329
9c8baf0c 330 return str.toArray(new String[] {});
a3b510ab
NR
331 }
332
333 /**
334 * Fix the size of the given {@link String} either with space-padding or by
335 * shortening it.
336 *
337 * @param string
338 * the {@link String} to fix
339 * @param size
340 * the size of the resulting {@link String}
341 *
342 * @return the fixed {@link String} of size <i>size</i>
343 */
344 static private String fixedString(String string, int size) {
345 int length = string.length();
346
347 if (length > size)
348 string = string.substring(0, size);
349 else if (length < size)
350 string = string
351 + new String(new char[size - length]).replace('\0', ' ');
352
353 return string;
354 }
355
9c8baf0c
NR
356 /**
357 * Add a {@link String} to the given {@link List}, but make sure it does not
358 * exceed the maximum size, and truncate it if needed to fit.
359 *
360 * @param list
361 * @param add
362 * @param currentSize
363 * @param maxSize
364 * @return
365 */
366 static private int addToList(List<String> list, String add,
367 int currentSize, int maxSize) {
368 if (add == null || add.length() == 0) {
369 if (add != null)
370 list.add(add);
371 return 0;
372 }
373
374 if (maxSize > -1) {
375 if (currentSize < maxSize) {
376 if (currentSize + add.length() >= maxSize) {
377 add = add.substring(0, maxSize - currentSize);
378 }
379 } else {
380 add = "";
381 }
382 }
383
384 if (add.length() > 0) {
385 list.add(add);
386 }
387
388 return add.length();
389 }
390
a3b510ab
NR
391 /**
392 * Return a {@link String} representation of this contact, in vCard 2.1,
393 * without BKeys.
394 *
395 * @return the {@link String} representation
396 */
397 public String toString() {
398 return toString(Format.VCard21, -1);
399 }
400
401 /**
402 * Update the information from this contact with the information in the
403 * given contact. Non present fields will be removed, new fields will be
404 * added, BKey'ed fields will be completed with the binary information known
405 * by this contact.
406 *
407 * @param vc
408 * the contact with the newer information and optional BKeys
409 */
410 public void updateFrom(Contact vc) {
411 updateBKeys(false);
412
413 List<Data> newDatas = new LinkedList<Data>(vc.datas);
414 for (int i = 0; i < newDatas.size(); i++) {
415 Data data = newDatas.get(i);
416 int bkey = Parser.getBKey(data);
417 if (bkey >= 0) {
418 if (binaries.containsKey(bkey)) {
419 newDatas.set(i, binaries.get(bkey));
420 }
421 }
422 }
423
424 this.datas = newDatas;
425 this.nextBKey = vc.nextBKey;
426
427 setParent(parent);
428 setDirty();
429 }
430
431 /**
432 * Mark all the binary fields with a BKey number.
433 *
434 * @param force
435 * force the marking, and reset all the numbers.
436 */
437 protected void updateBKeys(boolean force) {
438 if (force) {
439 binaries = new HashMap<Integer, Data>();
440 nextBKey = 1;
441 }
442
443 if (binaries == null) {
444 binaries = new HashMap<Integer, Data>();
445 }
446
447 for (Data data : datas) {
448 if (data.isBinary() && (data.getB64Key() <= 0 || force)) {
449 binaries.put(nextBKey, data);
450 data.resetB64Key(nextBKey++);
451 }
452 }
453 }
454
455 public boolean isDirty() {
456 return dirty;
457 }
458
459 /**
460 * Notify that this element has unsaved changes, and notify its parent of
461 * the same if any.
462 */
463 protected void setDirty() {
464 this.dirty = true;
465 if (this.parent != null)
466 this.parent.setDirty();
467 }
468
469 public void setParent(Card parent) {
470 this.parent = parent;
471 for (Data data : datas) {
472 data.setParent(this);
473 }
474 }
475}