VCard format: correctly co/decode escaped values
authorNiki Roo <niki@nikiroo.be>
Sat, 26 Mar 2016 19:23:01 +0000 (20:23 +0100)
committerNiki Roo <niki@nikiroo.be>
Sat, 26 Mar 2016 19:23:01 +0000 (20:23 +0100)
src/be/nikiroo/jvcard/BaseClass.java
src/be/nikiroo/jvcard/Data.java
src/be/nikiroo/jvcard/TypeInfo.java
src/be/nikiroo/jvcard/parsers/Vcard21Parser.java
src/be/nikiroo/jvcard/tui/panes/ContactDetails.java
src/be/nikiroo/jvcard/tui/panes/ContactDetailsRaw.java

index 1dd76442324ca845d2f603e5dd04c532bd7bdd32..026c5b43c0951a02ceb69e3bbda049be4303945c 100644 (file)
@@ -433,6 +433,39 @@ public abstract class BaseClass<E extends BaseClass<?>> implements List<E> {
                }
        }
 
+       /**
+        * Escape the given value to VCF standard.
+        * 
+        * @param value
+        *            the value to escape
+        * 
+        * @return the escaped value
+        */
+       protected String escape(String value) {
+               if (value == null)
+                       return null;
+
+               return value.replaceAll(",", "\\\\,").replaceAll(";", "\\\\;")
+                               .replaceAll("\n", "\\\\n");
+       }
+
+       /**
+        * Escape the given value to VCF standard.
+        * 
+        * @param value
+        *            the value to escape
+        * 
+        * @return the escaped value
+        */
+       protected String unescape(String value) {
+               if (value == null)
+                       return null;
+
+               return value.replaceAll("\\\\,", ",").replaceAll("\\\\;", ";")
+
+               .replaceAll("\\\\n", "\n");
+       }
+
        /**
         * Each element that leaves the parent will pass trough here.
         * 
index 63362332004fede408a44ee37d2fc3460fa0a64d..d6ba628d3353fa2f1b22d0f1440790a06c268631 100644 (file)
@@ -1,6 +1,7 @@
 package be.nikiroo.jvcard;
 
 import java.security.InvalidParameterException;
+import java.util.LinkedList;
 import java.util.List;
 
 /**
@@ -68,6 +69,15 @@ public class Data extends BaseClass<TypeInfo> {
         * @return the value
         */
        public String getValue() {
+               return unescape(value);
+       }
+       
+       /**
+        * Return the RAW value of this {@link Data}
+        * 
+        * @return the RAW value
+        */
+       public String getRawValue() {
                return value;
        }
 
@@ -78,6 +88,8 @@ public class Data extends BaseClass<TypeInfo> {
         *            the new value
         */
        public void setValue(String value) {
+               value = escape(value);
+
                if ((value == null && this.value != null)
                                || (value != null && !value.equals(this.value))) {
                        this.value = value;
@@ -85,6 +97,45 @@ public class Data extends BaseClass<TypeInfo> {
                }
        }
 
+       /**
+        * Return the {@link List} of comma-listed values from this {@link Data}.
+        * 
+        * @return the {@link List} of values
+        */
+       public List<String> getValues() {
+               return getList(',');
+       }
+
+       /**
+        * Set the {@link List} of comma-listed values from this {@link Data}.
+        * 
+        * @param values
+        *            the {@link List} of values
+        */
+       public void setValues(List<String> values) {
+               setList(values, ',');
+       }
+
+       /**
+        * Return the {@link List} of semi-column-listed fields from this
+        * {@link Data}.
+        * 
+        * @return the {@link List} of values
+        */
+       public List<String> getFields() {
+               return getList(';');
+       }
+
+       /**
+        * Set the {@link List} of comma-listed values from this {@link Data}.
+        * 
+        * @param values
+        *            the {@link List} of values
+        */
+       public void setFields(List<String> values) {
+               setList(values, ';');
+       }
+
        /**
         * Return the group of this {@link Data}
         * 
@@ -117,6 +168,30 @@ public class Data extends BaseClass<TypeInfo> {
                return b64;
        }
 
+       /**
+        * Check if this {@link Data} is binary
+        * 
+        * @return TRUE if it is
+        */
+       public boolean isBinary() {
+               return b64 >= 0;
+       }
+
+       /**
+        * Check if this {@link Data} has the "preferred" flag.
+        * 
+        * @return TRUE if it has
+        */
+       public boolean isPreferred() {
+               for (TypeInfo type : this) {
+                       if (type.getName().equals("TYPE") && type.getValue().equals("pref")) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
        /**
         * Change the bkey of this {@link Data}
         * 
@@ -138,27 +213,60 @@ public class Data extends BaseClass<TypeInfo> {
        }
 
        /**
-        * Check if this {@link Data} is binary
+        * Return the {@link List} of sep-listed values from this {@link String}
+        * data.
         * 
-        * @return TRUE if it is
+        * @param value
+        *            the data
+        * 
+        * @param the
+        *            separator
+        * 
+        * @return the {@link List} of values
         */
-       public boolean isBinary() {
-               return b64 >= 0;
+       private List<String> getList(char sep) {
+               List<String> rep = new LinkedList<String>();
+
+               if (value != null && value.length() > 0) {
+                       int last = 0;
+                       for (int i = 0; i < value.length(); i++) {
+                               if (value.charAt(i) == sep
+                                               && (i == 0 || value.charAt(i - 1) != '\\')) {
+                                       rep.add(value.substring(last, i - last));
+                               }
+                       }
+
+                       rep.add(value.substring(last));
+               }
+
+               return rep;
        }
 
        /**
-        * Check if this {@link Data} has the "preferred" flag.
+        * Create the {@link String}-encoded {@link List} of sep-listed values from
+        * the given values.
         * 
-        * @return TRUE if it has
+        * @param values
+        *            the {@link List} of values
+        * 
+        * @param sep
+        *            the separator
+        * 
+        * @return the {@link String}
         */
-       public boolean isPreferred() {
-               for (TypeInfo type : this) {
-                       if (type.getName().equals("TYPE") && type.getValue().equals("pref")) {
-                               return true;
-                       }
+       private void setList(List<String> values, char sep) {
+               StringBuilder builder = new StringBuilder();
+               boolean first = true;
+               for (String value : values) {
+                       if (!first)
+                               builder.append(sep);
+
+                       builder.append(escape(value));
+
+                       first = false;
                }
 
-               return false;
+               value = builder.toString();
        }
 
        @Override
index 847cc25189a65ee1e58291fce5d7813193b11e83..c172996bdb64e939a1718787ac48111a3e4aa87c 100644 (file)
@@ -6,7 +6,6 @@ package be.nikiroo.jvcard;
  * @author niki
  *
  */
-@SuppressWarnings("rawtypes")
 public class TypeInfo extends BaseClass<TypeInfo> {
        private String name;
        private String value;
@@ -19,12 +18,11 @@ public class TypeInfo extends BaseClass<TypeInfo> {
         * @param value
         *            its value (<b>MUST NOT</b> be NULL)
         */
-       @SuppressWarnings("unchecked")
        public TypeInfo(String name, String value) {
                super(null);
 
                this.name = name.toUpperCase();
-               this.value = value.toString(); // crash NOW if null
+               this.value = escape(value.toString()); // crash NOW if null
        }
 
        /**
@@ -42,6 +40,15 @@ public class TypeInfo extends BaseClass<TypeInfo> {
         * @return the value
         */
        public String getValue() {
+               return unescape(value);
+       }
+
+       /**
+        * Return the RAW value
+        * 
+        * @return the RAW value
+        */
+       public String getRawValue() {
                return value;
        }
 
index 61f901606496031c6001425e3a66a138680eaa57..5388783a48cda3bd75ddfb7d199d8ceaa92fcf89 100644 (file)
@@ -201,13 +201,13 @@ public class Vcard21Parser {
                        dataBuilder.append(type.getName());
                        if (type.getValue() != null && !type.getValue().trim().equals("")) {
                                dataBuilder.append('=');
-                               dataBuilder.append(type.getValue());
+                               dataBuilder.append(type.getRawValue());
                        }
                }
                dataBuilder.append(':');
 
                // TODO: bkey!
-               dataBuilder.append(data.getValue());
+               dataBuilder.append(data.getRawValue());
 
                // RFC says: Content lines SHOULD be folded to a maximum width of 75
                // octets -> since it is SHOULD, we will just cut it as 74/75 chars
index 4cbbdf7d38e2e27a6264880123c647d3fd2f7410..1d6beb9998bf01b461fb4590746edf1bb4d13868 100644 (file)
@@ -151,23 +151,25 @@ public class ContactDetails extends MainContent {
                                        if (all) {
                                                for (Data data : contact.getData(field)) {
                                                        if (data.isPreferred()) {
-                                                               infoPanel
-                                                                               .addComponent(el.createLabel(StringUtils
-                                                                                               .padString(label, labelSize)
-                                                                                               + contact
-                                                                                                               .getPreferredDataValue(field)));
+                                                               infoPanel.addComponent(el
+                                                                               .createLabel(StringUtils.padString(
+                                                                                               label, labelSize)
+                                                                                               + data.toString()));
                                                        } else {
                                                                infoPanel
-                                                                               .addComponent(UiColors.Element.VIEW_CONTACT_NORMAL.createLabel(StringUtils
-                                                                                               .padString(label, labelSize)
-                                                                                               + contact
-                                                                                                               .getPreferredDataValue(field)));
+                                                                               .addComponent(UiColors.Element.VIEW_CONTACT_NORMAL
+                                                                                               .createLabel(StringUtils
+                                                                                                               .padString(label,
+                                                                                                                               labelSize)
+                                                                                                               + data.toString()));
                                                        }
                                                }
                                        } else {
+                                               String val = contact.getPreferredDataValue(field);
+                                               if (val == null)
+                                                       val = "";
                                                infoPanel.addComponent(el.createLabel(StringUtils
-                                                               .padString(label, labelSize)
-                                                               + contact.getPreferredDataValue(field)));
+                                                               .padString(label, labelSize) + val));
                                        }
                                } else {
                                        String label = info;
@@ -184,7 +186,7 @@ public class ContactDetails extends MainContent {
                        String notes = contact.getPreferredDataValue("NOTE");
                        if (notes == null)
                                notes = "";
-                       note.setText(notes.replaceAll("\\\\n", "\n"));
+                       note.setText(notes);
 
                        Data photo = contact.getPreferredData("PHOTO");
                        if (photo != null) {
@@ -319,7 +321,7 @@ public class ContactDetails extends MainContent {
                        } else {
                                // TODO: configure size?
                                int w = getSize().getColumns() - 40;
-                               int h = getSize().getRows() - 5;
+                               int h = getSize().getRows() - 9;
                                if (w <= 0 || h <= 0)
                                        return null;
 
index d0cc8a872543b4b5942464ac2e7ccaca9ded720e..97fede0af7022a6c6b76ac0f56fd768ccb452a23 100644 (file)
@@ -61,7 +61,7 @@ public class ContactDetailsRaw extends MainContentList {
                        public String getDefaultAnswer() {
                                Data data = getData();
                                if (data != null) {
-                                       return data.getValue();
+                                       return data.getValue().replaceAll("\n", "\\\\n");
                                }
 
                                return null;
@@ -71,7 +71,7 @@ public class ContactDetailsRaw extends MainContentList {
                        public String callback(String answer) {
                                Data data = getData();
                                if (data != null) {
-                                       data.setValue(answer);
+                                       data.setValue(answer.replaceAll("\\\\n", "\n"));
                                        return null;
                                }
 
@@ -297,7 +297,7 @@ public class ContactDetailsRaw extends MainContentList {
 
                StringBuilder valueBuilder = new StringBuilder(" ");
                if (!extMode) {
-                       valueBuilder.append(data.getValue());
+                       valueBuilder.append(data.getValue().replaceAll("\n", "\\\\n"));
                        if (data.getGroup() != null && data.getGroup().length() > 0) {
                                valueBuilder.append("(");
                                valueBuilder.append(data.getGroup());
@@ -362,8 +362,7 @@ public class ContactDetailsRaw extends MainContentList {
                if (builder == null)
                        builder = new StringBuilder();
 
-               for (int indexType = 0; indexType < data.size(); indexType++) {
-                       TypeInfo type = data.get(indexType);
+               for (TypeInfo type : data) {
                        if (builder.length() > 1)
                                builder.append(", ");
                        builder.append(type.getName().replaceAll(",", "\\,"));