Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | * @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 | } |