+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
-------------
*
* @param in
* the data source
- * @param target
+ * @param out
* the target {@link OutputStream}
*
* @throws IOException
* 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
*
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;
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;
* 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;
}
}
/**
- * 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
*
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();
+ }
+ }
}
* 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);
/**
* 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
/**
* 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
* <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
* <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
/**
* 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
/**
* 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
/**
* 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
}
/**
- * 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
/**
* 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
/**
* 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
/**
* 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
*/
}
}
+ // 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);
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.
*
* configuration)
*/
public void reload(boolean resetToDefault) {
- setBundle(name, Locale.getDefault(), resetToDefault);
+ setBundle(keyType, Locale.getDefault(), resetToDefault);
}
/**
* @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();
}
* @return the name
*/
protected String getBundleDisplayName() {
- return name.toString();
+ return keyType.toString();
}
/**
* 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");
}
/**
* 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}
*
@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 "";
}
* <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> {
* the name of the {@link Bundles}
*/
public TransBundle(Class<E> type, Enum<?> name) {
- super(type, name);
+ super(type, name, null);
setLanguage(null);
}
* the language to use
*/
public TransBundle(Class<E> type, Enum<?> name, String language) {
- super(type, name);
+ super(type, name, null);
setLanguage(language);
}
* @return the known language codes
*/
public List<String> getKnownLanguages() {
- return getKnownLanguages(name);
+ return getKnownLanguages(keyType);
}
/**
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
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;
/**
* 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
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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();
+ }
+}
*/
private class B extends Bundle<E> {
protected B() {
- super(E.class, N.bundle_test);
+ super(E.class, N.bundle_test, null);
}
@Override
* @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
}
--- /dev/null
+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;
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ });
+ }
+}
* @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);
addSeries(new BundleTest(args));
addSeries(new IOUtilsTest(args));
addSeries(new VersionTest(args));
+ addSeries(new SerialTest(args));
+ addSeries(new StringUtilsTest(args));
}
/**
/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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
*
*
* @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)
import javax.swing.border.EmptyBorder;
import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.TransBundle;
/**
* A configuration panel for a {@link Bundle}.
* values.
*
* @author niki
- *
+ *
* @param <E>
* the type of {@link Bundle} to edit
*/
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();
}
JLabel nameLabel = new JLabel(name);
+ nameLabel.setToolTipText(tooltip);
nameLabel.setPreferredSize(new Dimension(400, 0));
this.add(nameLabel, BorderLayout.WEST);
* 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