Fix PREF handling (was not correct relative to the RFC!)
[jvcard.git] / src / be / nikiroo / jvcard / Contact.java
index 21ed69b86f66e532adee8bc06a5bf317d00f9021..d75d3380326a8bfde6c245f959dd6ff916b1f07f 100644 (file)
@@ -1,12 +1,17 @@
 package be.nikiroo.jvcard;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 import be.nikiroo.jvcard.parsers.Format;
 import be.nikiroo.jvcard.parsers.Parser;
+import be.nikiroo.jvcard.resources.StringUtils;
 
 /**
  * A contact is the information that represent a contact person or organisation.
@@ -14,12 +19,9 @@ import be.nikiroo.jvcard.parsers.Parser;
  * @author niki
  * 
  */
-public class Contact {
-       private List<Data> datas;
+public class Contact extends BaseClass<Data> {
        private int nextBKey = 1;
        private Map<Integer, Data> binaries;
-       private boolean dirty;
-       private Card parent;
 
        /**
         * Create a new Contact from the given information. Note that the BKeys data
@@ -29,63 +31,31 @@ public class Contact {
         *            the information about the contact
         */
        public Contact(List<Data> content) {
-               this.datas = new LinkedList<Data>();
-
-               boolean fn = false;
-               boolean n = false;
-               for (Data data : content) {
-                       if (data.getName().equals("N")) {
-                               n = true;
-                       } else if (data.getName().equals("FN")) {
-                               fn = true;
-                       }
-
-                       if (!data.getName().equals("VERSION")) {
-                               datas.add(data);
-                       }
-               }
-
-               // required fields:
-               if (!n) {
-                       datas.add(new Data(null, "N", "", null));
-               }
-               if (!fn) {
-                       datas.add(new Data(null, "FN", "", null));
-               }
-
+               super(load(content));
                updateBKeys(true);
        }
 
        /**
-        * Return the informations (note: this is the actual list, be careful).
-        * 
-        * @return the list of data anout this contact
-        */
-       public List<Data> getContent() {
-               return datas;
-       }
-
-       /**
-        * Return the preferred Data field with the given name, or NULL if none.
+        * Return the preferred Data field with the given name, the first one if
+        * none is preferred, or NULL if none at all.
         * 
         * @param name
         *            the name to look for
-        * @return the Data field, or NULL
+        * 
+        * @return the {@link Data} field, or NULL
         */
        public Data getPreferredData(String name) {
-               Data first = null;
+               Data pref = null;
+               int ipref = Integer.MAX_VALUE;
                for (Data data : getData(name)) {
-                       if (first == null)
-                               first = data;
-                       for (TypeInfo type : data.getTypes()) {
-                               if (type.getName().equals("TYPE")
-                                               && type.getValue().equals("pref")) {
-                                       return data;
-                               }
-                       }
+                       if (pref == null)
+                               pref = data;
+
+                       if (data.getPreferred() < ipref)
+                               pref = data;
                }
 
-               return first;
+               return pref;
        }
 
        /**
@@ -113,7 +83,7 @@ public class Contact {
        public List<Data> getData(String name) {
                List<Data> found = new LinkedList<Data>();
 
-               for (Data data : datas) {
+               for (Data data : this) {
                        if (data.getName().equals(name))
                                found.add(data);
                }
@@ -121,47 +91,54 @@ public class Contact {
                return found;
        }
 
-       /**
-        * Return a {@link String} representation of this contact.
-        * 
-        * @param format
-        *            the {@link Format} to use
-        * @param startingBKey
-        *            the starting BKey or -1 for no BKeys
-        * @return the {@link String} representation
-        */
-       public String toString(Format format, int startingBKey) {
-               updateBKeys(false);
-               return Parser.toString(this, format, startingBKey);
-       }
-
        /**
         * Return a {@link String} representation of this contact formated
         * accordingly to the given format.
         * 
+        * <p>
         * The format is basically a list of field names separated by a pipe and
-        * optionally parametrised. The parameters allows you to:
+        * optionally parametrised with the 'at' (@) symbol. The parameters allows
+        * you to:
         * <ul>
         * <li>@x: show only a present/not present info</li>
         * <li>@n: limit the size to a fixed value 'n'</li>
         * <li>@+: expand the size of this field as much as possible</li>
         * </ul>
+        * </p>
+        * 
+        * <p>
+        * In case of lists or multiple-fields values, you can select a specific
+        * list or field with:
+        * <ul>
+        * <li>FIELD@(0): select the first value in a list</li>
+        * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
+        * </ul>
+        * </p>
+        * 
+        * <p>
+        * You can also add a fixed text if it starts with a simple-quote (').
+        * </p>
         * 
-        * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+        * <p>
+        * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+        * </p>
         * 
         * @param format
         *            the format to use
+        * @param separator
+        *            the separator {@link String} to use between fields
         * 
         * @return the {@link String} representation
         */
-       public String toString(String format) {
-               return toString(format, "|", null, -1);
+       public String toString(String format, String separator) {
+               return toString(format, separator, null, -1, true, false);
        }
 
        /**
         * Return a {@link String} representation of this contact formated
         * accordingly to the given format.
         * 
+        * <p>
         * The format is basically a list of field names separated by a pipe and
         * optionally parametrised. The parameters allows you to:
         * <ul>
@@ -169,8 +146,24 @@ public class Contact {
         * <li>@n: limit the size to a fixed value 'n'</li>
         * <li>@+: expand the size of this field as much as possible</li>
         * </ul>
+        * </p>
         * 
-        * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+        * <p>
+        * In case of lists or multiple-fields values, you can select a specific
+        * list or field with:
+        * <ul>
+        * <li>FIELD@(0): select the first value in a list</li>
+        * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
+        * </ul>
+        * </p>
+        * 
+        * <p>
+        * You can also add a fixed text if it starts with a simple-quote (').
+        * </p>
+        * 
+        * <p>
+        * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+        * </p>
         * 
         * @param format
         *            the format to use
@@ -181,13 +174,17 @@ public class Contact {
         * @param width
         *            a fixed width or -1 for "as long as needed"
         * 
+        * @param unicode
+        *            allow Uniode or only ASCII characters
+        * 
         * @return the {@link String} representation
         */
        public String toString(String format, String separator, String padding,
-                       int width) {
+                       int width, boolean unicode, boolean removeAccents) {
                StringBuilder builder = new StringBuilder();
 
-               for (String str : toStringArray(format, separator, padding, width)) {
+               for (String str : toStringArray(format, separator, padding, width,
+                               unicode)) {
                        builder.append(str);
                }
 
@@ -198,6 +195,7 @@ public class Contact {
         * Return a {@link String} representation of this contact formated
         * accordingly to the given format, part by part.
         * 
+        * <p>
         * The format is basically a list of field names separated by a pipe and
         * optionally parametrised. The parameters allows you to:
         * <ul>
@@ -205,8 +203,24 @@ public class Contact {
         * <li>@n: limit the size to a fixed value 'n'</li>
         * <li>@+: expand the size of this field as much as possible</li>
         * </ul>
+        * </p>
         * 
-        * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+        * <p>
+        * In case of lists or multiple-fields values, you can select a specific
+        * list or field with:
+        * <ul>
+        * <li>FIELD@(0): select the first value in a list</li>
+        * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
+        * </ul>
+        * </p>
+        * 
+        * <p>
+        * You can also add a fixed text if it starts with a simple-quote (').
+        * </p>
+        * 
+        * <p>
+        * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+        * </p>
         * 
         * @param format
         *            the format to use
@@ -217,10 +231,13 @@ public class Contact {
         * @param width
         *            a fixed width or -1 for "as long as needed"
         * 
+        * @param unicode
+        *            allow Uniode or only ASCII characters
+        * 
         * @return the {@link String} representation
         */
        public String[] toStringArray(String format, String separator,
-                       String padding, int width) {
+                       String padding, int width, boolean unicode) {
                if (width > -1) {
                        int numOfFields = format.split("\\|").length;
                        if (separator != null)
@@ -235,7 +252,7 @@ public class Contact {
                List<String> str = new LinkedList<String>();
 
                boolean first = true;
-               for (String s : toStringArray(format, width)) {
+               for (String s : toStringArray(format, width, unicode)) {
                        if (!first) {
                                str.add(separator);
                        }
@@ -255,6 +272,7 @@ public class Contact {
         * Return a {@link String} representation of this contact formated
         * accordingly to the given format, part by part.
         * 
+        * <p>
         * The format is basically a list of field names separated by a pipe and
         * optionally parametrised. The parameters allows you to:
         * <ul>
@@ -262,17 +280,35 @@ public class Contact {
         * <li>@n: limit the size to a fixed value 'n'</li>
         * <li>@+: expand the size of this field as much as possible</li>
         * </ul>
+        * </p>
         * 
-        * Example: "N@10|FN@20|NICK@+|PHOTO@x"
+        * <p>
+        * In case of lists or multiple-fields values, you can select a specific
+        * list or field with:
+        * <ul>
+        * <li>FIELD@(0): select the first value in a list</li>
+        * <li>FIELD@[1]: select the second field in a multiple-fields value</li>
+        * </ul>
+        * </p>
+        * 
+        * <p>
+        * You can also add a fixed text if it starts with a simple-quote (').
+        * </p>
+        * 
+        * <p>
+        * Example: "'Contact: |N@10|FN@20|NICK@+|PHOTO@x"
+        * </p>
         * 
         * @param format
         *            the format to use
         * @param width
         *            a fixed width or -1 for "as long as needed"
-        * 
+        * @param unicode
+        *            allow Uniode or only ASCII characters
+        *
         * @return the {@link String} representation
         */
-       public String[] toStringArray(String format, int width) {
+       public String[] toStringArray(String format, int width, boolean unicode) {
                List<String> str = new LinkedList<String>();
 
                String[] formatFields = format.split("\\|");
@@ -286,7 +322,7 @@ public class Contact {
                        for (int i = 0; i < formatFields.length; i++) {
                                str.add("");
                        }
-                       
+
                        return str.toArray(new String[] {});
                }
 
@@ -296,8 +332,11 @@ public class Contact {
                        int size = -1;
                        boolean binary = false;
                        boolean expand = false;
+                       int fieldNum = -1;
+                       int valueNum = -1;
 
-                       if (field.contains("@")) {
+                       if (field.length() > 0 && field.charAt(0) != '\''
+                                       && field.contains("@")) {
                                String[] opts = field.split("@");
                                if (opts.length > 0)
                                        field = opts[0];
@@ -308,21 +347,56 @@ public class Contact {
                                        } else if (opt.equals("+")) {
                                                expand = true;
                                                numOfFieldsToExpand++;
+                                       } else if (opt.length() > 0 && opt.charAt(0) == '(') {
+                                               try {
+                                                       opt = opt.substring(1, opt.length() - 1);
+                                                       valueNum = Integer.parseInt(opt);
+                                               } catch (Exception e) {
+                                               }
+                                       } else if (opt.length() > 0 && opt.charAt(0) == '[') {
+                                               try {
+                                                       opt = opt.substring(1, opt.length() - 1);
+                                                       fieldNum = Integer.parseInt(opt);
+                                               } catch (Exception e) {
+                                               }
                                        } else {
                                                try {
                                                        size = Integer.parseInt(opt);
-                                               } catch (Exception e) {
+                                               } catch (NumberFormatException e) {
                                                }
                                        }
                                }
                        }
 
-                       String value = getPreferredDataValue(field);
-                       if (value == null)
+                       String value = null;
+                       if (field.length() > 0 && field.charAt(0) == '\'') {
+                               value = field.substring(1);
+                       } else if (valueNum >= 0) {
+                               List<String> vv = getPreferredData(field).getValues();
+                               if (valueNum < vv.size()) {
+                                       value = vv.get(valueNum);
+                               }
+                       } else if (fieldNum >= 0) {
+                               List<String> ff = getPreferredData(field).getFields();
+                               if (fieldNum < ff.size()) {
+                                       value = ff.get(fieldNum);
+                               }
+                       } else {
+                               // we don't need the *data* in binary mode...
+                               if (binary)
+                                       value = getData(field).size() > 0 ? "x" : null;
+                               else
+                                       value = getPreferredDataValue(field);
+                       }
+
+                       if (value == null) {
                                value = "";
+                       } else {
+                               value = StringUtils.sanitize(value, unicode);
+                       }
 
                        if (size > -1) {
-                               value = fixedString(value, size);
+                               value = StringUtils.padString(value, size);
                        }
 
                        expandedFields[i] = expand;
@@ -371,11 +445,13 @@ public class Contact {
                                for (int i = 0; i < values.length; i++) {
                                        if (expandedFields[i]) {
                                                if (remainder > 0) {
-                                                       values[i] = values[i] + fixedString("", remainder);
+                                                       values[i] = values[i]
+                                                                       + StringUtils.padString("", remainder);
                                                        remainder = 0;
                                                }
                                                if (padPerItem > 0) {
-                                                       values[i] = values[i] + fixedString("", padPerItem);
+                                                       values[i] = values[i]
+                                                                       + StringUtils.padString("", padPerItem);
                                                }
                                        }
                                }
@@ -392,72 +468,6 @@ public class Contact {
                return str.toArray(new String[] {});
        }
 
-       /**
-        * Fix the size of the given {@link String} either with space-padding or by
-        * shortening it.
-        * 
-        * @param string
-        *            the {@link String} to fix
-        * @param size
-        *            the size of the resulting {@link String}
-        * 
-        * @return the fixed {@link String} of size <i>size</i>
-        */
-       static private String fixedString(String string, int size) {
-               int length = string.length();
-
-               if (length > size) {
-                       string = string.substring(0, size);
-               } else if (length < size) {
-                       string = string
-                                       + new String(new char[size - length]).replace('\0', ' ');
-               }
-
-               return string;
-       }
-
-       /**
-        * Add a {@link String} to the given {@link List}, but make sure it does not
-        * exceed the maximum size, and truncate it if needed to fit.
-        * 
-        * @param list
-        * @param add
-        * @param currentSize
-        * @param maxSize
-        * @return
-        */
-       static private int addToList(List<String> list, String add,
-                       int currentSize, int maxSize) {
-               if (add == null || add.length() == 0) {
-                       if (add != null)
-                               list.add(add);
-                       return 0;
-               }
-
-               if (maxSize > -1) {
-                       if (currentSize < maxSize) {
-                               if (currentSize + add.length() >= maxSize) {
-                                       add = add.substring(0, maxSize - currentSize);
-                               }
-                       } else {
-                               add = "";
-                       }
-               }
-
-               list.add(add);
-               return add.length();
-       }
-
-       /**
-        * Return a {@link String} representation of this contact, in vCard 2.1,
-        * without BKeys.
-        * 
-        * @return the {@link String} representation
-        */
-       public String toString() {
-               return toString(Format.VCard21, -1);
-       }
-
        /**
         * Update the information from this contact with the information in the
         * given contact. Non present fields will be removed, new fields will be
@@ -470,7 +480,7 @@ public class Contact {
        public void updateFrom(Contact vc) {
                updateBKeys(false);
 
-               List<Data> newDatas = new LinkedList<Data>(vc.datas);
+               List<Data> newDatas = new LinkedList<Data>(vc);
                for (int i = 0; i < newDatas.size(); i++) {
                        Data data = newDatas.get(i);
                        int bkey = Parser.getBKey(data);
@@ -481,11 +491,29 @@ public class Contact {
                        }
                }
 
-               this.datas = newDatas;
+               replaceListContent(newDatas);
                this.nextBKey = vc.nextBKey;
+       }
+
+       @Override
+       public String getId() {
+               return "" + getPreferredDataValue("UID");
+       }
 
-               setParent(parent);
-               setDirty();
+       @Override
+       public String getState() {
+               return getId();
+       }
+
+       /**
+        * Return a {@link String} representation of this contact, in vCard 2.1,
+        * without BKeys.
+        * 
+        * @return the {@link String} representation
+        */
+       @Override
+       public String toString() {
+               return "[Contact: " + getPreferredDataValue("FN") + "]";
        }
 
        /**
@@ -504,7 +532,7 @@ public class Contact {
                        binaries = new HashMap<Integer, Data>();
                }
 
-               for (Data data : datas) {
+               for (Data data : this) {
                        if (data.isBinary() && (data.getB64Key() <= 0 || force)) {
                                binaries.put(nextBKey, data);
                                data.resetB64Key(nextBKey++);
@@ -512,24 +540,80 @@ public class Contact {
                }
        }
 
-       public boolean isDirty() {
-               return dirty;
+       /**
+        * Load the data from the given {@link File} under the given {@link Format}.
+        * 
+        * @param file
+        *            the {@link File} to load from
+        * @param format
+        *            the {@link Format} to load as
+        * 
+        * @return the list of elements
+        * @throws IOException
+        *             in case of IO error
+        */
+       static private List<Data> load(List<Data> content) {
+               List<Data> datas = new ArrayList<Data>();
+
+               boolean fn = false;
+               boolean n = false;
+               boolean uid = false;
+               if (content != null) {
+                       for (Data data : content) {
+                               if (data.getName().equals("N")) {
+                                       n = true;
+                               } else if (data.getName().equals("FN")) {
+                                       fn = true;
+                               } else if (data.getName().equals("UID")) {
+                                       uid = true;
+                               }
+
+                               if (!data.getName().equals("VERSION")) {
+                                       datas.add(data);
+                               }
+                       }
+               }
+
+               // required fields:
+               if (!n) // required since vCard 3.0, supported in 2.1
+                       datas.add(new Data(null, "N", "", null));
+               if (!fn) // not required anymore but still supported in 4.0
+                       datas.add(new Data(null, "FN", "", null));
+               if (!uid) // supported by vCard, required by this program
+                       datas.add(new Data(null, "UID", UUID.randomUUID().toString(), null));
+
+               return datas;
        }
 
        /**
-        * Notify that this element has unsaved changes, and notify its parent of
-        * the same if any.
+        * Add a {@link String} to the given {@link List}, but make sure it does not
+        * exceed the maximum size, and truncate it if needed to fit.
+        * 
+        * @param list
+        * @param add
+        * @param currentSize
+        * @param maxSize
+        * @return
         */
-       protected void setDirty() {
-               this.dirty = true;
-               if (this.parent != null)
-                       this.parent.setDirty();
-       }
+       static private int addToList(List<String> list, String add,
+                       int currentSize, int maxSize) {
+               if (add == null || add.length() == 0) {
+                       if (add != null)
+                               list.add(add);
+                       return 0;
+               }
 
-       public void setParent(Card parent) {
-               this.parent = parent;
-               for (Data data : datas) {
-                       data.setParent(this);
+               if (maxSize > -1) {
+                       if (currentSize < maxSize) {
+                               if (currentSize + add.length() >= maxSize) {
+                                       add = add.substring(0, maxSize - currentSize);
+                               }
+                       } else {
+                               add = "";
+                       }
                }
+
+               list.add(add);
+               return add.length();
        }
 }