Update to version 1.5.0 (breaking Bundle/Meta)
authorNiki Roo <niki@nikiroo.be>
Thu, 22 Jun 2017 19:04:17 +0000 (21:04 +0200)
committerNiki Roo <niki@nikiroo.be>
Thu, 22 Jun 2017 19:04:17 +0000 (21:04 +0200)
20 files changed:
VERSION
changelog
src/be/nikiroo/utils/IOUtils.java
src/be/nikiroo/utils/StringUtils.java
src/be/nikiroo/utils/resources/Bundle.java
src/be/nikiroo/utils/resources/Meta.java
src/be/nikiroo/utils/resources/TransBundle.java
src/be/nikiroo/utils/resources/package-info.java
src/be/nikiroo/utils/serial/CustomSerializer.java [new file with mode: 0644]
src/be/nikiroo/utils/serial/Exporter.java [new file with mode: 0644]
src/be/nikiroo/utils/serial/Importer.java [new file with mode: 0644]
src/be/nikiroo/utils/serial/SerialUtils.java [new file with mode: 0644]
src/be/nikiroo/utils/test/BundleTest.java
src/be/nikiroo/utils/test/SerialTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test/StringUtilsTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test/Test.java
src/be/nikiroo/utils/test/TestCase.java
src/be/nikiroo/utils/test/TestLauncher.java
src/be/nikiroo/utils/ui/ConfigEditor.java
src/be/nikiroo/utils/ui/ConfigItem.java

diff --git a/VERSION b/VERSION
index 428b770e3e234822240cb1d877687486143a0161..bc80560fad66ca670bdfbd1e5c973a024d4d0325 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.4.3
+1.5.0
index 57bc02984e9c94a0e72994b17130bfa6480a2257..0fc8a93dfd1e61edb15a9cc22ca7043dcd416743 100644 (file)
--- a/changelog
+++ b/changelog
@@ -1,3 +1,15 @@
+Version 1.5.0
+-------------
+
+Bundles: change in Bundles and meta data
+       The meta data is more complete now, but it breaks compatibility with
+       both Bundles and @Meta
+       A description can now be added to a bundle item in the graphical
+       editor as a tooltip
+
+Serialisation utilities
+       A new set of utilities to quickly serialise objects
+
 Version 1.4.3
 -------------
 
index 4a185c6386ba31b84a474540f82d78aa25d691dc..b0566432aeb7fad7aae1938e7162a9810ed8ae2a 100644 (file)
@@ -49,7 +49,7 @@ public class IOUtils {
         * 
         * @param in
         *            the data source
-        * @param target
+        * @param out
         *            the target {@link OutputStream}
         * 
         * @throws IOException
@@ -118,7 +118,7 @@ public class IOUtils {
         *            the source {@link File} (which can be a directory)
         * @param dest
         *            the destination <tt>.zip</tt> file
-        * @param srctIsRoot
+        * @param srcIsRoot
         *            FALSE if we need to add a {@link ZipEntry} for src, TRUE to
         *            add it at the root of the ZIP
         * 
index adebd7a50022b3f9d838a3b5e5d68345cea6e048..dc40e878d9c68a3b8b25af2ba03011ef39f57f7c 100644 (file)
@@ -4,6 +4,7 @@ import java.awt.Image;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -14,7 +15,9 @@ import java.text.Normalizer.Form;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.Scanner;
 import java.util.regex.Pattern;
+import java.util.zip.ZipInputStream;
 
 import javax.imageio.ImageIO;
 
@@ -186,15 +189,15 @@ public class StringUtils {
         * Convert between time as a {@link String} to milliseconds in a "static"
         * way (to exchange data over the wire, for instance).
         * 
-        * @param time
+        * @param displayTime
         *            the time as a {@link String}
         * 
         * @return the time in milliseconds
         */
-       static public long toTime(String display) {
+       static public long toTime(String displayTime) {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                try {
-                       return sdf.parse(display).getTime();
+                       return sdf.parse(displayTime).getTime();
                } catch (ParseException e) {
                        return -1;
                }
@@ -227,11 +230,11 @@ public class StringUtils {
        }
 
        /**
-        * Convert the given {@link File} image into a Base64 representation of the
-        * same {@link File}.
+        * Convert the given image into a Base64 representation of the same
+        * {@link File}.
         * 
-        * @param file
-        *            the {@link File} image to convert
+        * @param in
+        *            the image to convert
         * 
         * @return the Base64 representation
         * 
@@ -366,4 +369,26 @@ public class StringUtils {
                                HtmlEscapeType.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA,
                                HtmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT);
        }
+
+       public static String zip64(String data) {
+               try {
+                       return Base64.encodeBytes(data.getBytes(), Base64.GZIP);
+               } catch (IOException e) {
+                       e.printStackTrace();
+                       return null;
+               }
+       }
+
+       public static String unzip64(String data) throws IOException {
+               ByteArrayInputStream in = new ByteArrayInputStream(Base64.decode(data,
+                               Base64.GZIP));
+
+               Scanner scan = new Scanner(in);
+               scan.useDelimiter("\\A");
+               try {
+                       return scan.next();
+               } finally {
+                       scan.close();
+               }
+       }
 }
index 9da8d74836edca0be72584031ba8c83e06bfae65..bad7f3e6c46679c31622b8e34bb8b6b0c0bb955a 100644 (file)
@@ -28,29 +28,46 @@ import java.util.ResourceBundle;
  * It also sports a writable change map, and you can save back the
  * {@link Bundle} to file with {@link Bundle#updateFile(String)}.
  * 
- * @author niki
- * 
  * @param <E>
  *            the enum to use to get values out of this class
+ * 
+ * @author niki
  */
+
 public class Bundle<E extends Enum<E>> {
+       /** The type of E. */
        protected Class<E> type;
-       protected Enum<?> name;
-       private Map<String, String> map; // R/O map
-       private Map<String, String> changeMap; // R/W map
+       /**
+        * The {@link Enum} associated to this {@link Bundle} (all the keys used in
+        * this {@link Bundle} will be of this type).
+        */
+       protected Enum<?> keyType;
+
+       private TransBundle<E> descriptionBundle;
+
+       /** R/O map */
+       private Map<String, String> map;
+       /** R/W map */
+       private Map<String, String> changeMap;
 
        /**
         * Create a new {@link Bundles} of the given name.
         * 
         * @param type
         *            a runtime instance of the class of E
-        * 
         * @param name
         *            the name of the {@link Bundles}
+        * @param descriptionBundle
+        *            the description {@link TransBundle}, that is, a
+        *            {@link TransBundle} dedicated to the description of the values
+        *            of the given {@link Bundle} (can be NULL)
         */
-       protected Bundle(Class<E> type, Enum<?> name) {
+       protected Bundle(Class<E> type, Enum<?> name,
+                       TransBundle<E> descriptionBundle) {
                this.type = type;
-               this.name = name;
+               this.keyType = name;
+               this.descriptionBundle = descriptionBundle;
+
                this.map = new HashMap<String, String>();
                this.changeMap = new HashMap<String, String>();
                setBundle(name, Locale.getDefault(), false);
@@ -59,7 +76,7 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Return the value associated to the given id as a {@link String}.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * 
         * @return the associated value, or NULL if not found (not present in the
@@ -72,7 +89,7 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Set the value associated to the given id as a {@link String}.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * @param value
         *            the value
@@ -88,7 +105,7 @@ public class Bundle<E extends Enum<E>> {
         * <p>
         * Will only accept suffixes that form an existing id.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * @param suffix
         *            the runtime suffix
@@ -116,7 +133,7 @@ public class Bundle<E extends Enum<E>> {
         * <p>
         * Will only accept suffixes that form an existing id.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * @param suffix
         *            the runtime suffix
@@ -138,7 +155,7 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Return the value associated to the given id as a {@link Boolean}.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * 
         * @return the associated value
@@ -161,7 +178,7 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Return the value associated to the given id as a {@link Boolean}.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * @param def
         *            the default value when it is not present in the config file or
@@ -180,7 +197,7 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Return the value associated to the given id as an {@link Integer}.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * 
         * @return the associated value
@@ -195,9 +212,9 @@ public class Bundle<E extends Enum<E>> {
        }
 
        /**
-        * Return the value associated to the given id as a {@link int}.
+        * Return the value associated to the given id as an int.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * @param def
         *            the default value when it is not present in the config file or
@@ -216,7 +233,7 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Return the value associated to the given id as a {@link Character}.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * 
         * @return the associated value
@@ -233,7 +250,7 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Return the value associated to the given id as a {@link Character}.
         * 
-        * @param mame
+        * @param id
         *            the id of the value to get
         * @param def
         *            the default value when it is not present in the config file or
@@ -253,8 +270,8 @@ public class Bundle<E extends Enum<E>> {
        /**
         * Return the value associated to the given id as a {@link Color}.
         * 
-        * @param the
-        *            id of the value to get
+        * @param id
+        *            the id of the value to get
         * 
         * @return the associated value
         */
@@ -277,18 +294,41 @@ public class Bundle<E extends Enum<E>> {
                        }
                }
 
+               // Try by name if still not found
+               if (color == null) {
+                       try {
+                               Field field = Color.class.getField(bg);
+                               color = (Color) field.get(null);
+                       } catch (Exception e) {
+                       }
+               }
+               //
+
                return color;
        }
 
        /**
         * Set the value associated to the given id as a {@link Color}.
         * 
-        * @param the
-        *            id of the value to get
-        * 
-        * @return the associated value
+        * @param id
+        *            the id of the value to set
+        * @param color
+        *            the new color
         */
        public void setColor(E id, Color color) {
+               // Check for named colours first
+               try {
+                       Field[] fields = Color.class.getFields();
+                       for (Field field : fields) {
+                               if (field.equals(color)) {
+                                       setString(id, field.getName());
+                                       return;
+                               }
+                       }
+               } catch (Exception e) {
+               }
+               //
+
                String r = Integer.toString(color.getRed(), 16);
                String g = Integer.toString(color.getGreen(), 16);
                String b = Integer.toString(color.getBlue(), 16);
@@ -359,6 +399,17 @@ public class Bundle<E extends Enum<E>> {
                writer.close();
        }
 
+       /**
+        * The description {@link TransBundle}, that is, a {@link TransBundle}
+        * dedicated to the description of the values of the given {@link Bundle}
+        * (can be NULL).
+        * 
+        * @return the description {@link TransBundle}
+        */
+       public TransBundle<E> getDescriptionBundle() {
+               return descriptionBundle;
+       }
+
        /**
         * Reload the {@link Bundle} data files.
         * 
@@ -368,7 +419,7 @@ public class Bundle<E extends Enum<E>> {
         *            configuration)
         */
        public void reload(boolean resetToDefault) {
-               setBundle(name, Locale.getDefault(), resetToDefault);
+               setBundle(keyType, Locale.getDefault(), resetToDefault);
        }
 
        /**
@@ -427,44 +478,45 @@ public class Bundle<E extends Enum<E>> {
         * @return the information to display or NULL if none
         */
        protected String getMetaInfo(Meta meta) {
-               String what = meta.what();
-               String where = meta.where();
-               String format = meta.format();
+               String desc = meta.description();
+               boolean group = meta.group();
+               Meta.Format format = meta.format();
+               String[] list = meta.list();
+               boolean nullable = meta.nullable();
                String info = meta.info();
+               boolean array = meta.array();
 
-               int opt = what.length() + where.length() + format.length();
-               if (opt + info.length() == 0)
+               // Default, empty values -> NULL
+               if (desc.length() + list.length + info.length() == 0 && !group
+                               && nullable && format == Meta.Format.STRING) {
                        return null;
+               }
 
                StringBuilder builder = new StringBuilder();
-               builder.append("# ");
-
-               if (opt > 0) {
-                       builder.append("(");
-                       if (what.length() > 0) {
-                               builder.append("WHAT: " + what);
-                               if (where.length() + format.length() > 0)
-                                       builder.append(", ");
-                       }
-
-                       if (where.length() > 0) {
-                               builder.append("WHERE: " + where);
-                               if (format.length() > 0)
-                                       builder.append(", ");
-                       }
+               builder.append("# ").append(desc);
+               if (desc.length() > 20) {
+                       builder.append("\n#");
+               }
 
-                       if (format.length() > 0) {
-                               builder.append("FORMAT: " + format);
+               if (group) {
+                       builder.append("This item is used as a group, its content is not expected to be used.");
+               } else {
+                       builder.append(" (FORMAT: ").append(format)
+                                       .append(nullable ? "" : " (required)");
+                       builder.append(") ").append(info);
+
+                       if (list.length > 0) {
+                               builder.append("\n# ALLOWED VALUES:");
+                               for (String value : list) {
+                                       builder.append(" \"").append(value).append("\"");
+                               }
                        }
 
-                       builder.append(")");
-                       if (info.length() > 0) {
-                               builder.append("\n# ");
+                       if (array) {
+                               builder.append("\n# (This item accept a list of comma-separated values)");
                        }
                }
 
-               builder.append(info);
-
                return builder.toString();
        }
 
@@ -474,7 +526,7 @@ public class Bundle<E extends Enum<E>> {
         * @return the name
         */
        protected String getBundleDisplayName() {
-               return name.toString();
+               return keyType.toString();
        }
 
        /**
@@ -548,12 +600,9 @@ public class Bundle<E extends Enum<E>> {
         *            the path where the .properties files are
         * 
         * @return the source {@link File}
-        * 
-        * @throws IOException
-        *             in case of IO errors
         */
        protected File getUpdateFile(String path) {
-               return new File(path, name.name() + ".properties");
+               return new File(path, keyType.name() + ".properties");
        }
 
        /**
@@ -660,9 +709,9 @@ public class Bundle<E extends Enum<E>> {
         * Return the resource file that is closer to the {@link Locale}.
         * 
         * @param dir
-        *            the dirctory to look into
+        *            the directory to look into
         * @param name
-        *            the file basename (without <tt>.properties</tt>)
+        *            the file base name (without <tt>.properties</tt>)
         * @param locale
         *            the {@link Locale}
         * 
index 3e14557ba85a3c9c899297abc43c0847e04fb9c9..d377dcc5c10c1c08ed0c7e98a4ae6a3f7be5bbd8 100644 (file)
@@ -15,32 +15,95 @@ import java.lang.annotation.Target;
 @Target(ElementType.FIELD)
 public @interface Meta {
        /**
-        * What kind of item this key represent (a Key, a Label text, a format to
-        * use for something else...).
+        * The format of an item (the values it is expected to be of).
+        * <p>
+        * Note that the INI file can contain arbitrary data, but it is expected to
+        * be valid.
+        * 
+        * @author niki
+        */
+       public enum Format {
+               /** An integer value, can be negative. */
+               INT,
+               /** true or false. */
+               BOOLEAN,
+               /** Any text String. */
+               STRING,
+               /** A password field. */
+               PASSWORD,
+               /** A colour (either by name or #rrggbb or #aarrggbb). */
+               COLOR,
+               /** A locale code (e.g., fr-BE, en-GB, es...). */
+               LOCALE,
+               /** A path to a file. */
+               FILE,
+               /** A path to a directory. */
+               DIRECTORY,
+               /** A fixed list of values (see {@link Meta#list()} for the values). */
+               FIXED_LIST,
+               /**
+                * A fixed list of values (see {@link Meta#list()} for the values) OR a
+                * custom String value (basically, a {@link Format#FIXED_LIST} with an
+                * option to enter a not accounted for value).
+                */
+               COMBO_LIST
+       }
+
+       /**
+        * A description of this item.
         * 
         * @return what it is
         */
-       String what();
+       String description() default "";
 
        /**
-        * Where in the application will this key appear (in the action keys, in a
-        * menu, in a message...).
+        * This item is only used as a group, not as an option.
+        * <p>
+        * For instance, you could have LANGUAGE_CODE as a group for which you won't
+        * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN
+        * inside for which the value must be set.
         * 
-        * @return where it is
+        * @return the group
         */
-       String where();
+       boolean group() default false;
 
        /**
         * What format should/must this key be in.
         * 
         * @return the format it is in
         */
-       String format();
+       Format format() default Format.STRING;
+
+       /**
+        * The list of fixed values this item can be (either for
+        * {@link Format#FIXED_LIST} or {@link Format#COMBO_LIST}).
+        * 
+        * @return the list of values
+        */
+       String[] list() default {};
+
+       /**
+        * This item can be left unspecified.
+        * 
+        * @return TRUE if it can
+        */
+       boolean nullable() default true;
+
+       /**
+        * This item is a comma-separated list of values instead of a single value.
+        * 
+        * @return TRUE if it is
+        */
+       boolean array() default false;
 
        /**
-        * Free info text to help translate.
+        * An addition to the format.
+        * <p>
+        * Free info text to help translate, for instance the parameters order and
+        * type for String translations (i.e., %s = input file name, %d = file size
+        * in MB).
         * 
         * @return some info
         */
-       String info();
+       String info() default "";
 }
index 51101b0441ed56ee7648a9f4543ebfc4b53d44d9..51e090f6fab208f7bb6eb3f40fcb3910fe5c7901 100644 (file)
@@ -17,6 +17,9 @@ import java.util.regex.Pattern;
  * <li>DUMMY will return "[DUMMY]" (maybe with a suffix and/or "NOUTF")</li>
  * </ul>
  * 
+ * @param <E>
+ *            the enum to use to get values out of this class
+ * 
  * @author niki
  */
 public class TransBundle<E extends Enum<E>> extends Bundle<E> {
@@ -33,7 +36,7 @@ public class TransBundle<E extends Enum<E>> extends Bundle<E> {
         *            the name of the {@link Bundles}
         */
        public TransBundle(Class<E> type, Enum<?> name) {
-               super(type, name);
+               super(type, name, null);
                setLanguage(null);
        }
 
@@ -49,7 +52,7 @@ public class TransBundle<E extends Enum<E>> extends Bundle<E> {
         *            the language to use
         */
        public TransBundle(Class<E> type, Enum<?> name, String language) {
-               super(type, name);
+               super(type, name, null);
                setLanguage(language);
        }
 
@@ -156,7 +159,7 @@ public class TransBundle<E extends Enum<E>> extends Bundle<E> {
         * @return the known language codes
         */
        public List<String> getKnownLanguages() {
-               return getKnownLanguages(name);
+               return getKnownLanguages(keyType);
        }
 
        /**
@@ -169,12 +172,12 @@ public class TransBundle<E extends Enum<E>> extends Bundle<E> {
        private void setLanguage(String language) {
                defaultLocale = (language == null || language.length() == 0);
                locale = getLocaleFor(language);
-               setBundle(name, locale, false);
+               setBundle(keyType, locale, false);
        }
 
        @Override
        public void reload(boolean resetToDefault) {
-               setBundle(name, locale, resetToDefault);
+               setBundle(keyType, locale, resetToDefault);
        }
 
        @Override
@@ -224,10 +227,10 @@ public class TransBundle<E extends Enum<E>> extends Bundle<E> {
                String code = locale.toString();
                File file = null;
                if (!defaultLocale && code.length() > 0) {
-                       file = new File(path, name.name() + "_" + code + ".properties");
+                       file = new File(path, keyType.name() + "_" + code + ".properties");
                } else {
                        // Default properties file:
-                       file = new File(path, name.name() + ".properties");
+                       file = new File(path, keyType.name() + ".properties");
                }
 
                return file;
index 783cd03e3429617c14b7f9de408aeb4c88568307..bda940b99531bf3ce8ef84b39c97c4c1524a89fb 100644 (file)
@@ -1,6 +1,6 @@
 /**
  * This package encloses the classes needed to use 
- * {@link be.nikiroo.utils.resources.bundles.Bundle}s
+ * {@link be.nikiroo.utils.resources.Bundle}s
  * <p>
  * Those are basically a <tt>.properties</tt> resource linked to an enumeration
  * listing all the fields you can use. The classes can also be used to update
diff --git a/src/be/nikiroo/utils/serial/CustomSerializer.java b/src/be/nikiroo/utils/serial/CustomSerializer.java
new file mode 100644 (file)
index 0000000..b6928ee
--- /dev/null
@@ -0,0 +1,43 @@
+package be.nikiroo.utils.serial;
+
+public abstract class CustomSerializer {
+
+       protected abstract String toString(Object value);
+
+       protected abstract Object fromString(String content);
+
+       protected abstract String getType();
+
+       public void encode(StringBuilder builder, Object value) {
+               String customString = toString(value);
+               builder.append("custom:").append(getType()).append(":");
+               SerialUtils.encode(builder, customString);
+       }
+
+       public Object decode(String encodedValue) {
+               return fromString((String) SerialUtils.decode(contentOf(encodedValue)));
+       }
+
+       public static boolean isCustom(String encodedValue) {
+               int pos1 = encodedValue.indexOf(':');
+               int pos2 = encodedValue.indexOf(':', pos1 + 1);
+
+               return pos1 >= 0 && pos2 >= 0 && encodedValue.startsWith("custom:");
+       }
+
+       public static String typeOf(String encodedValue) {
+               int pos1 = encodedValue.indexOf(':');
+               int pos2 = encodedValue.indexOf(':', pos1 + 1);
+               String type = encodedValue.substring(pos1 + 1, pos2);
+
+               return type;
+       }
+
+       public static String contentOf(String encodedValue) {
+               int pos1 = encodedValue.indexOf(':');
+               int pos2 = encodedValue.indexOf(':', pos1 + 1);
+               String encodedContent = encodedValue.substring(pos2 + 1);
+
+               return encodedContent;
+       }
+}
diff --git a/src/be/nikiroo/utils/serial/Exporter.java b/src/be/nikiroo/utils/serial/Exporter.java
new file mode 100644 (file)
index 0000000..8b04111
--- /dev/null
@@ -0,0 +1,58 @@
+package be.nikiroo.utils.serial;
+
+import java.io.NotSerializableException;
+import java.util.HashMap;
+import java.util.Map;
+
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * A simple class to serialise objects to {@link String}.
+ * <p>
+ * This class does not support inner classes (it does support nested classes,
+ * though).
+ * 
+ * @author niki
+ */
+public class Exporter {
+       private Map<Integer, Object> map;
+       private StringBuilder builder;
+
+       public Exporter() {
+               map = new HashMap<Integer, Object>();
+               builder = new StringBuilder();
+       }
+
+       public Exporter append(Object o) throws NotSerializableException {
+               SerialUtils.append(builder, o, map);
+               return this;
+       }
+
+       public void clear() {
+               builder.setLength(0);
+               map.clear();
+       }
+
+       // null = auto
+       public String toString(Boolean zip) {
+               if (zip == null) {
+                       zip = builder.length() > 128;
+               }
+
+               if (zip) {
+                       return "ZIP:" + StringUtils.zip64(builder.toString());
+               } else {
+                       return builder.toString();
+               }
+       }
+
+       /**
+        * The exported items in a serialised form.
+        * 
+        * @return the items currently in this {@link Exporter}.
+        */
+       @Override
+       public String toString() {
+               return toString(null);
+       }
+}
\ No newline at end of file
diff --git a/src/be/nikiroo/utils/serial/Importer.java b/src/be/nikiroo/utils/serial/Importer.java
new file mode 100644 (file)
index 0000000..b3307fd
--- /dev/null
@@ -0,0 +1,223 @@
+package be.nikiroo.utils.serial;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * A simple class that can accept the output of {@link Exporter} to recreate
+ * objects as they were sent to said exporter.
+ * <p>
+ * This class requires the objects (and their potential enclosing objects) to
+ * have an empty constructor, and does not support inner classes (it does
+ * support nested classes, though).
+ * 
+ * @author niki
+ */
+public class Importer {
+       private Boolean link;
+       private Object me;
+       private Importer child;
+       private Map<String, Object> map;
+
+       private String currentFieldName;
+
+       public Importer() {
+               map = new HashMap<String, Object>();
+               map.put("NULL", null);
+       }
+
+       private Importer(Map<String, Object> map) {
+               this.map = map;
+       }
+
+       public Importer readLine(String line) {
+               try {
+                       processLine(line);
+               } catch (Exception e) {
+                       throw new IllegalArgumentException(e);
+               }
+               return this;
+       }
+
+       public Importer read(String data) {
+               try {
+                       if (data.startsWith("ZIP:")) {
+                               data = StringUtils.unzip64(data.substring("ZIP:".length()));
+                       }
+                       Scanner scan = new Scanner(data);
+                       scan.useDelimiter("\n");
+                       while (scan.hasNext()) {
+                               processLine(scan.next());
+                       }
+                       scan.close();
+               } catch (Exception e) {
+                       throw new IllegalArgumentException(e);
+               }
+               return this;
+       }
+
+       public boolean processLine(String line) throws IllegalArgumentException,
+                       NoSuchFieldException, SecurityException, IllegalAccessException,
+                       NoSuchMethodException, InstantiationException, ClassNotFoundException, InvocationTargetException {
+               // Defer to latest child if any
+               if (child != null) {
+                       if (child.processLine(line)) {
+                               if (currentFieldName != null) {
+                                       setField(currentFieldName, child.getValue());
+                                       currentFieldName = null;
+                               }
+                               child = null;
+                       }
+
+                       return false;
+               }
+
+               if (line.equals("{")) { // START: new child if needed
+                       if (link != null) {
+                               child = new Importer(map);
+                       }
+               } else if (line.equals("}")) { // STOP: report self to parent
+                       return true;
+               } else if (line.startsWith("REF ")) { // REF: create/link self
+                       String ref = line.substring(4).split("@")[1];
+                       link = map.containsKey(ref);
+                       if (link) {
+                               me = map.get(ref);
+                       } else {
+                               me = createSelf(line.substring(4).split("@")[0]);
+                               map.put(ref, me);
+                       }
+               } else { // FIELD: new field
+                       if (line.endsWith(":")) {
+                               // field value is compound
+                               currentFieldName = line.substring(0, line.length() - 1);
+                       } else {
+                               // field value is direct
+                               int pos = line.indexOf(":");
+                               String fieldName = line.substring(0, pos);
+                               String encodedValue = line.substring(pos + 1);
+                               Object value = null;
+                               value = SerialUtils.decode(encodedValue);
+
+                               // To support simple types directly:
+                               if (me == null) {
+                                       me = value;
+                               } else {
+                                       setField(fieldName, value);
+                               }
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Create an empty object of the given type.
+        * 
+        * @param type
+        *            the object type
+        * @return the object
+        * 
+        * @throws NoSuchMethodException
+        * @throws SecurityException
+        * @throws InstantiationException
+        * @throws IllegalAccessException
+        * @throws ClassNotFoundException
+        * @throws IllegalArgumentException
+        * @throws InvocationTargetException
+        */
+       private Object createSelf(String type) throws NoSuchMethodException,
+                       SecurityException, InstantiationException, IllegalAccessException,
+                       ClassNotFoundException, IllegalArgumentException,
+                       InvocationTargetException {
+
+               try {
+                       Class<?> clazz = getClass(type);
+                       if (clazz == null) {
+                               throw new ClassNotFoundException("Class not found: " + type);
+                       }
+
+                       String className = clazz.getName();
+                       Object[] args = null;
+                       Constructor<?> ctor = null;
+                       if (className.contains("$")) {
+                               Object javaParent = createSelf(className.substring(0,
+                                               className.lastIndexOf('$')));
+                               args = new Object[] { javaParent };
+                               ctor = clazz.getDeclaredConstructor(new Class[] { javaParent
+                                               .getClass() });
+                       } else {
+                               args = new Object[] {};
+                               ctor = clazz.getDeclaredConstructor();
+                       }
+
+                       ctor.setAccessible(true);
+                       return ctor.newInstance(args);
+               } catch (NoSuchMethodException e) {
+                       throw new NoSuchMethodException(
+                                       String.format(
+                                                       "Objects of type \"%s\" cannot be created by this code: maybe the class"
+                                                                       + " or its enclosing class doesn't have an empty constructor?",
+                                                       type));
+
+               }
+       }
+
+       private Class<?> getClass(String type) throws ClassNotFoundException,
+                       NoSuchMethodException {
+               Class<?> clazz = null;
+               try {
+                       clazz = Class.forName(type);
+               } catch (ClassNotFoundException e) {
+                       int pos = type.length();
+                       pos = type.lastIndexOf(".", pos);
+                       if (pos >= 0) {
+                               String parentType = type.substring(0, pos);
+                               String nestedType = type.substring(pos + 1);
+                               Class<?> javaParent = null;
+                               try {
+                                       javaParent = getClass(parentType);
+                                       parentType = javaParent.getName();
+                                       clazz = Class.forName(parentType + "$" + nestedType);
+                               } catch (Exception ee) {
+                               }
+
+                               if (javaParent == null) {
+                                       throw new NoSuchMethodException(
+                                                       "Class not found: "
+                                                                       + type
+                                                                       + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
+                               }
+                       }
+               }
+
+               return clazz;
+       }
+
+       private void setField(String name, Object value)
+                       throws NoSuchFieldException, SecurityException,
+                       IllegalArgumentException, IllegalAccessException {
+
+               try {
+                       Field field = me.getClass().getDeclaredField(name);
+
+                       field.setAccessible(true);
+                       field.set(me, value);
+               } catch (NoSuchFieldException e) {
+                       throw new NoSuchFieldException(String.format(
+                                       "Field \"%s\" was not found in object of type \"%s\".",
+                                       name, me.getClass().getCanonicalName()));
+               }
+       }
+
+       public Object getValue() {
+               return me;
+       }
+}
\ No newline at end of file
diff --git a/src/be/nikiroo/utils/serial/SerialUtils.java b/src/be/nikiroo/utils/serial/SerialUtils.java
new file mode 100644 (file)
index 0000000..657dbec
--- /dev/null
@@ -0,0 +1,214 @@
+package be.nikiroo.utils.serial;
+
+import java.io.NotSerializableException;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Small class to help serialise/deserialise objects.
+ * <p>
+ * Note that we do not support inner classes (but we do support nested classes)
+ * and all objects require an empty constructor to be deserialised.
+ * 
+ * @author niki
+ */
+class SerialUtils {
+       private static Map<String, CustomSerializer> customTypes;
+
+       static {
+               customTypes = new HashMap<String, CustomSerializer>();
+               // TODO: add "default" custom serialisers
+       }
+
+       static public void addCustomSerializer(CustomSerializer serializer) {
+               customTypes.put(serializer.getType(), serializer);
+       }
+
+       static void append(StringBuilder builder, Object o, Map<Integer, Object> map)
+                       throws NotSerializableException {
+
+               Field[] fields = new Field[] {};
+               String type = "";
+               String id = "NULL";
+
+               if (o != null) {
+                       int hash = System.identityHashCode(o);
+                       fields = o.getClass().getDeclaredFields();
+                       type = o.getClass().getCanonicalName();
+                       if (type == null) {
+                               throw new NotSerializableException(
+                                               String.format(
+                                                               "Cannot find the class for this object: %s (it could be an inner class, which is not supported)",
+                                                               o));
+                       }
+                       id = Integer.toString(hash);
+                       if (map.containsKey(hash)) {
+                               fields = new Field[] {};
+                       } else {
+                               map.put(hash, o);
+                       }
+               }
+
+               builder.append("{\nREF ").append(type).append("@").append(id);
+               try {
+                       for (Field field : fields) {
+                               field.setAccessible(true);
+
+                               if (field.getName().startsWith("this$")) {
+                                       // Do not keep this links of nested classes
+                                       continue;
+                               }
+
+                               builder.append("\n");
+                               builder.append(field.getName());
+                               builder.append(":");
+                               Object value;
+
+                               value = field.get(o);
+
+                               if (!encode(builder, value)) {
+                                       builder.append("\n");
+                                       append(builder, value, map);
+                               }
+                       }
+               } catch (IllegalArgumentException e) {
+                       e.printStackTrace(); // should not happen (see setAccessible)
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace(); // should not happen (see setAccessible)
+               }
+               builder.append("\n}");
+       }
+
+       // return true if encoded (supported)
+       static boolean encode(StringBuilder builder, Object value) {
+               if (value == null) {
+                       builder.append("NULL");
+               } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
+                       customTypes.get(value.getClass().getCanonicalName())//
+                                       .encode(builder, value);
+               } else if (value instanceof String) {
+                       encodeString(builder, (String) value);
+               } else if (value instanceof Boolean) {
+                       builder.append(value);
+               } else if (value instanceof Byte) {
+                       builder.append(value).append('b');
+               } else if (value instanceof Character) {
+                       encodeString(builder, (String) value);
+                       builder.append('c');
+               } else if (value instanceof Short) {
+                       builder.append(value).append('s');
+               } else if (value instanceof Integer) {
+                       builder.append(value);
+               } else if (value instanceof Long) {
+                       builder.append(value).append('L');
+               } else if (value instanceof Float) {
+                       builder.append(value).append('F');
+               } else if (value instanceof Double) {
+                       builder.append(value).append('d');
+               } else {
+                       return false;
+               }
+
+               return true;
+       }
+
+       static Object decode(String encodedValue) {
+               String cut = "";
+               if (encodedValue.length() > 1) {
+                       cut = encodedValue.substring(0, encodedValue.length() - 1);
+               }
+
+               if (CustomSerializer.isCustom(encodedValue)) {
+                       // custom:TYPE_NAME:"content is String-encoded"
+                       String type = CustomSerializer.typeOf(encodedValue);
+                       if (customTypes.containsKey(type)) {
+                               return customTypes.get(type).decode(encodedValue);
+                       } else {
+                               throw new java.util.UnknownFormatConversionException(
+                                               "Unknown custom type: " + type);
+                       }
+               } else if (encodedValue.equals("NULL") || encodedValue.equals("null")) {
+                       return null;
+               } else if (encodedValue.endsWith("\"")) {
+                       return decodeString(encodedValue);
+               } else if (encodedValue.equals("true")) {
+                       return true;
+               } else if (encodedValue.equals("false")) {
+                       return false;
+               } else if (encodedValue.endsWith("b")) {
+                       return Byte.parseByte(cut);
+               } else if (encodedValue.endsWith("c")) {
+                       return decodeString(cut).charAt(0);
+               } else if (encodedValue.endsWith("s")) {
+                       return Short.parseShort(cut);
+               } else if (encodedValue.endsWith("L")) {
+                       return Long.parseLong(cut);
+               } else if (encodedValue.endsWith("F")) {
+                       return Float.parseFloat(cut);
+               } else if (encodedValue.endsWith("d")) {
+                       return Double.parseDouble(cut);
+               } else {
+                       return Integer.parseInt(encodedValue);
+               }
+       }
+
+       // aa bb -> "aa\tbb"
+       private static void encodeString(StringBuilder builder, String raw) {
+               builder.append('\"');
+               for (char car : raw.toCharArray()) {
+                       switch (car) {
+                       case '\\':
+                               builder.append("\\\\");
+                               break;
+                       case '\r':
+                               builder.append("\\r");
+                               break;
+                       case '\n':
+                               builder.append("\\n");
+                               break;
+                       case '"':
+                               builder.append("\\\"");
+                               break;
+                       default:
+                               builder.append(car);
+                               break;
+                       }
+               }
+               builder.append('\"');
+       }
+
+       // "aa\tbb" -> aa bb
+       private static String decodeString(String escaped) {
+               StringBuilder builder = new StringBuilder();
+
+               boolean escaping = false;
+               for (char car : escaped.toCharArray()) {
+                       if (!escaping) {
+                               if (car == '\\') {
+                                       escaping = true;
+                               } else {
+                                       builder.append(car);
+                               }
+                       } else {
+                               switch (car) {
+                               case '\\':
+                                       builder.append('\\');
+                                       break;
+                               case 'r':
+                                       builder.append('\r');
+                                       break;
+                               case 'n':
+                                       builder.append('\n');
+                                       break;
+                               case '"':
+                                       builder.append('"');
+                                       break;
+                               }
+                               escaping = false;
+                       }
+               }
+
+               return builder.substring(1, builder.length() - 1).toString();
+       }
+}
index a881817239fb8644c9e3a12e476561b97e1f9736..c1c379f833bd0e114a54e367151b27a235217138 100644 (file)
@@ -204,7 +204,7 @@ class BundleTest extends TestLauncher {
         */
        private class B extends Bundle<E> {
                protected B() {
-                       super(E.class, N.bundle_test);
+                       super(E.class, N.bundle_test, null);
                }
 
                @Override
@@ -226,13 +226,13 @@ class BundleTest extends TestLauncher {
         * @author niki
         */
        private enum E {
-               @Meta(what = "", where = "", format = "", info = "")
+               @Meta
                ONE, //
-               @Meta(what = "", where = "", format = "", info = "")
+               @Meta
                ONE_SUFFIX, //
-               @Meta(what = "", where = "", format = "", info = "")
+               @Meta
                TWO, //
-               @Meta(what = "", where = "", format = "", info = "")
+               @Meta
                JAPANESE
        }
 
diff --git a/src/be/nikiroo/utils/test/SerialTest.java b/src/be/nikiroo/utils/test/SerialTest.java
new file mode 100644 (file)
index 0000000..f73c39e
--- /dev/null
@@ -0,0 +1,100 @@
+package be.nikiroo.utils.test;
+
+import be.nikiroo.utils.serial.Exporter;
+import be.nikiroo.utils.serial.Importer;
+
+class SerialTest extends TestLauncher {
+       private SerialTest() {
+               super("Serial test", null);
+       }
+
+       public SerialTest(String[] args) {
+               super("Serial test", args);
+
+               addTest(new TestCase("Simple class Import/Export") {
+                       @Override
+                       public void test() throws Exception {
+                               Data data = new Data(42);
+                               String encoded = new Exporter().append(data).toString(false);
+                               Object redata = new Importer().read(encoded).getValue();
+                               String reencoded = new Exporter().append(redata)
+                                               .toString(false);
+
+                               assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
+                                               reencoded.replaceAll("@[0-9]*", "@REF"));
+                       }
+               });
+
+               addTest(new TestCase("Import/Export with nested objects") {
+                       @Override
+                       public void test() throws Exception {
+                               Data data = new DataObject(new Data(21));
+                               String encoded = new Exporter().append(data).toString(false);
+                               Object redata = new Importer().read(encoded).getValue();
+                               String reencoded = new Exporter().append(redata)
+                                               .toString(false);
+
+                               assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
+                                               reencoded.replaceAll("@[0-9]*", "@REF"));
+                       }
+               });
+
+               addTest(new TestCase("Import/Export with nested objects forming a loop") {
+                       @Override
+                       public void test() throws Exception {
+                               DataLoop data = new DataLoop("looping");
+                               data.next = new DataLoop("level 2");
+                               data.next.next = data;
+
+                               String encoded = new Exporter().append(data).toString(false);
+                               Object redata = new Importer().read(encoded).getValue();
+                               String reencoded = new Exporter().append(redata)
+                                               .toString(false);
+
+                               assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
+                                               reencoded.replaceAll("@[0-9]*", "@REF"));
+                       }
+               });
+       }
+
+       @SuppressWarnings("unused")
+       class Data {
+               private int value;
+
+               private Data() {
+               }
+
+               public Data(int value) {
+                       this.value = value;
+               }
+       }
+
+       @SuppressWarnings("unused")
+       class DataObject extends Data {
+               private Data data;
+
+               @SuppressWarnings("synthetic-access")
+               private DataObject() {
+               }
+
+               @SuppressWarnings("synthetic-access")
+               public DataObject(Data data) {
+                       this.data = data;
+               }
+       }
+
+       @SuppressWarnings("unused")
+       class DataLoop extends Data {
+               public DataLoop next;
+               private String value;
+
+               @SuppressWarnings("synthetic-access")
+               private DataLoop() {
+               }
+
+               @SuppressWarnings("synthetic-access")
+               public DataLoop(String value) {
+                       this.value = value;
+               }
+       }
+}
diff --git a/src/be/nikiroo/utils/test/StringUtilsTest.java b/src/be/nikiroo/utils/test/StringUtilsTest.java
new file mode 100644 (file)
index 0000000..2b220b7
--- /dev/null
@@ -0,0 +1,19 @@
+package be.nikiroo.utils.test;
+
+import be.nikiroo.utils.StringUtils;
+
+class StringUtilsTest extends TestLauncher {
+       public StringUtilsTest(String[] args) {
+               super("StringUtils test", args);
+
+               addTest(new TestCase("zip64") {
+                       @Override
+                       public void test() throws Exception {
+                               String orig = "test";
+                               String zipped = StringUtils.zip64(orig);
+                               String unzipped = StringUtils.unzip64(zipped);
+                               assertEquals(orig, unzipped);
+                       }
+               });
+       }
+}
index df53648ce125ae32be26eba468df310189056fb8..16829559e6453525e390c8266f3e01cf3b1fa3ea 100644 (file)
@@ -6,6 +6,13 @@ package be.nikiroo.utils.test;
  * @author niki
  */
 public class Test extends TestLauncher {
+       /**
+        * Start the tests.
+        * 
+        * @param args
+        *            the arguments (which are passed as-is to the other test
+        *            classes)
+        */
        public Test(String[] args) {
                super("Nikiroo-utils", args);
 
@@ -13,6 +20,8 @@ public class Test extends TestLauncher {
                addSeries(new BundleTest(args));
                addSeries(new IOUtilsTest(args));
                addSeries(new VersionTest(args));
+               addSeries(new SerialTest(args));
+               addSeries(new StringUtilsTest(args));
        }
 
        /**
index 429200b4ceb3d800cb4ace07215ee82cc700fc69..243acac2b28243a781c5de9eef56b31ab76e5a9e 100644 (file)
@@ -113,8 +113,8 @@ abstract public class TestCase {
        /**
         * Check that 2 {@link Object}s are equals.
         * 
-        * @param the
-        *            error message to display if they differ
+        * @param errorMessage
+        *            the error message to display if they differ
         * @param expected
         *            the expected value
         * @param actual
@@ -155,8 +155,8 @@ abstract public class TestCase {
        /**
         * Check that 2 {@link Object}s are equals.
         * 
-        * @param the
-        *            error message to display if they differ
+        * @param errorMessage
+        *            the error message to display if they differ
         * @param expected
         *            the expected value
         * @param actual
@@ -189,8 +189,8 @@ abstract public class TestCase {
        /**
         * Check that 2 {@link Object}s are equals.
         * 
-        * @param the
-        *            error message to display if they differ
+        * @param errorMessage
+        *            the error message to display if they differ
         * @param expected
         *            the expected value
         * @param actual
@@ -223,8 +223,8 @@ abstract public class TestCase {
        /**
         * Check that 2 {@link Object}s are equals.
         * 
-        * @param the
-        *            error message to display if they differ
+        * @param errorMessage
+        *            the error message to display if they differ
         * @param expected
         *            the expected value
         * @param actual
@@ -241,8 +241,8 @@ abstract public class TestCase {
        /**
         * Check that given {@link Object} is not NULL.
         * 
-        * @param the
-        *            error message to display if it is NULL
+        * @param errorMessage
+        *            the error message to display if it is NULL
         * @param actual
         *            the actual value
         * 
index d01e0f802cf026f1aa886d6c99d2917538a67693..62f3f6f38918941cc005aa2ece45ad5773d924e6 100644 (file)
@@ -278,8 +278,8 @@ public class TestLauncher {
         * 
         * @param depth
         *            the level at which is the launcher (0 = main launcher)
-        * @param test
-        *            the {@link TestCase}
+        * @param name
+        *            the {@link TestCase} name
         */
        protected void print(int depth, String name) {
                name = prefix(depth, false)
index fb98eba01f72458ca21f5c97302350b058206ba5..2651db10e481abef512290a2859dcde672be31b2 100644 (file)
@@ -15,6 +15,7 @@ import javax.swing.JScrollPane;
 import javax.swing.border.EmptyBorder;
 
 import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.TransBundle;
 
 /**
  * A configuration panel for a {@link Bundle}.
@@ -24,7 +25,7 @@ import be.nikiroo.utils.resources.Bundle;
  * values.
  * 
  * @author niki
- *
+ * 
  * @param <E>
  *            the type of {@link Bundle} to edit
  */
index 2cec4cab381e300dc53382c3961f508ada6f801f..8545485d0dc2a98196796bb2ec305c502acb2755 100644 (file)
@@ -11,31 +11,51 @@ import javax.swing.JTextField;
 import javax.swing.border.EmptyBorder;
 
 import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.Meta;
 
 /**
  * A graphical item that reflect a configuration option from the given
  * {@link Bundle}.
  * 
  * @author niki
- *
+ * 
  * @param <E>
  *            the type of {@link Bundle} to edit
  */
 public class ConfigItem<E extends Enum<E>> extends JPanel {
        private static final long serialVersionUID = 1L;
+       private Class<E> type;
        private final Bundle<E> bundle;
        private final E id;
+
+       private Meta meta;
        private String value;
 
        private JTextField valueField;
 
        public ConfigItem(Class<E> type, Bundle<E> bundle, E id) {
+               this.type = type;
                this.bundle = bundle;
                this.id = id;
 
+               try {
+                       this.meta = type.getDeclaredField(id.name()).getAnnotation(
+                                       Meta.class);
+               } catch (NoSuchFieldException e) {
+               } catch (SecurityException e) {
+               }
+
                this.setLayout(new BorderLayout());
                this.setBorder(new EmptyBorder(2, 10, 2, 10));
 
+               String tooltip = null;
+               if (bundle.getDescriptionBundle() != null) {
+                       tooltip = bundle.getDescriptionBundle().getString(id);
+                       if (tooltip.trim().isEmpty()) {
+                               tooltip = null;
+                       }
+               }
+
                String name = id.toString();
                if (name.length() > 1) {
                        name = name.substring(0, 1) + name.substring(1).toLowerCase();
@@ -43,6 +63,7 @@ public class ConfigItem<E extends Enum<E>> extends JPanel {
                }
 
                JLabel nameLabel = new JLabel(name);
+               nameLabel.setToolTipText(tooltip);
                nameLabel.setPreferredSize(new Dimension(400, 0));
                this.add(nameLabel, BorderLayout.WEST);
 
@@ -73,6 +94,8 @@ public class ConfigItem<E extends Enum<E>> extends JPanel {
         * Create a list of {@link ConfigItem}, one for each of the item in the
         * given {@link Bundle}.
         * 
+        * @param <E>
+        *            the type of {@link Bundle} to edit
         * @param type
         *            a class instance of the item type to work on
         * @param bundle