Initial commit
[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 * @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) {
162 String str = null;
163
164 String[] formatFields = format.split("\\|");
165 String[] values = new String[formatFields.length];
166 Boolean[] expandedFields = new Boolean[formatFields.length];
167 Boolean[] fixedsizeFields = new Boolean[formatFields.length];
168 int numOfFieldsToExpand = 0;
169 int totalSize = 0;
170
171 if (width == 0) {
172 return "";
173 }
174
175 if (width > -1 && separator != null && separator.length() > 0
176 && formatFields.length > 1) {
177 int swidth = (formatFields.length - 1) * separator.length();
178 if (swidth >= width) {
179 str = separator;
180 while (str.length() < width) {
181 str += separator;
182 }
183
184 return str.substring(0, width);
185 }
186
187 width -= swidth;
188 }
189
190 for (int i = 0; i < formatFields.length; i++) {
191 String field = formatFields[i];
192
193 int size = -1;
194 boolean binary = false;
195 boolean expand = false;
196
197 if (field.contains("@")) {
198 String[] opts = field.split("@");
199 if (opts.length > 0)
200 field = opts[0];
201 for (int io = 1; io < opts.length; io++) {
202 String opt = opts[io];
203 if (opt.equals("x")) {
204 binary = true;
205 } else if (opt.equals("+")) {
206 expand = true;
207 numOfFieldsToExpand++;
208 } else {
209 try {
210 size = Integer.parseInt(opt);
211 } catch (Exception e) {
212 }
213 }
214 }
215 }
216
217 String value = getPreferredDataValue(field);
218 if (value == null)
219 value = "";
220
221 if (size > -1) {
222 value = fixedString(value, size);
223 }
224
225 expandedFields[i] = expand;
226 fixedsizeFields[i] = (size > -1);
227
228 if (binary) {
229 if (value != null && !value.equals(""))
230 values[i] = "x";
231 else
232 values[i] = " ";
233 totalSize++;
234 } else {
235 values[i] = value;
236 totalSize += value.length();
237 }
238 }
239
240 if (width > -1 && totalSize > width) {
241 int toDo = totalSize - width;
242 for (int i = fixedsizeFields.length - 1; toDo > 0 && i >= 0; i--) {
243 if (!fixedsizeFields[i]) {
244 int valueLength = values[i].length();
245 if (valueLength > 0) {
246 if (valueLength >= toDo) {
247 values[i] = values[i].substring(0, valueLength
248 - toDo);
249 toDo = 0;
250 } else {
251 values[i] = "";
252 toDo -= valueLength;
253 }
254 }
255 }
256 }
257
258 totalSize = width + toDo;
259 }
260
261 if (width > -1 && numOfFieldsToExpand > 0) {
262 int availablePadding = width - totalSize;
263
264 if (availablePadding > 0) {
265 int padPerItem = availablePadding / numOfFieldsToExpand;
266 int remainder = availablePadding % numOfFieldsToExpand;
267
268 for (int i = 0; i < values.length; i++) {
269 if (expandedFields[i]) {
270 if (remainder > 0) {
271 values[i] = values[i]
272 + new String(new char[remainder]).replace(
273 '\0', ' ');
274 remainder = 0;
275 }
276 if (padPerItem > 0) {
277 values[i] = values[i]
278 + new String(new char[padPerItem]).replace(
279 '\0', ' ');
280 }
281 }
282 }
283
284 totalSize = width;
285 }
286 }
287
288 for (String field : values) {
289 if (str == null) {
290 str = field;
291 } else {
292 str += separator + field;
293 }
294 }
295
296 if (str == null)
297 str = "";
298
299 if (width > -1) {
300 str = fixedString(str, width);
301 }
302
303 return str;
304 }
305
306 /**
307 * Fix the size of the given {@link String} either with space-padding or by
308 * shortening it.
309 *
310 * @param string
311 * the {@link String} to fix
312 * @param size
313 * the size of the resulting {@link String}
314 *
315 * @return the fixed {@link String} of size <i>size</i>
316 */
317 static private String fixedString(String string, int size) {
318 int length = string.length();
319
320 if (length > size)
321 string = string.substring(0, size);
322 else if (length < size)
323 string = string
324 + new String(new char[size - length]).replace('\0', ' ');
325
326 return string;
327 }
328
329 /**
330 * Return a {@link String} representation of this contact, in vCard 2.1,
331 * without BKeys.
332 *
333 * @return the {@link String} representation
334 */
335 public String toString() {
336 return toString(Format.VCard21, -1);
337 }
338
339 /**
340 * Update the information from this contact with the information in the
341 * given contact. Non present fields will be removed, new fields will be
342 * added, BKey'ed fields will be completed with the binary information known
343 * by this contact.
344 *
345 * @param vc
346 * the contact with the newer information and optional BKeys
347 */
348 public void updateFrom(Contact vc) {
349 updateBKeys(false);
350
351 List<Data> newDatas = new LinkedList<Data>(vc.datas);
352 for (int i = 0; i < newDatas.size(); i++) {
353 Data data = newDatas.get(i);
354 int bkey = Parser.getBKey(data);
355 if (bkey >= 0) {
356 if (binaries.containsKey(bkey)) {
357 newDatas.set(i, binaries.get(bkey));
358 }
359 }
360 }
361
362 this.datas = newDatas;
363 this.nextBKey = vc.nextBKey;
364
365 setParent(parent);
366 setDirty();
367 }
368
369 /**
370 * Mark all the binary fields with a BKey number.
371 *
372 * @param force
373 * force the marking, and reset all the numbers.
374 */
375 protected void updateBKeys(boolean force) {
376 if (force) {
377 binaries = new HashMap<Integer, Data>();
378 nextBKey = 1;
379 }
380
381 if (binaries == null) {
382 binaries = new HashMap<Integer, Data>();
383 }
384
385 for (Data data : datas) {
386 if (data.isBinary() && (data.getB64Key() <= 0 || force)) {
387 binaries.put(nextBKey, data);
388 data.resetB64Key(nextBKey++);
389 }
390 }
391 }
392
393 public boolean isDirty() {
394 return dirty;
395 }
396
397 /**
398 * Notify that this element has unsaved changes, and notify its parent of
399 * the same if any.
400 */
401 protected void setDirty() {
402 this.dirty = true;
403 if (this.parent != null)
404 this.parent.setDirty();
405 }
406
407 public void setParent(Card parent) {
408 this.parent = parent;
409 for (Data data : datas) {
410 data.setParent(this);
411 }
412 }
413 }