From db31c35860081535d6e7ddc83ab4af573bb0522e Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Thu, 22 Jun 2017 21:04:17 +0200 Subject: [PATCH] Update to version 1.5.0 (breaking Bundle/Meta) --- VERSION | 2 +- changelog | 12 + src/be/nikiroo/utils/IOUtils.java | 4 +- src/be/nikiroo/utils/StringUtils.java | 39 ++- src/be/nikiroo/utils/resources/Bundle.java | 169 ++++++++----- src/be/nikiroo/utils/resources/Meta.java | 83 ++++++- .../nikiroo/utils/resources/TransBundle.java | 17 +- .../nikiroo/utils/resources/package-info.java | 2 +- .../utils/serial/CustomSerializer.java | 43 ++++ src/be/nikiroo/utils/serial/Exporter.java | 58 +++++ src/be/nikiroo/utils/serial/Importer.java | 223 ++++++++++++++++++ src/be/nikiroo/utils/serial/SerialUtils.java | 214 +++++++++++++++++ src/be/nikiroo/utils/test/BundleTest.java | 10 +- src/be/nikiroo/utils/test/SerialTest.java | 100 ++++++++ .../nikiroo/utils/test/StringUtilsTest.java | 19 ++ src/be/nikiroo/utils/test/Test.java | 9 + src/be/nikiroo/utils/test/TestCase.java | 20 +- src/be/nikiroo/utils/test/TestLauncher.java | 4 +- src/be/nikiroo/utils/ui/ConfigEditor.java | 3 +- src/be/nikiroo/utils/ui/ConfigItem.java | 25 +- 20 files changed, 949 insertions(+), 107 deletions(-) create mode 100644 src/be/nikiroo/utils/serial/CustomSerializer.java create mode 100644 src/be/nikiroo/utils/serial/Exporter.java create mode 100644 src/be/nikiroo/utils/serial/Importer.java create mode 100644 src/be/nikiroo/utils/serial/SerialUtils.java create mode 100644 src/be/nikiroo/utils/test/SerialTest.java create mode 100644 src/be/nikiroo/utils/test/StringUtilsTest.java diff --git a/VERSION b/VERSION index 428b770..bc80560 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.3 +1.5.0 diff --git a/changelog b/changelog index 57bc029..0fc8a93 100644 --- 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 ------------- diff --git a/src/be/nikiroo/utils/IOUtils.java b/src/be/nikiroo/utils/IOUtils.java index 4a185c6..b056643 100644 --- a/src/be/nikiroo/utils/IOUtils.java +++ b/src/be/nikiroo/utils/IOUtils.java @@ -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 .zip 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 * diff --git a/src/be/nikiroo/utils/StringUtils.java b/src/be/nikiroo/utils/StringUtils.java index adebd7a..dc40e87 100644 --- a/src/be/nikiroo/utils/StringUtils.java +++ b/src/be/nikiroo/utils/StringUtils.java @@ -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(); + } + } } diff --git a/src/be/nikiroo/utils/resources/Bundle.java b/src/be/nikiroo/utils/resources/Bundle.java index 9da8d74..bad7f3e 100644 --- a/src/be/nikiroo/utils/resources/Bundle.java +++ b/src/be/nikiroo/utils/resources/Bundle.java @@ -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 * the enum to use to get values out of this class + * + * @author niki */ + public class Bundle> { + /** The type of E. */ protected Class type; - protected Enum name; - private Map map; // R/O map - private Map 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 descriptionBundle; + + /** R/O map */ + private Map map; + /** R/W map */ + private Map 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 type, Enum name) { + protected Bundle(Class type, Enum name, + TransBundle descriptionBundle) { this.type = type; - this.name = name; + this.keyType = name; + this.descriptionBundle = descriptionBundle; + this.map = new HashMap(); this.changeMap = new HashMap(); setBundle(name, Locale.getDefault(), false); @@ -59,7 +76,7 @@ public class Bundle> { /** * 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> { /** * 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> { *

* 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> { *

* 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> { /** * 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> { /** * 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> { /** * 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> { } /** - * 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> { /** * 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> { /** * 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> { /** * 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> { } } + // 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> { 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 getDescriptionBundle() { + return descriptionBundle; + } + /** * Reload the {@link Bundle} data files. * @@ -368,7 +419,7 @@ public class Bundle> { * configuration) */ public void reload(boolean resetToDefault) { - setBundle(name, Locale.getDefault(), resetToDefault); + setBundle(keyType, Locale.getDefault(), resetToDefault); } /** @@ -427,44 +478,45 @@ public class Bundle> { * @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> { * @return the name */ protected String getBundleDisplayName() { - return name.toString(); + return keyType.toString(); } /** @@ -548,12 +600,9 @@ public class Bundle> { * 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> { * 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 .properties) + * the file base name (without .properties) * @param locale * the {@link Locale} * diff --git a/src/be/nikiroo/utils/resources/Meta.java b/src/be/nikiroo/utils/resources/Meta.java index 3e14557..d377dcc 100644 --- a/src/be/nikiroo/utils/resources/Meta.java +++ b/src/be/nikiroo/utils/resources/Meta.java @@ -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). + *

+ * 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. + *

+ * 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. + *

+ * 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 ""; } diff --git a/src/be/nikiroo/utils/resources/TransBundle.java b/src/be/nikiroo/utils/resources/TransBundle.java index 51101b0..51e090f 100644 --- a/src/be/nikiroo/utils/resources/TransBundle.java +++ b/src/be/nikiroo/utils/resources/TransBundle.java @@ -17,6 +17,9 @@ import java.util.regex.Pattern; *

  • DUMMY will return "[DUMMY]" (maybe with a suffix and/or "NOUTF")
  • * * + * @param + * the enum to use to get values out of this class + * * @author niki */ public class TransBundle> extends Bundle { @@ -33,7 +36,7 @@ public class TransBundle> extends Bundle { * the name of the {@link Bundles} */ public TransBundle(Class type, Enum name) { - super(type, name); + super(type, name, null); setLanguage(null); } @@ -49,7 +52,7 @@ public class TransBundle> extends Bundle { * the language to use */ public TransBundle(Class type, Enum name, String language) { - super(type, name); + super(type, name, null); setLanguage(language); } @@ -156,7 +159,7 @@ public class TransBundle> extends Bundle { * @return the known language codes */ public List getKnownLanguages() { - return getKnownLanguages(name); + return getKnownLanguages(keyType); } /** @@ -169,12 +172,12 @@ public class TransBundle> extends Bundle { 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> extends Bundle { 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; diff --git a/src/be/nikiroo/utils/resources/package-info.java b/src/be/nikiroo/utils/resources/package-info.java index 783cd03..bda940b 100644 --- a/src/be/nikiroo/utils/resources/package-info.java +++ b/src/be/nikiroo/utils/resources/package-info.java @@ -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 *

    * Those are basically a .properties 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 index 0000000..b6928ee --- /dev/null +++ b/src/be/nikiroo/utils/serial/CustomSerializer.java @@ -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 index 0000000..8b04111 --- /dev/null +++ b/src/be/nikiroo/utils/serial/Exporter.java @@ -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}. + *

    + * This class does not support inner classes (it does support nested classes, + * though). + * + * @author niki + */ +public class Exporter { + private Map map; + private StringBuilder builder; + + public Exporter() { + map = new HashMap(); + 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 index 0000000..b3307fd --- /dev/null +++ b/src/be/nikiroo/utils/serial/Importer.java @@ -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. + *

    + * 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 map; + + private String currentFieldName; + + public Importer() { + map = new HashMap(); + map.put("NULL", null); + } + + private Importer(Map 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 index 0000000..657dbec --- /dev/null +++ b/src/be/nikiroo/utils/serial/SerialUtils.java @@ -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. + *

    + * 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 customTypes; + + static { + customTypes = new HashMap(); + // TODO: add "default" custom serialisers + } + + static public void addCustomSerializer(CustomSerializer serializer) { + customTypes.put(serializer.getType(), serializer); + } + + static void append(StringBuilder builder, Object o, Map 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(); + } +} diff --git a/src/be/nikiroo/utils/test/BundleTest.java b/src/be/nikiroo/utils/test/BundleTest.java index a881817..c1c379f 100644 --- a/src/be/nikiroo/utils/test/BundleTest.java +++ b/src/be/nikiroo/utils/test/BundleTest.java @@ -204,7 +204,7 @@ class BundleTest extends TestLauncher { */ private class B extends Bundle { 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 index 0000000..f73c39e --- /dev/null +++ b/src/be/nikiroo/utils/test/SerialTest.java @@ -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 index 0000000..2b220b7 --- /dev/null +++ b/src/be/nikiroo/utils/test/StringUtilsTest.java @@ -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); + } + }); + } +} diff --git a/src/be/nikiroo/utils/test/Test.java b/src/be/nikiroo/utils/test/Test.java index df53648..1682955 100644 --- a/src/be/nikiroo/utils/test/Test.java +++ b/src/be/nikiroo/utils/test/Test.java @@ -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)); } /** diff --git a/src/be/nikiroo/utils/test/TestCase.java b/src/be/nikiroo/utils/test/TestCase.java index 429200b..243acac 100644 --- a/src/be/nikiroo/utils/test/TestCase.java +++ b/src/be/nikiroo/utils/test/TestCase.java @@ -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 * diff --git a/src/be/nikiroo/utils/test/TestLauncher.java b/src/be/nikiroo/utils/test/TestLauncher.java index d01e0f8..62f3f6f 100644 --- a/src/be/nikiroo/utils/test/TestLauncher.java +++ b/src/be/nikiroo/utils/test/TestLauncher.java @@ -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) diff --git a/src/be/nikiroo/utils/ui/ConfigEditor.java b/src/be/nikiroo/utils/ui/ConfigEditor.java index fb98eba..2651db1 100644 --- a/src/be/nikiroo/utils/ui/ConfigEditor.java +++ b/src/be/nikiroo/utils/ui/ConfigEditor.java @@ -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 * the type of {@link Bundle} to edit */ diff --git a/src/be/nikiroo/utils/ui/ConfigItem.java b/src/be/nikiroo/utils/ui/ConfigItem.java index 2cec4ca..8545485 100644 --- a/src/be/nikiroo/utils/ui/ConfigItem.java +++ b/src/be/nikiroo/utils/ui/ConfigItem.java @@ -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 * the type of {@link Bundle} to edit */ public class ConfigItem> extends JPanel { private static final long serialVersionUID = 1L; + private Class type; private final Bundle bundle; private final E id; + + private Meta meta; private String value; private JTextField valueField; public ConfigItem(Class type, Bundle 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> 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> extends JPanel { * Create a list of {@link ConfigItem}, one for each of the item in the * given {@link Bundle}. * + * @param + * the type of {@link Bundle} to edit * @param type * a class instance of the item type to work on * @param bundle -- 2.27.0