+++ /dev/null
-# Eclipse
-.classpath
-.project
-.settings/
-bin/
-
-# Compiled class file
-*.class
-
-# Log file
-*.log
-
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-!*-sources.jar
-*.war
-*.nar
-*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+
+import be.nikiroo.utils.streams.MarkableFileInputStream;
+
+/**
+ * A generic cache system, with special support for {@link URL}s.
+ * <p>
+ * This cache also manages timeout information.
+ *
+ * @author niki
+ */
+public class Cache {
+ private File dir;
+ private long tooOldChanging;
+ private long tooOldStable;
+ private TraceHandler tracer = new TraceHandler();
+
+ /**
+ * Only for inheritance.
+ */
+ protected Cache() {
+ }
+
+ /**
+ * Create a new {@link Cache} object.
+ *
+ * @param dir
+ * the directory to use as cache
+ * @param hoursChanging
+ * the number of hours after which a cached file that is thought
+ * to change ~often is considered too old (or -1 for
+ * "never too old")
+ * @param hoursStable
+ * the number of hours after which a cached file that is thought
+ * to change rarely is considered too old (or -1 for
+ * "never too old")
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public Cache(File dir, int hoursChanging, int hoursStable)
+ throws IOException {
+ this.dir = dir;
+ this.tooOldChanging = 1000L * 60 * 60 * hoursChanging;
+ this.tooOldStable = 1000L * 60 * 60 * hoursStable;
+
+ if (dir != null && !dir.exists()) {
+ dir.mkdirs();
+ }
+
+ if (dir == null || !dir.exists()) {
+ throw new IOException("Cannot create the cache directory: "
+ + (dir == null ? "null" : dir.getAbsolutePath()));
+ }
+ }
+
+ /**
+ * The traces handler for this {@link Cache}.
+ *
+ * @return the traces handler
+ */
+ public TraceHandler getTraceHandler() {
+ return tracer;
+ }
+
+ /**
+ * The traces handler for this {@link Cache}.
+ *
+ * @param tracer
+ * the new traces handler
+ */
+ public void setTraceHandler(TraceHandler tracer) {
+ if (tracer == null) {
+ tracer = new TraceHandler(false, false, false);
+ }
+
+ this.tracer = tracer;
+ }
+
+ /**
+ * Check the resource to see if it is in the cache.
+ *
+ * @param uniqueID
+ * the resource to check
+ * @param allowTooOld
+ * allow files even if they are considered too old
+ * @param stable
+ * a stable file (that dones't change too often) -- parameter
+ * used to check if the file is too old to keep or not
+ *
+ * @return TRUE if it is
+ *
+ */
+ public boolean check(String uniqueID, boolean allowTooOld, boolean stable) {
+ return check(getCached(uniqueID), allowTooOld, stable);
+ }
+
+ /**
+ * Check the resource to see if it is in the cache.
+ *
+ * @param url
+ * the resource to check
+ * @param allowTooOld
+ * allow files even if they are considered too old
+ * @param stable
+ * a stable file (that dones't change too often) -- parameter
+ * used to check if the file is too old to keep or not
+ *
+ * @return TRUE if it is
+ *
+ */
+ public boolean check(URL url, boolean allowTooOld, boolean stable) {
+ return check(getCached(url), allowTooOld, stable);
+ }
+
+ /**
+ * Check the resource to see if it is in the cache.
+ *
+ * @param cached
+ * the resource to check
+ * @param allowTooOld
+ * allow files even if they are considered too old
+ * @param stable
+ * a stable file (that dones't change too often) -- parameter
+ * used to check if the file is too old to keep or not
+ *
+ * @return TRUE if it is
+ *
+ */
+ private boolean check(File cached, boolean allowTooOld, boolean stable) {
+ if (cached.exists() && cached.isFile()) {
+ if (!allowTooOld && isOld(cached, stable)) {
+ if (!cached.delete()) {
+ tracer.error("Cannot delete temporary file: "
+ + cached.getAbsolutePath());
+ }
+ } else {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Clean the cache (delete the cached items).
+ *
+ * @param onlyOld
+ * only clean the files that are considered too old for a stable
+ * resource
+ *
+ * @return the number of cleaned items
+ */
+ public int clean(boolean onlyOld) {
+ long ms = System.currentTimeMillis();
+
+ tracer.trace("Cleaning cache from old files...");
+
+ int num = clean(onlyOld, dir, -1);
+
+ tracer.trace(num + "cache items cleaned in "
+ + (System.currentTimeMillis() - ms) + " ms");
+
+ return num;
+ }
+
+ /**
+ * Clean the cache (delete the cached items) in the given cache directory.
+ *
+ * @param onlyOld
+ * only clean the files that are considered too old for stable
+ * resources
+ * @param cacheDir
+ * the cache directory to clean
+ * @param limit
+ * stop after limit files deleted, or -1 for unlimited
+ *
+ * @return the number of cleaned items
+ */
+ private int clean(boolean onlyOld, File cacheDir, int limit) {
+ int num = 0;
+ File[] files = cacheDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (limit >= 0 && num >= limit) {
+ return num;
+ }
+
+ if (file.isDirectory()) {
+ num += clean(onlyOld, file, limit);
+ file.delete(); // only if empty
+ } else {
+ if (!onlyOld || isOld(file, true)) {
+ if (file.delete()) {
+ num++;
+ } else {
+ tracer.error("Cannot delete temporary file: "
+ + file.getAbsolutePath());
+ }
+ }
+ }
+ }
+ }
+
+ return num;
+ }
+
+ /**
+ * Open a resource from the cache if it exists.
+ *
+ * @param uniqueID
+ * the unique ID
+ * @param allowTooOld
+ * allow files even if they are considered too old
+ * @param stable
+ * a stable file (that dones't change too often) -- parameter
+ * used to check if the file is too old to keep or not
+ *
+ * @return the opened resource if found, NULL if not
+ */
+ public InputStream load(String uniqueID, boolean allowTooOld, boolean stable) {
+ return load(getCached(uniqueID), allowTooOld, stable);
+ }
+
+ /**
+ * Open a resource from the cache if it exists.
+ *
+ * @param url
+ * the resource to open
+ * @param allowTooOld
+ * allow files even if they are considered too old
+ * @param stable
+ * a stable file (that doesn't change too often) -- parameter
+ * used to check if the file is too old to keep or not in the
+ * cache
+ *
+ * @return the opened resource if found, NULL if not
+ */
+ public InputStream load(URL url, boolean allowTooOld, boolean stable) {
+ return load(getCached(url), allowTooOld, stable);
+ }
+
+ /**
+ * Open a resource from the cache if it exists.
+ *
+ * @param cached
+ * the resource to open
+ * @param allowTooOld
+ * allow files even if they are considered too old
+ * @param stable
+ * a stable file (that dones't change too often) -- parameter
+ * used to check if the file is too old to keep or not
+ *
+ * @return the opened resource if found, NULL if not
+ */
+ private InputStream load(File cached, boolean allowTooOld, boolean stable) {
+ if (cached.exists() && cached.isFile()
+ && (allowTooOld || !isOld(cached, stable))) {
+ try {
+ return new MarkableFileInputStream(cached);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Save the given resource to the cache.
+ *
+ * @param in
+ * the input data
+ * @param uniqueID
+ * a unique ID used to locate the cached resource
+ *
+ * @return the number of bytes written
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public long save(InputStream in, String uniqueID) throws IOException {
+ File cached = getCached(uniqueID);
+ cached.getParentFile().mkdirs();
+ return save(in, cached);
+ }
+
+ /**
+ * Save the given resource to the cache.
+ *
+ * @param in
+ * the input data
+ * @param url
+ * the {@link URL} used to locate the cached resource
+ *
+ * @return the number of bytes written
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public long save(InputStream in, URL url) throws IOException {
+ File cached = getCached(url);
+ return save(in, cached);
+ }
+
+ /**
+ * Save the given resource to the cache.
+ * <p>
+ * Will also clean the {@link Cache} from old files.
+ *
+ * @param in
+ * the input data
+ * @param cached
+ * the cached {@link File} to save to
+ *
+ * @return the number of bytes written
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ private long save(InputStream in, File cached) throws IOException {
+ // We want to force at least an immediate SAVE/LOAD to work for some
+ // workflows, even if we don't accept cached files (times set to "0"
+ // -- and not "-1" or a positive value)
+ clean(true, dir, 10);
+ cached.getParentFile().mkdirs(); // in case we deleted our own parent
+ long bytes = IOUtils.write(in, cached);
+ return bytes;
+ }
+
+ /**
+ * Remove the given resource from the cache.
+ *
+ * @param uniqueID
+ * a unique ID used to locate the cached resource
+ *
+ * @return TRUE if it was removed
+ */
+ public boolean remove(String uniqueID) {
+ File cached = getCached(uniqueID);
+ return cached.delete();
+ }
+
+ /**
+ * Remove the given resource from the cache.
+ *
+ * @param url
+ * the {@link URL} used to locate the cached resource
+ *
+ * @return TRUE if it was removed
+ */
+ public boolean remove(URL url) {
+ File cached = getCached(url);
+ return cached.delete();
+ }
+
+ /**
+ * Check if the {@link File} is too old according to
+ * {@link Cache#tooOldChanging}.
+ *
+ * @param file
+ * the file to check
+ * @param stable
+ * TRUE to denote stable files, that are not supposed to change
+ * too often
+ *
+ * @return TRUE if it is
+ */
+ private boolean isOld(File file, boolean stable) {
+ long max = tooOldChanging;
+ if (stable) {
+ max = tooOldStable;
+ }
+
+ if (max < 0) {
+ return false;
+ }
+
+ long time = new Date().getTime() - file.lastModified();
+ if (time < 0) {
+ tracer.error("Timestamp in the future for file: "
+ + file.getAbsolutePath());
+ }
+
+ return time < 0 || time > max;
+ }
+
+ /**
+ * Return the associated cache {@link File} from this {@link URL}.
+ *
+ * @param url
+ * the {@link URL}
+ *
+ * @return the cached {@link File} version of this {@link URL}
+ */
+ private File getCached(URL url) {
+ File subdir;
+
+ String name = url.getHost();
+ if (name == null || name.isEmpty()) {
+ // File
+ File file = new File(url.getFile());
+ if (file.getParent() == null) {
+ subdir = new File("+");
+ } else {
+ subdir = new File(file.getParent().replace("..", "__"));
+ }
+ subdir = new File(dir, allowedChars(subdir.getPath()));
+ name = allowedChars(url.getFile());
+ } else {
+ // URL
+ File subsubDir = new File(dir, allowedChars(url.getHost()));
+ subdir = new File(subsubDir, "_" + allowedChars(url.getPath()));
+ name = allowedChars("_" + url.getQuery());
+ }
+
+ File cacheFile = new File(subdir, name);
+ subdir.mkdirs();
+
+ return cacheFile;
+ }
+
+ /**
+ * Get the basic cache resource file corresponding to this unique ID.
+ * <p>
+ * Note that you may need to add a sub-directory in some cases.
+ *
+ * @param uniqueID
+ * the id
+ *
+ * @return the cached version if present, NULL if not
+ */
+ private File getCached(String uniqueID) {
+ File file = new File(dir, allowedChars(uniqueID));
+ File subdir = new File(file.getParentFile(), "_");
+ return new File(subdir, file.getName());
+ }
+
+ /**
+ * Replace not allowed chars (in a {@link File}) by "_".
+ *
+ * @param raw
+ * the raw {@link String}
+ *
+ * @return the sanitised {@link String}
+ */
+ private String allowedChars(String raw) {
+ return raw.replace('/', '_').replace(':', '_').replace("\\", "_");
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A memory only version of {@link Cache}.
+ *
+ * @author niki
+ */
+public class CacheMemory extends Cache {
+ private Map<String, byte[]> data;
+
+ /**
+ * Create a new {@link CacheMemory}.
+ */
+ public CacheMemory() {
+ data = new HashMap<String, byte[]>();
+ }
+
+ @Override
+ public boolean check(String uniqueID, boolean allowTooOld, boolean stable) {
+ return data.containsKey(getKey(uniqueID));
+ }
+
+ @Override
+ public boolean check(URL url, boolean allowTooOld, boolean stable) {
+ return data.containsKey(getKey(url));
+ }
+
+ @Override
+ public int clean(boolean onlyOld) {
+ int cleaned = 0;
+ if (!onlyOld) {
+ cleaned = data.size();
+ data.clear();
+ }
+
+ return cleaned;
+ }
+
+ @Override
+ public InputStream load(String uniqueID, boolean allowTooOld, boolean stable) {
+ if (check(uniqueID, allowTooOld, stable)) {
+ return load(getKey(uniqueID));
+ }
+
+ return null;
+ }
+
+ @Override
+ public InputStream load(URL url, boolean allowTooOld, boolean stable) {
+ if (check(url, allowTooOld, stable)) {
+ return load(getKey(url));
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean remove(String uniqueID) {
+ return data.remove(getKey(uniqueID)) != null;
+ }
+
+ @Override
+ public boolean remove(URL url) {
+ return data.remove(getKey(url)) != null;
+ }
+
+ @Override
+ public long save(InputStream in, String uniqueID) throws IOException {
+ byte[] bytes = IOUtils.toByteArray(in);
+ data.put(getKey(uniqueID), bytes);
+ return bytes.length;
+ }
+
+ @Override
+ public long save(InputStream in, URL url) throws IOException {
+ byte[] bytes = IOUtils.toByteArray(in);
+ data.put(getKey(url), bytes);
+ return bytes.length;
+ }
+
+ /**
+ * Return a key mapping to the given unique ID.
+ *
+ * @param uniqueID the unique ID
+ *
+ * @return the key
+ */
+ private String getKey(String uniqueID) {
+ return "UID:" + uniqueID;
+ }
+
+ /**
+ * Return a key mapping to the given urm.
+ *
+ * @param url the url
+ *
+ * @return the key
+ */
+ private String getKey(URL url) {
+ return "URL:" + url.toString();
+ }
+
+ /**
+ * Load the given key.
+ *
+ * @param key the key to load
+ * @return the loaded data
+ */
+ private InputStream load(String key) {
+ byte[] data = this.data.get(key);
+ if (data != null) {
+ return new ByteArrayInputStream(data);
+ }
+
+ return null;
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.net.ssl.SSLException;
+
+import be.nikiroo.utils.streams.Base64InputStream;
+import be.nikiroo.utils.streams.Base64OutputStream;
+
+/**
+ * Small utility class to do AES encryption/decryption.
+ * <p>
+ * It is multi-thread compatible, but beware:
+ * <ul>
+ * <li>The encrypt/decrypt calls are serialized</li>
+ * <li>The streams are independent (and thus parallel)</li>
+ * </ul>
+ * <p>
+ * Do not assume it is secure; it just here to offer a more-or-less protected
+ * exchange of data because anonymous and self-signed certificates backed SSL is
+ * against Google wishes, and I need Android support.
+ *
+ * @author niki
+ */
+public class CryptUtils {
+ static private final String AES_NAME = "AES/CFB128/NoPadding";
+
+ private Cipher ecipher;
+ private Cipher dcipher;
+ private byte[] bytes32;
+
+ /**
+ * Small and lazy-easy way to initialize a 128 bits key with
+ * {@link CryptUtils}.
+ * <p>
+ * <b>Some</b> part of the key will be used to generate a 128 bits key and
+ * initialize the {@link CryptUtils}; even NULL will generate something.
+ * <p>
+ * <b>This is most probably not secure. Do not use if you actually care
+ * about security.</b>
+ *
+ * @param key
+ * the {@link String} to use as a base for the key, can be NULL
+ */
+ public CryptUtils(String key) {
+ try {
+ init(key2key(key));
+ } catch (InvalidKeyException e) {
+ // We made sure that the key is correct, so nothing here
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Create a new instance of {@link CryptUtils} with the given 128 bits key.
+ * <p>
+ * The key <b>must</b> be exactly 128 bits long.
+ *
+ * @param bytes32
+ * the 128 bits (32 bytes) of the key
+ *
+ * @throws InvalidKeyException
+ * if the key is not an array of 128 bits
+ */
+ public CryptUtils(byte[] bytes32) throws InvalidKeyException {
+ init(bytes32);
+ }
+
+ /**
+ * Wrap the given {@link InputStream} so it is transparently encrypted by
+ * the current {@link CryptUtils}.
+ *
+ * @param in
+ * the {@link InputStream} to wrap
+ * @return the auto-encode {@link InputStream}
+ */
+ public InputStream encrypt(InputStream in) {
+ Cipher ecipher = newCipher(Cipher.ENCRYPT_MODE);
+ return new CipherInputStream(in, ecipher);
+ }
+
+ /**
+ * Wrap the given {@link InputStream} so it is transparently encrypted by
+ * the current {@link CryptUtils} and encoded in base64.
+ *
+ * @param in
+ * the {@link InputStream} to wrap
+ *
+ * @return the auto-encode {@link InputStream}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public InputStream encrypt64(InputStream in) throws IOException {
+ return new Base64InputStream(encrypt(in), true);
+ }
+
+ /**
+ * Wrap the given {@link OutputStream} so it is transparently encrypted by
+ * the current {@link CryptUtils}.
+ *
+ * @param out
+ * the {@link OutputStream} to wrap
+ *
+ * @return the auto-encode {@link OutputStream}
+ */
+ public OutputStream encrypt(OutputStream out) {
+ Cipher ecipher = newCipher(Cipher.ENCRYPT_MODE);
+ return new CipherOutputStream(out, ecipher);
+ }
+
+ /**
+ * Wrap the given {@link OutputStream} so it is transparently encrypted by
+ * the current {@link CryptUtils} and encoded in base64.
+ *
+ * @param out
+ * the {@link OutputStream} to wrap
+ *
+ * @return the auto-encode {@link OutputStream}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public OutputStream encrypt64(OutputStream out) throws IOException {
+ return encrypt(new Base64OutputStream(out, true));
+ }
+
+ /**
+ * Wrap the given {@link OutputStream} so it is transparently decoded by the
+ * current {@link CryptUtils}.
+ *
+ * @param in
+ * the {@link InputStream} to wrap
+ *
+ * @return the auto-decode {@link InputStream}
+ */
+ public InputStream decrypt(InputStream in) {
+ Cipher dcipher = newCipher(Cipher.DECRYPT_MODE);
+ return new CipherInputStream(in, dcipher);
+ }
+
+ /**
+ * Wrap the given {@link OutputStream} so it is transparently decoded by the
+ * current {@link CryptUtils} and decoded from base64.
+ *
+ * @param in
+ * the {@link InputStream} to wrap
+ *
+ * @return the auto-decode {@link InputStream}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public InputStream decrypt64(InputStream in) throws IOException {
+ return decrypt(new Base64InputStream(in, false));
+ }
+
+ /**
+ * Wrap the given {@link OutputStream} so it is transparently decoded by the
+ * current {@link CryptUtils}.
+ *
+ * @param out
+ * the {@link OutputStream} to wrap
+ * @return the auto-decode {@link OutputStream}
+ */
+ public OutputStream decrypt(OutputStream out) {
+ Cipher dcipher = newCipher(Cipher.DECRYPT_MODE);
+ return new CipherOutputStream(out, dcipher);
+ }
+
+ /**
+ * Wrap the given {@link OutputStream} so it is transparently decoded by the
+ * current {@link CryptUtils} and decoded from base64.
+ *
+ * @param out
+ * the {@link OutputStream} to wrap
+ *
+ * @return the auto-decode {@link OutputStream}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public OutputStream decrypt64(OutputStream out) throws IOException {
+ return new Base64OutputStream(decrypt(out), false);
+ }
+
+ /**
+ * This method required an array of 128 bits.
+ *
+ * @param bytes32
+ * the array, which <b>must</b> be of 128 bits (32 bytes)
+ *
+ * @throws InvalidKeyException
+ * if the key is not an array of 128 bits (32 bytes)
+ */
+ private void init(byte[] bytes32) throws InvalidKeyException {
+ if (bytes32 == null || bytes32.length != 32) {
+ throw new InvalidKeyException(
+ "The size of the key must be of 128 bits (32 bytes), it is: "
+ + (bytes32 == null ? "null" : "" + bytes32.length)
+ + " bytes");
+ }
+
+ this.bytes32 = bytes32;
+ this.ecipher = newCipher(Cipher.ENCRYPT_MODE);
+ this.dcipher = newCipher(Cipher.DECRYPT_MODE);
+ }
+
+ /**
+ * Create a new {@link Cipher}of the given mode (see
+ * {@link Cipher#ENCRYPT_MODE} and {@link Cipher#ENCRYPT_MODE}).
+ *
+ * @param mode
+ * the mode ({@link Cipher#ENCRYPT_MODE} or
+ * {@link Cipher#ENCRYPT_MODE})
+ *
+ * @return the new {@link Cipher}
+ */
+ private Cipher newCipher(int mode) {
+ try {
+ // bytes32 = 32 bytes, 32 > 16
+ byte[] iv = new byte[16];
+ for (int i = 0; i < iv.length; i++) {
+ iv[i] = bytes32[i];
+ }
+ IvParameterSpec ivspec = new IvParameterSpec(iv);
+ Cipher cipher = Cipher.getInstance(AES_NAME);
+ cipher.init(mode, new SecretKeySpec(bytes32, "AES"), ivspec);
+ return cipher;
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException(
+ "Cannot initialize encryption sub-system", e);
+ }
+ }
+
+ /**
+ * Encrypt the data.
+ *
+ * @param data
+ * the data to encrypt
+ *
+ * @return the encrypted data
+ *
+ * @throws SSLException
+ * in case of I/O error (i.e., the data is not what you assumed
+ * it was)
+ */
+ public byte[] encrypt(byte[] data) throws SSLException {
+ synchronized (ecipher) {
+ try {
+ return ecipher.doFinal(data);
+ } catch (IllegalBlockSizeException e) {
+ throw new SSLException(e);
+ } catch (BadPaddingException e) {
+ throw new SSLException(e);
+ }
+ }
+ }
+
+ /**
+ * Encrypt the data.
+ *
+ * @param data
+ * the data to encrypt
+ *
+ * @return the encrypted data
+ *
+ * @throws SSLException
+ * in case of I/O error (i.e., the data is not what you assumed
+ * it was)
+ */
+ public byte[] encrypt(String data) throws SSLException {
+ return encrypt(StringUtils.getBytes(data));
+ }
+
+ /**
+ * Encrypt the data, then encode it into Base64.
+ *
+ * @param data
+ * the data to encrypt
+ * @param zip
+ * TRUE to also compress the data in GZIP format; remember that
+ * compressed and not-compressed content are different; you need
+ * to know which is which when decoding
+ *
+ * @return the encrypted data, encoded in Base64
+ *
+ * @throws SSLException
+ * in case of I/O error (i.e., the data is not what you assumed
+ * it was)
+ */
+ public String encrypt64(String data) throws SSLException {
+ return encrypt64(StringUtils.getBytes(data));
+ }
+
+ /**
+ * Encrypt the data, then encode it into Base64.
+ *
+ * @param data
+ * the data to encrypt
+ *
+ * @return the encrypted data, encoded in Base64
+ *
+ * @throws SSLException
+ * in case of I/O error (i.e., the data is not what you assumed
+ * it was)
+ */
+ public String encrypt64(byte[] data) throws SSLException {
+ try {
+ return StringUtils.base64(encrypt(data));
+ } catch (IOException e) {
+ // not exactly true, but we consider here that this error is a crypt
+ // error, not a normal I/O error
+ throw new SSLException(e);
+ }
+ }
+
+ /**
+ * Decode the data which is assumed to be encrypted with the same utilities.
+ *
+ * @param data
+ * the encrypted data to decode
+ *
+ * @return the original, decoded data
+ *
+ * @throws SSLException
+ * in case of I/O error
+ */
+ public byte[] decrypt(byte[] data) throws SSLException {
+ synchronized (dcipher) {
+ try {
+ return dcipher.doFinal(data);
+ } catch (IllegalBlockSizeException e) {
+ throw new SSLException(e);
+ } catch (BadPaddingException e) {
+ throw new SSLException(e);
+ }
+ }
+ }
+
+ /**
+ * Decode the data which is assumed to be encrypted with the same utilities
+ * and to be a {@link String}.
+ *
+ * @param data
+ * the encrypted data to decode
+ *
+ * @return the original, decoded data,as a {@link String}
+ *
+ * @throws SSLException
+ * in case of I/O error
+ */
+ public String decrypts(byte[] data) throws SSLException {
+ try {
+ return new String(decrypt(data), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // UTF-8 is required in all conform JVMs
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Decode the data which is assumed to be encrypted with the same utilities
+ * and is a Base64 encoded value.
+ *
+ * @param data
+ * the encrypted data to decode in Base64 format
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format
+ * automatically; if set to FALSE, zipped data can be returned
+ *
+ * @return the original, decoded data
+ *
+ * @throws SSLException
+ * in case of I/O error
+ */
+ public byte[] decrypt64(String data) throws SSLException {
+ try {
+ return decrypt(StringUtils.unbase64(data));
+ } catch (IOException e) {
+ // not exactly true, but we consider here that this error is a crypt
+ // error, not a normal I/O error
+ throw new SSLException(e);
+ }
+ }
+
+ /**
+ * Decode the data which is assumed to be encrypted with the same utilities
+ * and is a Base64 encoded value, then convert it into a String (this method
+ * assumes the data <b>was</b> indeed a UTF-8 encoded {@link String}).
+ *
+ * @param data
+ * the encrypted data to decode in Base64 format
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format
+ * automatically; if set to FALSE, zipped data can be returned
+ *
+ * @return the original, decoded data
+ *
+ * @throws SSLException
+ * in case of I/O error
+ */
+ public String decrypt64s(String data) throws SSLException {
+ try {
+ return new String(decrypt(StringUtils.unbase64(data)), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // UTF-8 is required in all conform JVMs
+ e.printStackTrace();
+ return null;
+ } catch (IOException e) {
+ // not exactly true, but we consider here that this error is a crypt
+ // error, not a normal I/O error
+ throw new SSLException(e);
+ }
+ }
+
+ /**
+ * This is probably <b>NOT</b> secure!
+ *
+ * @param input
+ * some {@link String} input
+ *
+ * @return a 128 bits key computed from the given input
+ */
+ static private byte[] key2key(String input) {
+ return StringUtils.getMd5Hash("" + input).getBytes();
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * This class will help you download content from Internet Sites ({@link URL}
+ * based).
+ * <p>
+ * It allows you to control some options often required on web sites that do not
+ * want to simply serve HTML, but actively makes your life difficult with stupid
+ * checks.
+ *
+ * @author niki
+ */
+public class Downloader {
+ private String UA;
+ private CookieManager cookies;
+ private TraceHandler tracer = new TraceHandler();
+ private Cache cache;
+ private boolean offline;
+
+ /**
+ * Create a new {@link Downloader}.
+ *
+ * @param UA
+ * the User-Agent to use to download the resources -- note that
+ * some websites require one, some actively blacklist real UAs
+ * like the one from wget, some whitelist a couple of browsers
+ * only (!)
+ */
+ public Downloader(String UA) {
+ this(UA, null);
+ }
+
+ /**
+ * Create a new {@link Downloader}.
+ *
+ * @param UA
+ * the User-Agent to use to download the resources -- note that
+ * some websites require one, some actively blacklist real UAs
+ * like the one from wget, some whitelist a couple of browsers
+ * only (!)
+ * @param cache
+ * the {@link Cache} to use for all access (can be NULL)
+ */
+ public Downloader(String UA, Cache cache) {
+ this.UA = UA;
+
+ cookies = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
+ CookieHandler.setDefault(cookies);
+
+ setCache(cache);
+ }
+
+ /**
+ * This {@link Downloader} is forbidden to try and connect to the network.
+ * <p>
+ * If TRUE, it will only check the cache if any.
+ * <p>
+ * Default is FALSE.
+ *
+ * @return TRUE if offline
+ */
+ public boolean isOffline() {
+ return offline;
+ }
+
+ /**
+ * This {@link Downloader} is forbidden to try and connect to the network.
+ * <p>
+ * If TRUE, it will only check the cache if any.
+ * <p>
+ * Default is FALSE.
+ *
+ * @param offline TRUE for offline, FALSE for online
+ */
+ public void setOffline(boolean offline) {
+ this.offline = offline;
+ }
+
+ /**
+ * The traces handler for this {@link Cache}.
+ *
+ * @return the traces handler
+ */
+ public TraceHandler getTraceHandler() {
+ return tracer;
+ }
+
+ /**
+ * The traces handler for this {@link Cache}.
+ *
+ * @param tracer
+ * the new traces handler
+ */
+ public void setTraceHandler(TraceHandler tracer) {
+ if (tracer == null) {
+ tracer = new TraceHandler(false, false, false);
+ }
+
+ this.tracer = tracer;
+ }
+
+ /**
+ * The {@link Cache} to use for all access (can be NULL).
+ *
+ * @return the cache
+ */
+ public Cache getCache() {
+ return cache;
+ }
+
+ /**
+ * The {@link Cache} to use for all access (can be NULL).
+ *
+ * @param cache
+ * the new cache
+ */
+ public void setCache(Cache cache) {
+ this.cache = cache;
+ }
+
+ /**
+ * Clear all the cookies currently in the jar.
+ * <p>
+ * As long as you don't, the cookies are kept.
+ */
+ public void clearCookies() {
+ cookies.getCookieStore().removeAll();
+ }
+
+ /**
+ * Open the given {@link URL} and update the cookies.
+ *
+ * @param url
+ * the {@link URL} to open
+ * @return the {@link InputStream} of the opened page
+ *
+ * @throws IOException
+ * in case of I/O error
+ **/
+ public InputStream open(URL url) throws IOException {
+ return open(url, false);
+ }
+
+ /**
+ * Open the given {@link URL} and update the cookies.
+ *
+ * @param url
+ * the {@link URL} to open
+ * @param stable
+ * stable a stable file (that doesn't change too often) --
+ * parameter used to check if the file is too old to keep or not
+ * in the cache (default is false)
+ *
+ * @return the {@link InputStream} of the opened page
+ *
+ * @throws IOException
+ * in case of I/O error
+ **/
+ public InputStream open(URL url, boolean stable) throws IOException {
+ return open(url, url, url, null, null, null, null, stable);
+ }
+
+ /**
+ * Open the given {@link URL} and update the cookies.
+ *
+ * @param url
+ * the {@link URL} to open
+ * @param currentReferer
+ * the current referer, for websites that needs this info
+ * @param cookiesValues
+ * the cookies
+ * @param postParams
+ * the POST parameters
+ * @param getParams
+ * the GET parameters (priority over POST)
+ * @param oauth
+ * OAuth authorization (aka, "bearer XXXXXXX")
+ *
+ * @return the {@link InputStream} of the opened page
+ *
+ * @throws IOException
+ * in case of I/O error (including offline mode + not in cache)
+ */
+ public InputStream open(URL url, URL currentReferer,
+ Map<String, String> cookiesValues, Map<String, String> postParams,
+ Map<String, String> getParams, String oauth) throws IOException {
+ return open(url, currentReferer, cookiesValues, postParams, getParams,
+ oauth, false);
+ }
+
+ /**
+ * Open the given {@link URL} and update the cookies.
+ *
+ * @param url
+ * the {@link URL} to open
+ * @param currentReferer
+ * the current referer, for websites that needs this info
+ * @param cookiesValues
+ * the cookies
+ * @param postParams
+ * the POST parameters
+ * @param getParams
+ * the GET parameters (priority over POST)
+ * @param oauth
+ * OAuth authorization (aka, "bearer XXXXXXX")
+ * @param stable
+ * stable a stable file (that doesn't change too often) --
+ * parameter used to check if the file is too old to keep or not
+ * in the cache (default is false)
+ *
+ * @return the {@link InputStream} of the opened page
+ *
+ * @throws IOException
+ * in case of I/O error (including offline mode + not in cache)
+ */
+ public InputStream open(URL url, URL currentReferer,
+ Map<String, String> cookiesValues, Map<String, String> postParams,
+ Map<String, String> getParams, String oauth, boolean stable)
+ throws IOException {
+ return open(url, url, currentReferer, cookiesValues, postParams,
+ getParams, oauth, stable);
+ }
+
+ /**
+ * Open the given {@link URL} and update the cookies.
+ *
+ * @param url
+ * the {@link URL} to open
+ * @param originalUrl
+ * the original {@link URL} before any redirection occurs, which
+ * is also used for the cache ID if needed (so we can retrieve
+ * the content with this URL if needed)
+ * @param currentReferer
+ * the current referer, for websites that needs this info
+ * @param cookiesValues
+ * the cookies
+ * @param postParams
+ * the POST parameters
+ * @param getParams
+ * the GET parameters (priority over POST)
+ * @param oauth
+ * OAuth authorisation (aka, "bearer XXXXXXX")
+ * @param stable
+ * a stable file (that doesn't change too often) -- parameter
+ * used to check if the file is too old to keep or not in the
+ * cache
+ *
+ * @return the {@link InputStream} of the opened page
+ *
+ * @throws IOException
+ * in case of I/O error (including offline mode + not in cache)
+ */
+ public InputStream open(URL url, final URL originalUrl, URL currentReferer,
+ Map<String, String> cookiesValues, Map<String, String> postParams,
+ Map<String, String> getParams, String oauth, boolean stable)
+ throws IOException {
+
+ tracer.trace("Request: " + url);
+
+ if (cache != null) {
+ InputStream in = cache.load(originalUrl, false, stable);
+ if (in != null) {
+ tracer.trace("Use the cache: " + url);
+ tracer.trace("Original URL : " + originalUrl);
+ return in;
+ }
+ }
+
+ String protocol = originalUrl == null ? null : originalUrl
+ .getProtocol();
+ if (isOffline() && !"file".equalsIgnoreCase(protocol)) {
+ tracer.error("Downloader OFFLINE, cannot proceed to URL: " + url);
+ throw new IOException("Downloader is currently OFFLINE, cannot download: " + url);
+ }
+
+ tracer.trace("Download: " + url);
+
+ URLConnection conn = openConnectionWithCookies(url, currentReferer,
+ cookiesValues);
+
+ // Priority: GET over POST
+ Map<String, String> params = getParams;
+ if (getParams == null) {
+ params = postParams;
+ }
+
+ StringBuilder requestData = null;
+ if ((params != null || oauth != null)
+ && conn instanceof HttpURLConnection) {
+ if (params != null) {
+ requestData = new StringBuilder();
+ for (Map.Entry<String, String> param : params.entrySet()) {
+ if (requestData.length() != 0)
+ requestData.append('&');
+ requestData.append(URLEncoder.encode(param.getKey(),
+ "UTF-8"));
+ requestData.append('=');
+ requestData.append(URLEncoder.encode(
+ String.valueOf(param.getValue()), "UTF-8"));
+ }
+
+ if (getParams == null && postParams != null) {
+ ((HttpURLConnection) conn).setRequestMethod("POST");
+ }
+
+ conn.setRequestProperty("Content-Type",
+ "application/x-www-form-urlencoded");
+ conn.setRequestProperty("Content-Length",
+ Integer.toString(requestData.length()));
+ }
+
+ if (oauth != null) {
+ conn.setRequestProperty("Authorization", oauth);
+ }
+
+ if (requestData != null) {
+ conn.setDoOutput(true);
+ OutputStreamWriter writer = new OutputStreamWriter(
+ conn.getOutputStream());
+ try {
+ writer.write(requestData.toString());
+ writer.flush();
+ } finally {
+ writer.close();
+ }
+ }
+ }
+
+ // Manual redirection, much better for POST data
+ if (conn instanceof HttpURLConnection) {
+ ((HttpURLConnection) conn).setInstanceFollowRedirects(false);
+ }
+
+ conn.connect();
+
+ // Check if redirect
+ // BEWARE! POST data cannot be redirected (some webservers complain) for
+ // HTTP codes 302 and 303
+ if (conn instanceof HttpURLConnection) {
+ int repCode = 0;
+ try {
+ // Can fail in some circumstances
+ repCode = ((HttpURLConnection) conn).getResponseCode();
+ } catch (IOException e) {
+ }
+
+ if (repCode / 100 == 3) {
+ String newUrl = conn.getHeaderField("Location");
+ return open(new URL(newUrl), originalUrl, currentReferer,
+ cookiesValues, //
+ (repCode == 302 || repCode == 303) ? null : postParams, //
+ getParams, oauth, stable);
+ }
+ }
+
+ try {
+ InputStream in = conn.getInputStream();
+ if ("gzip".equals(conn.getContentEncoding())) {
+ in = new GZIPInputStream(in);
+ }
+
+ if (in == null) {
+ throw new IOException("No InputStream!");
+ }
+
+ if (cache != null) {
+ String size = conn.getContentLength() < 0 ? "unknown size"
+ : StringUtils.formatNumber(conn.getContentLength())
+ + "bytes";
+ tracer.trace("Save to cache (" + size + "): " + originalUrl);
+ try {
+ try {
+ long bytes = cache.save(in, originalUrl);
+ tracer.trace("Saved to cache: "
+ + StringUtils.formatNumber(bytes) + "bytes");
+ } finally {
+ in.close();
+ }
+ in = cache.load(originalUrl, true, true);
+ } catch (IOException e) {
+ tracer.error(new IOException(
+ "Cannot save URL to cache, will ignore cache: "
+ + url, e));
+ }
+ }
+
+ if (in == null) {
+ throw new IOException(
+ "Cannot retrieve the file after storing it in the cache (??)");
+ }
+
+ return in;
+ } catch (IOException e) {
+ throw new IOException(String.format(
+ "Cannot find %s (current URL: %s)", originalUrl, url), e);
+ }
+ }
+
+ /**
+ * Open a connection on the given {@link URL}, and manage the cookies that
+ * come with it.
+ *
+ * @param url
+ * the {@link URL} to open
+ *
+ * @return the connection
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ private URLConnection openConnectionWithCookies(URL url,
+ URL currentReferer, Map<String, String> cookiesValues)
+ throws IOException {
+ URLConnection conn = url.openConnection();
+
+ String cookies = generateCookies(cookiesValues);
+ if (cookies != null && !cookies.isEmpty()) {
+ conn.setRequestProperty("Cookie", cookies);
+ }
+
+ conn.setRequestProperty("User-Agent", UA);
+ conn.setRequestProperty("Accept-Encoding", "gzip");
+ conn.setRequestProperty("Accept", "*/*");
+ conn.setRequestProperty("Charset", "utf-8");
+
+ if (currentReferer != null) {
+ conn.setRequestProperty("Referer", currentReferer.toString());
+ conn.setRequestProperty("Host", currentReferer.getHost());
+ }
+
+ return conn;
+ }
+
+ /**
+ * Generate the cookie {@link String} from the local {@link CookieStore} so
+ * it is ready to be passed.
+ *
+ * @return the cookie
+ */
+ private String generateCookies(Map<String, String> cookiesValues) {
+ StringBuilder builder = new StringBuilder();
+ for (HttpCookie cookie : cookies.getCookieStore().getCookies()) {
+ if (builder.length() > 0) {
+ builder.append(';');
+ }
+
+ builder.append(cookie.toString());
+ }
+
+ if (cookiesValues != null) {
+ for (Map.Entry<String, String> set : cookiesValues.entrySet()) {
+ if (builder.length() > 0) {
+ builder.append(';');
+ }
+ builder.append(set.getKey());
+ builder.append('=');
+ builder.append(set.getValue());
+ }
+ }
+
+ return builder.toString();
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import be.nikiroo.utils.streams.MarkableFileInputStream;
+
+/**
+ * This class offer some utilities based around Streams and Files.
+ *
+ * @author niki
+ */
+public class IOUtils {
+ /**
+ * Write the data to the given {@link File}.
+ *
+ * @param in
+ * the data source
+ * @param target
+ * the target {@link File}
+ *
+ * @return the number of bytes written
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static long write(InputStream in, File target) throws IOException {
+ OutputStream out = new FileOutputStream(target);
+ try {
+ return write(in, out);
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Write the data to the given {@link OutputStream}.
+ *
+ * @param in
+ * the data source
+ * @param out
+ * the target {@link OutputStream}
+ *
+ * @return the number of bytes written
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static long write(InputStream in, OutputStream out)
+ throws IOException {
+ long written = 0;
+ byte buffer[] = new byte[4096];
+ int len = in.read(buffer);
+ while (len > -1) {
+ out.write(buffer, 0, len);
+ written += len;
+ len = in.read(buffer);
+ }
+
+ return written;
+ }
+
+ /**
+ * Recursively Add a {@link File} (which can thus be a directory, too) to a
+ * {@link ZipOutputStream}.
+ *
+ * @param zip
+ * the stream
+ * @param base
+ * the path to prepend to the ZIP info before the actual
+ * {@link File} path
+ * @param target
+ * the source {@link File} (which can be a directory)
+ * @param targetIsRoot
+ * FALSE if we need to add a {@link ZipEntry} for base/target,
+ * TRUE to add it at the root of the ZIP
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static void zip(ZipOutputStream zip, String base, File target,
+ boolean targetIsRoot) throws IOException {
+ if (target.isDirectory()) {
+ if (!targetIsRoot) {
+ if (base == null || base.isEmpty()) {
+ base = target.getName();
+ } else {
+ base += "/" + target.getName();
+ }
+ zip.putNextEntry(new ZipEntry(base + "/"));
+ }
+
+ File[] files = target.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ zip(zip, base, file, false);
+ }
+ }
+ } else {
+ if (base == null || base.isEmpty()) {
+ base = target.getName();
+ } else {
+ base += "/" + target.getName();
+ }
+ zip.putNextEntry(new ZipEntry(base));
+ FileInputStream in = new FileInputStream(target);
+ try {
+ IOUtils.write(in, zip);
+ } finally {
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * Zip the given source into dest.
+ *
+ * @param src
+ * the source {@link File} (which can be a directory)
+ * @param dest
+ * the destination <tt>.zip</tt> file
+ * @param srcIsRoot
+ * FALSE if we need to add a {@link ZipEntry} for src, TRUE to
+ * add it at the root of the ZIP
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static void zip(File src, File dest, boolean srcIsRoot)
+ throws IOException {
+ OutputStream out = new FileOutputStream(dest);
+ try {
+ ZipOutputStream zip = new ZipOutputStream(out);
+ try {
+ IOUtils.zip(zip, "", src, srcIsRoot);
+ } finally {
+ zip.close();
+ }
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Unzip the given ZIP file into the target directory.
+ *
+ * @param zipFile
+ * the ZIP file
+ * @param targetDirectory
+ * the target directory
+ *
+ * @return the number of extracted files (not directories)
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static long unzip(File zipFile, File targetDirectory)
+ throws IOException {
+ long count = 0;
+
+ if (targetDirectory.exists() && targetDirectory.isFile()) {
+ throw new IOException("Cannot unzip " + zipFile + " into "
+ + targetDirectory + ": it is not a directory");
+ }
+
+ targetDirectory.mkdir();
+ if (!targetDirectory.exists()) {
+ throw new IOException("Cannot create target directory "
+ + targetDirectory);
+ }
+
+ FileInputStream in = new FileInputStream(zipFile);
+ try {
+ ZipInputStream zipStream = new ZipInputStream(in);
+ try {
+ for (ZipEntry entry = zipStream.getNextEntry(); entry != null; entry = zipStream
+ .getNextEntry()) {
+ File file = new File(targetDirectory, entry.getName());
+ if (entry.isDirectory()) {
+ file.mkdirs();
+ } else {
+ IOUtils.write(zipStream, file);
+ count++;
+ }
+ }
+ } finally {
+ zipStream.close();
+ }
+ } finally {
+ in.close();
+ }
+
+ return count;
+ }
+
+ /**
+ * Write the {@link String} content to {@link File}.
+ *
+ * @param dir
+ * the directory where to write the {@link File}
+ * @param filename
+ * the {@link File} name
+ * @param content
+ * the content
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static void writeSmallFile(File dir, String filename, String content)
+ throws IOException {
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ writeSmallFile(new File(dir, filename), content);
+ }
+
+ /**
+ * Write the {@link String} content to {@link File}.
+ *
+ * @param file
+ * the {@link File} to write
+ * @param content
+ * the content
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static void writeSmallFile(File file, String content)
+ throws IOException {
+ FileOutputStream out = new FileOutputStream(file);
+ try {
+ out.write(StringUtils.getBytes(content));
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Read the whole {@link File} content into a {@link String}.
+ *
+ * @param file
+ * the {@link File}
+ *
+ * @return the content
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static String readSmallFile(File file) throws IOException {
+ InputStream stream = new FileInputStream(file);
+ try {
+ return readSmallStream(stream);
+ } finally {
+ stream.close();
+ }
+ }
+
+ /**
+ * Read the whole {@link InputStream} content into a {@link String}.
+ *
+ * @param stream
+ * the {@link InputStream}
+ *
+ * @return the content
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static String readSmallStream(InputStream stream) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ write(stream, out);
+ return out.toString("UTF-8");
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Recursively delete the given {@link File}, which may of course also be a
+ * directory.
+ * <p>
+ * Will either silently continue or throw an exception in case of error,
+ * depending upon the parameters.
+ *
+ * @param target
+ * the target to delete
+ * @param exception
+ * TRUE to throw an {@link IOException} in case of error, FALSE
+ * to silently continue
+ *
+ * @return TRUE if all files were deleted, FALSE if an error occurred
+ *
+ * @throws IOException
+ * if an error occurred and the parameters allow an exception to
+ * be thrown
+ */
+ public static boolean deltree(File target, boolean exception)
+ throws IOException {
+ List<File> list = deltree(target, null);
+ if (exception && !list.isEmpty()) {
+ StringBuilder slist = new StringBuilder();
+ for (File file : list) {
+ slist.append("\n").append(file.getPath());
+ }
+
+ throw new IOException("Cannot delete all the files from: <" //
+ + target + ">:" + slist.toString());
+ }
+
+ return list.isEmpty();
+ }
+
+ /**
+ * Recursively delete the given {@link File}, which may of course also be a
+ * directory.
+ * <p>
+ * Will silently continue in case of error.
+ *
+ * @param target
+ * the target to delete
+ *
+ * @return TRUE if all files were deleted, FALSE if an error occurred
+ */
+ public static boolean deltree(File target) {
+ return deltree(target, null).isEmpty();
+ }
+
+ /**
+ * Recursively delete the given {@link File}, which may of course also be a
+ * directory.
+ * <p>
+ * Will collect all {@link File} that cannot be deleted in the given
+ * accumulator.
+ *
+ * @param target
+ * the target to delete
+ * @param errorAcc
+ * the accumulator to use for errors, or NULL to create a new one
+ *
+ * @return the errors accumulator
+ */
+ public static List<File> deltree(File target, List<File> errorAcc) {
+ if (errorAcc == null) {
+ errorAcc = new ArrayList<File>();
+ }
+
+ File[] files = target.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ errorAcc = deltree(file, errorAcc);
+ }
+ }
+
+ if (!target.delete()) {
+ errorAcc.add(target);
+ }
+
+ return errorAcc;
+ }
+
+ /**
+ * Open the resource next to the given {@link Class}.
+ *
+ * @param location
+ * the location where to look for the resource
+ * @param name
+ * the resource name (only the filename, no path)
+ *
+ * @return the opened resource if found, NULL if not
+ */
+ public static InputStream openResource(
+ @SuppressWarnings("rawtypes") Class location, String name) {
+ String loc = location.getName().replace(".", "/")
+ .replaceAll("/[^/]*$", "/");
+ return openResource(loc + name);
+ }
+
+ /**
+ * Open the given /-separated resource (from the binary root).
+ *
+ * @param name
+ * the resource name (the full path, with "/" as separator)
+ *
+ * @return the opened resource if found, NULL if not
+ */
+ public static InputStream openResource(String name) {
+ ClassLoader loader = IOUtils.class.getClassLoader();
+ if (loader == null) {
+ loader = ClassLoader.getSystemClassLoader();
+ }
+
+ return loader.getResourceAsStream(name);
+ }
+
+ /**
+ * Return a resetable {@link InputStream} from this stream, and reset it.
+ *
+ * @param in
+ * the input stream
+ * @return the resetable stream, which <b>may</b> be the same
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static InputStream forceResetableStream(InputStream in)
+ throws IOException {
+ boolean resetable = in.markSupported();
+ if (resetable) {
+ try {
+ in.reset();
+ } catch (IOException e) {
+ resetable = false;
+ }
+ }
+
+ if (resetable) {
+ return in;
+ }
+
+ final File tmp = File.createTempFile(".tmp-stream.", ".tmp");
+ try {
+ write(in, tmp);
+ in.close();
+
+ return new MarkableFileInputStream(tmp) {
+ @Override
+ public void close() throws IOException {
+ try {
+ super.close();
+ } finally {
+ tmp.delete();
+ }
+ }
+ };
+ } catch (IOException e) {
+ tmp.delete();
+ throw e;
+ }
+ }
+
+ /**
+ * Convert the {@link InputStream} into a byte array.
+ *
+ * @param in
+ * the input stream
+ *
+ * @return the array
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static byte[] toByteArray(InputStream in) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ write(in, out);
+ return out.toByteArray();
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Convert the {@link File} into a byte array.
+ *
+ * @param file
+ * the input {@link File}
+ *
+ * @return the array
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static byte[] toByteArray(File file) throws IOException {
+ FileInputStream fis = new FileInputStream(file);
+ try {
+ return toByteArray(fis);
+ } finally {
+ fis.close();
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+
+import be.nikiroo.utils.streams.Base64InputStream;
+import be.nikiroo.utils.streams.MarkableFileInputStream;
+
+/**
+ * This class represents an image data.
+ *
+ * @author niki
+ */
+public class Image implements Closeable, Serializable {
+ static private final long serialVersionUID = 1L;
+
+ static private File tempRoot;
+ static private TempFiles tmpRepository;
+ static private long count = 0;
+ static private Object lock = new Object();
+
+ private Object instanceLock = new Object();
+ private File data;
+ private long size;
+
+ /**
+ * Do not use -- for serialisation purposes only.
+ */
+ @SuppressWarnings("unused")
+ private Image() {
+ }
+
+ /**
+ * Create a new {@link Image} with the given data.
+ *
+ * @param data
+ * the data
+ */
+ public Image(byte[] data) {
+ ByteArrayInputStream in = new ByteArrayInputStream(data);
+ try {
+ this.data = getTemporaryFile();
+ size = IOUtils.write(in, this.data);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ try {
+ in.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Create an image from Base64 encoded data.
+ *
+ * <p>
+ * Please use {@link Image#Image(InputStream)} when possible instead, with a
+ * {@link Base64InputStream}; it can be much more efficient.
+ *
+ * @param base64EncodedData
+ * the Base64 encoded data as a String
+ *
+ * @throws IOException
+ * in case of I/O error or badly formated Base64
+ */
+ public Image(String base64EncodedData) throws IOException {
+ this(new Base64InputStream(new ByteArrayInputStream(
+ StringUtils.getBytes(base64EncodedData)), false));
+ }
+
+ /**
+ * Create a new {@link Image} from a stream.
+ *
+ * @param in
+ * the stream
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public Image(InputStream in) throws IOException {
+ data = getTemporaryFile();
+ size = IOUtils.write(in, data);
+ }
+
+ /**
+ * The size of the enclosed image in bytes.
+ *
+ * @return the size
+ */
+ public long getSize() {
+ return size;
+ }
+
+ /**
+ * Generate an {@link InputStream} that you can {@link InputStream#reset()}
+ * for this {@link Image}.
+ * <p>
+ * This {@link InputStream} will (always) be a new one, and <b>you</b> are
+ * responsible for it.
+ * <p>
+ * Note: take care that the {@link InputStream} <b>must not</b> live past
+ * the {@link Image} life time!
+ *
+ * @return the stream
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public InputStream newInputStream() throws IOException {
+ return new MarkableFileInputStream(data);
+ }
+
+ /**
+ * <b>Read</b> the actual image data, as a byte array.
+ *
+ * @deprecated if possible, prefer the {@link Image#newInputStream()}
+ * method, as it can be more efficient
+ *
+ * @return the image data
+ */
+ @Deprecated
+ public byte[] getData() {
+ try {
+ InputStream in = newInputStream();
+ try {
+ return IOUtils.toByteArray(in);
+ } finally {
+ in.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Convert the given {@link Image} object into a Base64 representation of
+ * the same {@link Image} object.
+ *
+ * @deprecated Please use {@link Image#newInputStream()} instead, it is more
+ * efficient
+ *
+ * @return the Base64 representation
+ */
+ @Deprecated
+ public String toBase64() {
+ try {
+ Base64InputStream stream = new Base64InputStream(newInputStream(),
+ true);
+ try {
+ return IOUtils.readSmallStream(stream);
+ } finally {
+ stream.close();
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Closing the {@link Image} will delete the associated temporary file on
+ * disk.
+ * <p>
+ * Note that even if you don't, the program will still <b>try</b> to delete
+ * all the temporary files at JVM termination.
+ */
+ @Override
+ public void close() throws IOException {
+ synchronized (instanceLock) {
+ if (size >= 0) {
+ size = -1;
+ data.delete();
+ data = null;
+
+ synchronized (lock) {
+ count--;
+ if (count <= 0) {
+ count = 0;
+ tmpRepository.close();
+ tmpRepository = null;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Return a newly created temporary file to work on.
+ *
+ * @return the file
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ private File getTemporaryFile() throws IOException {
+ synchronized (lock) {
+ if (tmpRepository == null) {
+ tmpRepository = new TempFiles(tempRoot, "images");
+ count = 0;
+ }
+
+ count++;
+
+ return tmpRepository.createTempFile("image");
+ }
+ }
+
+ /**
+ * Write this {@link Image} for serialization purposes; that is, write the
+ * content of the backing temporary file.
+ *
+ * @param out
+ * the {@link OutputStream} to write to
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ InputStream in = newInputStream();
+ try {
+ IOUtils.write(in, out);
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Read an {@link Image} written by
+ * {@link Image#writeObject(java.io.ObjectOutputStream)}; that is, create a
+ * new temporary file with the saved content.
+ *
+ * @param in
+ * the {@link InputStream} to read from
+ * @throws IOException
+ * in case of I/O error
+ * @throws ClassNotFoundException
+ * will not be thrown by this method
+ */
+ @SuppressWarnings("unused")
+ private void readObject(ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ data = getTemporaryFile();
+ IOUtils.write(in, data);
+ }
+
+ /**
+ * Change the temporary root directory used by the program.
+ * <p>
+ * Caution: the directory will be <b>owned</b> by the system, all its files
+ * now belong to us (and will most probably be deleted).
+ * <p>
+ * Note: it may take some time until the new temporary root is used, we
+ * first need to make sure the previous one is not used anymore (i.e., we
+ * must reach a point where no unclosed {@link Image} remains in memory) to
+ * switch the temporary root.
+ *
+ * @param root
+ * the new temporary root, which will be <b>owned</b> by the
+ * system
+ */
+ public static void setTemporaryFilesRoot(File root) {
+ tempRoot = root;
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import be.nikiroo.utils.serial.SerialUtils;
+
+/**
+ * This class offer some utilities based around images.
+ *
+ * @author niki
+ */
+public abstract class ImageUtils {
+ private static ImageUtils instance = newObject();
+
+ /**
+ * Get a (unique) instance of an {@link ImageUtils} compatible with your
+ * system.
+ *
+ * @return an {@link ImageUtils}
+ */
+ public static ImageUtils getInstance() {
+ return instance;
+ }
+
+ /**
+ * Save the given resource as an image on disk using the given image format
+ * for content, or with "png" format if it fails.
+ *
+ * @param img
+ * the resource
+ * @param target
+ * the target file
+ * @param format
+ * the file format ("png", "jpeg", "bmp"...)
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public abstract void saveAsImage(Image img, File target, String format)
+ throws IOException;
+
+ /**
+ * Return the EXIF transformation flag of this image if any.
+ *
+ * <p>
+ * Note: this code has been found on internet; thank you anonymous coder.
+ * </p>
+ *
+ * @param in
+ * the data {@link InputStream}
+ *
+ * @return the transformation flag if any
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ protected static int getExifTransorm(InputStream in) throws IOException {
+ int[] exif_data = new int[100];
+ int set_flag = 0;
+ int is_motorola = 0;
+
+ /* Read File head, check for JPEG SOI + Exif APP1 */
+ for (int i = 0; i < 4; i++)
+ exif_data[i] = in.read();
+
+ if (exif_data[0] != 0xFF || exif_data[1] != 0xD8
+ || exif_data[2] != 0xFF || exif_data[3] != 0xE1)
+ return -2;
+
+ /* Get the marker parameter length count */
+ int length = (in.read() << 8 | in.read());
+
+ /* Length includes itself, so must be at least 2 */
+ /* Following Exif data length must be at least 6 */
+ if (length < 8)
+ return -1;
+ length -= 8;
+ /* Read Exif head, check for "Exif" */
+ for (int i = 0; i < 6; i++)
+ exif_data[i] = in.read();
+
+ if (exif_data[0] != 0x45 || exif_data[1] != 0x78
+ || exif_data[2] != 0x69 || exif_data[3] != 0x66
+ || exif_data[4] != 0 || exif_data[5] != 0)
+ return -1;
+
+ /* Read Exif body */
+ length = length > exif_data.length ? exif_data.length : length;
+ for (int i = 0; i < length; i++)
+ exif_data[i] = in.read();
+
+ if (length < 12)
+ return -1; /* Length of an IFD entry */
+
+ /* Discover byte order */
+ if (exif_data[0] == 0x49 && exif_data[1] == 0x49)
+ is_motorola = 0;
+ else if (exif_data[0] == 0x4D && exif_data[1] == 0x4D)
+ is_motorola = 1;
+ else
+ return -1;
+
+ /* Check Tag Mark */
+ if (is_motorola == 1) {
+ if (exif_data[2] != 0)
+ return -1;
+ if (exif_data[3] != 0x2A)
+ return -1;
+ } else {
+ if (exif_data[3] != 0)
+ return -1;
+ if (exif_data[2] != 0x2A)
+ return -1;
+ }
+
+ /* Get first IFD offset (offset to IFD0) */
+ int offset;
+ if (is_motorola == 1) {
+ if (exif_data[4] != 0)
+ return -1;
+ if (exif_data[5] != 0)
+ return -1;
+ offset = exif_data[6];
+ offset <<= 8;
+ offset += exif_data[7];
+ } else {
+ if (exif_data[7] != 0)
+ return -1;
+ if (exif_data[6] != 0)
+ return -1;
+ offset = exif_data[5];
+ offset <<= 8;
+ offset += exif_data[4];
+ }
+ if (offset > length - 2)
+ return -1; /* check end of data segment */
+
+ /* Get the number of directory entries contained in this IFD */
+ int number_of_tags;
+ if (is_motorola == 1) {
+ number_of_tags = exif_data[offset];
+ number_of_tags <<= 8;
+ number_of_tags += exif_data[offset + 1];
+ } else {
+ number_of_tags = exif_data[offset + 1];
+ number_of_tags <<= 8;
+ number_of_tags += exif_data[offset];
+ }
+ if (number_of_tags == 0)
+ return -1;
+ offset += 2;
+
+ /* Search for Orientation Tag in IFD0 */
+ for (;;) {
+ if (offset > length - 12)
+ return -1; /* check end of data segment */
+ /* Get Tag number */
+ int tagnum;
+ if (is_motorola == 1) {
+ tagnum = exif_data[offset];
+ tagnum <<= 8;
+ tagnum += exif_data[offset + 1];
+ } else {
+ tagnum = exif_data[offset + 1];
+ tagnum <<= 8;
+ tagnum += exif_data[offset];
+ }
+ if (tagnum == 0x0112)
+ break; /* found Orientation Tag */
+ if (--number_of_tags == 0)
+ return -1;
+ offset += 12;
+ }
+
+ /* Get the Orientation value */
+ if (is_motorola == 1) {
+ if (exif_data[offset + 8] != 0)
+ return -1;
+ set_flag = exif_data[offset + 9];
+ } else {
+ if (exif_data[offset + 9] != 0)
+ return -1;
+ set_flag = exif_data[offset + 8];
+ }
+ if (set_flag > 8)
+ return -1;
+
+ return set_flag;
+ }
+
+ /**
+ * Check that the class can operate (for instance, that all the required
+ * libraries or frameworks are present).
+ *
+ * @return TRUE if it works
+ */
+ abstract protected boolean check();
+
+ /**
+ * Create a new {@link ImageUtils}.
+ *
+ * @return the {@link ImageUtils}
+ */
+ private static ImageUtils newObject() {
+ for (String clazz : new String[] { "be.nikiroo.utils.ui.ImageUtilsAwt",
+ "be.nikiroo.utils.android.ImageUtilsAndroid" }) {
+ try {
+ ImageUtils obj = (ImageUtils) SerialUtils.createObject(clazz);
+ if (obj.check()) {
+ return obj;
+ }
+ } catch (Throwable e) {
+ }
+ }
+
+ return null;
+ }
+}
+++ /dev/null
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<https://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<https://www.gnu.org/licenses/why-not-lgpl.html>.
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+/**
+ * Class was moved to {@link be.nikiroo.utils.streams.MarkableFileInputStream}.
+ *
+ * @author niki
+ */
+@Deprecated
+public class MarkableFileInputStream extends
+ be.nikiroo.utils.streams.MarkableFileInputStream {
+ public MarkableFileInputStream(File file) throws FileNotFoundException {
+ super(file);
+ }
+
+ public MarkableFileInputStream(FileInputStream fis) {
+ super(fis);
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.util.ArrayList;
+import java.util.EventListener;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Progress reporting system, possibly nested.
+ *
+ * @author niki
+ */
+public class Progress {
+ /**
+ * This event listener is designed to report progress events from
+ * {@link Progress}.
+ *
+ * @author niki
+ */
+ public interface ProgressListener extends EventListener {
+ /**
+ * A progression event.
+ *
+ * @param progress
+ * the {@link Progress} object that generated it, not
+ * necessarily the same as the one where the listener was
+ * attached (it could be a child {@link Progress} of this
+ * {@link Progress}).
+ * @param name
+ * the first non-null name of the {@link Progress} step that
+ * generated this event
+ */
+ public void progress(Progress progress, String name);
+ }
+
+ private Progress parent = null;
+ private Object lock = new Object();
+ private String name;
+ private Map<Progress, Double> children;
+ private List<ProgressListener> listeners;
+ private int min;
+ private int max;
+ private double relativeLocalProgress;
+ private double relativeProgress; // children included
+
+ /**
+ * Create a new default unnamed {@link Progress}, from 0 to 100.
+ */
+ public Progress() {
+ this(null);
+ }
+
+ /**
+ * Create a new default {@link Progress}, from 0 to 100.
+ *
+ * @param name
+ * the name of this {@link Progress} step
+ */
+ public Progress(String name) {
+ this(name, 0, 100);
+ }
+
+ /**
+ * Create a new unnamed {@link Progress}, from min to max.
+ *
+ * @param min
+ * the minimum progress value (and starting value) -- must be
+ * non-negative
+ * @param max
+ * the maximum progress value
+ */
+ public Progress(int min, int max) {
+ this(null, min, max);
+ }
+
+ /**
+ * Create a new {@link Progress}, from min to max.
+ *
+ * @param name
+ * the name of this {@link Progress} step
+ * @param min
+ * the minimum progress value (and starting value) -- must be
+ * non-negative
+ * @param max
+ * the maximum progress value
+ */
+ public Progress(String name, int min, int max) {
+ this.name = name;
+ this.children = new HashMap<Progress, Double>();
+ this.listeners = new ArrayList<Progress.ProgressListener>();
+ setMinMax(min, max);
+ setProgress(min);
+ }
+
+ /**
+ * The name of this {@link Progress} step.
+ *
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * The name of this {@link Progress} step.
+ *
+ * @param name
+ * the new name
+ */
+ public void setName(String name) {
+ this.name = name;
+ changed(this, name);
+ }
+
+ /**
+ * The minimum progress value.
+ *
+ * @return the min
+ */
+ public int getMin() {
+ return min;
+ }
+
+ /**
+ * The minimum progress value.
+ *
+ * @param min
+ * the min to set
+ *
+ *
+ * @throws RuntimeException
+ * if min < 0 or if min > max
+ */
+ public void setMin(int min) {
+ if (min < 0) {
+ throw new RuntimeException("negative values not supported");
+ }
+
+ synchronized (lock) {
+ if (min > max) {
+ throw new RuntimeException(
+ "The minimum progress value must be <= the maximum progress value");
+ }
+
+ this.min = min;
+ }
+ }
+
+ /**
+ * The maximum progress value.
+ *
+ * @return the max
+ */
+ public int getMax() {
+ return max;
+ }
+
+ /**
+ * The maximum progress value (must be >= the minimum progress value).
+ *
+ * @param max
+ * the max to set
+ *
+ *
+ * @throws RuntimeException
+ * if max < min
+ */
+ public void setMax(int max) {
+ synchronized (lock) {
+ if (max < min) {
+ throw new Error(
+ "The maximum progress value must be >= the minimum progress value");
+ }
+
+ this.max = max;
+ }
+ }
+
+ /**
+ * Set both the minimum and maximum progress values.
+ *
+ * @param min
+ * the min
+ * @param max
+ * the max
+ *
+ * @throws RuntimeException
+ * if min < 0 or if min > max
+ */
+ public void setMinMax(int min, int max) {
+ if (min < 0) {
+ throw new RuntimeException("negative values not supported");
+ }
+
+ if (min > max) {
+ throw new RuntimeException(
+ "The minimum progress value must be <= the maximum progress value");
+ }
+
+ synchronized (lock) {
+ this.min = min;
+ this.max = max;
+ }
+ }
+
+ /**
+ * Get the total progress value (including the optional children
+ * {@link Progress}) on a {@link Progress#getMin()} to
+ * {@link Progress#getMax()} scale.
+ *
+ * @return the progress the value
+ */
+ public int getProgress() {
+ return (int) Math.round(relativeProgress * (max - min));
+ }
+
+ /**
+ * Set the local progress value (not including the optional children
+ * {@link Progress}), on a {@link Progress#getMin()} to
+ * {@link Progress#getMax()} scale.
+ *
+ * @param progress
+ * the progress to set
+ */
+ public void setProgress(int progress) {
+ synchronized (lock) {
+ double childrenProgress = relativeProgress - relativeLocalProgress;
+
+ relativeLocalProgress = ((double) progress) / (max - min);
+
+ setRelativeProgress(this, name, relativeLocalProgress
+ + childrenProgress);
+ }
+ }
+
+ /**
+ * Get the total progress value (including the optional children
+ * {@link Progress}) on a 0.0 to 1.0 scale.
+ *
+ * @return the progress
+ */
+ public double getRelativeProgress() {
+ return relativeProgress;
+ }
+
+ /**
+ * Set the total progress value (including the optional children
+ * {@link Progress}), on a 0 to 1 scale.
+ *
+ * @param pg
+ * the {@link Progress} to report as the progression emitter
+ * @param name
+ * the current name (if it is NULL, the first non-null name in
+ * the hierarchy will overwrite it) of the {@link Progress} who
+ * emitted this change
+ * @param relativeProgress
+ * the progress to set
+ */
+ private void setRelativeProgress(Progress pg, String name,
+ double relativeProgress) {
+ synchronized (lock) {
+ relativeProgress = Math.max(0, relativeProgress);
+ relativeProgress = Math.min(1, relativeProgress);
+ this.relativeProgress = relativeProgress;
+
+ changed(pg, name);
+ }
+ }
+
+ /**
+ * Get the total progress value (including the optional children
+ * {@link Progress}) on a 0 to 1 scale.
+ *
+ * @return the progress the value
+ */
+ private int getLocalProgress() {
+ return (int) Math.round(relativeLocalProgress * (max - min));
+ }
+
+ /**
+ * Add some value to the current progression of this {@link Progress}.
+ *
+ * @param step
+ * the amount to add
+ */
+ public void add(int step) {
+ synchronized (lock) {
+ setProgress(getLocalProgress() + step);
+ }
+ }
+
+ /**
+ * Check if the action corresponding to this {@link Progress} is done (i.e.,
+ * if its progress value == its max value).
+ *
+ * @return TRUE if it is
+ */
+ public boolean isDone() {
+ return getProgress() == max;
+ }
+
+ /**
+ * Mark the {@link Progress} as done by setting its value to max.
+ */
+ public void done() {
+ synchronized (lock) {
+ double childrenProgress = relativeProgress - relativeLocalProgress;
+ relativeLocalProgress = 1 - childrenProgress;
+ setRelativeProgress(this, name, 1d);
+ }
+ }
+
+ /**
+ * Return the list of direct children of this {@link Progress}.
+ *
+ * @return the children (Who will think of the children??)
+ */
+ public List<Progress> getChildren() {
+ synchronized (lock) {
+ return new ArrayList<Progress>(children.keySet());
+ }
+ }
+
+ /**
+ * Notify the listeners that this {@link Progress} changed value.
+ *
+ * @param pg
+ * the emmiter, that is, the (sub-){link Progress} that just
+ * reported some change, not always the same as <tt>this</tt>
+ * @param name
+ * the current name (if it is NULL, the first non-null name in
+ * the hierarchy will overwrite it) of the {@link Progress} who
+ * emitted this change
+ */
+ private void changed(Progress pg, String name) {
+ if (pg == null) {
+ pg = this;
+ }
+
+ if (name == null) {
+ name = this.name;
+ }
+
+ synchronized (lock) {
+ for (ProgressListener l : listeners) {
+ l.progress(pg, name);
+ }
+ }
+ }
+
+ /**
+ * Add a {@link ProgressListener} that will trigger on progress changes.
+ * <p>
+ * Note: the {@link Progress} that will be reported will be the active
+ * progress, not necessarily the same as the current one (it could be a
+ * child {@link Progress} of this {@link Progress}).
+ *
+ * @param l
+ * the listener
+ */
+ public void addProgressListener(ProgressListener l) {
+ synchronized (lock) {
+ this.listeners.add(l);
+ }
+ }
+
+ /**
+ * Remove a {@link ProgressListener} that would trigger on progress changes.
+ *
+ * @param l
+ * the listener
+ *
+ * @return TRUE if it was found (and removed)
+ */
+ public boolean removeProgressListener(ProgressListener l) {
+ synchronized (lock) {
+ return this.listeners.remove(l);
+ }
+ }
+
+ /**
+ * Add a child {@link Progress} of the given weight.
+ *
+ * @param progress
+ * the child {@link Progress} to add
+ * @param weight
+ * the weight (on a {@link Progress#getMin()} to
+ * {@link Progress#getMax()} scale) of this child
+ * {@link Progress} in relation to its parent
+ *
+ * @throws RuntimeException
+ * if weight exceed {@link Progress#getMax()} or if progress
+ * already has a parent
+ */
+ public void addProgress(Progress progress, double weight) {
+ if (weight < min || weight > max) {
+ throw new RuntimeException(String.format(
+ "Progress object %s cannot have a weight of %f, "
+ + "it is outside of its parent (%s) range (%d)",
+ progress.name, weight, name, max));
+ }
+
+ if (progress.parent != null) {
+ throw new RuntimeException(String.format(
+ "Progress object %s cannot be added to %s, "
+ + "as it already has a parent (%s)", progress.name,
+ name, progress.parent.name));
+ }
+
+ ProgressListener progressListener = new ProgressListener() {
+ @Override
+ public void progress(Progress pg, String name) {
+ synchronized (lock) {
+ double total = relativeLocalProgress;
+ for (Entry<Progress, Double> entry : children.entrySet()) {
+ total += (entry.getValue() / (max - min))
+ * entry.getKey().getRelativeProgress();
+ }
+
+ setRelativeProgress(pg, name, total);
+ }
+ }
+ };
+
+ synchronized (lock) {
+ progress.parent = this;
+ this.children.put(progress, weight);
+ progress.addProgressListener(progressListener);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+
+/**
+ * Simple proxy helper to select a default internet proxy.
+ *
+ * @author niki
+ */
+public class Proxy {
+ /**
+ * Use the proxy described by this string:
+ * <ul>
+ * <li><tt>((user(:pass)@)proxy:port)</tt></li>
+ * <li>System proxy is noted <tt>:</tt></li>
+ * </ul>
+ * Some examples:
+ * <ul>
+ * <li><tt></tt> → do not use any proxy</li>
+ * <li><tt>:</tt> → use the system proxy</li>
+ * <li><tt>user@prox.com</tt> → use the proxy "prox.com" with default port
+ * and user "user"</li>
+ * <li><tt>prox.com:8080</tt> → use the proxy "prox.com" on port 8080</li>
+ * <li><tt>user:pass@prox.com:8080</tt> → use "prox.com" on port 8080
+ * authenticated as "user" with password "pass"</li>
+ * <li><tt>user:pass@:</tt> → use the system proxy authenticated as user
+ * "user" with password "pass"</li>
+ * </ul>
+ *
+ * @param proxy
+ * the proxy
+ */
+ static public void use(String proxy) {
+ if (proxy != null && !proxy.isEmpty()) {
+ String user = null;
+ String password = null;
+ int port = 8080;
+
+ if (proxy.contains("@")) {
+ int pos = proxy.indexOf("@");
+ user = proxy.substring(0, pos);
+ proxy = proxy.substring(pos + 1);
+ if (user.contains(":")) {
+ pos = user.indexOf(":");
+ password = user.substring(pos + 1);
+ user = user.substring(0, pos);
+ }
+ }
+
+ if (proxy.equals(":")) {
+ proxy = null;
+ } else if (proxy.contains(":")) {
+ int pos = proxy.indexOf(":");
+ try {
+ port = Integer.parseInt(proxy.substring(0, pos));
+ proxy = proxy.substring(pos + 1);
+ } catch (Exception e) {
+ }
+ }
+
+ if (proxy == null) {
+ Proxy.useSystemProxy(user, password);
+ } else {
+ Proxy.useProxy(proxy, port, user, password);
+ }
+ }
+ }
+
+ /**
+ * Use the system proxy.
+ */
+ static public void useSystemProxy() {
+ useSystemProxy(null, null);
+ }
+
+ /**
+ * Use the system proxy with the given login/password, for authenticated
+ * proxies.
+ *
+ * @param user
+ * the user name or login
+ * @param password
+ * the password
+ */
+ static public void useSystemProxy(String user, String password) {
+ System.setProperty("java.net.useSystemProxies", "true");
+ auth(user, password);
+ }
+
+ /**
+ * Use the give proxy.
+ *
+ * @param host
+ * the proxy host name or IP address
+ * @param port
+ * the port to use
+ */
+ static public void useProxy(String host, int port) {
+ useProxy(host, port, null, null);
+ }
+
+ /**
+ * Use the given proxy with the given login/password, for authenticated
+ * proxies.
+ *
+ * @param user
+ * the user name or login
+ * @param password
+ * the password
+ * @param host
+ * the proxy host name or IP address
+ * @param port
+ * the port to use
+ * @param user
+ * the user name or login
+ * @param password
+ * the password
+ */
+ static public void useProxy(String host, int port, String user,
+ String password) {
+ System.setProperty("http.proxyHost", host);
+ System.setProperty("http.proxyPort", Integer.toString(port));
+ auth(user, password);
+ }
+
+ /**
+ * Select the default authenticator for proxy requests.
+ *
+ * @param user
+ * the user name or login
+ * @param password
+ * the password
+ */
+ static private void auth(final String user, final String password) {
+ if (user != null && password != null) {
+ Authenticator proxy = new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ if (getRequestorType() == RequestorType.PROXY) {
+ return new PasswordAuthentication(user,
+ password.toCharArray());
+ }
+ return null;
+ }
+ };
+ Authenticator.setDefault(proxy);
+ }
+ }
+}
--- /dev/null
+/*
+ * This file was taken from:
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2017 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ *
+ * I added some changes to integrate it here.
+ * @author Niki
+ */
+package be.nikiroo.utils;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * StringJustifier contains methods to convert one or more long lines of strings
+ * into justified text paragraphs.
+ */
+class StringJustifier {
+ /**
+ * Process the given text into a list of left-justified lines of a given
+ * max-width.
+ *
+ * @param data
+ * the text to justify
+ * @param width
+ * the maximum width of a line
+ *
+ * @return the list of justified lines
+ */
+ static List<String> left(final String data, final int width) {
+ return left(data, width, false);
+ }
+
+ /**
+ * Right-justify a string into a list of lines.
+ *
+ * @param str
+ * the string
+ * @param n
+ * the maximum number of characters in a line
+ * @return the list of lines
+ */
+ static List<String> right(final String str, final int n) {
+ List<String> result = new LinkedList<String>();
+
+ /*
+ * Same as left(), but preceed each line with spaces to make it n chars
+ * long.
+ */
+ List<String> lines = left(str, n);
+ for (String line : lines) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < n - line.length(); i++) {
+ sb.append(' ');
+ }
+ sb.append(line);
+ result.add(sb.toString());
+ }
+
+ return result;
+ }
+
+ /**
+ * Center a string into a list of lines.
+ *
+ * @param str
+ * the string
+ * @param n
+ * the maximum number of characters in a line
+ * @return the list of lines
+ */
+ static List<String> center(final String str, final int n) {
+ List<String> result = new LinkedList<String>();
+
+ /*
+ * Same as left(), but preceed/succeed each line with spaces to make it
+ * n chars long.
+ */
+ List<String> lines = left(str, n);
+ for (String line : lines) {
+ StringBuilder sb = new StringBuilder();
+ int l = (n - line.length()) / 2;
+ int r = n - line.length() - l;
+ for (int i = 0; i < l; i++) {
+ sb.append(' ');
+ }
+ sb.append(line);
+ for (int i = 0; i < r; i++) {
+ sb.append(' ');
+ }
+ result.add(sb.toString());
+ }
+
+ return result;
+ }
+
+ /**
+ * Fully-justify a string into a list of lines.
+ *
+ * @param str
+ * the string
+ * @param n
+ * the maximum number of characters in a line
+ * @return the list of lines
+ */
+ static List<String> full(final String str, final int n) {
+ List<String> result = new LinkedList<String>();
+
+ /*
+ * Same as left(true), but insert spaces between words to make each line
+ * n chars long. The "algorithm" here is pretty dumb: it performs a
+ * split on space and then re-inserts multiples of n between words.
+ */
+ List<String> lines = left(str, n, true);
+ for (int lineI = 0; lineI < lines.size() - 1; lineI++) {
+ String line = lines.get(lineI);
+ String[] words = line.split(" ");
+ if (words.length > 1) {
+ int charCount = 0;
+ for (int i = 0; i < words.length; i++) {
+ charCount += words[i].length();
+ }
+ int spaceCount = n - charCount;
+ int q = spaceCount / (words.length - 1);
+ int r = spaceCount % (words.length - 1);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < words.length - 1; i++) {
+ sb.append(words[i]);
+ for (int j = 0; j < q; j++) {
+ sb.append(' ');
+ }
+ if (r > 0) {
+ sb.append(' ');
+ r--;
+ }
+ }
+ for (int j = 0; j < r; j++) {
+ sb.append(' ');
+ }
+ sb.append(words[words.length - 1]);
+ result.add(sb.toString());
+ } else {
+ result.add(line);
+ }
+ }
+ if (lines.size() > 0) {
+ result.add(lines.get(lines.size() - 1));
+ }
+
+ return result;
+ }
+
+ /**
+ * Process the given text into a list of left-justified lines of a given
+ * max-width.
+ *
+ * @param data
+ * the text to justify
+ * @param width
+ * the maximum width of a line
+ * @param minTwoWords
+ * use 2 words per line minimum if the text allows it
+ *
+ * @return the list of justified lines
+ */
+ static private List<String> left(final String data, final int width,
+ boolean minTwoWords) {
+ List<String> lines = new LinkedList<String>();
+
+ for (String dataLine : data.split("\n")) {
+ String line = rightTrim(dataLine.replace("\t", " "));
+
+ if (width > 0 && line.length() > width) {
+ while (line.length() > 0) {
+ int i = Math.min(line.length(), width - 1); // -1 for "-"
+
+ boolean needDash = true;
+ // find the best space if any and if needed
+ int prevSpace = 0;
+ if (i < line.length()) {
+ prevSpace = -1;
+ int space = line.indexOf(' ');
+ int numOfSpaces = 0;
+
+ while (space > -1 && space <= i) {
+ prevSpace = space;
+ space = line.indexOf(' ', space + 1);
+ numOfSpaces++;
+ }
+
+ if (prevSpace > 0 && (!minTwoWords || numOfSpaces >= 2)) {
+ i = prevSpace;
+ needDash = false;
+ }
+ }
+ //
+
+ // no dash before space/dash
+ if ((i + 1) < line.length()) {
+ char car = line.charAt(i);
+ char nextCar = line.charAt(i + 1);
+ if (car == ' ' || car == '-' || nextCar == ' ') {
+ needDash = false;
+ } else if (i > 0) {
+ char prevCar = line.charAt(i - 1);
+ if (prevCar == ' ' || prevCar == '-') {
+ needDash = false;
+ i--;
+ }
+ }
+ }
+
+ // if the space freed by the removed dash allows it, or if
+ // it is the last char, add the next char
+ if (!needDash || i >= line.length() - 1) {
+ int checkI = Math.min(i + 1, line.length());
+ if (checkI == i || checkI <= width) {
+ needDash = false;
+ i = checkI;
+ }
+ }
+
+ // no dash before parenthesis (but cannot add one more
+ // after)
+ if ((i + 1) < line.length()) {
+ char nextCar = line.charAt(i + 1);
+ if (nextCar == '(' || nextCar == ')') {
+ needDash = false;
+ }
+ }
+
+ if (needDash) {
+ lines.add(rightTrim(line.substring(0, i)) + "-");
+ } else {
+ lines.add(rightTrim(line.substring(0, i)));
+ }
+
+ // full trim (remove spaces when cutting)
+ line = line.substring(i).trim();
+ }
+ } else {
+ lines.add(line);
+ }
+ }
+
+ return lines;
+ }
+
+ /**
+ * Trim the given {@link String} on the right only.
+ *
+ * @param data
+ * the source {@link String}
+ * @return the right-trimmed String or Empty if it was NULL
+ */
+ static private String rightTrim(String data) {
+ if (data == null)
+ return "";
+
+ return ("|" + data).trim().substring(1);
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.unbescape.html.HtmlEscape;
+import org.unbescape.html.HtmlEscapeLevel;
+import org.unbescape.html.HtmlEscapeType;
+
+import be.nikiroo.utils.streams.Base64InputStream;
+import be.nikiroo.utils.streams.Base64OutputStream;
+
+/**
+ * This class offer some utilities based around {@link String}s.
+ *
+ * @author niki
+ */
+public class StringUtils {
+ /**
+ * This enum type will decide the alignment of a {@link String} when padding
+ * or justification is applied (if there is enough horizontal space for it
+ * to be aligned).
+ */
+ public enum Alignment {
+ /** Aligned at left. */
+ LEFT,
+ /** Centered. */
+ CENTER,
+ /** Aligned at right. */
+ RIGHT,
+ /** Full justified (to both left and right). */
+ JUSTIFY,
+
+ // Old Deprecated values:
+
+ /** DEPRECATED: please use LEFT. */
+ @Deprecated
+ Beginning,
+ /** DEPRECATED: please use CENTER. */
+ @Deprecated
+ Center,
+ /** DEPRECATED: please use RIGHT. */
+ @Deprecated
+ End;
+
+ /**
+ * Return the non-deprecated version of this enum if needed (or return
+ * self if not).
+ *
+ * @return the non-deprecated value
+ */
+ Alignment undeprecate() {
+ if (this == Beginning)
+ return LEFT;
+ if (this == Center)
+ return CENTER;
+ if (this == End)
+ return RIGHT;
+ return this;
+ }
+ }
+
+ static private Pattern marks = getMarks();
+
+ /**
+ * Fix the size of the given {@link String} either with space-padding or by
+ * shortening it.
+ *
+ * @param text
+ * the {@link String} to fix
+ * @param width
+ * the size of the resulting {@link String} or -1 for a noop
+ *
+ * @return the resulting {@link String} of size <i>size</i>
+ */
+ static public String padString(String text, int width) {
+ return padString(text, width, true, null);
+ }
+
+ /**
+ * Fix the size of the given {@link String} either with space-padding or by
+ * optionally shortening it.
+ *
+ * @param text
+ * the {@link String} to fix
+ * @param width
+ * the size of the resulting {@link String} if the text fits or
+ * if cut is TRUE or -1 for a noop
+ * @param cut
+ * cut the {@link String} shorter if needed
+ * @param align
+ * align the {@link String} in this position if we have enough
+ * space (default is Alignment.Beginning)
+ *
+ * @return the resulting {@link String} of size <i>size</i> minimum
+ */
+ static public String padString(String text, int width, boolean cut,
+ Alignment align) {
+
+ if (align == null) {
+ align = Alignment.LEFT;
+ }
+
+ align = align.undeprecate();
+
+ if (width >= 0) {
+ if (text == null)
+ text = "";
+
+ int diff = width - text.length();
+
+ if (diff < 0) {
+ if (cut)
+ text = text.substring(0, width);
+ } else if (diff > 0) {
+ if (diff < 2 && align != Alignment.RIGHT)
+ align = Alignment.LEFT;
+
+ switch (align) {
+ case RIGHT:
+ text = new String(new char[diff]).replace('\0', ' ') + text;
+ break;
+ case CENTER:
+ int pad1 = (diff) / 2;
+ int pad2 = (diff + 1) / 2;
+ text = new String(new char[pad1]).replace('\0', ' ') + text
+ + new String(new char[pad2]).replace('\0', ' ');
+ break;
+ case LEFT:
+ default:
+ text = text + new String(new char[diff]).replace('\0', ' ');
+ break;
+ }
+ }
+ }
+
+ return text;
+ }
+
+ /**
+ * Justify a text into width-sized (at the maximum) lines and return all the
+ * lines concatenated into a single '\\n'-separated line of text.
+ *
+ * @param text
+ * the {@link String} to justify
+ * @param width
+ * the maximum size of the resulting lines
+ *
+ * @return a list of justified text lines concatenated into a single
+ * '\\n'-separated line of text
+ */
+ static public String justifyTexts(String text, int width) {
+ StringBuilder builder = new StringBuilder();
+ for (String line : justifyText(text, width, null)) {
+ if (builder.length() > 0) {
+ builder.append('\n');
+ }
+ builder.append(line);
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Justify a text into width-sized (at the maximum) lines.
+ *
+ * @param text
+ * the {@link String} to justify
+ * @param width
+ * the maximum size of the resulting lines
+ *
+ * @return a list of justified text lines
+ */
+ static public List<String> justifyText(String text, int width) {
+ return justifyText(text, width, null);
+ }
+
+ /**
+ * Justify a text into width-sized (at the maximum) lines.
+ *
+ * @param text
+ * the {@link String} to justify
+ * @param width
+ * the maximum size of the resulting lines
+ * @param align
+ * align the lines in this position (default is
+ * Alignment.Beginning)
+ *
+ * @return a list of justified text lines
+ */
+ static public List<String> justifyText(String text, int width,
+ Alignment align) {
+ if (align == null) {
+ align = Alignment.LEFT;
+ }
+
+ align = align.undeprecate();
+
+ switch (align) {
+ case CENTER:
+ return StringJustifier.center(text, width);
+ case RIGHT:
+ return StringJustifier.right(text, width);
+ case JUSTIFY:
+ return StringJustifier.full(text, width);
+ case LEFT:
+ default:
+ return StringJustifier.left(text, width);
+ }
+ }
+
+ /**
+ * Justify a text into width-sized (at the maximum) lines.
+ *
+ * @param text
+ * the {@link String} to justify
+ * @param width
+ * the maximum size of the resulting lines
+ *
+ * @return a list of justified text lines
+ */
+ static public List<String> justifyText(List<String> text, int width) {
+ return justifyText(text, width, null);
+ }
+
+ /**
+ * Justify a text into width-sized (at the maximum) lines.
+ *
+ * @param text
+ * the {@link String} to justify
+ * @param width
+ * the maximum size of the resulting lines
+ * @param align
+ * align the lines in this position (default is
+ * Alignment.Beginning)
+ *
+ * @return a list of justified text lines
+ */
+ static public List<String> justifyText(List<String> text, int width,
+ Alignment align) {
+ List<String> result = new ArrayList<String>();
+
+ // Content <-> Bullet spacing (null = no spacing)
+ List<Entry<String, String>> lines = new ArrayList<Entry<String, String>>();
+ StringBuilder previous = null;
+ StringBuilder tmp = new StringBuilder();
+ String previousItemBulletSpacing = null;
+ String itemBulletSpacing = null;
+ for (String inputLine : text) {
+ boolean previousLineComplete = true;
+
+ String current = inputLine.replace("\t", " ");
+ itemBulletSpacing = getItemSpacing(current);
+ boolean bullet = isItemLine(current);
+ if ((previousItemBulletSpacing == null || itemBulletSpacing
+ .length() <= previousItemBulletSpacing.length()) && !bullet) {
+ itemBulletSpacing = null;
+ }
+
+ if (itemBulletSpacing != null) {
+ current = current.trim();
+ if (!current.isEmpty() && bullet) {
+ current = current.substring(1);
+ }
+ current = current.trim();
+ previousLineComplete = bullet;
+ } else {
+ tmp.setLength(0);
+ for (String word : current.split(" ")) {
+ if (word.isEmpty()) {
+ continue;
+ }
+
+ if (tmp.length() > 0) {
+ tmp.append(' ');
+ }
+ tmp.append(word.trim());
+ }
+ current = tmp.toString();
+
+ previousLineComplete = current.isEmpty()
+ || previousItemBulletSpacing != null
+ || (previous != null && isFullLine(previous))
+ || isHrLine(current) || isHrLine(previous);
+ }
+
+ if (previous == null) {
+ previous = new StringBuilder();
+ } else {
+ if (previousLineComplete) {
+ lines.add(new AbstractMap.SimpleEntry<String, String>(
+ previous.toString(), previousItemBulletSpacing));
+ previous.setLength(0);
+ previousItemBulletSpacing = itemBulletSpacing;
+ } else {
+ previous.append(' ');
+ }
+ }
+
+ previous.append(current);
+
+ }
+
+ if (previous != null) {
+ lines.add(new AbstractMap.SimpleEntry<String, String>(previous
+ .toString(), previousItemBulletSpacing));
+ }
+
+ for (Entry<String, String> line : lines) {
+ String content = line.getKey();
+ String spacing = line.getValue();
+
+ String bullet = "- ";
+ if (spacing == null) {
+ bullet = "";
+ spacing = "";
+ }
+
+ if (spacing.length() > width + 3) {
+ spacing = "";
+ }
+
+ for (String subline : StringUtils.justifyText(content, width
+ - (spacing.length() + bullet.length()), align)) {
+ result.add(spacing + bullet + subline);
+ if (!bullet.isEmpty()) {
+ bullet = " ";
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Sanitise the given input to make it more Terminal-friendly by removing
+ * combining characters.
+ *
+ * @param input
+ * the input to sanitise
+ * @param allowUnicode
+ * allow Unicode or only allow ASCII Latin characters
+ *
+ * @return the sanitised {@link String}
+ */
+ static public String sanitize(String input, boolean allowUnicode) {
+ return sanitize(input, allowUnicode, !allowUnicode);
+ }
+
+ /**
+ * Sanitise the given input to make it more Terminal-friendly by removing
+ * combining characters.
+ *
+ * @param input
+ * the input to sanitise
+ * @param allowUnicode
+ * allow Unicode or only allow ASCII Latin characters
+ * @param removeAllAccents
+ * TRUE to replace all accentuated characters by their non
+ * accentuated counter-parts
+ *
+ * @return the sanitised {@link String}
+ */
+ static public String sanitize(String input, boolean allowUnicode,
+ boolean removeAllAccents) {
+
+ if (removeAllAccents) {
+ input = Normalizer.normalize(input, Form.NFKD);
+ if (marks != null) {
+ input = marks.matcher(input).replaceAll("");
+ }
+ }
+
+ input = Normalizer.normalize(input, Form.NFKC);
+
+ if (!allowUnicode) {
+ StringBuilder builder = new StringBuilder();
+ for (int index = 0; index < input.length(); index++) {
+ char car = input.charAt(index);
+ // displayable chars in ASCII are in the range 32<->255,
+ // except DEL (127)
+ if (car >= 32 && car <= 255 && car != 127) {
+ builder.append(car);
+ }
+ }
+ input = builder.toString();
+ }
+
+ return input;
+ }
+
+ /**
+ * Convert between the time in milliseconds to a {@link String} in a "fixed"
+ * way (to exchange data over the wire, for instance).
+ * <p>
+ * Precise to the second.
+ *
+ * @param time
+ * the specified number of milliseconds since the standard base
+ * time known as "the epoch", namely January 1, 1970, 00:00:00
+ * GMT
+ *
+ * @return the time as a {@link String}
+ */
+ static public String fromTime(long time) {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ return sdf.format(new Date(time));
+ }
+
+ /**
+ * Convert between the time as a {@link String} to milliseconds in a "fixed"
+ * way (to exchange data over the wire, for instance).
+ * <p>
+ * Precise to the second.
+ *
+ * @param displayTime
+ * the time as a {@link String}
+ *
+ * @return the number of milliseconds since the standard base time known as
+ * "the epoch", namely January 1, 1970, 00:00:00 GMT, or -1 in case
+ * of error
+ *
+ * @throws ParseException
+ * in case of parse error
+ */
+ static public long toTime(String displayTime) throws ParseException {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ return sdf.parse(displayTime).getTime();
+ }
+
+ /**
+ * Return a hash of the given {@link String}.
+ *
+ * @param input
+ * the input data
+ *
+ * @return the hash
+ */
+ static public String getMd5Hash(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.update(getBytes(input));
+ byte byteData[] = md.digest();
+
+ StringBuffer hexString = new StringBuffer();
+ for (int i = 0; i < byteData.length; i++) {
+ String hex = Integer.toHexString(0xff & byteData[i]);
+ if (hex.length() == 1)
+ hexString.append('0');
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ return input;
+ }
+ }
+
+ /**
+ * Remove the HTML content from the given input, and un-html-ize the rest.
+ *
+ * @param html
+ * the HTML-encoded content
+ *
+ * @return the HTML-free equivalent content
+ */
+ public static String unhtml(String html) {
+ StringBuilder builder = new StringBuilder();
+
+ int inTag = 0;
+ for (char car : html.toCharArray()) {
+ if (car == '<') {
+ inTag++;
+ } else if (car == '>') {
+ inTag--;
+ } else if (inTag <= 0) {
+ builder.append(car);
+ }
+ }
+
+ char nbsp = ' '; // non-breakable space (a special char)
+ char space = ' ';
+ return HtmlEscape.unescapeHtml(builder.toString()).replace(nbsp, space);
+ }
+
+ /**
+ * Escape the given {@link String} so it can be used in XML, as content.
+ *
+ * @param input
+ * the input {@link String}
+ *
+ * @return the escaped {@link String}
+ */
+ public static String xmlEscape(String input) {
+ if (input == null) {
+ return "";
+ }
+
+ return HtmlEscape.escapeHtml(input,
+ HtmlEscapeType.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA,
+ HtmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT);
+ }
+
+ /**
+ * Escape the given {@link String} so it can be used in XML, as text content
+ * inside double-quotes.
+ *
+ * @param input
+ * the input {@link String}
+ *
+ * @return the escaped {@link String}
+ */
+ public static String xmlEscapeQuote(String input) {
+ if (input == null) {
+ return "";
+ }
+
+ return HtmlEscape.escapeHtml(input,
+ HtmlEscapeType.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA,
+ HtmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT);
+ }
+
+ /**
+ * Zip the data and then encode it into Base64.
+ *
+ * @param data
+ * the data
+ *
+ * @return the Base64 zipped version
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static String zip64(String data) throws IOException {
+ try {
+ return zip64(getBytes(data));
+ } catch (UnsupportedEncodingException e) {
+ // All conforming JVM are required to support UTF-8
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Zip the data and then encode it into Base64.
+ *
+ * @param data
+ * the data
+ *
+ * @return the Base64 zipped version
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static String zip64(byte[] data) throws IOException {
+ // 1. compress
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ try {
+ OutputStream out = new GZIPOutputStream(bout);
+ try {
+ out.write(data);
+ } finally {
+ out.close();
+ }
+ } finally {
+ data = bout.toByteArray();
+ bout.close();
+ }
+
+ // 2. base64
+ InputStream in = new ByteArrayInputStream(data);
+ try {
+ in = new Base64InputStream(in, true);
+ return new String(IOUtils.toByteArray(in), "UTF-8");
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Unconvert from Base64 then unzip the content, which is assumed to be a
+ * String.
+ *
+ * @param data
+ * the data in Base64 format
+ *
+ * @return the raw data
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static String unzip64s(String data) throws IOException {
+ return new String(unzip64(data), "UTF-8");
+ }
+
+ /**
+ * Unconvert from Base64 then unzip the content.
+ *
+ * @param data
+ * the data in Base64 format
+ *
+ * @return the raw data
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static byte[] unzip64(String data) throws IOException {
+ InputStream in = new Base64InputStream(new ByteArrayInputStream(
+ getBytes(data)), false);
+ try {
+ in = new GZIPInputStream(in);
+ return IOUtils.toByteArray(in);
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Convert the given data to Base64 format.
+ *
+ * @param data
+ * the data to convert
+ *
+ * @return the Base64 {@link String} representation of the data
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static String base64(String data) throws IOException {
+ return base64(getBytes(data));
+ }
+
+ /**
+ * Convert the given data to Base64 format.
+ *
+ * @param data
+ * the data to convert
+ *
+ * @return the Base64 {@link String} representation of the data
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static String base64(byte[] data) throws IOException {
+ Base64InputStream in = new Base64InputStream(new ByteArrayInputStream(
+ data), true);
+ try {
+ return new String(IOUtils.toByteArray(in), "UTF-8");
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back to a raw array of bytes.
+ *
+ * @param data
+ * the data to unconvert
+ *
+ * @return the raw data represented by the given Base64 {@link String},
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static byte[] unbase64(String data) throws IOException {
+ Base64InputStream in = new Base64InputStream(new ByteArrayInputStream(
+ getBytes(data)), false);
+ try {
+ return IOUtils.toByteArray(in);
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Unonvert the given data from Base64 format back to a {@link String}.
+ *
+ * @param data
+ * the data to unconvert
+ *
+ * @return the {@link String} represented by the given Base64 {@link String}
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static String unbase64s(String data) throws IOException {
+ return new String(unbase64(data), "UTF-8");
+ }
+
+ /**
+ * Return a display {@link String} for the given value, which can be
+ * suffixed with "k" or "M" depending upon the number, if it is big enough.
+ * <p>
+ * <p>
+ * Examples:
+ * <ul>
+ * <li><tt>8 765</tt> becomes "8 k"</li>
+ * <li><tt>998 765</tt> becomes "998 k"</li>
+ * <li><tt>12 987 364</tt> becomes "12 M"</li>
+ * <li><tt>5 534 333 221</tt> becomes "5 G"</li>
+ * </ul>
+ *
+ * @param value
+ * the value to convert
+ *
+ * @return the display value
+ */
+ public static String formatNumber(long value) {
+ return formatNumber(value, 0);
+ }
+
+ /**
+ * Return a display {@link String} for the given value, which can be
+ * suffixed with "k" or "M" depending upon the number, if it is big enough.
+ * <p>
+ * Examples (assuming decimalPositions = 1):
+ * <ul>
+ * <li><tt>8 765</tt> becomes "8.7 k"</li>
+ * <li><tt>998 765</tt> becomes "998.7 k"</li>
+ * <li><tt>12 987 364</tt> becomes "12.9 M"</li>
+ * <li><tt>5 534 333 221</tt> becomes "5.5 G"</li>
+ * </ul>
+ *
+ * @param value
+ * the value to convert
+ * @param decimalPositions
+ * the number of decimal positions to keep
+ *
+ * @return the display value
+ */
+ public static String formatNumber(long value, int decimalPositions) {
+ long userValue = value;
+ String suffix = " ";
+ long mult = 1;
+
+ if (value >= 1000000000l) {
+ mult = 1000000000l;
+ userValue = value / 1000000000l;
+ suffix = " G";
+ } else if (value >= 1000000l) {
+ mult = 1000000l;
+ userValue = value / 1000000l;
+ suffix = " M";
+ } else if (value >= 1000l) {
+ mult = 1000l;
+ userValue = value / 1000l;
+ suffix = " k";
+ }
+
+ String deci = "";
+ if (decimalPositions > 0) {
+ deci = Long.toString(value % mult);
+ int size = Long.toString(mult).length() - 1;
+ while (deci.length() < size) {
+ deci = "0" + deci;
+ }
+
+ deci = deci.substring(0, Math.min(decimalPositions, deci.length()));
+ while (deci.length() < decimalPositions) {
+ deci += "0";
+ }
+
+ deci = "." + deci;
+ }
+
+ return Long.toString(userValue) + deci + suffix;
+ }
+
+ /**
+ * The reverse operation to {@link StringUtils#formatNumber(long)}: it will
+ * read a "display" number that can contain a "M" or "k" suffix and return
+ * the full value.
+ * <p>
+ * Of course, the conversion to and from display form is lossy (example:
+ * <tt>6870</tt> to "6.5k" to <tt>6500</tt>).
+ *
+ * @param value
+ * the value in display form with possible "M" and "k" suffixes,
+ * can be NULL
+ *
+ * @return the value as a number, or 0 if not possible to convert
+ */
+ public static long toNumber(String value) {
+ return toNumber(value, 0l);
+ }
+
+ /**
+ * The reverse operation to {@link StringUtils#formatNumber(long)}: it will
+ * read a "display" number that can contain a "M" or "k" suffix and return
+ * the full value.
+ * <p>
+ * Of course, the conversion to and from display form is lossy (example:
+ * <tt>6870</tt> to "6.5k" to <tt>6500</tt>).
+ *
+ * @param value
+ * the value in display form with possible "M" and "k" suffixes,
+ * can be NULL
+ * @param def
+ * the default value if it is not possible to convert the given
+ * value to a number
+ *
+ * @return the value as a number, or 0 if not possible to convert
+ */
+ public static long toNumber(String value, long def) {
+ long count = def;
+ if (value != null) {
+ value = value.trim().toLowerCase();
+ try {
+ long mult = 1;
+ if (value.endsWith("g")) {
+ value = value.substring(0, value.length() - 1).trim();
+ mult = 1000000000;
+ } else if (value.endsWith("m")) {
+ value = value.substring(0, value.length() - 1).trim();
+ mult = 1000000;
+ } else if (value.endsWith("k")) {
+ value = value.substring(0, value.length() - 1).trim();
+ mult = 1000;
+ }
+
+ long deci = 0;
+ if (value.contains(".")) {
+ String[] tab = value.split("\\.");
+ if (tab.length != 2) {
+ throw new NumberFormatException(value);
+ }
+ double decimal = Double.parseDouble("0."
+ + tab[tab.length - 1]);
+ deci = ((long) (mult * decimal));
+ value = tab[0];
+ }
+ count = mult * Long.parseLong(value) + deci;
+ } catch (Exception e) {
+ }
+ }
+
+ return count;
+ }
+
+ /**
+ * Return the bytes array representation of the given {@link String} in
+ * UTF-8.
+ *
+ * @param str
+ * the {@link String} to transform into bytes
+ * @return the content in bytes
+ */
+ static public byte[] getBytes(String str) {
+ try {
+ return str.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // All conforming JVM must support UTF-8
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * The "remove accents" pattern.
+ *
+ * @return the pattern, or NULL if a problem happens
+ */
+ private static Pattern getMarks() {
+ try {
+ return Pattern
+ .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
+ } catch (Exception e) {
+ // Can fail on Android...
+ return null;
+ }
+ }
+
+ //
+ // justify List<String> related:
+ //
+
+ /**
+ * Check if this line ends as a complete line (ends with a "." or similar).
+ * <p>
+ * Note that we consider an empty line as full, and a line ending with
+ * spaces as not complete.
+ *
+ * @param line
+ * the line to check
+ *
+ * @return TRUE if it does
+ */
+ static private boolean isFullLine(StringBuilder line) {
+ if (line.length() == 0) {
+ return true;
+ }
+
+ char lastCar = line.charAt(line.length() - 1);
+ switch (lastCar) {
+ case '.': // points
+ case '?':
+ case '!':
+
+ case '\'': // quotes
+ case '‘':
+ case '’':
+
+ case '"': // double quotes
+ case '”':
+ case '“':
+ case '»':
+ case '«':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check if this line represent an item in a list or description (i.e.,
+ * check that the first non-space char is "-").
+ *
+ * @param line
+ * the line to check
+ *
+ * @return TRUE if it is
+ */
+ static private boolean isItemLine(String line) {
+ String spacing = getItemSpacing(line);
+ return spacing != null && !spacing.isEmpty()
+ && line.charAt(spacing.length()) == '-';
+ }
+
+ /**
+ * Return all the spaces that start this line (or Empty if none).
+ *
+ * @param line
+ * the line to get the starting spaces from
+ *
+ * @return the left spacing
+ */
+ static private String getItemSpacing(String line) {
+ int i;
+ for (i = 0; i < line.length(); i++) {
+ if (line.charAt(i) != ' ') {
+ return line.substring(0, i);
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * This line is an horizontal spacer line.
+ *
+ * @param line
+ * the line to test
+ *
+ * @return TRUE if it is
+ */
+ static private boolean isHrLine(CharSequence line) {
+ int count = 0;
+ if (line != null) {
+ for (int i = 0; i < line.length(); i++) {
+ char car = line.charAt(i);
+ if (car == ' ' || car == '\t' || car == '*' || car == '-'
+ || car == '_' || car == '~' || car == '=' || car == '/'
+ || car == '\\') {
+ count++;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return count > 2;
+ }
+
+ // Deprecated functions, please do not use //
+
+ /**
+ * @deprecated please use {@link StringUtils#zip64(byte[])} or
+ * {@link StringUtils#base64(byte[])} instead.
+ *
+ * @param data
+ * the data to encode
+ * @param zip
+ * TRUE to zip it before Base64 encoding it, FALSE for Base64
+ * encoding only
+ *
+ * @return the encoded data
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ @Deprecated
+ public static String base64(String data, boolean zip) throws IOException {
+ return base64(getBytes(data), zip);
+ }
+
+ /**
+ * @deprecated please use {@link StringUtils#zip64(String)} or
+ * {@link StringUtils#base64(String)} instead.
+ *
+ * @param data
+ * the data to encode
+ * @param zip
+ * TRUE to zip it before Base64 encoding it, FALSE for Base64
+ * encoding only
+ *
+ * @return the encoded data
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ @Deprecated
+ public static String base64(byte[] data, boolean zip) throws IOException {
+ if (zip) {
+ return zip64(data);
+ }
+
+ Base64InputStream b64 = new Base64InputStream(new ByteArrayInputStream(
+ data), true);
+ try {
+ return IOUtils.readSmallStream(b64);
+ } finally {
+ b64.close();
+ }
+ }
+
+ /**
+ * @deprecated please use {@link Base64OutputStream} and
+ * {@link GZIPOutputStream} instead.
+ *
+ * @param breakLines
+ * NOT USED ANYMORE, it is always considered FALSE now
+ */
+ @Deprecated
+ public static OutputStream base64(OutputStream data, boolean zip,
+ boolean breakLines) throws IOException {
+ OutputStream out = new Base64OutputStream(data);
+ if (zip) {
+ out = new java.util.zip.GZIPOutputStream(out);
+ }
+
+ return out;
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back to a raw array of bytes.
+ * <p>
+ * Will automatically detect zipped data and also uncompress it before
+ * returning, unless ZIP is false.
+ *
+ * @deprecated DO NOT USE ANYMORE (bad perf, will be dropped)
+ *
+ * @param data
+ * the data to unconvert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format
+ * automatically; if set to FALSE, zipped data can be returned
+ *
+ * @return the raw data represented by the given Base64 {@link String},
+ * optionally compressed with GZIP
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ @Deprecated
+ public static byte[] unbase64(String data, boolean zip) throws IOException {
+ byte[] buffer = unbase64(data);
+ if (!zip) {
+ return buffer;
+ }
+
+ try {
+ GZIPInputStream zipped = new GZIPInputStream(
+ new ByteArrayInputStream(buffer));
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ IOUtils.write(zipped, out);
+ return out.toByteArray();
+ } finally {
+ out.close();
+ }
+ } finally {
+ zipped.close();
+ }
+ } catch (Exception e) {
+ return buffer;
+ }
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back to a raw array of bytes.
+ * <p>
+ * Will automatically detect zipped data and also uncompress it before
+ * returning, unless ZIP is false.
+ *
+ * @deprecated DO NOT USE ANYMORE (bad perf, will be dropped)
+ *
+ * @param data
+ * the data to unconvert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format
+ * automatically; if set to FALSE, zipped data can be returned
+ *
+ * @return the raw data represented by the given Base64 {@link String},
+ * optionally compressed with GZIP
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ @Deprecated
+ public static InputStream unbase64(InputStream data, boolean zip)
+ throws IOException {
+ return new ByteArrayInputStream(unbase64(IOUtils.readSmallStream(data),
+ zip));
+ }
+
+ /**
+ * @deprecated DO NOT USE ANYMORE (bad perf, will be dropped)
+ */
+ @Deprecated
+ public static byte[] unbase64(byte[] data, int offset, int count,
+ boolean zip) throws IOException {
+ byte[] dataPart = Arrays.copyOfRange(data, offset, offset + count);
+ return unbase64(new String(dataPart, "UTF-8"), zip);
+ }
+
+ /**
+ * @deprecated DO NOT USE ANYMORE (bad perf, will be dropped)
+ */
+ @Deprecated
+ public static String unbase64s(String data, boolean zip) throws IOException {
+ return new String(unbase64(data, zip), "UTF-8");
+ }
+
+ /**
+ * @deprecated DO NOT USE ANYMORE (bad perf, will be dropped)
+ */
+ @Deprecated
+ public static String unbase64s(byte[] data, int offset, int count,
+ boolean zip) throws IOException {
+ return new String(unbase64(data, offset, count, zip), "UTF-8");
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A small utility class to generate auto-delete temporary files in a
+ * centralised location.
+ *
+ * @author niki
+ */
+public class TempFiles implements Closeable {
+ /**
+ * Root directory of this instance, owned by it, where all temporary files
+ * must reside.
+ */
+ protected File root;
+
+ /**
+ * Create a new {@link TempFiles} -- each instance is separate and have a
+ * dedicated sub-directory in a shared temporary root.
+ * <p>
+ * The whole repository will be deleted on close (if you fail to call it,
+ * the program will <b>try</b> to call it on JVM termination).
+ *
+ * @param name
+ * the instance name (will be <b>part</b> of the final directory
+ * name)
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public TempFiles(String name) throws IOException {
+ this(null, name);
+ }
+
+ /**
+ * Create a new {@link TempFiles} -- each instance is separate and have a
+ * dedicated sub-directory in a given temporary root.
+ * <p>
+ * The whole repository will be deleted on close (if you fail to call it,
+ * the program will <b>try</b> to call it on JVM termination).
+ * <p>
+ * Be careful, this instance will <b>own</b> the given root directory, and
+ * will most probably delete all its files.
+ *
+ * @param base
+ * the root base directory to use for all the temporary files of
+ * this instance (if NULL, will be the default temporary
+ * directory of the OS)
+ * @param name
+ * the instance name (will be <b>part</b> of the final directory
+ * name)
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public TempFiles(File base, String name) throws IOException {
+ if (base == null) {
+ base = File.createTempFile(".temp", "");
+ }
+
+ root = base;
+
+ if (root.exists()) {
+ IOUtils.deltree(root, true);
+ }
+
+ root = new File(root.getParentFile(), ".temp");
+ root.mkdir();
+ if (!root.exists()) {
+ throw new IOException("Cannot create root directory: " + root);
+ }
+
+ root.deleteOnExit();
+
+ root = createTempFile(name);
+ IOUtils.deltree(root, true);
+
+ root.mkdir();
+ if (!root.exists()) {
+ throw new IOException("Cannot create root subdirectory: " + root);
+ }
+ }
+
+ /**
+ * Create an auto-delete temporary file.
+ *
+ * @param name
+ * a base for the final filename (only a <b>part</b> of said
+ * filename)
+ *
+ * @return the newly created file
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public synchronized File createTempFile(String name) throws IOException {
+ name += "_";
+ while (name.length() < 3) {
+ name += "_";
+ }
+
+ while (true) {
+ File tmp = File.createTempFile(name, "");
+ IOUtils.deltree(tmp, true);
+
+ File test = new File(root, tmp.getName());
+ if (!test.exists()) {
+ test.createNewFile();
+ if (!test.exists()) {
+ throw new IOException(
+ "Cannot create temporary file: " + test);
+ }
+
+ test.deleteOnExit();
+ return test;
+ }
+ }
+ }
+
+ /**
+ * Create an auto-delete temporary directory.
+ * <p>
+ * Note that creating 2 temporary directories with the same name will result
+ * in two <b>different</b> directories, even if the final name is the same
+ * (the absolute path will be different).
+ *
+ * @param name
+ * the actual directory name (not path)
+ *
+ * @return the newly created file
+ *
+ * @throws IOException
+ * in case of I/O errors, or if the name was a path instead of a
+ * name
+ */
+ public synchronized File createTempDir(String name) throws IOException {
+ File localRoot = createTempFile(name);
+ IOUtils.deltree(localRoot, true);
+
+ localRoot.mkdir();
+ if (!localRoot.exists()) {
+ throw new IOException("Cannot create subdirectory: " + localRoot);
+ }
+
+ File dir = new File(localRoot, name);
+ if (!dir.getName().equals(name)) {
+ throw new IOException(
+ "Cannot create temporary directory with a path, only names are allowed: "
+ + dir);
+ }
+
+ dir.mkdir();
+ dir.deleteOnExit();
+
+ if (!dir.exists()) {
+ throw new IOException("Cannot create subdirectory: " + dir);
+ }
+
+ return dir;
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ File root = this.root;
+ this.root = null;
+
+ if (root != null) {
+ IOUtils.deltree(root);
+
+ // Since we allocate temp directories from a base point,
+ // try and remove that base point
+ root.getParentFile().delete(); // (only works if empty)
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+/**
+ * A handler when a trace message is sent or when a recoverable exception was
+ * caught by the program.
+ *
+ * @author niki
+ */
+public class TraceHandler {
+ private final boolean showErrors;
+ private final boolean showErrorDetails;
+ private final int traceLevel;
+ private final int maxPrintSize;
+
+ /**
+ * Create a default {@link TraceHandler} that will print errors on stderr
+ * (without details) and no traces.
+ */
+ public TraceHandler() {
+ this(true, false, false);
+ }
+
+ /**
+ * Create a default {@link TraceHandler}.
+ *
+ * @param showErrors
+ * show errors on stderr
+ * @param showErrorDetails
+ * show more details when printing errors
+ * @param showTraces
+ * show level 1 traces on stderr, or no traces at all
+ */
+ public TraceHandler(boolean showErrors, boolean showErrorDetails,
+ boolean showTraces) {
+ this(showErrors, showErrorDetails, showTraces ? 1 : 0);
+ }
+
+ /**
+ * Create a default {@link TraceHandler}.
+ *
+ * @param showErrors
+ * show errors on stderr
+ * @param showErrorDetails
+ * show more details when printing errors
+ * @param traceLevel
+ * show traces of this level or lower (0 means "no traces",
+ * higher means more traces)
+ */
+ public TraceHandler(boolean showErrors, boolean showErrorDetails,
+ int traceLevel) {
+ this(showErrors, showErrorDetails, traceLevel, -1);
+ }
+
+ /**
+ * Create a default {@link TraceHandler}.
+ *
+ * @param showErrors
+ * show errors on stderr
+ * @param showErrorDetails
+ * show more details when printing errors
+ * @param traceLevel
+ * show traces of this level or lower (0 means "no traces",
+ * higher means more traces)
+ * @param maxPrintSize
+ * the maximum size at which to truncate traces data (or -1 for
+ * "no limit")
+ */
+ public TraceHandler(boolean showErrors, boolean showErrorDetails,
+ int traceLevel, int maxPrintSize) {
+ this.showErrors = showErrors;
+ this.showErrorDetails = showErrorDetails;
+ this.traceLevel = Math.max(traceLevel, 0);
+ this.maxPrintSize = maxPrintSize;
+ }
+
+ /**
+ * The trace level of this {@link TraceHandler}.
+ *
+ * @return the level
+ */
+ public int getTraceLevel() {
+ return traceLevel;
+ }
+
+ /**
+ * An exception happened, log it.
+ *
+ * @param e
+ * the exception
+ */
+ public void error(Exception e) {
+ if (showErrors) {
+ if (showErrorDetails) {
+ long now = System.currentTimeMillis();
+ System.err.print(StringUtils.fromTime(now) + ": ");
+ e.printStackTrace();
+ } else {
+ error(e.toString());
+ }
+ }
+ }
+
+ /**
+ * An error happened, log it.
+ *
+ * @param message
+ * the error message
+ */
+ public void error(String message) {
+ if (showErrors) {
+ long now = System.currentTimeMillis();
+ System.err.println(StringUtils.fromTime(now) + ": " + message);
+ }
+ }
+
+ /**
+ * A trace happened, show it.
+ * <p>
+ * By default, will only be effective if {@link TraceHandler#traceLevel} is
+ * not 0.
+ * <p>
+ * A call to this method is equivalent to a call to
+ * {@link TraceHandler#trace(String, int)} with a level of 1.
+ *
+ * @param message
+ * the trace message
+ */
+ public void trace(String message) {
+ trace(message, 1);
+ }
+
+ /**
+ * A trace happened, show it.
+ * <p>
+ * By default, will only be effective if {@link TraceHandler#traceLevel} is
+ * not 0 and the level is lower or equal to it.
+ *
+ * @param message
+ * the trace message
+ * @param level
+ * the trace level
+ */
+ public void trace(String message, int level) {
+ if (traceLevel > 0 && level <= traceLevel) {
+ long now = System.currentTimeMillis();
+ System.err.print(StringUtils.fromTime(now) + ": ");
+ if (maxPrintSize > 0 && message.length() > maxPrintSize) {
+
+ System.err
+ .println(message.substring(0, maxPrintSize) + "[...]");
+ } else {
+ System.err.println(message);
+ }
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This class describe a program {@link Version}.
+ *
+ * @author niki
+ */
+public class Version implements Comparable<Version> {
+ private String version;
+ private int major;
+ private int minor;
+ private int patch;
+ private String tag;
+ private int tagVersion;
+
+ /**
+ * Create a new, empty {@link Version}.
+ *
+ */
+ public Version() {
+ }
+
+ /**
+ * Create a new {@link Version} with the given values.
+ *
+ * @param major
+ * the major version
+ * @param minor
+ * the minor version
+ * @param patch
+ * the patch version
+ */
+ public Version(int major, int minor, int patch) {
+ this(major, minor, patch, null, -1);
+ }
+
+ /**
+ * Create a new {@link Version} with the given values.
+ *
+ * @param major
+ * the major version
+ * @param minor
+ * the minor version
+ * @param patch
+ * the patch version
+ * @param tag
+ * a tag name for this version
+ */
+ public Version(int major, int minor, int patch, String tag) {
+ this(major, minor, patch, tag, -1);
+ }
+
+ /**
+ * Create a new {@link Version} with the given values.
+ *
+ * @param major
+ * the major version
+ * @param minor
+ * the minor version
+ * @param patch
+ * the patch version the patch version
+ * @param tag
+ * a tag name for this version
+ * @param tagVersion
+ * the version of the tagged version
+ */
+ public Version(int major, int minor, int patch, String tag, int tagVersion) {
+ if (tagVersion >= 0 && tag == null) {
+ throw new java.lang.IllegalArgumentException(
+ "A tag version cannot be used without a tag");
+ }
+
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.tag = tag;
+ this.tagVersion = tagVersion;
+
+ this.version = generateVersion();
+ }
+
+ /**
+ * Create a new {@link Version} with the given value, which must be in the
+ * form <tt>MAJOR.MINOR.PATCH(-TAG(TAG_VERSION))</tt>.
+ *
+ * @param version
+ * the version (<tt>MAJOR.MINOR.PATCH</tt>,
+ * <tt>MAJOR.MINOR.PATCH-TAG</tt> or
+ * <tt>MAJOR.MINOR.PATCH-TAGVERSIONTAG</tt>)
+ */
+ public Version(String version) {
+ try {
+ String[] tab = version.split("\\.");
+ this.major = Integer.parseInt(tab[0].trim());
+ this.minor = Integer.parseInt(tab[1].trim());
+ if (tab[2].contains("-")) {
+ int posInVersion = version.indexOf('.');
+ posInVersion = version.indexOf('.', posInVersion + 1);
+ String rest = version.substring(posInVersion + 1);
+
+ int posInRest = rest.indexOf('-');
+ this.patch = Integer.parseInt(rest.substring(0, posInRest)
+ .trim());
+
+ posInVersion = version.indexOf('-');
+ this.tag = version.substring(posInVersion + 1).trim();
+ this.tagVersion = -1;
+
+ StringBuilder str = new StringBuilder();
+ while (!tag.isEmpty() && tag.charAt(tag.length() - 1) >= '0'
+ && tag.charAt(tag.length() - 1) <= '9') {
+ str.insert(0, tag.charAt(tag.length() - 1));
+ tag = tag.substring(0, tag.length() - 1);
+ }
+
+ if (str.length() > 0) {
+ this.tagVersion = Integer.parseInt(str.toString());
+ }
+ } else {
+ this.patch = Integer.parseInt(tab[2].trim());
+ this.tag = null;
+ this.tagVersion = -1;
+ }
+
+ this.version = generateVersion();
+ } catch (Exception e) {
+ this.major = 0;
+ this.minor = 0;
+ this.patch = 0;
+ this.tag = null;
+ this.tagVersion = -1;
+ this.version = null;
+ }
+ }
+
+ /**
+ * The 'major' version.
+ * <p>
+ * This version should only change when API-incompatible changes are made to
+ * the program.
+ *
+ * @return the major version
+ */
+ public int getMajor() {
+ return major;
+ }
+
+ /**
+ * The 'minor' version.
+ * <p>
+ * This version should only change when new, backwards-compatible
+ * functionality has been added to the program.
+ *
+ * @return the minor version
+ */
+ public int getMinor() {
+ return minor;
+ }
+
+ /**
+ * The 'patch' version.
+ * <p>
+ * This version should change when backwards-compatible bugfixes have been
+ * added to the program.
+ *
+ * @return the patch version
+ */
+ public int getPatch() {
+ return patch;
+ }
+
+ /**
+ * A tag name for this version.
+ *
+ * @return the tag
+ */
+ public String getTag() {
+ return tag;
+ }
+
+ /**
+ * The version of the tag, or -1 for no version.
+ *
+ * @return the tag version
+ */
+ public int getTagVersion() {
+ return tagVersion;
+ }
+
+ /**
+ * Check if this {@link Version} is "empty" (i.e., the version was not
+ * parse-able or not given).
+ *
+ * @return TRUE if it is empty
+ */
+ public boolean isEmpty() {
+ return version == null;
+ }
+
+ /**
+ * Check if we are more recent than the given {@link Version}.
+ * <p>
+ * Note that a tagged version is considered newer than a non-tagged version,
+ * but two tagged versions with different tags are not comparable.
+ * <p>
+ * Also, an empty version is always considered older.
+ *
+ * @param o
+ * the other {@link Version}
+ * @return TRUE if this {@link Version} is more recent than the given one
+ */
+ public boolean isNewerThan(Version o) {
+ if (isEmpty()) {
+ return false;
+ } else if (o.isEmpty()) {
+ return true;
+ }
+
+ if (major > o.major) {
+ return true;
+ }
+
+ if (major == o.major && minor > o.minor) {
+ return true;
+ }
+
+ if (major == o.major && minor == o.minor && patch > o.patch) {
+ return true;
+ }
+
+ // a tagged version is considered newer than a non-tagged one
+ if (major == o.major && minor == o.minor && patch == o.patch
+ && tag != null && o.tag == null) {
+ return true;
+ }
+
+ // 2 <> tagged versions are not comparable
+ boolean sameTag = (tag == null && o.tag == null)
+ || (tag != null && tag.equals(o.tag));
+ if (major == o.major && minor == o.minor && patch == o.patch && sameTag
+ && tagVersion > o.tagVersion) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if we are older than the given {@link Version}.
+ * <p>
+ * Note that a tagged version is considered newer than a non-tagged version,
+ * but two tagged versions with different tags are not comparable.
+ * <p>
+ * Also, an empty version is always considered older.
+ *
+ * @param o
+ * the other {@link Version}
+ * @return TRUE if this {@link Version} is older than the given one
+ */
+ public boolean isOlderThan(Version o) {
+ if (o.isEmpty()) {
+ return false;
+ } else if (isEmpty()) {
+ return true;
+ }
+
+ // 2 <> tagged versions are not comparable
+ boolean sameTag = (tag == null && o.tag == null)
+ || (tag != null && tag.equals(o.tag));
+ if (major == o.major && minor == o.minor && patch == o.patch
+ && !sameTag) {
+ return false;
+ }
+
+ return !equals(o) && !isNewerThan(o);
+ }
+
+ /**
+ * Return the version of the running program if it follows the VERSION
+ * convention (i.e., if it has a file called VERSION containing the version
+ * as a {@link String} in its binary root, and if this {@link String}
+ * follows the Major/Minor/Patch convention).
+ * <p>
+ * If it does not, return an empty {@link Version} object.
+ *
+ * @return the {@link Version} of the program, or an empty {@link Version}
+ * (does not return NULL)
+ */
+ public static Version getCurrentVersion() {
+ String version = null;
+
+ InputStream in = IOUtils.openResource("VERSION");
+ if (in != null) {
+ try {
+ ByteArrayOutputStream ba = new ByteArrayOutputStream();
+ IOUtils.write(in, ba);
+ in.close();
+
+ version = ba.toString("UTF-8").trim();
+ } catch (IOException e) {
+ }
+ }
+
+ return new Version(version);
+ }
+
+ @Override
+ public int compareTo(Version o) {
+ if (equals(o)) {
+ return 0;
+ } else if (isNewerThan(o)) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Version) {
+ Version o = (Version) obj;
+ if (isEmpty()) {
+ return o.isEmpty();
+ }
+
+ boolean sameTag = (tag == null && o.tag == null)
+ || (tag != null && tag.equals(o.tag));
+ return o.major == major && o.minor == minor && o.patch == patch
+ && sameTag && o.tagVersion == tagVersion;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return version == null ? 0 : version.hashCode();
+ }
+
+ /**
+ * Return a user-readable form of this {@link Version}.
+ */
+ @Override
+ public String toString() {
+ return version == null ? "[unknown]" : version;
+ }
+
+ /**
+ * Generate the clean version {@link String} from the current values.
+ *
+ * @return the clean version string
+ */
+ private String generateVersion() {
+ String tagSuffix = "";
+ if (tag != null) {
+ tagSuffix = "-" + tag
+ + (tagVersion >= 0 ? Integer.toString(tagVersion) : "");
+ }
+
+ return String.format("%d.%d.%d%s", major, minor, patch, tagSuffix);
+ }
+}
--- /dev/null
+package be.nikiroo.utils.android;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.stream.Stream;
+
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.ImageUtils;
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * This class offer some utilities based around images and uses the Android
+ * framework.
+ *
+ * @author niki
+ */
+public class ImageUtilsAndroid extends ImageUtils {
+ @Override
+ protected boolean check() {
+ // If we can get the class, it means we have access to it
+ Config c = Config.ALPHA_8;
+ return true;
+ }
+
+ @Override
+ public void saveAsImage(Image img, File target, String format)
+ throws IOException {
+ FileOutputStream fos = new FileOutputStream(target);
+ try {
+ Bitmap image = fromImage(img);
+
+ boolean ok = false;
+ try {
+ ok = image.compress(
+ Bitmap.CompressFormat.valueOf(format.toUpperCase()),
+ 90, fos);
+ } catch (Exception e) {
+ ok = false;
+ }
+
+ // Some formats are not reliable
+ // Second chance: PNG
+ if (!ok && !format.equals("png")) {
+ ok = image.compress(Bitmap.CompressFormat.PNG, 90, fos);
+ }
+
+ if (!ok) {
+ throw new IOException(
+ "Cannot find a writer for this image and format: "
+ + format);
+ }
+ } catch (IOException e) {
+ throw new IOException("Cannot write image to " + target, e);
+ } finally {
+ fos.close();
+ }
+ }
+
+ /**
+ * Convert the given {@link Image} into a {@link Bitmap} object.
+ *
+ * @param img
+ * the {@link Image}
+ * @return the {@link Image} object
+ * @throws IOException
+ * in case of IO error
+ */
+ static public Bitmap fromImage(Image img) throws IOException {
+ InputStream stream = img.newInputStream();
+ try {
+ Bitmap image = BitmapFactory.decodeStream(stream);
+ if (image == null) {
+ String extra = "";
+ if (img.getSize() <= 2048) {
+ try {
+ extra = ", content: "
+ + new String(img.getData(), "UTF-8");
+ } catch (Exception e) {
+ extra = ", content unavailable";
+ }
+ }
+ String ssize = StringUtils.formatNumber(img.getSize());
+ throw new IOException(
+ "Failed to convert input to image, size was: " + ssize
+ + extra);
+ }
+
+ return image;
+ } finally {
+ stream.close();
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.android.test;
+
+import be.nikiroo.utils.android.ImageUtilsAndroid;
+
+public class TestAndroid {
+ ImageUtilsAndroid a = new ImageUtilsAndroid();
+}
--- /dev/null
+package be.nikiroo.utils.main;
+
+import be.nikiroo.utils.TraceHandler;
+import be.nikiroo.utils.serial.server.ServerBridge;
+
+/**
+ * Serialiser bridge (starts a {@link ServerBridge} and can thus intercept
+ * communication between a client and a server).
+ *
+ * @author niki
+ */
+public class bridge {
+ /**
+ * The optional options that can be passed to the program.
+ *
+ * @author niki
+ */
+ private enum Option {
+ /**
+ * The encryption key for the input data (optional, but can also be
+ * empty <b>which is different</b> (it will then use an empty encryption
+ * key)).
+ */
+ KEY,
+ /**
+ * The encryption key for the output data (optional, but can also be
+ * empty <b>which is different</b> (it will then use an empty encryption
+ * key)).
+ */
+ FORWARD_KEY,
+ /** The trace level (1, 2, 3.. default is 1). */
+ TRACE_LEVEL,
+ /**
+ * The maximum length after which to truncate data to display (the whole
+ * data will still be sent).
+ */
+ MAX_DISPLAY_SIZE,
+ /** The help message. */
+ HELP,
+ }
+
+ static private String getSyntax() {
+ return "Syntax: (--options) (--) [NAME] [PORT] [FORWARD_HOST] [FORWARD_PORT]\n"//
+ + "\tNAME : the bridge name for display/debug purposes\n"//
+ + "\tPORT : the port to listen on\n"//
+ + "\tFORWARD_HOST : the host to connect to\n"//
+ + "\tFORWARD_PORT : the port to connect to\n"//
+ + "\n" //
+ + "\tOptions: \n" //
+ + "\t-- : no more options in the rest of the parameters\n" //
+ + "\t--help : this help message\n" //
+ + "\t--key : the INCOMING encryption key\n" //
+ + "\t--forward-key : the OUTGOING encryption key\n" //
+ + "\t--trace-level : the trace level (1, 2, 3... default is 1)\n" //
+ + "\t--max-display-size : the maximum size after which to \n"//
+ + "\t truncate the messages to display (the full message will still be sent)\n" //
+ ;
+ }
+
+ /**
+ * Start a bridge between 2 servers.
+ *
+ * @param args
+ * the parameters, which can be seen by passing "--help" or just
+ * calling the program without parameters
+ */
+ public static void main(String[] args) {
+ final TraceHandler tracer = new TraceHandler(true, false, 0);
+ try {
+ if (args.length == 0) {
+ tracer.error(getSyntax());
+ System.exit(0);
+ }
+
+ String key = null;
+ String fkey = null;
+ int traceLevel = 1;
+ int maxPrintSize = 0;
+
+ int i = 0;
+ while (args[i].startsWith("--")) {
+ String arg = args[i];
+ i++;
+
+ if (arg.equals("--")) {
+ break;
+ }
+
+ arg = arg.substring(2).toUpperCase().replace("-", "_");
+ try {
+ Option opt = Enum.valueOf(Option.class, arg);
+ switch (opt) {
+ case HELP:
+ tracer.trace(getSyntax());
+ System.exit(0);
+ break;
+ case FORWARD_KEY:
+ fkey = args[i++];
+ break;
+ case KEY:
+ key = args[i++];
+ break;
+ case MAX_DISPLAY_SIZE:
+ maxPrintSize = Integer.parseInt(args[i++]);
+ break;
+ case TRACE_LEVEL:
+ traceLevel = Integer.parseInt(args[i++]);
+ break;
+ }
+ } catch (Exception e) {
+ tracer.error(getSyntax());
+ System.exit(1);
+ }
+ }
+
+ if ((args.length - i) != 4) {
+ tracer.error(getSyntax());
+ System.exit(2);
+ }
+
+ String name = args[i++];
+ int port = Integer.parseInt(args[i++]);
+ String fhost = args[i++];
+ int fport = Integer.parseInt(args[i++]);
+
+ ServerBridge bridge = new ServerBridge(name, port, key, fhost,
+ fport, fkey);
+ bridge.setTraceHandler(new TraceHandler(true, true, traceLevel,
+ maxPrintSize));
+ bridge.run();
+ } catch (Exception e) {
+ tracer.error(e);
+ System.exit(42);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.main;
+
+import java.awt.Dimension;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.ui.ImageTextAwt;
+import be.nikiroo.utils.ui.ImageTextAwt.Mode;
+import be.nikiroo.utils.ui.ImageUtilsAwt;
+
+/**
+ * Image to ASCII conversion.
+ *
+ * @author niki
+ */
+public class img2aa {
+ /**
+ * Syntax: (--mode=MODE) (--width=WIDTH) (--height=HEIGHT) (--size=SIZE)
+ * (--output=OUTPUT) (--invert) (--help)
+ * <p>
+ * See "--help".
+ *
+ * @param args
+ */
+ public static void main(String[] args) {
+ Dimension size = null;
+ Mode mode = null;
+ boolean invert = false;
+ List<String> inputs = new ArrayList<String>();
+ File output = null;
+
+ String lastArg = "";
+ try {
+ int height = -1;
+ int width = -1;
+
+ for (String arg : args) {
+ lastArg = arg;
+
+ if (arg.startsWith("--mode=")) {
+ mode = Mode.valueOf(arg.substring("--mode=".length()));
+ } else if (arg.startsWith("--width=")) {
+ width = Integer
+ .parseInt(arg.substring("--width=".length()));
+ } else if (arg.startsWith("--height=")) {
+ height = Integer.parseInt(arg.substring("--height="
+ .length()));
+ } else if (arg.startsWith("--size=")) {
+ String content = arg.substring("--size=".length()).replace(
+ "X", "x");
+ width = Integer.parseInt(content.split("x")[0]);
+ height = Integer.parseInt(content.split("x")[1]);
+ } else if (arg.startsWith("--ouput=")) {
+ if (!arg.equals("--output=-")) {
+ output = new File(arg.substring("--output=".length()));
+ }
+ } else if (arg.equals("--invert")) {
+ invert = true;
+ } else if (arg.equals("--help")) {
+ System.out
+ .println("Syntax: (--mode=MODE) (--width=WIDTH) (--height=HEIGHT) (--size=SIZE) (--output=OUTPUT) (--invert) (--help)");
+ System.out.println("\t --help: will show this screen");
+ System.out
+ .println("\t --invert: will invert the 'colours'");
+ System.out
+ .println("\t --mode: will select the rendering mode (default: ASCII):");
+ System.out
+ .println("\t\t ASCII: ASCI output mode, that is, characters \" .-+=o8#\"");
+ System.out
+ .println("\t\t DITHERING: Use 5 different \"colours\" which are actually"
+ + "\n\t\t Unicode characters \" ░▒▓█\"");
+ System.out
+ .println("\t\t DOUBLE_RESOLUTION: Use \"block\" Unicode characters up to quarter"
+ + "\n\t\t blocks, thus in effect doubling the resolution both in vertical"
+ + "\n\t\t and horizontal space."
+ + "\n\t\t Note that since 2 characters next to each other are square,"
+ + "\n\t\t 4 blocks per 2 blocks for w/h resolution.");
+ System.out
+ .println("\t\t DOUBLE_DITHERING: Use characters from both DOUBLE_RESOLUTION"
+ + "\n\t\t and DITHERING");
+ return;
+ } else {
+ inputs.add(arg);
+ }
+ }
+
+ size = new Dimension(width, height);
+ if (inputs.size() == 0) {
+ inputs.add("-"); // by default, stdin
+ }
+ } catch (Exception e) {
+ System.err.println("Syntax error: \"" + lastArg + "\" is invalid");
+ System.exit(1);
+ }
+
+ try {
+ if (mode == null) {
+ mode = Mode.ASCII;
+ }
+
+ for (String input : inputs) {
+ InputStream in = null;
+
+ try {
+ if (input.equals("-")) {
+ in = System.in;
+ } else {
+ in = new FileInputStream(input);
+ }
+ BufferedImage image = ImageUtilsAwt
+ .fromImage(new Image(in));
+ ImageTextAwt img = new ImageTextAwt(image, size, mode,
+ invert);
+ if (output == null) {
+ System.out.println(img.getText());
+ } else {
+ IOUtils.writeSmallFile(output, img.getText());
+ }
+ } finally {
+ if (!input.equals("-")) {
+ in.close();
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ System.exit(2);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.main;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.StringUtils.Alignment;
+
+/**
+ * Text justification (left, right, center, justify).
+ *
+ * @author niki
+ */
+public class justify {
+ /**
+ * Syntax: $0 ([left|right|center|justify]) (max width)
+ * <p>
+ * <ul>
+ * <li>mode: left, right, center or full justification (defaults to left)</li>
+ * <li>max width: the maximum width of a line, or "" for "no maximum"
+ * (defaults to "no maximum")</li>
+ * </ul>
+ *
+ * @param args
+ */
+ public static void main(String[] args) {
+ int width = -1;
+ StringUtils.Alignment align = Alignment.LEFT;
+
+ if (args.length >= 1) {
+ align = Alignment.valueOf(args[0].toUpperCase());
+ }
+ if (args.length >= 2) {
+ width = Integer.parseInt(args[1]);
+ }
+
+ Scanner scan = new Scanner(System.in);
+ scan.useDelimiter("\r\n|[\r\n]");
+ try {
+ List<String> lines = new ArrayList<String>();
+ while (scan.hasNext()) {
+ lines.add(scan.next());
+ }
+
+ for (String line : StringUtils.justifyText(lines, width, align)) {
+ System.out.println(line);
+ }
+ } finally {
+ scan.close();
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.resources;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
+
+import be.nikiroo.utils.resources.Meta.Format;
+
+/**
+ * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to
+ * retrieve values associated to an enumeration, and allows some additional
+ * methods.
+ * <p>
+ * It also sports a writable change map, and you can save back the
+ * {@link Bundle} to file with {@link Bundle#updateFile(String)}.
+ *
+ * @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;
+ /**
+ * 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,
+ TransBundle<E> descriptionBundle) {
+ this.type = type;
+ this.keyType = name;
+ this.descriptionBundle = descriptionBundle;
+
+ this.map = new HashMap<String, String>();
+ this.changeMap = new HashMap<String, String>();
+ setBundle(name, Locale.getDefault(), false);
+ }
+
+ /**
+ * Check if the setting is set into this {@link Bundle}.
+ *
+ * @param id
+ * the id of the setting to check
+ * @param includeDefaultValue
+ * TRUE to only return false when the setting is not set AND
+ * there is no default value
+ *
+ * @return TRUE if the setting is set
+ */
+ public boolean isSet(E id, boolean includeDefaultValue) {
+ return isSet(id.name(), includeDefaultValue);
+ }
+
+ /**
+ * Check if the setting is set into this {@link Bundle}.
+ *
+ * @param name
+ * the id of the setting to check
+ * @param includeDefaultValue
+ * TRUE to only return false when the setting is explicitly set
+ * to NULL (and not just "no set") in the change maps
+ *
+ * @return TRUE if the setting is set
+ */
+ protected boolean isSet(String name, boolean includeDefaultValue) {
+ if (getString(name, null) == null) {
+ if (!includeDefaultValue || getString(name, "") == null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String}.
+ *
+ * @param id
+ * the id of the value to get
+ *
+ * @return the associated value, or NULL if not found (not present in the
+ * resource file)
+ */
+ public String getString(E id) {
+ return getString(id, null);
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file
+ *
+ * @return the associated value, or NULL if not found (not present in the
+ * resource file)
+ */
+ public String getString(E id, String def) {
+ return getString(id, def, -1);
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String}.
+ * <p>
+ * If no value is associated (or if it is empty!), take the default one if
+ * any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the associated value, or NULL if not found (not present in the
+ * resource file)
+ */
+ public String getString(E id, String def, int item) {
+ String rep = getString(id.name(), null);
+ if (rep == null) {
+ rep = getMetaDef(id.name());
+ }
+
+ if (rep == null || rep.isEmpty()) {
+ return def;
+ }
+
+ if (item >= 0) {
+ List<String> values = BundleHelper.parseList(rep, item);
+ if (values != null && item < values.size()) {
+ return values.get(item);
+ }
+
+ return null;
+ }
+
+ return rep;
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link String}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ *
+ */
+ public void setString(E id, String value) {
+ setString(id.name(), value);
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link String}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ */
+ public void setString(E id, String value, int item) {
+ if (item < 0) {
+ setString(id.name(), value);
+ } else {
+ List<String> values = getList(id);
+ setString(id.name(), BundleHelper.fromList(values, value, item));
+ }
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String} suffixed
+ * with the runtime value "_suffix" (that is, "_" and suffix).
+ * <p>
+ * Will only accept suffixes that form an existing id.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param suffix
+ * the runtime suffix
+ *
+ * @return the associated value, or NULL if not found (not present in the
+ * resource file)
+ */
+ public String getStringX(E id, String suffix) {
+ return getStringX(id, suffix, null, -1);
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String} suffixed
+ * with the runtime value "_suffix" (that is, "_" and suffix).
+ * <p>
+ * Will only accept suffixes that form an existing id.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param suffix
+ * the runtime suffix
+ * @param def
+ * the default value when it is not present in the config file
+ *
+ * @return the associated value, or NULL if not found (not present in the
+ * resource file)
+ */
+ public String getStringX(E id, String suffix, String def) {
+ return getStringX(id, suffix, def, -1);
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String} suffixed
+ * with the runtime value "_suffix" (that is, "_" and suffix).
+ * <p>
+ * Will only accept suffixes that form an existing id.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param suffix
+ * the runtime suffix
+ * @param def
+ * the default value when it is not present in the config file
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the associated value, or NULL if not found (not present in the
+ * resource file)
+ */
+ public String getStringX(E id, String suffix, String def, int item) {
+ String key = id.name()
+ + (suffix == null ? "" : "_" + suffix.toUpperCase());
+
+ try {
+ id = Enum.valueOf(type, key);
+ return getString(id, def, item);
+ } catch (IllegalArgumentException e) {
+ }
+
+ return null;
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link String} suffixed
+ * with the runtime value "_suffix" (that is, "_" and suffix).
+ * <p>
+ * Will only accept suffixes that form an existing id.
+ *
+ * @param id
+ * the id of the value to set
+ * @param suffix
+ * the runtime suffix
+ * @param value
+ * the value
+ */
+ public void setStringX(E id, String suffix, String value) {
+ setStringX(id, suffix, value, -1);
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link String} suffixed
+ * with the runtime value "_suffix" (that is, "_" and suffix).
+ * <p>
+ * Will only accept suffixes that form an existing id.
+ *
+ * @param id
+ * the id of the value to set
+ * @param suffix
+ * the runtime suffix
+ * @param value
+ * the value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ */
+ public void setStringX(E id, String suffix, String value, int item) {
+ String key = id.name()
+ + (suffix == null ? "" : "_" + suffix.toUpperCase());
+
+ try {
+ id = Enum.valueOf(type, key);
+ setString(id, value, item);
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Boolean}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ *
+ * @return the associated value
+ */
+ public Boolean getBoolean(E id) {
+ return BundleHelper.parseBoolean(getString(id), -1);
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Boolean}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a boolean value
+ *
+ * @return the associated value
+ */
+ public boolean getBoolean(E id, boolean def) {
+ Boolean value = getBoolean(id);
+ if (value != null) {
+ return value;
+ }
+
+ return def;
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Boolean}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a boolean value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the associated value
+ */
+ public Boolean getBoolean(E id, boolean def, int item) {
+ String value = getString(id);
+ if (value != null) {
+ return BundleHelper.parseBoolean(value, item);
+ }
+
+ return def;
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link Boolean}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ *
+ */
+ public void setBoolean(E id, boolean value) {
+ setBoolean(id, value, -1);
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link Boolean}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ */
+ public void setBoolean(E id, boolean value, int item) {
+ setString(id, BundleHelper.fromBoolean(value), item);
+ }
+
+ /**
+ * Return the value associated to the given id as an {@link Integer}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ *
+ * @return the associated value
+ */
+ public Integer getInteger(E id) {
+ String value = getString(id);
+ if (value != null) {
+ return BundleHelper.parseInteger(value, -1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the value associated to the given id as an int.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a int value
+ *
+ * @return the associated value
+ */
+ public int getInteger(E id, int def) {
+ Integer value = getInteger(id);
+ if (value != null) {
+ return value;
+ }
+
+ return def;
+ }
+
+ /**
+ * Return the value associated to the given id as an int.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a int value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the associated value
+ */
+ public Integer getInteger(E id, int def, int item) {
+ String value = getString(id);
+ if (value != null) {
+ return BundleHelper.parseInteger(value, item);
+ }
+
+ return def;
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link Integer}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ *
+ */
+ public void setInteger(E id, int value) {
+ setInteger(id, value, -1);
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link Integer}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ */
+ public void setInteger(E id, int value, int item) {
+ setString(id, BundleHelper.fromInteger(value), item);
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Character}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ *
+ * @return the associated value
+ */
+ public Character getCharacter(E id) {
+ return BundleHelper.parseCharacter(getString(id), -1);
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Character}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a char value
+ *
+ * @return the associated value
+ */
+ public char getCharacter(E id, char def) {
+ Character value = getCharacter(id);
+ if (value != null) {
+ return value;
+ }
+
+ return def;
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Character}.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a char value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the associated value
+ */
+ public Character getCharacter(E id, char def, int item) {
+ String value = getString(id);
+ if (value != null) {
+ return BundleHelper.parseCharacter(value, item);
+ }
+
+ return def;
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link Character}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ *
+ */
+ public void setCharacter(E id, char value) {
+ setCharacter(id, value, -1);
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link Character}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ */
+ public void setCharacter(E id, char value, int item) {
+ setString(id, BundleHelper.fromCharacter(value), item);
+ }
+
+ /**
+ * Return the value associated to the given id as a colour if it is found
+ * and can be parsed.
+ * <p>
+ * The returned value is an ARGB value.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ *
+ * @return the associated value
+ */
+ public Integer getColor(E id) {
+ return BundleHelper.parseColor(getString(id), -1);
+ }
+
+ /**
+ * Return the value associated to the given id as a colour if it is found
+ * and can be parsed.
+ * <p>
+ * The returned value is an ARGB value.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a char value
+ *
+ * @return the associated value
+ */
+ public int getColor(E id, int def) {
+ Integer value = getColor(id);
+ if (value != null) {
+ return value;
+ }
+
+ return def;
+ }
+
+ /**
+ * Return the value associated to the given id as a colour if it is found
+ * and can be parsed.
+ * <p>
+ * The returned value is an ARGB value.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a char value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the associated value
+ */
+ public Integer getColor(E id, int def, int item) {
+ String value = getString(id);
+ if (value != null) {
+ return BundleHelper.parseColor(value, item);
+ }
+
+ return def;
+ }
+
+ /**
+ * Set the value associated to the given id as a colour.
+ * <p>
+ * The value is a BGRA value.
+ *
+ * @param id
+ * the id of the value to set
+ * @param color
+ * the new colour
+ */
+ public void setColor(E id, Integer color) {
+ setColor(id, color, -1);
+ }
+
+ /**
+ * Set the value associated to the given id as a Color.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ */
+ public void setColor(E id, int value, int item) {
+ setString(id, BundleHelper.fromColor(value), item);
+ }
+
+ /**
+ * Return the value associated to the given id as a list of values if it is
+ * found and can be parsed.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ *
+ * @return the associated list, empty if the value is empty, NULL if it is
+ * not found or cannot be parsed as a list
+ */
+ public List<String> getList(E id) {
+ return BundleHelper.parseList(getString(id), -1);
+ }
+
+ /**
+ * Return the value associated to the given id as a list of values if it is
+ * found and can be parsed.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a char value
+ *
+ * @return the associated list, empty if the value is empty, NULL if it is
+ * not found or cannot be parsed as a list
+ */
+ public List<String> getList(E id, List<String> def) {
+ List<String> value = getList(id);
+ if (value != null) {
+ return value;
+ }
+
+ return def;
+ }
+
+ /**
+ * Return the value associated to the given id as a list of values if it is
+ * found and can be parsed.
+ * <p>
+ * If no value is associated, take the default one if any.
+ *
+ * @param id
+ * the id of the value to get
+ * @param def
+ * the default value when it is not present in the config file or
+ * if it is not a char value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the associated list, empty if the value is empty, NULL if it is
+ * not found or cannot be parsed as a list
+ */
+ public List<String> getList(E id, List<String> def, int item) {
+ String value = getString(id);
+ if (value != null) {
+ return BundleHelper.parseList(value, item);
+ }
+
+ return def;
+ }
+
+ /**
+ * Set the value associated to the given id as a list of values.
+ *
+ * @param id
+ * the id of the value to set
+ * @param list
+ * the new list of values
+ */
+ public void setList(E id, List<String> list) {
+ setList(id, list, -1);
+ }
+
+ /**
+ * Set the value associated to the given id as a {@link List}.
+ *
+ * @param id
+ * the id of the value to set
+ * @param value
+ * the value
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays
+ *
+ */
+ public void setList(E id, List<String> value, int item) {
+ setString(id, BundleHelper.fromList(value), item);
+ }
+
+ /**
+ * Create/update the .properties file.
+ * <p>
+ * Will use the most likely candidate as base if the file does not already
+ * exists and this resource is translatable (for instance, "en_US" will use
+ * "en" as a base if the resource is a translation file).
+ * <p>
+ * Will update the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
+ * be set.
+ *
+ * @throws IOException
+ * in case of IO errors
+ */
+ public void updateFile() throws IOException {
+ updateFile(Bundles.getDirectory());
+ }
+
+ /**
+ * Create/update the .properties file.
+ * <p>
+ * Will use the most likely candidate as base if the file does not already
+ * exists and this resource is translatable (for instance, "en_US" will use
+ * "en" as a base if the resource is a translation file).
+ *
+ * @param path
+ * the path where the .properties files are, <b>MUST NOT</b> be
+ * NULL
+ *
+ * @throws IOException
+ * in case of IO errors
+ */
+ public void updateFile(String path) throws IOException {
+ File file = getUpdateFile(path);
+
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
+ new FileOutputStream(file), "UTF-8"));
+
+ writeHeader(writer);
+ writer.write("\n");
+ writer.write("\n");
+
+ for (Field field : type.getDeclaredFields()) {
+ Meta meta = field.getAnnotation(Meta.class);
+ if (meta != null) {
+ E id = Enum.valueOf(type, field.getName());
+ String info = getMetaInfo(meta);
+
+ if (info != null) {
+ writer.write(info);
+ writer.write("\n");
+ }
+
+ writeValue(writer, id);
+ }
+ }
+
+ writer.close();
+ }
+
+ /**
+ * Delete the .properties file.
+ * <p>
+ * Will use the most likely candidate as base if the file does not already
+ * exists and this resource is translatable (for instance, "en_US" will use
+ * "en" as a base if the resource is a translation file).
+ * <p>
+ * Will delete the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
+ * be set.
+ *
+ * @return TRUE if the file was deleted
+ */
+ public boolean deleteFile() {
+ return deleteFile(Bundles.getDirectory());
+ }
+
+ /**
+ * Delete the .properties file.
+ * <p>
+ * Will use the most likely candidate as base if the file does not already
+ * exists and this resource is translatable (for instance, "en_US" will use
+ * "en" as a base if the resource is a translation file).
+ *
+ * @param path
+ * the path where the .properties files are, <b>MUST NOT</b> be
+ * NULL
+ *
+ * @return TRUE if the file was deleted
+ */
+ public boolean deleteFile(String path) {
+ File file = getUpdateFile(path);
+ return file.delete();
+ }
+
+ /**
+ * 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.
+ *
+ * @param resetToDefault
+ * reset to the default configuration (do not look into the
+ * possible user configuration files, only take the original
+ * configuration)
+ */
+ public void reload(boolean resetToDefault) {
+ setBundle(keyType, Locale.getDefault(), resetToDefault);
+ }
+
+ /**
+ * Check if the internal map contains the given key.
+ *
+ * @param key
+ * the key to check for
+ *
+ * @return true if it does
+ */
+ protected boolean containsKey(String key) {
+ return changeMap.containsKey(key) || map.containsKey(key);
+ }
+
+ /**
+ * The default {@link MetaInfo.def} value for the given enumeration name.
+ *
+ * @param id
+ * the enumeration name (the "id")
+ *
+ * @return the def value in the {@link MetaInfo} or "" if none (never NULL)
+ */
+ protected String getMetaDef(String id) {
+ String rep = "";
+ try {
+ Meta meta = type.getDeclaredField(id).getAnnotation(Meta.class);
+ rep = meta.def();
+ } catch (NoSuchFieldException e) {
+ } catch (SecurityException e) {
+ }
+
+ if (rep == null) {
+ rep = "";
+ }
+
+ return rep;
+ }
+
+ /**
+ * Get the value for the given key if it exists in the internal map, or
+ * <tt>def</tt> if not.
+ * <p>
+ * DO NOT get the default meta value (MetaInfo.def()).
+ *
+ * @param key
+ * the key to check for
+ * @param def
+ * the default value when it is not present in the internal map
+ *
+ * @return the value, or <tt>def</tt> if not found
+ */
+ protected String getString(String key, String def) {
+ if (changeMap.containsKey(key)) {
+ return changeMap.get(key);
+ }
+
+ if (map.containsKey(key)) {
+ return map.get(key);
+ }
+
+ return def;
+ }
+
+ /**
+ * Set the value for this key, in the change map (it is kept in memory, not
+ * yet on disk).
+ *
+ * @param key
+ * the key
+ * @param value
+ * the associated value
+ */
+ protected void setString(String key, String value) {
+ changeMap.put(key, value == null ? null : value.trim());
+ }
+
+ /**
+ * Return formated, display-able information from the {@link Meta} field
+ * given. Each line will always starts with a "#" character.
+ *
+ * @param meta
+ * the {@link Meta} field
+ *
+ * @return the information to display or NULL if none
+ */
+ protected String getMetaInfo(Meta meta) {
+ String desc = meta.description();
+ boolean group = meta.group();
+ Meta.Format format = meta.format();
+ String[] list = meta.list();
+ boolean nullable = meta.nullable();
+ String def = meta.def();
+ boolean array = meta.array();
+
+ // Default, empty values -> NULL
+ if (desc.length() + list.length + def.length() == 0 && !group
+ && nullable && format == Format.STRING) {
+ return null;
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (String line : desc.split("\n")) {
+ builder.append("# ").append(line).append("\n");
+ }
+
+ 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(") ");
+
+ if (list.length > 0) {
+ builder.append("\n# ALLOWED VALUES: ");
+ boolean first = true;
+ for (String value : list) {
+ if (!first) {
+ builder.append(", ");
+ }
+ builder.append(BundleHelper.escape(value));
+ first = false;
+ }
+ }
+
+ if (array) {
+ builder.append("\n# (This item accepts a list of ^escaped comma-separated values)");
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * The display name used in the <tt>.properties file</tt>.
+ *
+ * @return the name
+ */
+ protected String getBundleDisplayName() {
+ return keyType.toString();
+ }
+
+ /**
+ * Write the header found in the configuration <tt>.properties</tt> file of
+ * this {@link Bundles}.
+ *
+ * @param writer
+ * the {@link Writer} to write the header in
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ protected void writeHeader(Writer writer) throws IOException {
+ writer.write("# " + getBundleDisplayName() + "\n");
+ writer.write("#\n");
+ }
+
+ /**
+ * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
+ * followed by a new line.
+ * <p>
+ * Will prepend a # sign if the is is not set (see
+ * {@link Bundle#isSet(Enum, boolean)}).
+ *
+ * @param writer
+ * the {@link Writer} to write into
+ * @param id
+ * the id to write
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ protected void writeValue(Writer writer, E id) throws IOException {
+ boolean set = isSet(id, false);
+ writeValue(writer, id.name(), getString(id), set);
+ }
+
+ /**
+ * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
+ * followed by a new line.
+ * <p>
+ * Will prepend a # sign if the is is not set.
+ *
+ * @param writer
+ * the {@link Writer} to write into
+ * @param id
+ * the id to write
+ * @param value
+ * the id's value
+ * @param set
+ * the value is set in this {@link Bundle}
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ protected void writeValue(Writer writer, String id, String value,
+ boolean set) throws IOException {
+
+ if (!set) {
+ writer.write('#');
+ }
+
+ writer.write(id);
+ writer.write(" = ");
+
+ if (value == null) {
+ value = "";
+ }
+
+ String[] lines = value.replaceAll("\t", "\\\\\\t").split("\n");
+ for (int i = 0; i < lines.length; i++) {
+ writer.write(lines[i]);
+ if (i < lines.length - 1) {
+ writer.write("\\n\\");
+ }
+ writer.write("\n");
+ }
+ }
+
+ /**
+ * Return the source file for this {@link Bundles} from the given path.
+ *
+ * @param path
+ * the path where the .properties files are
+ *
+ * @return the source {@link File}
+ */
+ protected File getUpdateFile(String path) {
+ return new File(path, keyType.name() + ".properties");
+ }
+
+ /**
+ * Change the currently used bundle, and reset all changes.
+ *
+ * @param name
+ * the name of the bundle to load
+ * @param locale
+ * the {@link Locale} to use
+ * @param resetToDefault
+ * reset to the default configuration (do not look into the
+ * possible user configuration files, only take the original
+ * configuration)
+ */
+ protected void setBundle(Enum<?> name, Locale locale, boolean resetToDefault) {
+ changeMap.clear();
+ String dir = Bundles.getDirectory();
+ String bname = type.getPackage().getName() + "." + name.name();
+
+ boolean found = false;
+ if (!resetToDefault && dir != null) {
+ // Look into Bundles.getDirectory() for .properties files
+ try {
+ File file = getPropertyFile(dir, name.name(), locale);
+ if (file != null) {
+ Reader reader = new InputStreamReader(new FileInputStream(
+ file), "UTF-8");
+ resetMap(new PropertyResourceBundle(reader));
+ found = true;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!found) {
+ // Look into the package itself for resources
+ try {
+ resetMap(ResourceBundle
+ .getBundle(bname, locale, type.getClassLoader(),
+ new FixedResourceBundleControl()));
+ found = true;
+ } catch (MissingResourceException e) {
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!found) {
+ // We have no bundle for this Bundle
+ System.err.println("No bundle found for: " + bname);
+ resetMap(null);
+ }
+ }
+
+ /**
+ * Reset the backing map to the content of the given bundle, or with NULL
+ * values if bundle is NULL.
+ *
+ * @param bundle
+ * the bundle to copy
+ */
+ protected void resetMap(ResourceBundle bundle) {
+ this.map.clear();
+ for (Field field : type.getDeclaredFields()) {
+ try {
+ Meta meta = field.getAnnotation(Meta.class);
+ if (meta != null) {
+ E id = Enum.valueOf(type, field.getName());
+
+ String value;
+ if (bundle != null) {
+ value = bundle.getString(id.name());
+ } else {
+ value = null;
+ }
+
+ this.map.put(id.name(), value == null ? null : value.trim());
+ }
+ } catch (MissingResourceException e) {
+ }
+ }
+ }
+
+ /**
+ * Take a snapshot of the changes in memory in this {@link Bundle} made by
+ * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the
+ * current time.
+ *
+ * @return a snapshot to use with {@link Bundle#restoreSnapshot(Object)}
+ */
+ public Object takeSnapshot() {
+ return new HashMap<String, String>(changeMap);
+ }
+
+ /**
+ * Restore a snapshot taken with {@link Bundle}, or reset the current
+ * changes if the snapshot is NULL.
+ *
+ * @param snap
+ * the snapshot or NULL
+ */
+ @SuppressWarnings("unchecked")
+ public void restoreSnapshot(Object snap) {
+ if (snap == null) {
+ changeMap.clear();
+ } else {
+ if (snap instanceof Map) {
+ changeMap = (Map<String, String>) snap;
+ } else {
+ throw new RuntimeException(
+ "Restoring changes in a Bundle must be done on a changes snapshot, "
+ + "or NULL to discard current changes");
+ }
+ }
+ }
+
+ /**
+ * Return the resource file that is closer to the {@link Locale}.
+ *
+ * @param dir
+ * the directory to look into
+ * @param name
+ * the file base name (without <tt>.properties</tt>)
+ * @param locale
+ * the {@link Locale}
+ *
+ * @return the closest match or NULL if none
+ */
+ private File getPropertyFile(String dir, String name, Locale locale) {
+ List<String> locales = new ArrayList<String>();
+ if (locale != null) {
+ String country = locale.getCountry() == null ? "" : locale
+ .getCountry();
+ String language = locale.getLanguage() == null ? "" : locale
+ .getLanguage();
+ if (!language.isEmpty() && !country.isEmpty()) {
+ locales.add("_" + language + "-" + country);
+ }
+ if (!language.isEmpty()) {
+ locales.add("_" + language);
+ }
+ }
+
+ locales.add("");
+
+ File file = null;
+ for (String loc : locales) {
+ file = new File(dir, name + loc + ".properties");
+ if (file.exists()) {
+ break;
+ }
+
+ file = null;
+ }
+
+ return file;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.resources;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Internal class used to convert data to/from {@link String}s in the context of
+ * {@link Bundle}s.
+ *
+ * @author niki
+ */
+class BundleHelper {
+ /**
+ * Convert the given {@link String} into a {@link Boolean} if it represents
+ * a {@link Boolean}, or NULL if it doesn't.
+ * <p>
+ * Note: null, "strange text", ""... will all be converted to NULL.
+ *
+ * @param str
+ * the input {@link String}
+ * @param item
+ * the item number to use for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the converted {@link Boolean} or NULL
+ */
+ static public Boolean parseBoolean(String str, int item) {
+ str = getItem(str, item);
+ if (str == null) {
+ return null;
+ }
+
+ if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("on")
+ || str.equalsIgnoreCase("yes"))
+ return true;
+ if (str.equalsIgnoreCase("false") || str.equalsIgnoreCase("off")
+ || str.equalsIgnoreCase("no"))
+ return false;
+
+ return null;
+ }
+
+ /**
+ * Return a {@link String} representation of the given {@link Boolean}.
+ *
+ * @param value
+ * the input value
+ *
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public String fromBoolean(boolean value) {
+ return Boolean.toString(value);
+ }
+
+ /**
+ * Convert the given {@link String} into a {@link Integer} if it represents
+ * a {@link Integer}, or NULL if it doesn't.
+ * <p>
+ * Note: null, "strange text", ""... will all be converted to NULL.
+ *
+ * @param str
+ * the input {@link String}
+ * @param item
+ * the item number to use for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the converted {@link Integer} or NULL
+ */
+ static public Integer parseInteger(String str, int item) {
+ str = getItem(str, item);
+ if (str == null) {
+ return null;
+ }
+
+ try {
+ return Integer.parseInt(str);
+ } catch (Exception e) {
+ }
+
+ return null;
+ }
+
+ /**
+ * Return a {@link String} representation of the given {@link Integer}.
+ *
+ * @param value
+ * the input value
+ *
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public String fromInteger(int value) {
+ return Integer.toString(value);
+ }
+
+ /**
+ * Convert the given {@link String} into a {@link Character} if it
+ * represents a {@link Character}, or NULL if it doesn't.
+ * <p>
+ * Note: null, "strange text", ""... will all be converted to NULL
+ * (remember: any {@link String} whose length is not 1 is <b>not</b> a
+ * {@link Character}).
+ *
+ * @param str
+ * the input {@link String}
+ * @param item
+ * the item number to use for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the converted {@link Character} or NULL
+ */
+ static public Character parseCharacter(String str, int item) {
+ str = getItem(str, item);
+ if (str == null) {
+ return null;
+ }
+
+ String s = str.trim();
+ if (s.length() == 1) {
+ return s.charAt(0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Return a {@link String} representation of the given {@link Boolean}.
+ *
+ * @param value
+ * the input value
+ *
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public String fromCharacter(char value) {
+ return Character.toString(value);
+ }
+
+ /**
+ * Convert the given {@link String} into a colour (represented here as an
+ * {@link Integer}) if it represents a colour, or NULL if it doesn't.
+ * <p>
+ * The returned colour value is an ARGB value.
+ *
+ * @param str
+ * the input {@link String}
+ * @param item
+ * the item number to use for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the converted colour as an {@link Integer} value or NULL
+ */
+ static Integer parseColor(String str, int item) {
+ str = getItem(str, item);
+ if (str == null) {
+ return null;
+ }
+
+ Integer rep = null;
+
+ str = str.trim();
+ int r = 0, g = 0, b = 0, a = -1;
+ if (str.startsWith("#") && (str.length() == 7 || str.length() == 9)) {
+ try {
+ r = Integer.parseInt(str.substring(1, 3), 16);
+ g = Integer.parseInt(str.substring(3, 5), 16);
+ b = Integer.parseInt(str.substring(5, 7), 16);
+ if (str.length() == 9) {
+ a = Integer.parseInt(str.substring(7, 9), 16);
+ } else {
+ a = 255;
+ }
+
+ } catch (NumberFormatException e) {
+ // no changes
+ }
+ }
+
+ // Try by name if still not found
+ if (a == -1) {
+ if ("black".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 0;
+ g = 0;
+ b = 0;
+ } else if ("white".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 255;
+ g = 255;
+ b = 255;
+ } else if ("red".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 255;
+ g = 0;
+ b = 0;
+ } else if ("green".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 0;
+ g = 255;
+ b = 0;
+ } else if ("blue".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 0;
+ g = 0;
+ b = 255;
+ } else if ("grey".equalsIgnoreCase(str)
+ || "gray".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 128;
+ g = 128;
+ b = 128;
+ } else if ("cyan".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 0;
+ g = 255;
+ b = 255;
+ } else if ("magenta".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 255;
+ g = 0;
+ b = 255;
+ } else if ("yellow".equalsIgnoreCase(str)) {
+ a = 255;
+ r = 255;
+ g = 255;
+ b = 0;
+ }
+ }
+
+ if (a != -1) {
+ rep = ((a & 0xFF) << 24) //
+ | ((r & 0xFF) << 16) //
+ | ((g & 0xFF) << 8) //
+ | ((b & 0xFF) << 0);
+ }
+
+ return rep;
+ }
+
+ /**
+ * Return a {@link String} representation of the given colour.
+ * <p>
+ * The colour value is interpreted as an ARGB value.
+ *
+ * @param color
+ * the ARGB colour value
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public String fromColor(int color) {
+ int a = (color >> 24) & 0xFF;
+ int r = (color >> 16) & 0xFF;
+ int g = (color >> 8) & 0xFF;
+ int b = (color >> 0) & 0xFF;
+
+ String rs = Integer.toString(r, 16);
+ String gs = Integer.toString(g, 16);
+ String bs = Integer.toString(b, 16);
+ String as = "";
+ if (a < 255) {
+ as = Integer.toString(a, 16);
+ }
+
+ return "#" + rs + gs + bs + as;
+ }
+
+ /**
+ * The size of this raw list (note than a NULL list is of size 0).
+ *
+ * @param raw
+ * the raw list
+ *
+ * @return its size if it is a list (NULL is an empty list), -1 if it is not
+ * a list
+ */
+ static public int getListSize(String raw) {
+ if (raw == null) {
+ return 0;
+ }
+
+ List<String> list = parseList(raw, -1);
+ if (list == null) {
+ return -1;
+ }
+
+ return list.size();
+ }
+
+ /**
+ * Return a {@link String} representation of the given list of values.
+ * <p>
+ * The list of values is comma-separated and each value is surrounded by
+ * double-quotes; caret (^) and double-quotes (") are escaped by a caret.
+ *
+ * @param str
+ * the input value
+ * @param item
+ * the item number to use for an array of values, or -1 for
+ * non-arrays
+ *
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public List<String> parseList(String str, int item) {
+ if (str == null) {
+ return null;
+ }
+
+ if (item >= 0) {
+ str = getItem(str, item);
+ }
+
+ List<String> list = new ArrayList<String>();
+ try {
+ boolean inQuote = false;
+ boolean prevIsBackSlash = false;
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < str.length(); i++) {
+ char car = str.charAt(i);
+
+ if (prevIsBackSlash) {
+ // We don't process it here
+ builder.append(car);
+ prevIsBackSlash = false;
+ } else {
+ switch (car) {
+ case '"':
+ // We don't process it here
+ builder.append(car);
+
+ if (inQuote) {
+ list.add(unescape(builder.toString()));
+ builder.setLength(0);
+ }
+
+ inQuote = !inQuote;
+ break;
+ case '^':
+ // We don't process it here
+ builder.append(car);
+ prevIsBackSlash = true;
+ break;
+ case ' ':
+ case '\n':
+ case '\r':
+ if (inQuote) {
+ builder.append(car);
+ }
+ break;
+
+ case ',':
+ if (!inQuote) {
+ break;
+ }
+ // continue to default
+ default:
+ if (!inQuote) {
+ // Bad format!
+ return null;
+ }
+
+ builder.append(car);
+ break;
+ }
+ }
+ }
+
+ if (inQuote || prevIsBackSlash) {
+ // Bad format!
+ return null;
+ }
+
+ } catch (Exception e) {
+ return null;
+ }
+
+ return list;
+ }
+
+ /**
+ * Return a {@link String} representation of the given list of values.
+ * <p>
+ * NULL will be assimilated to an empty {@link String} if later non-null
+ * values exist, or just ignored if not.
+ * <p>
+ * Example:
+ * <ul>
+ * <li><tt>1</tt>,<tt>NULL</tt>, <tt>3</tt> will become <tt>1</tt>,
+ * <tt>""</tt>, <tt>3</tt></li>
+ * <li><tt>1</tt>,<tt>NULL</tt>, <tt>NULL</tt> will become <tt>1</tt></li>
+ * <li><tt>NULL</tt>, <tt>NULL</tt>, <tt>NULL</tt> will become an empty list
+ * </li>
+ * </ul>
+ *
+ * @param list
+ * the input value
+ *
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public String fromList(List<String> list) {
+ if (list == null) {
+ list = new ArrayList<String>();
+ }
+
+ int last = list.size() - 1;
+ for (int i = 0; i < list.size(); i++) {
+ if (list.get(i) != null) {
+ last = i;
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i <= last; i++) {
+ String item = list.get(i);
+ if (item == null) {
+ item = "";
+ }
+
+ if (builder.length() > 0) {
+ builder.append(", ");
+ }
+ builder.append(escape(item));
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Return a {@link String} representation of the given list of values.
+ * <p>
+ * NULL will be assimilated to an empty {@link String} if later non-null
+ * values exist, or just ignored if not.
+ * <p>
+ * Example:
+ * <ul>
+ * <li><tt>1</tt>,<tt>NULL</tt>, <tt>3</tt> will become <tt>1</tt>,
+ * <tt>""</tt>, <tt>3</tt></li>
+ * <li><tt>1</tt>,<tt>NULL</tt>, <tt>NULL</tt> will become <tt>1</tt></li>
+ * <li><tt>NULL</tt>, <tt>NULL</tt>, <tt>NULL</tt> will become an empty list
+ * </li>
+ * </ul>
+ *
+ * @param list
+ * the input value
+ * @param value
+ * the value to insert
+ * @param item
+ * the position to insert it at
+ *
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public String fromList(List<String> list, String value, int item) {
+ if (list == null) {
+ list = new ArrayList<String>();
+ }
+
+ while (item >= list.size()) {
+ list.add(null);
+ }
+ list.set(item, value);
+
+ return fromList(list);
+ }
+
+ /**
+ * Return a {@link String} representation of the given list of values.
+ * <p>
+ * NULL will be assimilated to an empty {@link String} if later non-null
+ * values exist, or just ignored if not.
+ * <p>
+ * Example:
+ * <ul>
+ * <li><tt>1</tt>,<tt>NULL</tt>, <tt>3</tt> will become <tt>1</tt>,
+ * <tt>""</tt>, <tt>3</tt></li>
+ * <li><tt>1</tt>,<tt>NULL</tt>, <tt>NULL</tt> will become <tt>1</tt></li>
+ * <li><tt>NULL</tt>, <tt>NULL</tt>, <tt>NULL</tt> will become an empty list
+ * </li>
+ * </ul>
+ *
+ * @param list
+ * the input value
+ * @param value
+ * the value to insert
+ * @param item
+ * the position to insert it at
+ *
+ * @return the raw {@link String} value that correspond to it
+ */
+ static public String fromList(String list, String value, int item) {
+ return fromList(parseList(list, -1), value, item);
+ }
+
+ /**
+ * Escape the given value for list formating (no carets, no NEWLINES...).
+ * <p>
+ * You can unescape it with {@link BundleHelper#unescape(String)}
+ *
+ * @param value
+ * the value to escape
+ *
+ * @return an escaped value that can unquoted by the reverse operation
+ * {@link BundleHelper#unescape(String)}
+ */
+ static public String escape(String value) {
+ return '"' + value//
+ .replace("^", "^^") //
+ .replace("\"", "^\"") //
+ .replace("\n", "^\n") //
+ .replace("\r", "^\r") //
+ + '"';
+ }
+
+ /**
+ * Unescape the given value for list formating (change ^n into NEWLINE and
+ * so on).
+ * <p>
+ * You can escape it with {@link BundleHelper#escape(String)}
+ *
+ * @param value
+ * the value to escape
+ *
+ * @return an unescaped value that can reverted by the reverse operation
+ * {@link BundleHelper#escape(String)}, or NULL if it was badly
+ * formated
+ */
+ static public String unescape(String value) {
+ if (value.length() < 2 || !value.startsWith("\"")
+ || !value.endsWith("\"")) {
+ // Bad format
+ return null;
+ }
+
+ value = value.substring(1, value.length() - 1);
+
+ boolean prevIsBackslash = false;
+ StringBuilder builder = new StringBuilder();
+ for (char car : value.toCharArray()) {
+ if (prevIsBackslash) {
+ switch (car) {
+ case 'n':
+ case 'N':
+ builder.append('\n');
+ break;
+ case 'r':
+ case 'R':
+ builder.append('\r');
+ break;
+ default: // includes ^ and "
+ builder.append(car);
+ break;
+ }
+ prevIsBackslash = false;
+ } else {
+ if (car == '^') {
+ prevIsBackslash = true;
+ } else {
+ builder.append(car);
+ }
+ }
+ }
+
+ if (prevIsBackslash) {
+ // Bad format
+ return null;
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Retrieve the specific item in the given value, assuming it is an array.
+ *
+ * @param value
+ * the value to look into
+ * @param item
+ * the item number to get for an array of values, or -1 for
+ * non-arrays (in that case, simply return the value as-is)
+ *
+ * @return the value as-is for non arrays, the item <tt>item</tt> if found,
+ * NULL if not
+ */
+ static private String getItem(String value, int item) {
+ if (item >= 0) {
+ value = null;
+ List<String> values = parseList(value, -1);
+ if (values != null && item < values.size()) {
+ value = values.get(item);
+ }
+ }
+
+ return value;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.resources;
+
+import java.util.ResourceBundle;
+
+/**
+ * This class help you get UTF-8 bundles for this application.
+ *
+ * @author niki
+ */
+public class Bundles {
+ /**
+ * The configuration directory where we try to get the <tt>.properties</tt>
+ * in priority, or NULL to get the information from the compiled resources.
+ */
+ static private String confDir = null;
+
+ /**
+ * Set the primary configuration directory to look for <tt>.properties</tt>
+ * files in.
+ *
+ * All {@link ResourceBundle}s returned by this class after that point will
+ * respect this new directory.
+ *
+ * @param confDir
+ * the new directory
+ */
+ static public void setDirectory(String confDir) {
+ Bundles.confDir = confDir;
+ }
+
+ /**
+ * Get the primary configuration directory to look for <tt>.properties</tt>
+ * files in.
+ *
+ * @return the directory
+ */
+ static public String getDirectory() {
+ return Bundles.confDir;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Locale;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
+import java.util.ResourceBundle.Control;
+
+/**
+ * Fixed ResourceBundle.Control class. It will use UTF-8 for the files to load.
+ *
+ * Also support an option to first check into the given path before looking into
+ * the resources.
+ *
+ * @author niki
+ *
+ */
+class FixedResourceBundleControl extends Control {
+ @Override
+ public ResourceBundle newBundle(String baseName, Locale locale,
+ String format, ClassLoader loader, boolean reload)
+ throws IllegalAccessException, InstantiationException, IOException {
+ // The below is a copy of the default implementation.
+ String bundleName = toBundleName(baseName, locale);
+ String resourceName = toResourceName(bundleName, "properties");
+
+ ResourceBundle bundle = null;
+ InputStream stream = null;
+ if (reload) {
+ URL url = loader.getResource(resourceName);
+ if (url != null) {
+ URLConnection connection = url.openConnection();
+ if (connection != null) {
+ connection.setUseCaches(false);
+ stream = connection.getInputStream();
+ }
+ }
+ } else {
+ stream = loader.getResourceAsStream(resourceName);
+ }
+
+ if (stream != null) {
+ try {
+ // This line is changed to make it to read properties files
+ // as UTF-8.
+ // How can someone use an archaic encoding such as ISO 8859-1 by
+ // *DEFAULT* is beyond me...
+ bundle = new PropertyResourceBundle(new InputStreamReader(
+ stream, "UTF-8"));
+ } finally {
+ stream.close();
+ }
+ }
+ return bundle;
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.resources;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used to give some information about the translation keys, so the
+ * translation .properties file can be created programmatically.
+ *
+ * @author niki
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Meta {
+ /**
+ * 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 for this item: what it is or does, how to explain that item
+ * to the user including what can be used here (i.e., %s = file name, %d =
+ * file size...).
+ * <p>
+ * For group, the first line ('\\n'-separated) will be used as a title while
+ * the rest will be the description.
+ *
+ * @return what it is
+ */
+ String description() default "";
+
+ /**
+ * 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 TRUE if it is a group
+ */
+ boolean group() default false;
+
+ /**
+ * What format should/must this key be in.
+ *
+ * @return the format it is in
+ */
+ 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;
+
+ /**
+ * The default value of this item.
+ *
+ * @return the value
+ */
+ String def() default "";
+
+ /**
+ * This item is a comma-separated list of values instead of a single value.
+ * <p>
+ * The list items are separated by a comma, each surrounded by
+ * double-quotes, with backslashes and double-quotes escaped by a backslash.
+ * <p>
+ * Example: <tt>"un", "deux"</tt>
+ *
+ * @return TRUE if it is
+ */
+ boolean array() default false;
+
+ /**
+ * @deprecated add the info into the description, as only the description
+ * will be translated.
+ */
+ @Deprecated
+ String info() default "";
+}
--- /dev/null
+package be.nikiroo.utils.resources;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import be.nikiroo.utils.resources.Meta.Format;
+
+/**
+ * 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 MetaInfo<E extends Enum<E>> implements Iterable<MetaInfo<E>> {
+ private final Bundle<E> bundle;
+ private final E id;
+
+ private Meta meta;
+ private List<MetaInfo<E>> children = new ArrayList<MetaInfo<E>>();
+
+ private String value;
+ private List<Runnable> reloadedListeners = new ArrayList<Runnable>();
+ private List<Runnable> saveListeners = new ArrayList<Runnable>();
+
+ private String name;
+ private String description;
+
+ private boolean dirty;
+
+ /**
+ * Create a new {@link MetaInfo} from a value (without children).
+ * <p>
+ * For instance, you can call
+ * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>.
+ *
+ * @param type
+ * the type of enum the value is
+ * @param bundle
+ * the bundle this value belongs to
+ * @param id
+ * the value itself
+ */
+ public MetaInfo(Class<E> type, Bundle<E> bundle, E id) {
+ this.bundle = bundle;
+ this.id = id;
+
+ try {
+ this.meta = type.getDeclaredField(id.name()).getAnnotation(
+ Meta.class);
+ } catch (NoSuchFieldException e) {
+ } catch (SecurityException e) {
+ }
+
+ // We consider that if a description bundle is used, everything is in it
+
+ String description = null;
+ if (bundle.getDescriptionBundle() != null) {
+ description = bundle.getDescriptionBundle().getString(id);
+ if (description != null && description.trim().isEmpty()) {
+ description = null;
+ }
+ }
+ if (description == null) {
+ description = meta.description();
+ if (description == null) {
+ description = "";
+ }
+ }
+
+ String name = idToName(id, null);
+
+ // Special rules for groups:
+ if (meta.group()) {
+ String groupName = description.split("\n")[0];
+ description = description.substring(groupName.length()).trim();
+ if (!groupName.isEmpty()) {
+ name = groupName;
+ }
+ }
+
+ if (meta.def() != null && !meta.def().isEmpty()) {
+ if (!description.isEmpty()) {
+ description += "\n\n";
+ }
+ description += "(Default value: " + meta.def() + ")";
+ }
+
+ this.name = name;
+ this.description = description;
+
+ reload();
+ }
+
+ /**
+ * For normal items, this is the name of this item, deduced from its ID (or
+ * in other words, it is the ID but presented in a displayable form).
+ * <p>
+ * For group items, this is the first line of the description if it is not
+ * empty (else, it is the ID in the same way as normal items).
+ * <p>
+ * Never NULL.
+ *
+ *
+ * @return the name, never NULL
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * A description for this item: what it is or does, how to explain that item
+ * to the user including what can be used here (i.e., %s = file name, %d =
+ * file size...).
+ * <p>
+ * For group, the first line ('\\n'-separated) will be used as a title while
+ * the rest will be the description.
+ * <p>
+ * If a default value is known, it will be specified here, too.
+ * <p>
+ * Never NULL.
+ *
+ * @return the description, not NULL
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * The format this item is supposed to follow
+ *
+ * @return the format
+ */
+ public Format getFormat() {
+ return meta.format();
+ }
+
+ /**
+ * The allowed list of values that a {@link Format#FIXED_LIST} item is
+ * allowed to be, or a list of suggestions for {@link Format#COMBO_LIST}
+ * items. Also works for {@link Format#LOCALE}.
+ * <p>
+ * Will always allow an empty string in addition to the rest.
+ *
+ * @return the list of values
+ */
+ public String[] getAllowedValues() {
+ String[] list = meta.list();
+
+ String[] withEmpty = new String[list.length + 1];
+ withEmpty[0] = "";
+ for (int i = 0; i < list.length; i++) {
+ withEmpty[i + 1] = list[i];
+ }
+
+ return withEmpty;
+ }
+
+ /**
+ * Return all the languages known by the program for this bundle.
+ * <p>
+ * This only works for {@link TransBundle}, and will return an empty list if
+ * this is not a {@link TransBundle}.
+ *
+ * @return the known language codes
+ */
+ public List<String> getKnownLanguages() {
+ if (bundle instanceof TransBundle) {
+ return ((TransBundle<E>) bundle).getKnownLanguages();
+ }
+
+ return new ArrayList<String>();
+ }
+
+ /**
+ * This item is a comma-separated list of values instead of a single value.
+ * <p>
+ * The list items are separated by a comma, each surrounded by
+ * double-quotes, with backslashes and double-quotes escaped by a backslash.
+ * <p>
+ * Example: <tt>"un", "deux"</tt>
+ *
+ * @return TRUE if it is
+ */
+ public boolean isArray() {
+ return meta.array();
+ }
+
+ /**
+ * A manual flag to specify if the data has been changed or not, which can
+ * be used by {@link MetaInfo#save(boolean)}.
+ *
+ * @return TRUE if it is dirty (if it has changed)
+ */
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ /**
+ * A manual flag to specify that the data has been changed, which can be
+ * used by {@link MetaInfo#save(boolean)}.
+ */
+ public void setDirty() {
+ this.dirty = true;
+ }
+
+ /**
+ * The number of items in this item if it {@link MetaInfo#isArray()}, or -1
+ * if not.
+ *
+ * @param useDefaultIfEmpty
+ * check the size of the default list instead if the list is
+ * empty
+ *
+ * @return -1 or the number of items
+ */
+ public int getListSize(boolean useDefaultIfEmpty) {
+ if (!isArray()) {
+ return -1;
+ }
+
+ return BundleHelper.getListSize(getString(-1, useDefaultIfEmpty));
+ }
+
+ /**
+ * 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 TRUE if it is a group
+ */
+ public boolean isGroup() {
+ return meta.group();
+ }
+
+ /**
+ * The value stored by this item, as a {@link String}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public String getString(int item, boolean useDefaultIfEmpty) {
+ if (isArray() && item >= 0) {
+ List<String> values = BundleHelper.parseList(value, -1);
+ if (values != null && item < values.size()) {
+ return values.get(item);
+ }
+
+ if (useDefaultIfEmpty) {
+ return getDefaultString(item);
+ }
+
+ return null;
+ }
+
+ if (value == null && useDefaultIfEmpty) {
+ return getDefaultString(item);
+ }
+
+ return value;
+ }
+
+ /**
+ * The default value of this item, as a {@link String}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public String getDefaultString(int item) {
+ if (isArray() && item >= 0) {
+ List<String> values = BundleHelper.parseList(meta.def(), item);
+ if (values != null && item < values.size()) {
+ return values.get(item);
+ }
+
+ return null;
+ }
+
+ return meta.def();
+ }
+
+ /**
+ * The value stored by this item, as a {@link Boolean}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Boolean getBoolean(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper
+ .parseBoolean(getString(item, useDefaultIfEmpty), -1);
+ }
+
+ /**
+ * The default value of this item, as a {@link Boolean}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public Boolean getDefaultBoolean(int item) {
+ return BundleHelper.parseBoolean(getDefaultString(item), -1);
+ }
+
+ /**
+ * The value stored by this item, as a {@link Character}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Character getCharacter(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper.parseCharacter(getString(item, useDefaultIfEmpty),
+ -1);
+ }
+
+ /**
+ * The default value of this item, as a {@link Character}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public Character getDefaultCharacter(int item) {
+ return BundleHelper.parseCharacter(getDefaultString(item), -1);
+ }
+
+ /**
+ * The value stored by this item, as an {@link Integer}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Integer getInteger(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper
+ .parseInteger(getString(item, useDefaultIfEmpty), -1);
+ }
+
+ /**
+ * The default value of this item, as an {@link Integer}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public Integer getDefaultInteger(int item) {
+ return BundleHelper.parseInteger(getDefaultString(item), -1);
+ }
+
+ /**
+ * The value stored by this item, as a colour (represented here as an
+ * {@link Integer}) if it represents a colour, or NULL if it doesn't.
+ * <p>
+ * The returned colour value is an ARGB value.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Integer getColor(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper.parseColor(getString(item, useDefaultIfEmpty), -1);
+ }
+
+ /**
+ * The default value stored by this item, as a colour (represented here as
+ * an {@link Integer}) if it represents a colour, or NULL if it doesn't.
+ * <p>
+ * The returned colour value is an ARGB value.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the value
+ */
+ public Integer getDefaultColor(int item) {
+ return BundleHelper.parseColor(getDefaultString(item), -1);
+ }
+
+ /**
+ * A {@link String} representation of the list of values.
+ * <p>
+ * The list of values is comma-separated and each value is surrounded by
+ * double-quotes; backslashes and double-quotes are escaped by a backslash.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public List<String> getList(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper.parseList(getString(item, useDefaultIfEmpty), -1);
+ }
+
+ /**
+ * A {@link String} representation of the default list of values.
+ * <p>
+ * The list of values is comma-separated and each value is surrounded by
+ * double-quotes; backslashes and double-quotes are escaped by a backslash.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the value
+ */
+ public List<String> getDefaultList(int item) {
+ return BundleHelper.parseList(getDefaultString(item), -1);
+ }
+
+ /**
+ * The value stored by this item, as a {@link String}.
+ *
+ * @param value
+ * the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setString(String value, int item) {
+ if (isArray() && item >= 0) {
+ this.value = BundleHelper.fromList(this.value, value, item);
+ } else {
+ this.value = value;
+ }
+ }
+
+ /**
+ * The value stored by this item, as a {@link Boolean}.
+ *
+ * @param value
+ * the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setBoolean(boolean value, int item) {
+ setString(BundleHelper.fromBoolean(value), item);
+ }
+
+ /**
+ * The value stored by this item, as a {@link Character}.
+ *
+ * @param value
+ * the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setCharacter(char value, int item) {
+ setString(BundleHelper.fromCharacter(value), item);
+ }
+
+ /**
+ * The value stored by this item, as an {@link Integer}.
+ *
+ * @param value
+ * the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setInteger(int value, int item) {
+ setString(BundleHelper.fromInteger(value), item);
+ }
+
+ /**
+ * The value stored by this item, as a colour (represented here as an
+ * {@link Integer}) if it represents a colour, or NULL if it doesn't.
+ * <p>
+ * The colour value is an ARGB value.
+ *
+ * @param value
+ * the value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setColor(int value, int item) {
+ setString(BundleHelper.fromColor(value), item);
+ }
+
+ /**
+ * A {@link String} representation of the default list of values.
+ * <p>
+ * The list of values is comma-separated and each value is surrounded by
+ * double-quotes; backslashes and double-quotes are escaped by a backslash.
+ *
+ * @param value
+ * the {@link String} representation
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setList(List<String> value, int item) {
+ setString(BundleHelper.fromList(value), item);
+ }
+
+ /**
+ * Reload the value from the {@link Bundle}, so the last value that was
+ * saved will be used.
+ */
+ public void reload() {
+ if (bundle.isSet(id, false)) {
+ value = bundle.getString(id);
+ } else {
+ value = null;
+ }
+
+ // Copy the list so we can create new listener in a listener
+ for (Runnable listener : new ArrayList<Runnable>(reloadedListeners)) {
+ try {
+ listener.run();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Add a listener that will be called <b>after</b> a reload operation.
+ * <p>
+ * You could use it to refresh the UI for instance.
+ *
+ * @param listener
+ * the listener
+ */
+ public void addReloadedListener(Runnable listener) {
+ reloadedListeners.add(listener);
+ }
+
+ /**
+ * Save the current value to the {@link Bundle}.
+ * <p>
+ * Note that listeners will be called <b>before</b> the dirty check and
+ * <b>before</b> saving the value.
+ *
+ * @param onlyIfDirty
+ * only save the data if the dirty flag is set (will reset the
+ * dirty flag)
+ */
+ public void save(boolean onlyIfDirty) {
+ // Copy the list so we can create new listener in a listener
+ for (Runnable listener : new ArrayList<Runnable>(saveListeners)) {
+ try {
+ listener.run();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!onlyIfDirty || isDirty()) {
+ bundle.setString(id, value);
+ }
+ }
+
+ /**
+ * Add a listener that will be called <b>before</b> a save operation.
+ * <p>
+ * You could use it to make some modification to the stored value before it
+ * is saved.
+ *
+ * @param listener
+ * the listener
+ */
+ public void addSaveListener(Runnable listener) {
+ saveListeners.add(listener);
+ }
+
+ /**
+ * The sub-items if any (if no sub-items, will return an empty list).
+ * <p>
+ * Sub-items are declared when a {@link Meta} has an ID that starts with the
+ * ID of a {@link Meta#group()} {@link MetaInfo}.
+ * <p>
+ * For instance:
+ * <ul>
+ * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
+ * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
+ * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
+ * </ul>
+ *
+ * @return the sub-items if any
+ */
+ public List<MetaInfo<E>> getChildren() {
+ return children;
+ }
+
+ /**
+ * The number of sub-items, if any.
+ *
+ * @return the number or 0
+ */
+ public int size() {
+ return children.size();
+ }
+
+ @Override
+ public Iterator<MetaInfo<E>> iterator() {
+ return children.iterator();
+ }
+
+ /**
+ * Create a list of {@link MetaInfo}, 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
+ * the {@link Bundle} to sort through
+ *
+ * @return the list
+ */
+ static public <E extends Enum<E>> List<MetaInfo<E>> getItems(Class<E> type,
+ Bundle<E> bundle) {
+ List<MetaInfo<E>> list = new ArrayList<MetaInfo<E>>();
+ List<MetaInfo<E>> shadow = new ArrayList<MetaInfo<E>>();
+ for (E id : type.getEnumConstants()) {
+ MetaInfo<E> info = new MetaInfo<E>(type, bundle, id);
+ list.add(info);
+ shadow.add(info);
+ }
+
+ for (int i = 0; i < list.size(); i++) {
+ MetaInfo<E> info = list.get(i);
+
+ MetaInfo<E> parent = findParent(info, shadow);
+ if (parent != null) {
+ list.remove(i--);
+ parent.children.add(info);
+ info.name = idToName(info.id, parent.id);
+ }
+ }
+
+ return list;
+ }
+
+ /**
+ * Find the longest parent of the given {@link MetaInfo}, which means:
+ * <ul>
+ * <li>the parent is a {@link Meta#group()}</li>
+ * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
+ * <li>there is no other parent sharing a substring for this
+ * {@link MetaInfo} with a longer Id</li>
+ * </ul>
+ *
+ * @param <E>
+ * the kind of enum
+ * @param info
+ * the info to look for a parent for
+ * @param candidates
+ * the list of potential parents
+ *
+ * @return the longest parent or NULL if no parent is found
+ */
+ static private <E extends Enum<E>> MetaInfo<E> findParent(MetaInfo<E> info,
+ List<MetaInfo<E>> candidates) {
+ String id = info.id.toString();
+ MetaInfo<E> group = null;
+ for (MetaInfo<E> pcandidate : candidates) {
+ if (pcandidate.isGroup()) {
+ String candidateId = pcandidate.id.toString();
+ if (!id.equals(candidateId) && id.startsWith(candidateId)) {
+ if (group == null
+ || group.id.toString().length() < candidateId
+ .length()) {
+ group = pcandidate;
+ }
+ }
+ }
+ }
+
+ return group;
+ }
+
+ static private <E extends Enum<E>> String idToName(E id, E prefix) {
+ String name = id.toString();
+ if (prefix != null && name.startsWith(prefix.toString())) {
+ name = name.substring(prefix.toString().length());
+ }
+
+ if (name.length() > 0) {
+ name = name.substring(0, 1).toUpperCase()
+ + name.substring(1).toLowerCase();
+ }
+
+ name = name.replace("_", " ");
+
+ return name.trim();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.resources;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * This class manages a translation-dedicated Bundle.
+ * <p>
+ * Two special cases are handled for the used enum:
+ * <ul>
+ * <li>NULL will always will return an empty {@link String}</li>
+ * <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> {
+ private boolean utf = true;
+ private Locale locale;
+ private boolean defaultLocale = false;
+
+ /**
+ * Create a translation service with the default language.
+ *
+ * @param type
+ * a runtime instance of the class of E
+ * @param name
+ * the name of the {@link Bundles}
+ */
+ public TransBundle(Class<E> type, Enum<?> name) {
+ this(type, name, (Locale) null);
+ }
+
+ /**
+ * Create a translation service for the given language (will fall back to
+ * the default one i not found).
+ *
+ * @param type
+ * a runtime instance of the class of E
+ * @param name
+ * the name of the {@link Bundles}
+ * @param language
+ * the language to use, can be NULL for default
+ */
+ public TransBundle(Class<E> type, Enum<?> name, String language) {
+ super(type, name, null);
+ setLocale(language);
+ }
+
+ /**
+ * Create a translation service for the given language (will fall back to
+ * the default one i not found).
+ *
+ * @param type
+ * a runtime instance of the class of E
+ * @param name
+ * the name of the {@link Bundles}
+ * @param language
+ * the language to use, can be NULL for default
+ */
+ public TransBundle(Class<E> type, Enum<?> name, Locale language) {
+ super(type, name, null);
+ setLocale(language);
+ }
+
+ /**
+ * Translate the given id into user text.
+ *
+ * @param stringId
+ * the ID to translate
+ * @param values
+ * the values to insert instead of the place holders in the
+ * translation
+ *
+ * @return the translated text with the given value where required or NULL
+ * if not found (not present in the resource file)
+ */
+ public String getString(E stringId, Object... values) {
+ return getStringX(stringId, "", values);
+ }
+
+ /**
+ * Translate the given id into user text.
+ *
+ * @param stringId
+ * the ID to translate
+ * @param values
+ * the values to insert instead of the place holders in the
+ * translation
+ *
+ * @return the translated text with the given value where required or NULL
+ * if not found (not present in the resource file)
+ */
+ public String getStringNOUTF(E stringId, Object... values) {
+ return getStringX(stringId, "NOUTF", values);
+ }
+
+ /**
+ * Translate the given id suffixed with the runtime value "_suffix" (that
+ * is, "_" and suffix) into user text.
+ *
+ * @param stringId
+ * the ID to translate
+ * @param values
+ * the values to insert instead of the place holders in the
+ * translation
+ * @param suffix
+ * the runtime suffix
+ *
+ * @return the translated text with the given value where required or NULL
+ * if not found (not present in the resource file)
+ */
+ public String getStringX(E stringId, String suffix, Object... values) {
+ E id = stringId;
+ String result = "";
+
+ String key = id.name()
+ + ((suffix == null || suffix.isEmpty()) ? "" : "_"
+ + suffix.toUpperCase());
+
+ if (!isUnicode()) {
+ if (containsKey(key + "_NOUTF")) {
+ key += "_NOUTF";
+ }
+ }
+
+ if ("NULL".equals(id.name().toUpperCase())) {
+ result = "";
+ } else if ("DUMMY".equals(id.name().toUpperCase())) {
+ result = "[" + key.toLowerCase() + "]";
+ } else if (containsKey(key)) {
+ result = getString(key, null);
+ if (result == null) {
+ result = getMetaDef(id.name());
+ }
+ } else {
+ result = null;
+ }
+
+ if (values != null && values.length > 0 && result != null) {
+ return String.format(locale, result, values);
+ }
+
+ return result;
+ }
+
+ /**
+ * Check if unicode characters should be used.
+ *
+ * @return TRUE to allow unicode
+ */
+ public boolean isUnicode() {
+ return utf;
+ }
+
+ /**
+ * Allow or disallow unicode characters in the program.
+ *
+ * @param utf
+ * TRUE to allow unuciode, FALSE to only allow ASCII characters
+ */
+ public void setUnicode(boolean utf) {
+ this.utf = utf;
+ }
+
+ /**
+ * Return all the languages known by the program for this bundle.
+ *
+ * @return the known language codes
+ */
+ public List<String> getKnownLanguages() {
+ return getKnownLanguages(keyType);
+ }
+
+ /**
+ * The current language (which can be the default one, but NOT NULL).
+ *
+ * @return the language, not NULL
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /**
+ * The current language (which can be the default one, but NOT NULL).
+ *
+ * @return the language, not NULL, in a display format (fr-BE, en-GB, es,
+ * de...)
+ */
+ public String getLocaleString() {
+ String lang = locale.getLanguage();
+ String country = locale.getCountry();
+ if (country != null && !country.isEmpty()) {
+ return lang + "-" + country;
+ }
+ return lang;
+ }
+
+ /**
+ * Initialise the translation mappings for the given language.
+ *
+ * @param language
+ * the language to initialise, in the form "en-GB" or "fr" for
+ * instance
+ */
+ private void setLocale(String language) {
+ setLocale(getLocaleFor(language));
+ }
+
+ /**
+ * Initialise the translation mappings for the given language.
+ *
+ * @param language
+ * the language to initialise, or NULL for default
+ */
+ private void setLocale(Locale language) {
+ if (language != null) {
+ defaultLocale = false;
+ locale = language;
+ } else {
+ defaultLocale = true;
+ locale = Locale.getDefault();
+ }
+
+ setBundle(keyType, locale, false);
+ }
+
+ @Override
+ public void reload(boolean resetToDefault) {
+ setBundle(keyType, locale, resetToDefault);
+ }
+
+ @Override
+ public String getString(E id) {
+ return getString(id, (Object[]) null);
+ }
+
+ /**
+ * Create/update the .properties files for each supported language and for
+ * the default language.
+ * <p>
+ * Note: this method is <b>NOT</b> thread-safe.
+ *
+ * @param path
+ * the path where the .properties files are
+ *
+ * @throws IOException
+ * in case of IO errors
+ */
+ @Override
+ public void updateFile(String path) throws IOException {
+ String prev = locale.getLanguage();
+ Object status = takeSnapshot();
+
+ // default locale
+ setLocale((Locale) null);
+ if (prev.equals(Locale.getDefault().getLanguage())) {
+ // restore snapshot if default locale = current locale
+ restoreSnapshot(status);
+ }
+ super.updateFile(path);
+
+ for (String lang : getKnownLanguages()) {
+ setLocale(lang);
+ if (lang.equals(prev)) {
+ restoreSnapshot(status);
+ }
+ super.updateFile(path);
+ }
+
+ setLocale(prev);
+ restoreSnapshot(status);
+ }
+
+ @Override
+ protected File getUpdateFile(String path) {
+ String code = locale.toString();
+ File file = null;
+ if (!defaultLocale && code.length() > 0) {
+ file = new File(path, keyType.name() + "_" + code + ".properties");
+ } else {
+ // Default properties file:
+ file = new File(path, keyType.name() + ".properties");
+ }
+
+ return file;
+ }
+
+ @Override
+ protected void writeHeader(Writer writer) throws IOException {
+ String code = locale.toString();
+ String name = locale.getDisplayCountry(locale);
+
+ if (name.length() == 0) {
+ name = locale.getDisplayLanguage(locale);
+ }
+
+ if (name.length() == 0) {
+ name = "default";
+ }
+
+ if (code.length() > 0) {
+ name = name + " (" + code + ")";
+ }
+
+ name = (name + " " + getBundleDisplayName()).trim();
+
+ writer.write("# " + name + " translation file (UTF-8)\n");
+ writer.write("# \n");
+ writer.write("# Note that any key can be doubled with a _NOUTF suffix\n");
+ writer.write("# to use when the NOUTF env variable is set to 1\n");
+ writer.write("# \n");
+ writer.write("# Also, the comments always refer to the key below them.\n");
+ writer.write("# \n");
+ }
+
+ @Override
+ protected void writeValue(Writer writer, E id) throws IOException {
+ super.writeValue(writer, id);
+
+ String name = id.name() + "_NOUTF";
+ if (containsKey(name)) {
+ String value = getString(name, null);
+ if (value == null) {
+ value = getMetaDef(id.name());
+ }
+ boolean set = isSet(id, false);
+ writeValue(writer, name, value, set);
+ }
+ }
+
+ /**
+ * Return the {@link Locale} representing the given language.
+ *
+ * @param language
+ * the language to initialise, in the form "en-GB" or "fr" for
+ * instance
+ *
+ * @return the corresponding {@link Locale} or NULL if it is not known
+ */
+ static private Locale getLocaleFor(String language) {
+ Locale locale;
+
+ if (language == null || language.trim().isEmpty()) {
+ return null;
+ }
+
+ language = language.replaceAll("_", "-");
+ String lang = language;
+ String country = null;
+ if (language.contains("-")) {
+ lang = language.split("-")[0];
+ country = language.split("-")[1];
+ }
+
+ if (country != null)
+ locale = new Locale(lang, country);
+ else
+ locale = new Locale(lang);
+
+ return locale;
+ }
+
+ /**
+ * Return all the languages known by the program.
+ *
+ * @param name
+ * the enumeration on which we translate
+ *
+ * @return the known language codes
+ */
+ static protected List<String> getKnownLanguages(Enum<?> name) {
+ List<String> resources = new LinkedList<String>();
+
+ String regex = ".*" + name.name() + "[_a-zA-Za]*\\.properties$";
+
+ for (String res : TransBundle_ResourceList.getResources(Pattern
+ .compile(regex))) {
+ String resource = res;
+ int index = resource.lastIndexOf('/');
+ if (index >= 0 && index < (resource.length() - 1))
+ resource = resource.substring(index + 1);
+ if (resource.startsWith(name.name())) {
+ resource = resource.substring(0, resource.length()
+ - ".properties".length());
+ resource = resource.substring(name.name().length());
+ if (resource.startsWith("_")) {
+ resource = resource.substring(1);
+ resources.add(resource);
+ }
+ }
+ }
+
+ return resources;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.resources;
+
+// code copied from from:
+// http://forums.devx.com/showthread.php?t=153784,
+// via:
+// http://stackoverflow.com/questions/3923129/get-a-list-of-resources-from-classpath-directory
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+/**
+ * list resources available from the classpath @ *
+ */
+class TransBundle_ResourceList {
+
+ /**
+ * for all elements of java.class.path get a Collection of resources Pattern
+ * pattern = Pattern.compile(".*"); gets all resources
+ *
+ * @param pattern
+ * the pattern to match
+ * @return the resources in the order they are found
+ */
+ public static Collection<String> getResources(final Pattern pattern) {
+ final ArrayList<String> retval = new ArrayList<String>();
+ final String classPath = System.getProperty("java.class.path", ".");
+ final String[] classPathElements = classPath.split(System
+ .getProperty("path.separator"));
+ for (final String element : classPathElements) {
+ retval.addAll(getResources(element, pattern));
+ }
+
+ return retval;
+ }
+
+ private static Collection<String> getResources(final String element,
+ final Pattern pattern) {
+ final ArrayList<String> retval = new ArrayList<String>();
+ final File file = new File(element);
+ if (file.isDirectory()) {
+ retval.addAll(getResourcesFromDirectory(file, pattern));
+ } else {
+ retval.addAll(getResourcesFromJarFile(file, pattern));
+ }
+
+ return retval;
+ }
+
+ private static Collection<String> getResourcesFromJarFile(final File file,
+ final Pattern pattern) {
+ final ArrayList<String> retval = new ArrayList<String>();
+ ZipFile zf;
+ try {
+ zf = new ZipFile(file);
+ } catch (final ZipException e) {
+ throw new Error(e);
+ } catch (final IOException e) {
+ throw new Error(e);
+ }
+ final Enumeration<? extends ZipEntry> e = zf.entries();
+ while (e.hasMoreElements()) {
+ final ZipEntry ze = e.nextElement();
+ final String fileName = ze.getName();
+ final boolean accept = pattern.matcher(fileName).matches();
+ if (accept) {
+ retval.add(fileName);
+ }
+ }
+ try {
+ zf.close();
+ } catch (final IOException e1) {
+ throw new Error(e1);
+ }
+
+ return retval;
+ }
+
+ private static Collection<String> getResourcesFromDirectory(
+ final File directory, final Pattern pattern) {
+ List<String> acc = new ArrayList<String>();
+ List<File> dirs = new ArrayList<File>();
+ getResourcesFromDirectory(acc, dirs, directory, pattern);
+
+ List<String> rep = new ArrayList<String>();
+ for (String value : acc) {
+ if (pattern.matcher(value).matches()) {
+ rep.add(value);
+ }
+ }
+
+ return rep;
+ }
+
+ private static void getResourcesFromDirectory(List<String> acc,
+ List<File> dirs, final File directory, final Pattern pattern) {
+ final File[] fileList = directory.listFiles();
+ if (fileList != null) {
+ for (final File file : fileList) {
+ if (!dirs.contains(file)) {
+ try {
+ String key = file.getCanonicalPath();
+ if (!acc.contains(key)) {
+ if (file.isDirectory()) {
+ dirs.add(file);
+ getResourcesFromDirectory(acc, dirs, file,
+ pattern);
+ } else {
+ acc.add(key);
+ }
+ }
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * This package encloses the classes needed to use
+ * {@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
+ * the linked <tt>.properties</tt> files (or export them, which is useful when
+ * you work from a JAR file).
+ * <p>
+ * All those classes expect UTF-8 content only.
+ *
+ * @author niki
+ */
+package be.nikiroo.utils.resources;
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import be.nikiroo.utils.streams.BufferedInputStream;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.streams.ReplaceOutputStream;
+
+/**
+ * A {@link CustomSerializer} supports and generates values in the form:
+ * <ul>
+ * <li><tt>custom^<i>TYPE</i>^<i>ENCODED_VALUE</i></tt></li>
+ * </ul>
+ * <p>
+ * In this scheme, the values are:
+ * <ul>
+ * <li><tt>custom</tt>: a fixed keyword</li>
+ * <li><tt>^</tt>: a fixed separator character (the
+ * <tt><i>ENCODED_VALUE</i></tt> can still use it inside its content, though</li>
+ * <li><tt><i>TYPE</i></tt>: the object type of this value</li>
+ * <li><tt><i>ENCODED_VALUE</i></tt>: the custom encoded value</li>
+ * </ul>
+ * <p>
+ * To create a new {@link CustomSerializer}, you are expected to implement the
+ * abstract methods of this class. The rest should be taken care of bythe
+ * system.
+ *
+ * @author niki
+ */
+public abstract class CustomSerializer {
+ /**
+ * Generate the custom <tt><i>ENCODED_VALUE</i></tt> from this
+ * <tt>value</tt>.
+ * <p>
+ * The <tt>value</tt> will always be of the supported type.
+ *
+ * @param out
+ * the {@link OutputStream} to write the value to
+ * @param value
+ * the value to serialize
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ protected abstract void toStream(OutputStream out, Object value)
+ throws IOException;
+
+ /**
+ * Regenerate the value from the custom <tt><i>ENCODED_VALUE</i></tt>.
+ * <p>
+ * The value in the {@link InputStream} <tt>in</tt> will always be of the
+ * supported type.
+ *
+ * @param in
+ * the {@link InputStream} containing the
+ * <tt><i>ENCODED_VALUE</i></tt>
+ *
+ * @return the regenerated object
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ protected abstract Object fromStream(InputStream in) throws IOException;
+
+ /**
+ * Return the supported type name.
+ * <p>
+ * It <b>must</b> be the name returned by {@link Object#getClass()
+ * #getCanonicalName()}.
+ *
+ * @return the supported class name
+ */
+ protected abstract String getType();
+
+ /**
+ * Encode the object into the given {@link OutputStream}, i.e., generate the
+ * <tt><i>ENCODED_VALUE</i></tt> part.
+ * <p>
+ * Use whatever scheme you wish, the system shall ensure that the content is
+ * correctly encoded and that you will receive the same content at decode
+ * time.
+ *
+ * @param out
+ * the builder to append to
+ * @param value
+ * the object to encode
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public void encode(OutputStream out, Object value) throws IOException {
+ ReplaceOutputStream replace = new ReplaceOutputStream(out, //
+ new String[] { "\\", "\n" }, //
+ new String[] { "\\\\", "\\n" });
+
+ try {
+ SerialUtils.write(replace, "custom^");
+ SerialUtils.write(replace, getType());
+ SerialUtils.write(replace, "^");
+ toStream(replace, value);
+ } finally {
+ replace.close(false);
+ }
+ }
+
+ /**
+ * Decode the value back into the supported object type.
+ * <p>
+ * We do <b>not</b> expect the full content here but only:
+ * <ul>
+ * <li>ENCODED_VALUE
+ * <li>
+ * </ul>
+ * That is, we do not expect the "<tt>custom</tt>^<tt><i>TYPE</i></tt>^"
+ * part.
+ *
+ * @param in
+ * the encoded value
+ *
+ * @return the object
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public Object decode(InputStream in) throws IOException {
+ ReplaceInputStream replace = new ReplaceInputStream(in, //
+ new String[] { "\\\\", "\\n" }, //
+ new String[] { "\\", "\n" });
+
+ try {
+ return fromStream(replace);
+ } finally {
+ replace.close(false);
+ }
+ }
+
+ public static boolean isCustom(BufferedInputStream in) throws IOException {
+ return in.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;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.serial;
+
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 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 OutputStream out;
+
+ /**
+ * Create a new {@link Exporter}.
+ *
+ * @param out
+ * export the data to this stream
+ */
+ public Exporter(OutputStream out) {
+ if (out == null) {
+ throw new NullPointerException(
+ "Cannot create an be.nikiroo.utils.serials.Exporter that will export to NULL");
+ }
+
+ this.out = out;
+ map = new HashMap<Integer, Object>();
+ }
+
+ /**
+ * Serialise the given object and add it to the list.
+ * <p>
+ * <b>Important: </b>If the operation fails (with a
+ * {@link NotSerializableException}), the {@link Exporter} will be corrupted
+ * (will contain bad, most probably not importable data).
+ *
+ * @param o
+ * the object to serialise
+ * @return this (for easier appending of multiple values)
+ *
+ * @throws NotSerializableException
+ * if the object cannot be serialised (in this case, the
+ * {@link Exporter} can contain bad, most probably not
+ * importable data)
+ * @throws IOException
+ * in case of I/O error
+ */
+ public Exporter append(Object o) throws NotSerializableException,
+ IOException {
+ SerialUtils.append(out, o, map);
+ return this;
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.Base64InputStream;
+import be.nikiroo.utils.streams.BufferedInputStream;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+
+/**
+ * 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;
+
+ /**
+ * Create a new {@link Importer}.
+ */
+ public Importer() {
+ map = new HashMap<String, Object>();
+ map.put("NULL", null);
+ }
+
+ private Importer(Map<String, Object> map) {
+ this.map = map;
+ }
+
+ /**
+ * Read some data into this {@link Importer}: it can be the full serialised
+ * content, or a number of lines of it (any given line <b>MUST</b> be
+ * complete though) and accumulate it with the already present data.
+ *
+ * @param in
+ * the data to parse
+ *
+ * @return itself so it can be chained
+ *
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ * @throws IOException
+ * if the content cannot be read (for instance, corrupt data)
+ * @throws NullPointerException
+ * if the stream is empty
+ */
+ public Importer read(InputStream in) throws NoSuchFieldException,
+ NoSuchMethodException, ClassNotFoundException, IOException,
+ NullPointerException {
+
+ NextableInputStream stream = new NextableInputStream(in,
+ new NextableInputStreamStep('\n'));
+
+ try {
+ if (in == null) {
+ throw new NullPointerException("InputStream is null");
+ }
+
+ boolean first = true;
+ while (stream.next()) {
+ if (stream.eof()) {
+ if (first) {
+ throw new NullPointerException(
+ "InputStream empty, normal termination");
+ }
+ return this;
+ }
+ first = false;
+
+ boolean zip = stream.startsWith("ZIP:");
+ boolean b64 = stream.startsWith("B64:");
+
+ if (zip || b64) {
+ stream.skip("XXX:".length());
+
+ InputStream decoded = stream.open();
+ if (zip) {
+ decoded = new GZIPInputStream(decoded);
+ }
+ decoded = new Base64InputStream(decoded, false);
+
+ try {
+ read(decoded);
+ } finally {
+ decoded.close();
+ }
+ } else {
+ processLine(stream);
+ }
+ }
+ } finally {
+ stream.close(false);
+ }
+
+ return this;
+ }
+
+ /**
+ * Read a single (whole) line of serialised data into this {@link Importer}
+ * and accumulate it with the already present data.
+ *
+ * @param in
+ * the line to parse
+ *
+ * @return TRUE if we are just done with one object or sub-object
+ *
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ * @throws IOException
+ * if the content cannot be read (for instance, corrupt data)
+ */
+ private boolean processLine(BufferedInputStream in)
+ throws NoSuchFieldException, NoSuchMethodException,
+ ClassNotFoundException, IOException {
+
+ // Defer to latest child if any
+ if (child != null) {
+ if (child.processLine(in)) {
+ if (currentFieldName != null) {
+ setField(currentFieldName, child.getValue());
+ currentFieldName = null;
+ }
+ child = null;
+ }
+
+ return false;
+ }
+
+ // Start/Stop object
+ if (in.is("{")) { // START: new child if needed
+ if (link != null) {
+ child = new Importer(map);
+ }
+ in.end();
+ return false;
+ } else if (in.is("}")) { // STOP: report self to parent
+ in.end();
+ return true;
+ }
+
+ // Custom objects
+ if (CustomSerializer.isCustom(in)) {
+ // not a field value but a direct value
+ me = SerialUtils.decode(in);
+ return false;
+ }
+
+ // REF: (object)
+ if (in.startsWith("REF ")) { // REF: create/link self
+ // here, line is REF type@999:xxx
+ // xxx is optional
+
+ NextableInputStream stream = new NextableInputStream(in,
+ new NextableInputStreamStep(':'));
+ try {
+ stream.next();
+
+ stream.skip("REF ".length());
+ String header = IOUtils.readSmallStream(stream);
+
+ String[] tab = header.split("@");
+ if (tab.length != 2) {
+ throw new IOException("Bad import header line: " + header);
+ }
+ String type = tab[0];
+ String ref = tab[1];
+
+ stream.nextAll();
+
+ link = map.containsKey(ref);
+ if (link) {
+ me = map.get(ref);
+ stream.end();
+ } else {
+ if (stream.eof()) {
+ // construct
+ me = SerialUtils.createObject(type);
+ } else {
+ // direct value
+ me = SerialUtils.decode(stream);
+ }
+ map.put(ref, me);
+ }
+ } finally {
+ stream.close(false);
+ }
+
+ return false;
+ }
+
+ if (SerialUtils.isDirectValue(in)) {
+ // not a field value but a direct value
+ me = SerialUtils.decode(in);
+ return false;
+ }
+
+ if (in.startsWith("^")) {
+ in.skip(1);
+
+ NextableInputStream nameThenContent = new NextableInputStream(in,
+ new NextableInputStreamStep(':'));
+
+ try {
+ nameThenContent.next();
+ String fieldName = IOUtils.readSmallStream(nameThenContent);
+
+ if (nameThenContent.nextAll() && !nameThenContent.eof()) {
+ // field value is direct or custom
+ Object value = null;
+ value = SerialUtils.decode(nameThenContent);
+
+ // To support simple types directly:
+ if (me == null) {
+ me = value;
+ } else {
+ setField(fieldName, value);
+ }
+ } else {
+ // field value is compound
+ currentFieldName = fieldName;
+ }
+ } finally {
+ nameThenContent.close(false);
+ }
+
+ return false;
+ }
+
+ String line = IOUtils.readSmallStream(in);
+ throw new IOException("Line cannot be processed: <" + line + ">");
+ }
+
+ private void setField(String name, Object value)
+ throws NoSuchFieldException {
+
+ 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()));
+ } catch (Exception e) {
+ throw new NoSuchFieldException(String.format(
+ "Internal error when setting \"%s.%s\": %s", me.getClass()
+ .getCanonicalName(), name, e.getMessage()));
+ }
+ }
+
+ /**
+ * Return the current deserialised value.
+ *
+ * @return the current value
+ */
+ public Object getValue() {
+ return me;
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.NotSerializableException;
+import java.io.OutputStream;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UnknownFormatConversionException;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.streams.Base64InputStream;
+import be.nikiroo.utils.streams.Base64OutputStream;
+import be.nikiroo.utils.streams.BufferedInputStream;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+
+/**
+ * Small class to help with serialisation.
+ * <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.
+ * <p>
+ * It is possible to add support to custom types (both the encoder and the
+ * decoder will require the custom classes) -- see {@link CustomSerializer}.
+ * <p>
+ * Default supported types are:
+ * <ul>
+ * <li>NULL (as a null value)</li>
+ * <li>String</li>
+ * <li>Boolean</li>
+ * <li>Byte</li>
+ * <li>Character</li>
+ * <li>Short</li>
+ * <li>Long</li>
+ * <li>Float</li>
+ * <li>Double</li>
+ * <li>Integer</li>
+ * <li>Enum (any enum whose name and value is known by the caller)</li>
+ * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
+ * <li>An array of the above (as a {@link CustomSerializer})</li>
+ * <li>URL</li>
+ * </ul>
+ *
+ * @author niki
+ */
+public class SerialUtils {
+ private static Map<String, CustomSerializer> customTypes;
+
+ static {
+ customTypes = new HashMap<String, CustomSerializer>();
+
+ // Array types:
+ customTypes.put("[]", new CustomSerializer() {
+ @Override
+ protected void toStream(OutputStream out, Object value)
+ throws IOException {
+
+ String type = value.getClass().getCanonicalName();
+ type = type.substring(0, type.length() - 2); // remove the []
+
+ write(out, type);
+ try {
+ for (int i = 0; true; i++) {
+ Object item = Array.get(value, i);
+
+ // encode it normally if direct value
+ write(out, "\r");
+ if (!SerialUtils.encode(out, item)) {
+ try {
+ write(out, "B64:");
+ OutputStream out64 = new Base64OutputStream(
+ out, true);
+ new Exporter(out64).append(item);
+ out64.flush();
+ } catch (NotSerializableException e) {
+ throw new UnknownFormatConversionException(e
+ .getMessage());
+ }
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Done.
+ }
+ }
+
+ @Override
+ protected Object fromStream(InputStream in) throws IOException {
+ NextableInputStream stream = new NextableInputStream(in,
+ new NextableInputStreamStep('\r'));
+
+ try {
+ List<Object> list = new ArrayList<Object>();
+ stream.next();
+ String type = IOUtils.readSmallStream(stream);
+
+ while (stream.next()) {
+ Object value = new Importer().read(stream).getValue();
+ list.add(value);
+ }
+
+ Object array = Array.newInstance(
+ SerialUtils.getClass(type), list.size());
+ for (int i = 0; i < list.size(); i++) {
+ Array.set(array, i, list.get(i));
+ }
+
+ return array;
+ } catch (Exception e) {
+ if (e instanceof IOException) {
+ throw (IOException) e;
+ }
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ @Override
+ protected String getType() {
+ return "[]";
+ }
+ });
+
+ // URL:
+ customTypes.put("java.net.URL", new CustomSerializer() {
+ @Override
+ protected void toStream(OutputStream out, Object value)
+ throws IOException {
+ String val = "";
+ if (value != null) {
+ val = ((URL) value).toString();
+ }
+
+ out.write(StringUtils.getBytes(val));
+ }
+
+ @Override
+ protected Object fromStream(InputStream in) throws IOException {
+ String val = IOUtils.readSmallStream(in);
+ if (!val.isEmpty()) {
+ return new URL(val);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected String getType() {
+ return "java.net.URL";
+ }
+ });
+
+ // Images (this is currently the only supported image type by default)
+ customTypes.put("be.nikiroo.utils.Image", new CustomSerializer() {
+ @Override
+ protected void toStream(OutputStream out, Object value)
+ throws IOException {
+ Image img = (Image) value;
+ OutputStream encoded = new Base64OutputStream(out, true);
+ try {
+ InputStream in = img.newInputStream();
+ try {
+ IOUtils.write(in, encoded);
+ } finally {
+ in.close();
+ }
+ } finally {
+ encoded.flush();
+ // Cannot close!
+ }
+ }
+
+ @Override
+ protected String getType() {
+ return "be.nikiroo.utils.Image";
+ }
+
+ @Override
+ protected Object fromStream(InputStream in) throws IOException {
+ try {
+ // Cannot close it!
+ InputStream decoded = new Base64InputStream(in, false);
+ return new Image(decoded);
+ } catch (IOException e) {
+ throw new UnknownFormatConversionException(e.getMessage());
+ }
+ }
+ });
+ }
+
+ /**
+ * Create an empty object of the given type.
+ *
+ * @param type
+ * the object type (its class name)
+ *
+ * @return the new object
+ *
+ * @throws ClassNotFoundException
+ * if the class cannot be found
+ * @throws NoSuchMethodException
+ * if the given class is not compatible with this code
+ */
+ public static Object createObject(String type)
+ throws ClassNotFoundException, NoSuchMethodException {
+
+ String desc = null;
+ try {
+ Class<?> clazz = getClass(type);
+ String className = clazz.getName();
+ List<Object> args = new ArrayList<Object>();
+ List<Class<?>> classes = new ArrayList<Class<?>>();
+ Constructor<?> ctor = null;
+ if (className.contains("$")) {
+ for (String parentName = className.substring(0,
+ className.lastIndexOf('$'));; parentName = parentName
+ .substring(0, parentName.lastIndexOf('$'))) {
+ Object parent = createObject(parentName);
+ args.add(parent);
+ classes.add(parent.getClass());
+
+ if (!parentName.contains("$")) {
+ break;
+ }
+ }
+
+ // Better error description in case there is no empty
+ // constructor:
+ desc = "";
+ String end = "";
+ for (Class<?> parent = clazz; parent != null
+ && !parent.equals(Object.class); parent = parent
+ .getSuperclass()) {
+ if (!desc.isEmpty()) {
+ desc += " [:";
+ end += "]";
+ }
+ desc += parent;
+ }
+ desc += end;
+ //
+
+ try {
+ ctor = clazz.getDeclaredConstructor(classes
+ .toArray(new Class[] {}));
+ } catch (NoSuchMethodException nsme) {
+ // TODO: it seems we do not always need a parameter for each
+ // level, so we currently try "ALL" levels or "FIRST" level
+ // only -> we should check the actual rule and use it
+ ctor = clazz.getDeclaredConstructor(classes.get(0));
+ Object firstParent = args.get(0);
+ args.clear();
+ args.add(firstParent);
+ }
+ desc = null;
+ } else {
+ ctor = clazz.getDeclaredConstructor();
+ }
+
+ ctor.setAccessible(true);
+ return ctor.newInstance(args.toArray());
+ } catch (ClassNotFoundException e) {
+ throw e;
+ } catch (NoSuchMethodException e) {
+ if (desc != null) {
+ throw new NoSuchMethodException("Empty constructor not found: "
+ + desc);
+ }
+ throw e;
+ } catch (Exception e) {
+ throw new NoSuchMethodException("Cannot instantiate: " + type);
+ }
+ }
+
+ /**
+ * Insert a custom serialiser that will take precedence over the default one
+ * or the target class.
+ *
+ * @param serializer
+ * the custom serialiser
+ */
+ static public void addCustomSerializer(CustomSerializer serializer) {
+ customTypes.put(serializer.getType(), serializer);
+ }
+
+ /**
+ * Serialise the given object into this {@link OutputStream}.
+ * <p>
+ * <b>Important: </b>If the operation fails (with a
+ * {@link NotSerializableException}), the {@link StringBuilder} will be
+ * corrupted (will contain bad, most probably not importable data).
+ *
+ * @param out
+ * the output {@link OutputStream} to serialise to
+ * @param o
+ * the object to serialise
+ * @param map
+ * the map of already serialised objects (if the given object or
+ * one of its descendant is already present in it, only an ID
+ * will be serialised)
+ *
+ * @throws NotSerializableException
+ * if the object cannot be serialised (in this case, the
+ * {@link StringBuilder} can contain bad, most probably not
+ * importable data)
+ * @throws IOException
+ * in case of I/O errors
+ */
+ static void append(OutputStream out, Object o, Map<Integer, Object> map)
+ throws NotSerializableException, IOException {
+
+ 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) {
+ // Anonymous inner classes support
+ type = o.getClass().getName();
+ }
+ id = Integer.toString(hash);
+ if (map.containsKey(hash)) {
+ fields = new Field[] {};
+ } else {
+ map.put(hash, o);
+ }
+ }
+
+ write(out, "{\nREF ");
+ write(out, type);
+ write(out, "@");
+ write(out, id);
+ write(out, ":");
+
+ if (!encode(out, o)) { // check if direct value
+ try {
+ for (Field field : fields) {
+ field.setAccessible(true);
+
+ if (field.getName().startsWith("this$")
+ || field.isSynthetic()
+ || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
+ // Do not keep this links of nested classes
+ // Do not keep synthetic fields
+ // Do not keep final fields
+ continue;
+ }
+
+ write(out, "\n^");
+ write(out, field.getName());
+ write(out, ":");
+
+ Object value = field.get(o);
+
+ if (!encode(out, value)) {
+ write(out, "\n");
+ append(out, value, map);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace(); // should not happen (see
+ // setAccessible)
+ } catch (IllegalAccessException e) {
+ e.printStackTrace(); // should not happen (see
+ // setAccessible)
+ }
+
+ write(out, "\n}");
+ }
+ }
+
+ /**
+ * Encode the object into the given {@link OutputStream} if possible and if
+ * supported.
+ * <p>
+ * A supported object in this context means an object we can directly
+ * encode, like an Integer or a String. Custom objects and arrays are also
+ * considered supported, but <b>compound objects are not supported here</b>.
+ * <p>
+ * For compound objects, you should use {@link Exporter}.
+ *
+ * @param out
+ * the {@link OutputStream} to append to
+ * @param value
+ * the object to encode (can be NULL, which will be encoded)
+ *
+ * @return TRUE if success, FALSE if not (the content of the
+ * {@link OutputStream} won't be changed in case of failure)
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ static boolean encode(OutputStream out, Object value) throws IOException {
+ if (value == null) {
+ write(out, "NULL");
+ } else if (value.getClass().getSimpleName().endsWith("[]")) {
+ // Simple name does support [] suffix and do not return NULL for
+ // inner anonymous classes
+ customTypes.get("[]").encode(out, value);
+ } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
+ customTypes.get(value.getClass().getCanonicalName())//
+ .encode(out, value);
+ } else if (value instanceof String) {
+ encodeString(out, (String) value);
+ } else if (value instanceof Boolean) {
+ write(out, value);
+ } else if (value instanceof Byte) {
+ write(out, "b");
+ write(out, value);
+ } else if (value instanceof Character) {
+ write(out, "c");
+ encodeString(out, "" + value);
+ } else if (value instanceof Short) {
+ write(out, "s");
+ write(out, value);
+ } else if (value instanceof Integer) {
+ write(out, "i");
+ write(out, value);
+ } else if (value instanceof Long) {
+ write(out, "l");
+ write(out, value);
+ } else if (value instanceof Float) {
+ write(out, "f");
+ write(out, value);
+ } else if (value instanceof Double) {
+ write(out, "d");
+ write(out, value);
+ } else if (value instanceof Enum) {
+ write(out, "E:");
+ String type = value.getClass().getCanonicalName();
+ write(out, type);
+ write(out, ".");
+ write(out, ((Enum<?>) value).name());
+ write(out, ";");
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ static boolean isDirectValue(BufferedInputStream encodedValue)
+ throws IOException {
+ if (CustomSerializer.isCustom(encodedValue)) {
+ return false;
+ }
+
+ for (String fullValue : new String[] { "NULL", "null", "true", "false" }) {
+ if (encodedValue.is(fullValue)) {
+ return true;
+ }
+ }
+
+ for (String prefix : new String[] { "c\"", "\"", "b", "s", "i", "l",
+ "f", "d", "E:" }) {
+ if (encodedValue.startsWith(prefix)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Decode the data into an equivalent supported source object.
+ * <p>
+ * A supported object in this context means an object we can directly
+ * encode, like an Integer or a String (see
+ * {@link SerialUtils#decode(String)}.
+ * <p>
+ * Custom objects and arrays are also considered supported here, but
+ * <b>compound objects are not</b>.
+ * <p>
+ * For compound objects, you should use {@link Importer}.
+ *
+ * @param encodedValue
+ * the encoded data, cannot be NULL
+ *
+ * @return the object (can be NULL for NULL encoded values)
+ *
+ * @throws IOException
+ * if the content cannot be converted
+ */
+ static Object decode(BufferedInputStream encodedValue) throws IOException {
+ if (CustomSerializer.isCustom(encodedValue)) {
+ // custom^TYPE^ENCODED_VALUE
+ NextableInputStream content = new NextableInputStream(encodedValue,
+ new NextableInputStreamStep('^'));
+ try {
+ content.next();
+ @SuppressWarnings("unused")
+ String custom = IOUtils.readSmallStream(content);
+ content.next();
+ String type = IOUtils.readSmallStream(content);
+ content.nextAll();
+ if (customTypes.containsKey(type)) {
+ return customTypes.get(type).decode(content);
+ }
+ content.end();
+ throw new IOException("Unknown custom type: " + type);
+ } finally {
+ content.close(false);
+ encodedValue.end();
+ }
+ }
+
+ String encodedString = IOUtils.readSmallStream(encodedValue);
+ return decode(encodedString);
+ }
+
+ /**
+ * Decode the data into an equivalent supported source object.
+ * <p>
+ * A supported object in this context means an object we can directly
+ * encode, like an Integer or a String.
+ * <p>
+ * For custom objects and arrays, you should use
+ * {@link SerialUtils#decode(InputStream)} or directly {@link Importer}.
+ * <p>
+ * For compound objects, you should use {@link Importer}.
+ *
+ * @param encodedValue
+ * the encoded data, cannot be NULL
+ *
+ * @return the object (can be NULL for NULL encoded values)
+ *
+ * @throws IOException
+ * if the content cannot be converted
+ */
+ static Object decode(String encodedValue) throws IOException {
+ try {
+ String cut = "";
+ if (encodedValue.length() > 1) {
+ cut = encodedValue.substring(1);
+ }
+
+ if (encodedValue.equals("NULL") || encodedValue.equals("null")) {
+ return null;
+ } else if (encodedValue.startsWith("\"")) {
+ return decodeString(encodedValue);
+ } else if (encodedValue.equals("true")) {
+ return true;
+ } else if (encodedValue.equals("false")) {
+ return false;
+ } else if (encodedValue.startsWith("b")) {
+ return Byte.parseByte(cut);
+ } else if (encodedValue.startsWith("c")) {
+ return decodeString(cut).charAt(0);
+ } else if (encodedValue.startsWith("s")) {
+ return Short.parseShort(cut);
+ } else if (encodedValue.startsWith("l")) {
+ return Long.parseLong(cut);
+ } else if (encodedValue.startsWith("f")) {
+ return Float.parseFloat(cut);
+ } else if (encodedValue.startsWith("d")) {
+ return Double.parseDouble(cut);
+ } else if (encodedValue.startsWith("i")) {
+ return Integer.parseInt(cut);
+ } else if (encodedValue.startsWith("E:")) {
+ cut = cut.substring(1);
+ return decodeEnum(cut);
+ } else {
+ throw new IOException("Unrecognized value: " + encodedValue);
+ }
+ } catch (Exception e) {
+ if (e instanceof IOException) {
+ throw (IOException) e;
+ }
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Write the given {@link String} into the given {@link OutputStream} in
+ * UTF-8.
+ *
+ * @param out
+ * the {@link OutputStream}
+ * @param data
+ * the data to write, cannot be NULL
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ static void write(OutputStream out, Object data) throws IOException {
+ out.write(StringUtils.getBytes(data.toString()));
+ }
+
+ /**
+ * Return the corresponding class or throw an {@link Exception} if it
+ * cannot.
+ *
+ * @param type
+ * the class name to look for
+ *
+ * @return the class (will never be NULL)
+ *
+ * @throws ClassNotFoundException
+ * if the class cannot be found
+ * @throws NoSuchMethodException
+ * if the class cannot be created (usually because it or its
+ * enclosing class doesn't have an empty constructor)
+ */
+ static 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?)");
+ }
+ }
+ }
+
+ if (clazz == null) {
+ throw new ClassNotFoundException("Class not found: " + type);
+ }
+
+ return clazz;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ static private Enum<?> decodeEnum(String escaped) {
+ // escaped: be.xxx.EnumType.VALUE;
+ int pos = escaped.lastIndexOf(".");
+ String type = escaped.substring(0, pos);
+ String name = escaped.substring(pos + 1, escaped.length() - 1);
+
+ try {
+ return Enum.valueOf((Class<Enum>) getClass(type), name);
+ } catch (Exception e) {
+ throw new UnknownFormatConversionException("Unknown enum: <" + type
+ + "> " + name);
+ }
+ }
+
+ // aa bb -> "aa\tbb"
+ static void encodeString(OutputStream out, String raw) throws IOException {
+ // TODO: not. efficient.
+ out.write('\"');
+ for (char car : raw.toCharArray()) {
+ encodeString(out, car);
+ }
+ out.write('\"');
+ }
+
+ // for encoding string, NOT to encode a char by itself!
+ static void encodeString(OutputStream out, char raw) throws IOException {
+ switch (raw) {
+ case '\\':
+ out.write('\\');
+ out.write('\\');
+ break;
+ case '\r':
+ out.write('\\');
+ out.write('r');
+ break;
+ case '\n':
+ out.write('\\');
+ out.write('n');
+ break;
+ case '"':
+ out.write('\\');
+ out.write('\"');
+ break;
+ default:
+ out.write(raw);
+ break;
+ }
+ }
+
+ // "aa\tbb" -> aa bb
+ 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);
+ }
+}
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+import javax.net.ssl.SSLException;
+
+import be.nikiroo.utils.CryptUtils;
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.serial.Exporter;
+import be.nikiroo.utils.serial.Importer;
+import be.nikiroo.utils.streams.BufferedOutputStream;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.streams.ReplaceOutputStream;
+
+/**
+ * Base class used for the client/server basic handling.
+ * <p>
+ * It represents a single action: a client is expected to only execute one
+ * action, while a server is expected to execute one action for each client
+ * action.
+ *
+ * @author niki
+ */
+abstract class ConnectAction {
+ // We separate each "packet" we send with this character and make sure it
+ // does not occurs in the message itself.
+ static private char STREAM_SEP = '\b';
+ static private String[] STREAM_RAW = new String[] { "\\", "\b" };
+ static private String[] STREAM_CODED = new String[] { "\\\\", "\\b" };
+
+ private Socket s;
+ private boolean server;
+
+ private Version clientVersion;
+ private Version serverVersion;
+
+ private CryptUtils crypt;
+
+ private Object lock = new Object();
+ private NextableInputStream in;
+ private BufferedOutputStream out;
+ private boolean contentToSend;
+
+ /**
+ * Method that will be called when an action is performed on either the
+ * client or server this {@link ConnectAction} represent.
+ *
+ * @param version
+ * the version on the other side of the communication (client or
+ * server)
+ *
+ * @throws Exception
+ * in case of I/O error
+ */
+ abstract protected void action(Version version) throws Exception;
+
+ /**
+ * Method called when we negotiate the version with the client.
+ * <p>
+ * Thus, it is only called on the server.
+ * <p>
+ * Will return the actual server version by default.
+ *
+ * @param clientVersion
+ * the client version
+ *
+ * @return the version to send to the client
+ */
+ abstract protected Version negotiateVersion(Version clientVersion);
+
+ /**
+ * Handler called when an unexpected error occurs in the code.
+ *
+ * @param e
+ * the exception that occurred, SSLException usually denotes a
+ * crypt error
+ */
+ abstract protected void onError(Exception e);
+
+ /**
+ * Create a new {@link ConnectAction}.
+ *
+ * @param s
+ * the socket to bind to
+ * @param server
+ * TRUE for a server action, FALSE for a client action (will
+ * impact the process)
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param version
+ * the client-or-server version (depending upon the boolean
+ * parameter <tt>server</tt>)
+ */
+ protected ConnectAction(Socket s, boolean server, String key,
+ Version version) {
+ this.s = s;
+ this.server = server;
+ if (key != null) {
+ crypt = new CryptUtils(key);
+ }
+
+ if (version == null) {
+ version = new Version();
+ }
+
+ if (server) {
+ serverVersion = version;
+ } else {
+ clientVersion = version;
+ }
+ }
+
+ /**
+ * The version of this client-or-server.
+ *
+ * @return the version
+ */
+ public Version getVersion() {
+ if (server) {
+ return serverVersion;
+ }
+
+ return clientVersion;
+ }
+
+ /**
+ * The total amount of bytes received.
+ *
+ * @return the amount of bytes received
+ */
+ public long getBytesReceived() {
+ return in.getBytesRead();
+ }
+
+ /**
+ * The total amount of bytes sent.
+ *
+ * @return the amount of bytes sent
+ */
+ public long getBytesWritten() {
+ return out.getBytesWritten();
+ }
+
+ /**
+ * Actually start the process (this is synchronous).
+ */
+ public void connect() {
+ try {
+ in = new NextableInputStream(s.getInputStream(),
+ new NextableInputStreamStep(STREAM_SEP));
+ try {
+ out = new BufferedOutputStream(s.getOutputStream());
+ try {
+ // Negotiate version
+ Version version;
+ if (server) {
+ String HELLO = recString();
+ if (HELLO == null || !HELLO.startsWith("VERSION ")) {
+ throw new SSLException(
+ "Client used bad encryption key");
+ }
+ version = negotiateVersion(new Version(
+ HELLO.substring("VERSION ".length())));
+ sendString("VERSION " + version);
+ } else {
+ String HELLO = sendString("VERSION " + clientVersion);
+ if (HELLO == null || !HELLO.startsWith("VERSION ")) {
+ throw new SSLException(
+ "Server did not accept the encryption key");
+ }
+ version = new Version(HELLO.substring("VERSION "
+ .length()));
+ }
+
+ // Actual code
+ action(version);
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+ } catch (Exception e) {
+ onError(e);
+ } finally {
+ try {
+ s.close();
+ } catch (Exception e) {
+ onError(e);
+ }
+ }
+ }
+
+ /**
+ * Serialise and send the given object to the counter part (and, only for
+ * client, return the deserialised answer -- the server will always receive
+ * NULL).
+ *
+ * @param data
+ * the data to send
+ *
+ * @return the answer (which can be NULL if no answer, or NULL for an answer
+ * which is NULL) if this action is a client, always NULL if it is a
+ * server
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ */
+ protected Object sendObject(Object data) throws IOException,
+ NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
+ return send(out, data, false);
+ }
+
+ /**
+ * Reserved for the server: flush the data to the client and retrieve its
+ * answer.
+ * <p>
+ * Also used internally for the client (only do something if there is
+ * contentToSend).
+ * <p>
+ * Will only flush the data if there is contentToSend.
+ *
+ * @return the deserialised answer (which can actually be NULL)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ * @throws java.lang.NullPointerException
+ * if the counter part has no data to send
+ */
+ protected Object recObject() throws IOException, NoSuchFieldException,
+ NoSuchMethodException, ClassNotFoundException,
+ java.lang.NullPointerException {
+ return rec(false);
+ }
+
+ /**
+ * Send the given string to the counter part (and, only for client, return
+ * the answer -- the server will always receive NULL).
+ *
+ * @param line
+ * the data to send (we will add a line feed)
+ *
+ * @return the answer if this action is a client (without the added line
+ * feed), NULL if it is a server
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws SSLException
+ * in case of crypt error
+ */
+ protected String sendString(String line) throws IOException {
+ try {
+ return (String) send(out, line, true);
+ } catch (NoSuchFieldException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (ClassNotFoundException e) {
+ // Cannot happen
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * Reserved for the server (externally): flush the data to the client and
+ * retrieve its answer.
+ * <p>
+ * Also used internally for the client (only do something if there is
+ * contentToSend).
+ * <p>
+ * Will only flush the data if there is contentToSend.
+ *
+ * @return the answer (which can be NULL if no more content)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws SSLException
+ * in case of crypt error
+ */
+ protected String recString() throws IOException {
+ try {
+ return (String) rec(true);
+ } catch (NoSuchFieldException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (ClassNotFoundException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (NullPointerException e) {
+ // Should happen
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * Serialise and send the given object to the counter part (and, only for
+ * client, return the deserialised answer -- the server will always receive
+ * NULL).
+ *
+ * @param out
+ * the stream to write to
+ * @param data
+ * the data to write
+ * @param asString
+ * TRUE to write it as a String, FALSE to write it as an Object
+ *
+ * @return the answer (which can be NULL if no answer, or NULL for an answer
+ * which is NULL) if this action is a client, always NULL if it is a
+ * server
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws SSLException
+ * in case of crypt error
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ */
+ private Object send(BufferedOutputStream out, Object data, boolean asString)
+ throws IOException, NoSuchFieldException, NoSuchMethodException,
+ ClassNotFoundException, java.lang.NullPointerException {
+
+ synchronized (lock) {
+ OutputStream sub;
+ if (crypt != null) {
+ sub = crypt.encrypt64(out.open());
+ } else {
+ sub = out.open();
+ }
+
+ sub = new ReplaceOutputStream(sub, STREAM_RAW, STREAM_CODED);
+ try {
+ if (asString) {
+ sub.write(StringUtils.getBytes(data.toString()));
+ } else {
+ new Exporter(sub).append(data);
+ }
+ } finally {
+ sub.close();
+ }
+
+ out.write(STREAM_SEP);
+
+ if (server) {
+ out.flush();
+ return null;
+ }
+
+ contentToSend = true;
+ try {
+ return rec(asString);
+ } catch (NullPointerException e) {
+ // We accept no data here for Objects
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Reserved for the server: flush the data to the client and retrieve its
+ * answer.
+ * <p>
+ * Also used internally for the client (only do something if there is
+ * contentToSend).
+ * <p>
+ * Will only flush the data if there is contentToSend.
+ * <p>
+ * Note that the behaviour is slightly different for String and Object
+ * reading regarding exceptions:
+ * <ul>
+ * <li>NULL means that the counter part has no more data to send</li>
+ * <li>All the exceptions except {@link IOException} are there for Object
+ * conversion</li>
+ * </ul>
+ *
+ * @param asString
+ * TRUE for String reading, FALSE for Object reading (which can
+ * still be a String)
+ *
+ * @return the deserialised answer (which can actually be NULL)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ * @throws java.lang.NullPointerException
+ * for Objects only: if the counter part has no data to send
+ */
+ @SuppressWarnings("resource")
+ private Object rec(boolean asString) throws IOException,
+ NoSuchFieldException, NoSuchMethodException,
+ ClassNotFoundException, java.lang.NullPointerException {
+
+ synchronized (lock) {
+ if (server || contentToSend) {
+ if (contentToSend) {
+ out.flush();
+ contentToSend = false;
+ }
+
+ if (in.next() && !in.eof()) {
+ InputStream read = new ReplaceInputStream(in.open(),
+ STREAM_CODED, STREAM_RAW);
+ try {
+ if (crypt != null) {
+ read = crypt.decrypt64(read);
+ }
+
+ if (asString) {
+ return IOUtils.readSmallStream(read);
+ }
+
+ return new Importer().read(read).getValue();
+ } finally {
+ read.close();
+ }
+ }
+
+ if (!asString) {
+ throw new NullPointerException();
+ }
+ }
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import be.nikiroo.utils.Version;
+
+/**
+ * Base class used for the client basic handling.
+ * <p>
+ * It represents a single action: a client is expected to only execute one
+ * action.
+ *
+ * @author niki
+ */
+abstract class ConnectActionClient {
+ /**
+ * The underlying {@link ConnectAction}.
+ * <p>
+ * Cannot be NULL.
+ */
+ protected ConnectAction action;
+
+ /**
+ * Create a new {@link ConnectActionClient}, using the current version of
+ * the program.
+ *
+ * @param host
+ * the host to bind to
+ * @param port
+ * the port to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the host is not known
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ConnectActionClient(String host, int port, String key)
+ throws IOException {
+ this(host, port, key, Version.getCurrentVersion());
+ }
+
+ /**
+ * Create a new {@link ConnectActionClient}.
+ *
+ * @param host
+ * the host to bind to
+ * @param port
+ * the port to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param clientVersion
+ * the client version
+ *
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the host is not known
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ConnectActionClient(String host, int port, String key,
+ Version clientVersion) throws IOException {
+ this(new Socket(host, port), key, clientVersion);
+ }
+
+ /**
+ * Create a new {@link ConnectActionClient}, using the current version of
+ * the program.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ */
+ public ConnectActionClient(Socket s, String key) {
+ this(s, key, Version.getCurrentVersion());
+ }
+
+ /**
+ * Create a new {@link ConnectActionClient}.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param clientVersion
+ * the client version
+ */
+ public ConnectActionClient(Socket s, String key, Version clientVersion) {
+ action = new ConnectAction(s, false, key, clientVersion) {
+ @Override
+ protected void action(Version serverVersion) throws Exception {
+ ConnectActionClient.this.action(serverVersion);
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ ConnectActionClient.this.onError(e);
+ }
+
+ @Override
+ protected Version negotiateVersion(Version clientVersion) {
+ new Exception("Should never be called on a client")
+ .printStackTrace();
+ return null;
+ }
+ };
+ }
+
+ /**
+ * Actually start the process and call the action (synchronous).
+ */
+ public void connect() {
+ action.connect();
+ }
+
+ /**
+ * Actually start the process and call the action (asynchronous).
+ */
+ public void connectAsync() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ connect();
+ }
+ }).start();
+ }
+
+ /**
+ * Method that will be called when an action is performed on the client.
+ *
+ * @param serverVersion
+ * the version of the server connected to this client
+ *
+ * @throws Exception
+ * in case of I/O error
+ */
+ @SuppressWarnings("unused")
+ public void action(Version serverVersion) throws Exception {
+ }
+
+ /**
+ * Handler called when an unexpected error occurs in the code.
+ * <p>
+ * Will just ignore the error by default.
+ *
+ * @param e
+ * the exception that occurred
+ */
+ protected void onError(@SuppressWarnings("unused") Exception e) {
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import be.nikiroo.utils.Version;
+
+/**
+ * Class used for the client basic handling.
+ * <p>
+ * It represents a single action: a client is expected to only execute one
+ * action.
+ *
+ * @author niki
+ */
+public class ConnectActionClientObject extends ConnectActionClient {
+ /**
+ * Create a new {@link ConnectActionClientObject} .
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ */
+ public ConnectActionClientObject(Socket s, String key) {
+ super(s, key);
+ }
+
+ /**
+ * Create a new {@link ConnectActionClientObject} .
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param clientVersion
+ * the version of the client
+ */
+ public ConnectActionClientObject(Socket s, String key, Version clientVersion) {
+ super(s, key, clientVersion);
+ }
+
+ /**
+ * Create a new {@link ConnectActionClientObject}.
+ *
+ * @param host
+ * the host to bind to
+ * @param port
+ * the port to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ConnectActionClientObject(String host, int port, String key)
+ throws IOException {
+ super(host, port, key);
+ }
+
+ /**
+ * Create a new {@link ConnectActionClientObject}.
+ *
+ * @param host
+ * the host to bind to
+ * @param port
+ * the port to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param clientVersion
+ * the version of the client
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ConnectActionClientObject(String host, int port, String key,
+ Version clientVersion) throws IOException {
+ super(host, port, key, clientVersion);
+ }
+
+ /**
+ * Serialise and send the given object to the server (and return the
+ * deserialised answer).
+ *
+ * @param data
+ * the data to send
+ *
+ * @return the answer, which can be NULL
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ */
+ public Object send(Object data) throws IOException, NoSuchFieldException,
+ NoSuchMethodException, ClassNotFoundException {
+ return action.sendObject(data);
+ }
+
+ // Deprecated //
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ConnectActionClientObject(String host, int port, boolean ssl)
+ throws IOException {
+ this(host, port, ssl ? "" : null);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ConnectActionClientObject(String host, int port, boolean ssl,
+ Version version) throws IOException {
+ this(host, port, ssl ? "" : null, version);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ public ConnectActionClientObject(Socket s, boolean ssl) throws IOException {
+ this(s, ssl ? "" : null);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ public ConnectActionClientObject(Socket s, boolean ssl, Version version)
+ throws IOException {
+ this(s, ssl ? "" : null, version);
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import be.nikiroo.utils.Version;
+
+/**
+ * Class used for the client basic handling.
+ * <p>
+ * It represents a single action: a client is expected to only execute one
+ * action.
+ *
+ * @author niki
+ */
+public class ConnectActionClientString extends ConnectActionClient {
+ /**
+ * Create a new {@link ConnectActionClientString}.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ */
+ public ConnectActionClientString(Socket s, String key) {
+ super(s, key);
+ }
+
+ /**
+ * Create a new {@link ConnectActionClientString}.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param clientVersion
+ * the version of this client
+ */
+ public ConnectActionClientString(Socket s, String key, Version clientVersion) {
+ super(s, key, clientVersion);
+ }
+
+ /**
+ * Create a new {@link ConnectActionClientString}.
+ *
+ * @param host
+ * the host to bind to
+ * @param port
+ * the port to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ConnectActionClientString(String host, int port, String key)
+ throws IOException {
+ super(host, port, key);
+ }
+
+ /**
+ * Create a new {@link ConnectActionClientString}.
+ *
+ * @param host
+ * the host to bind to
+ * @param port
+ * the port to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param clientVersion
+ * the version of this client
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ConnectActionClientString(String host, int port, String key,
+ Version clientVersion) throws IOException {
+ super(host, port, key, clientVersion);
+ }
+
+ /**
+ * Send the given object to the server (and return the answer).
+ *
+ * @param data
+ * the data to send
+ *
+ * @return the answer, which can be NULL
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public String send(String data) throws IOException {
+ return action.sendString(data);
+ }
+
+ // Deprecated //
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ConnectActionClientString(String host, int port, boolean ssl)
+ throws IOException {
+ this(host, port, ssl ? "" : null);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ConnectActionClientString(String host, int port, boolean ssl,
+ Version version) throws IOException {
+ this(host, port, ssl ? "" : null, version);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ public ConnectActionClientString(Socket s, boolean ssl) throws IOException {
+ this(s, ssl ? "" : null);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ public ConnectActionClientString(Socket s, boolean ssl, Version version)
+ throws IOException {
+ this(s, ssl ? "" : null, version);
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.net.Socket;
+
+import be.nikiroo.utils.Version;
+
+/**
+ * Base class used for the server basic handling.
+ * <p>
+ * It represents a single action: a server is expected to execute one action for
+ * each client action.
+ *
+ * @author niki
+ */
+abstract class ConnectActionServer {
+ private boolean closing;
+
+ /**
+ * The underlying {@link ConnectAction}.
+ * <p>
+ * Cannot be NULL.
+ */
+ protected ConnectAction action;
+
+ /**
+ * Create a new {@link ConnectActionServer}, using the current version.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ */
+ public ConnectActionServer(Socket s, String key) {
+ this(s, key, Version.getCurrentVersion());
+ }
+
+ /**
+ * Create a new {@link ConnectActionServer}.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param serverVersion
+ * the version of this server,that will be sent to the client
+ */
+ public ConnectActionServer(Socket s, String key, Version serverVersion) {
+ action = new ConnectAction(s, true, key, serverVersion) {
+ @Override
+ protected void action(Version clientVersion) throws Exception {
+ ConnectActionServer.this.action(clientVersion);
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ ConnectActionServer.this.onError(e);
+ }
+
+ @Override
+ protected Version negotiateVersion(Version clientVersion) {
+ return ConnectActionServer.this.negotiateVersion(clientVersion);
+ }
+ };
+ }
+
+ /**
+ * Actually start the process and call the action (synchronous).
+ */
+ public void connect() {
+ action.connect();
+ }
+
+ /**
+ * Actually start the process and call the action (asynchronous).
+ */
+ public void connectAsync() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ connect();
+ }
+ }).start();
+ }
+
+ /**
+ * Stop the client/server connection on behalf of the server (usually, the
+ * client connects then is allowed to send as many requests as it wants; in
+ * some cases, though, the server may wish to forcefully close the
+ * connection and can do via this value, when it is set to TRUE).
+ * <p>
+ * Example of usage: the client failed an authentication check, cut the
+ * connection here and now.
+ *
+ * @return TRUE when it is
+ */
+ public boolean isClosing() {
+ return closing;
+ }
+
+ /**
+ * Can be called to stop the client/server connection on behalf of the
+ * server (usually, the client connects then is allowed to send as many
+ * requests as it wants; in some cases, though, the server may wish to
+ * forcefully close the connection and can do so by calling this method).
+ * <p>
+ * Example of usage: the client failed an authentication check, cut the
+ * connection here and now.
+ */
+ public void close() {
+ closing = true;
+ }
+
+ /**
+ * The total amount of bytes received.
+ *
+ * @return the amount of bytes received
+ */
+ public long getBytesReceived() {
+ return action.getBytesReceived();
+ }
+
+ /**
+ * The total amount of bytes sent.
+ *
+ * @return the amount of bytes sent
+ */
+ public long getBytesSent() {
+ return action.getBytesWritten();
+ }
+
+ /**
+ * Method that will be called when an action is performed on the server.
+ *
+ * @param clientVersion
+ * the version of the client connected to this server
+ *
+ * @throws Exception
+ * in case of I/O error
+ */
+ @SuppressWarnings("unused")
+ public void action(Version clientVersion) throws Exception {
+ }
+
+ /**
+ * Handler called when an unexpected error occurs in the code.
+ * <p>
+ * Will just ignore the error by default.
+ *
+ * @param e
+ * the exception that occurred
+ */
+ protected void onError(@SuppressWarnings("unused") Exception e) {
+ }
+
+ /**
+ * Method called when we negotiate the version with the client.
+ * <p>
+ * Will return the actual server version by default.
+ *
+ * @param clientVersion
+ * the client version
+ *
+ * @return the version to send to the client
+ */
+ protected Version negotiateVersion(
+ @SuppressWarnings("unused") Version clientVersion) {
+ return action.getVersion();
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * Class used for the server basic handling.
+ * <p>
+ * It represents a single action: a server is expected to execute one action for
+ * each client action.
+ *
+ * @author niki
+ */
+public class ConnectActionServerObject extends ConnectActionServer {
+ /**
+ * Create a new {@link ConnectActionServerObject} as the server version.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ */
+ public ConnectActionServerObject(Socket s, String key) {
+ super(s, key);
+ }
+
+ /**
+ * Serialise and send the given object to the client.
+ *
+ * @param data
+ * the data to send
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ */
+ public void send(Object data) throws IOException, NoSuchFieldException,
+ NoSuchMethodException, ClassNotFoundException {
+ action.sendObject(data);
+ }
+
+ /**
+ * (Flush the data to the client if needed and) retrieve its answer.
+ *
+ * @return the deserialised answer (which can actually be NULL)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ * @throws java.lang.NullPointerException
+ * if the counter part has no data to send
+ */
+ public Object rec() throws NoSuchFieldException, NoSuchMethodException,
+ ClassNotFoundException, IOException, java.lang.NullPointerException {
+ return action.recObject();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * Class used for the server basic handling.
+ * <p>
+ * It represents a single action: a server is expected to execute one action for
+ * each client action.
+ *
+ * @author niki
+ */
+public class ConnectActionServerString extends ConnectActionServer {
+ /**
+ * Create a new {@link ConnectActionServerString} as the server version.
+ *
+ * @param s
+ * the socket to bind to
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ */
+ public ConnectActionServerString(Socket s, String key) {
+ super(s, key);
+ }
+
+ /**
+ * Serialise and send the given object to the client.
+ *
+ * @param data
+ * the data to send
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public void send(String data) throws IOException {
+ action.sendString(data);
+ }
+
+ /**
+ * (Flush the data to the client if needed and) retrieve its answer.
+ *
+ * @return the answer if it is available, or NULL if not
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public String rec() throws IOException {
+ return action.recString();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import be.nikiroo.utils.TraceHandler;
+
+/**
+ * This class implements a simple server that can listen for connections and
+ * send/receive objects.
+ * <p>
+ * Note: this {@link Server} has to be discarded after use (cannot be started
+ * twice).
+ *
+ * @author niki
+ */
+abstract class Server implements Runnable {
+ protected final String key;
+ protected long id = 0;
+
+ private final String name;
+ private final Object lock = new Object();
+ private final Object counterLock = new Object();
+
+ private ServerSocket ss;
+ private int port;
+
+ private boolean started;
+ private boolean exiting = false;
+ private int counter;
+
+ private long bytesReceived;
+ private long bytesSent;
+
+ private TraceHandler tracer = new TraceHandler();
+
+ /**
+ * Create a new {@link ConnectActionServer} to handle a request.
+ *
+ * @param s
+ * the socket to service
+ *
+ * @return the action
+ */
+ abstract ConnectActionServer createConnectActionServer(Socket s);
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link Server#start()} is called.
+ *
+ * @param port
+ * the port to listen on, or 0 to assign any unallocated port
+ * found (which can later on be queried via
+ * {@link Server#getPort()}
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public Server(int port, String key) throws IOException {
+ this((String) null, port, key);
+ }
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link Server#start()} is called.
+ * <p>
+ * All the communications will happen in plain text.
+ *
+ * @param name
+ * the server name (only used for debug info and traces)
+ * @param port
+ * the port to listen on
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public Server(String name, int port) throws IOException {
+ this(name, port, null);
+ }
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link Server#start()} is called.
+ *
+ * @param name
+ * the server name (only used for debug info and traces)
+ * @param port
+ * the port to listen on
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public Server(String name, int port, String key) throws IOException {
+ this.name = name;
+ this.port = port;
+ this.key = key;
+ this.ss = new ServerSocket(port);
+
+ if (this.port == 0) {
+ this.port = this.ss.getLocalPort();
+ }
+ }
+
+ /**
+ * The traces handler for this {@link Server}.
+ *
+ * @return the traces handler
+ */
+ public TraceHandler getTraceHandler() {
+ return tracer;
+ }
+
+ /**
+ * The traces handler for this {@link Server}.
+ *
+ * @param tracer
+ * the new traces handler
+ */
+ public void setTraceHandler(TraceHandler tracer) {
+ if (tracer == null) {
+ tracer = new TraceHandler(false, false, false);
+ }
+
+ this.tracer = tracer;
+ }
+
+ /**
+ * The name of this {@link Server} if any.
+ * <p>
+ * Used for traces and debug purposes only.
+ *
+ * @return the name or NULL
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Return the assigned port.
+ *
+ * @return the assigned port
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * The total amount of bytes received.
+ *
+ * @return the amount of bytes received
+ */
+ public long getBytesReceived() {
+ return bytesReceived;
+ }
+
+ /**
+ * The total amount of bytes sent.
+ *
+ * @return the amount of bytes sent
+ */
+ public long getBytesSent() {
+ return bytesSent;
+ }
+
+ /**
+ * Start the server (listen on the network for new connections).
+ * <p>
+ * Can only be called once.
+ * <p>
+ * This call is asynchronous, and will just start a new {@link Thread} on
+ * itself (see {@link Server#run()}).
+ */
+ public void start() {
+ new Thread(this).start();
+ }
+
+ /**
+ * Start the server (listen on the network for new connections).
+ * <p>
+ * Can only be called once.
+ * <p>
+ * You may call it via {@link Server#start()} for an asynchronous call, too.
+ */
+ @Override
+ public void run() {
+ ServerSocket ss = null;
+ boolean alreadyStarted = false;
+ synchronized (lock) {
+ ss = this.ss;
+ if (!started && ss != null) {
+ started = true;
+ } else {
+ alreadyStarted = started;
+ }
+ }
+
+ if (alreadyStarted) {
+ tracer.error(name + ": cannot start server on port " + port
+ + ", it is already started");
+ return;
+ }
+
+ if (ss == null) {
+ tracer.error(name + ": cannot start server on port " + port
+ + ", it has already been used");
+ return;
+ }
+
+ try {
+ tracer.trace(name + ": server starting on port " + port + " ("
+ + (key != null ? "encrypted" : "plain text") + ")");
+
+ while (started && !exiting) {
+ count(1);
+ final Socket s = ss.accept();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ ConnectActionServer action = null;
+ try {
+ action = createConnectActionServer(s);
+ action.connect();
+ } finally {
+ count(-1);
+ if (action != null) {
+ bytesReceived += action.getBytesReceived();
+ bytesSent += action.getBytesSent();
+ }
+ }
+ }
+ }).start();
+ }
+
+ // Will be covered by @link{Server#stop(long)} for timeouts
+ while (counter > 0) {
+ Thread.sleep(10);
+ }
+ } catch (Exception e) {
+ if (counter > 0) {
+ onError(e);
+ }
+ } finally {
+ try {
+ ss.close();
+ } catch (Exception e) {
+ onError(e);
+ }
+
+ this.ss = null;
+
+ started = false;
+ exiting = false;
+ counter = 0;
+
+ tracer.trace(name + ": client terminated on port " + port);
+ }
+ }
+
+ /**
+ * Will stop the server, synchronously and without a timeout.
+ */
+ public void stop() {
+ tracer.trace(name + ": stopping server");
+ stop(0, true);
+ }
+
+ /**
+ * Stop the server.
+ *
+ * @param timeout
+ * the maximum timeout to wait for existing actions to complete,
+ * or 0 for "no timeout"
+ * @param wait
+ * wait for the server to be stopped before returning
+ * (synchronous) or not (asynchronous)
+ */
+ public void stop(final long timeout, final boolean wait) {
+ if (wait) {
+ stop(timeout);
+ } else {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ stop(timeout);
+ }
+ }).start();
+ }
+ }
+
+ /**
+ * Stop the server (synchronous).
+ *
+ * @param timeout
+ * the maximum timeout to wait for existing actions to complete,
+ * or 0 for "no timeout"
+ */
+ private void stop(long timeout) {
+ tracer.trace(name + ": server stopping on port " + port);
+ synchronized (lock) {
+ if (started && !exiting) {
+ exiting = true;
+
+ try {
+ getConnectionToMe().connect();
+ long time = 0;
+ while (ss != null && timeout > 0 && timeout > time) {
+ Thread.sleep(10);
+ time += 10;
+ }
+ } catch (Exception e) {
+ if (ss != null) {
+ counter = 0; // will stop the main thread
+ onError(e);
+ }
+ }
+ }
+ }
+
+ // return only when stopped
+ while (started || exiting) {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ /**
+ * Return a connection to this server (used by the Exit code to send an exit
+ * message).
+ *
+ * @return the connection
+ *
+ * @throws UnknownHostException
+ * the host should always be NULL (localhost)
+ * @throws IOException
+ * in case of I/O error
+ */
+ abstract protected ConnectActionClient getConnectionToMe()
+ throws UnknownHostException, IOException;
+
+ /**
+ * Change the number of currently serviced actions.
+ *
+ * @param change
+ * the number to increase or decrease
+ *
+ * @return the current number after this operation
+ */
+ private int count(int change) {
+ synchronized (counterLock) {
+ counter += change;
+ return counter;
+ }
+ }
+
+ /**
+ * This method will be called on errors.
+ * <p>
+ * By default, it will only call the trace handler (so you may want to call
+ * super {@link Server#onError} if you override it).
+ *
+ * @param e
+ * the error
+ */
+ protected void onError(Exception e) {
+ tracer.error(e);
+ }
+
+ /**
+ * Return the next ID to use.
+ *
+ * @return the next ID
+ */
+ protected synchronized long getNextId() {
+ return id++;
+ }
+
+ /**
+ * Method called when
+ * {@link ServerObject#onRequest(ConnectActionServerObject, Object, long)}
+ * has successfully finished.
+ * <p>
+ * Can be used to know how much data was transmitted.
+ *
+ * @param id
+ * the ID used to identify the request
+ * @param bytesReceived
+ * the bytes received during the request
+ * @param bytesSent
+ * the bytes sent during the request
+ */
+ @SuppressWarnings("unused")
+ protected void onRequestDone(long id, long bytesReceived, long bytesSent) {
+ }
+}
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.TraceHandler;
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.serial.Importer;
+
+/**
+ * This class implements a simple server that can bridge two other
+ * {@link Server}s.
+ * <p>
+ * It can, of course, inspect the data that goes through it (by default, it
+ * prints traces of the data).
+ * <p>
+ * Note: this {@link ServerBridge} has to be discarded after use (cannot be
+ * started twice).
+ *
+ * @author niki
+ */
+public class ServerBridge extends Server {
+ private final String forwardToHost;
+ private final int forwardToPort;
+ private final String forwardToKey;
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link ServerBridge#start()} is called.
+ *
+ * @param port
+ * the port to listen on, or 0 to assign any unallocated port
+ * found (which can later on be queried via
+ * {@link ServerBridge#getPort()}
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param forwardToHost
+ * the host server to forward the calls to
+ * @param forwardToPort
+ * the host port to forward the calls to
+ * @param forwardToKey
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ServerBridge(int port, String key, String forwardToHost,
+ int forwardToPort, String forwardToKey) throws IOException {
+ super(port, key);
+ this.forwardToHost = forwardToHost;
+ this.forwardToPort = forwardToPort;
+ this.forwardToKey = forwardToKey;
+ }
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link ServerBridge#start()} is called.
+ *
+ * @param name
+ * the server name (only used for debug info and traces)
+ * @param port
+ * the port to listen on
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ * @param forwardToHost
+ * the host server to forward the calls to
+ * @param forwardToPort
+ * the host port to forward the calls to
+ * @param forwardToKey
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text) use an SSL connection
+ * for the forward server or not
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ServerBridge(String name, int port, String key,
+ String forwardToHost, int forwardToPort, String forwardToKey)
+ throws IOException {
+ super(name, port, key);
+ this.forwardToHost = forwardToHost;
+ this.forwardToPort = forwardToPort;
+ this.forwardToKey = forwardToKey;
+ }
+
+ /**
+ * The traces handler for this {@link Server}.
+ * <p>
+ * The trace levels are handled as follow:
+ * <ul>
+ * <li>1: it will only print basic IN/OUT messages with length</li>
+ * <li>2: it will try to interpret it as an object (SLOW) and print the
+ * object class if possible</li>
+ * <li>3: it will try to print the {@link Object#toString()} value, or the
+ * data if it is not an object</li>
+ * <li>4: it will also print the unzipped serialised value if it is an
+ * object</li>
+ * </ul>
+ *
+ * @param tracer
+ * the new traces handler
+ */
+ @Override
+ public void setTraceHandler(TraceHandler tracer) {
+ super.setTraceHandler(tracer);
+ }
+
+ @Override
+ protected ConnectActionServer createConnectActionServer(Socket s) {
+ // Bad impl, not up to date (should work, but not efficient)
+ return new ConnectActionServerString(s, key) {
+ @Override
+ public void action(Version clientVersion) throws Exception {
+ onClientContact(clientVersion);
+ final ConnectActionServerString bridge = this;
+
+ try {
+ new ConnectActionClientString(forwardToHost, forwardToPort,
+ forwardToKey) {
+ @Override
+ public void action(Version serverVersion)
+ throws Exception {
+ onServerContact(serverVersion);
+
+ for (String fromClient = bridge.rec(); fromClient != null; fromClient = bridge
+ .rec()) {
+ onRec(fromClient);
+ String fromServer = send(fromClient);
+ onSend(fromServer);
+ bridge.send(fromServer);
+ }
+
+ getTraceHandler().trace("=== DONE", 1);
+ getTraceHandler().trace("", 1);
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ ServerBridge.this.onError(e);
+ }
+ }.connect();
+ } catch (Exception e) {
+ ServerBridge.this.onError(e);
+ }
+ }
+ };
+ }
+
+ /**
+ * This is the method that is called each time a client contact us.
+ */
+ protected void onClientContact(Version clientVersion) {
+ getTraceHandler().trace(">>> CLIENT " + clientVersion);
+ }
+
+ /**
+ * This is the method that is called each time a client contact us.
+ */
+ protected void onServerContact(Version serverVersion) {
+ getTraceHandler().trace("<<< SERVER " + serverVersion);
+ getTraceHandler().trace("");
+ }
+
+ /**
+ * This is the method that is called each time a client contact us.
+ *
+ * @param data
+ * the data sent by the client
+ */
+ protected void onRec(String data) {
+ trace(">>> CLIENT", data);
+ }
+
+ /**
+ * This is the method that is called each time the forwarded server contact
+ * us.
+ *
+ * @param data
+ * the data sent by the client
+ */
+ protected void onSend(String data) {
+ trace("<<< SERVER", data);
+ }
+
+ @Override
+ protected ConnectActionClient getConnectionToMe()
+ throws UnknownHostException, IOException {
+ return new ConnectActionClientString(new Socket((String) null,
+ getPort()), key);
+ }
+
+ @Override
+ public void run() {
+ getTraceHandler().trace(
+ getName() + ": will forward to " + forwardToHost + ":"
+ + forwardToPort + " ("
+ + (forwardToKey != null ? "encrypted" : "plain text")
+ + ")");
+ super.run();
+ }
+
+ /**
+ * Trace the data with the given prefix.
+ *
+ * @param prefix
+ * the prefix (client, server, version...)
+ * @param data
+ * the data to trace
+ */
+ private void trace(String prefix, String data) {
+ int size = data == null ? 0 : data.length();
+ String ssize = StringUtils.formatNumber(size) + "bytes";
+
+ getTraceHandler().trace(prefix + ": " + ssize, 1);
+
+ if (getTraceHandler().getTraceLevel() >= 2) {
+ try {
+ while (data.startsWith("ZIP:") || data.startsWith("B64:")) {
+ if (data.startsWith("ZIP:")) {
+ data = StringUtils.unzip64s(data.substring(4));
+ } else if (data.startsWith("B64:")) {
+ data = StringUtils.unzip64s(data.substring(4));
+ }
+ }
+
+ InputStream stream = new ByteArrayInputStream(
+ StringUtils.getBytes(data));
+ try {
+ Object obj = new Importer().read(stream).getValue();
+ if (obj == null) {
+ getTraceHandler().trace("NULL", 2);
+ getTraceHandler().trace("NULL", 3);
+ getTraceHandler().trace("NULL", 4);
+ } else {
+ if (obj.getClass().isArray()) {
+ getTraceHandler().trace(
+ "(" + obj.getClass() + ") with "
+ + Array.getLength(obj)
+ + "element(s)", 3);
+ } else {
+ getTraceHandler().trace("(" + obj.getClass() + ")",
+ 2);
+ }
+ getTraceHandler().trace("" + obj.toString(), 3);
+ getTraceHandler().trace(data, 4);
+ }
+ } finally {
+ stream.close();
+ }
+ } catch (NoSuchMethodException e) {
+ getTraceHandler().trace("(not an object)", 2);
+ getTraceHandler().trace(data, 3);
+ getTraceHandler().trace("", 4);
+ } catch (NoSuchFieldException e) {
+ getTraceHandler().trace(
+ "(incompatible: " + e.getMessage() + ")", 2);
+ getTraceHandler().trace(data, 3);
+ getTraceHandler().trace("", 4);
+ } catch (ClassNotFoundException e) {
+ getTraceHandler().trace(
+ "(unknown object: " + e.getMessage() + ")", 2);
+ getTraceHandler().trace(data, 3);
+ getTraceHandler().trace("", 4);
+ } catch (Exception e) {
+ getTraceHandler().trace(
+ "(decode error: " + e.getMessage() + ")", 2);
+ getTraceHandler().trace(data, 3);
+ getTraceHandler().trace("", 4);
+ }
+
+ getTraceHandler().trace("", 2);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import be.nikiroo.utils.Version;
+
+/**
+ * This class implements a simple server that can listen for connections and
+ * send/receive objects.
+ * <p>
+ * Note: this {@link ServerObject} has to be discarded after use (cannot be
+ * started twice).
+ *
+ * @author niki
+ */
+abstract public class ServerObject extends Server {
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link ServerObject#start()} is called.
+ *
+ * @param port
+ * the port to listen on, or 0 to assign any unallocated port
+ * found (which can later on be queried via
+ * {@link ServerObject#getPort()}
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ServerObject(int port, String key) throws IOException {
+ super(port, key);
+ }
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link ServerObject#start()} is called.
+ *
+ * @param name
+ * the server name (only used for debug info and traces)
+ * @param port
+ * the port to listen on
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ServerObject(String name, int port, String key) throws IOException {
+ super(name, port, key);
+ }
+
+ @Override
+ protected ConnectActionServer createConnectActionServer(Socket s) {
+ return new ConnectActionServerObject(s, key) {
+ @Override
+ public void action(Version clientVersion) throws Exception {
+ long id = getNextId();
+ try {
+ for (Object data = rec(); true; data = rec()) {
+ Object rep = null;
+ try {
+ rep = onRequest(this, clientVersion, data, id);
+ if (isClosing()) {
+ return;
+ }
+ } catch (Exception e) {
+ onError(e);
+ }
+
+ send(rep);
+ }
+ } catch (NullPointerException e) {
+ // Client has no data any more, we quit
+ onRequestDone(id, getBytesReceived(), getBytesSent());
+ }
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ ServerObject.this.onError(e);
+ }
+ };
+ }
+
+ @Override
+ protected ConnectActionClient getConnectionToMe()
+ throws UnknownHostException, IOException {
+ return new ConnectActionClientObject(new Socket((String) null,
+ getPort()), key);
+ }
+
+ /**
+ * This is the method that is called on each client request.
+ * <p>
+ * You are expected to react to it and return an answer (which can be NULL).
+ *
+ * @param action
+ * the client action
+ * @param data
+ * the data sent by the client (which can be NULL)
+ * @param id
+ * an ID to identify this request (will also be re-used for
+ * {@link ServerObject#onRequestDone(long, long, long)}.
+ *
+ * @return the answer to return to the client (which can be NULL)
+ *
+ * @throws Exception
+ * in case of an exception, the error will only be logged
+ */
+ protected Object onRequest(ConnectActionServerObject action,
+ Version clientVersion, Object data,
+ @SuppressWarnings("unused") long id) throws Exception {
+ // TODO: change to abstract when deprecated method is removed
+ // Default implementation for compat
+ return onRequest(action, clientVersion, data);
+ }
+
+ // Deprecated //
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ServerObject(int port, boolean ssl) throws IOException {
+ this(port, ssl ? "" : null);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ServerObject(String name, int port, boolean ssl) throws IOException {
+ this(name, port, ssl ? "" : null);
+ }
+
+ /**
+ * Will be called if the correct version is not overrided.
+ *
+ * @deprecated use the version with the id.
+ *
+ * @param action
+ * the client action
+ * @param data
+ * the data sent by the client
+ *
+ * @return the answer to return to the client
+ *
+ * @throws Exception
+ * in case of an exception, the error will only be logged
+ */
+ @Deprecated
+ @SuppressWarnings("unused")
+ protected Object onRequest(ConnectActionServerObject action,
+ Version version, Object data) throws Exception {
+ return null;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.serial.server;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import be.nikiroo.utils.Version;
+
+/**
+ * This class implements a simple server that can listen for connections and
+ * send/receive Strings.
+ * <p>
+ * Note: this {@link ServerString} has to be discarded after use (cannot be
+ * started twice).
+ *
+ * @author niki
+ */
+abstract public class ServerString extends Server {
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link ServerString#start()} is called.
+ *
+ * @param port
+ * the port to listen on, or 0 to assign any unallocated port
+ * found (which can later on be queried via
+ * {@link ServerString#getPort()}
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ServerString(int port, String key) throws IOException {
+ super(port, key);
+ }
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link ServerString#start()} is called.
+ *
+ * @param name
+ * the server name (only used for debug info and traces)
+ * @param port
+ * the port to listen on
+ * @param key
+ * an optional key to encrypt all the communications (if NULL,
+ * everything will be sent in clear text)
+ *
+ * @throws IOException
+ * in case of I/O error
+ * @throws UnknownHostException
+ * if the IP address of the host could not be determined
+ * @throws IllegalArgumentException
+ * if the port parameter is outside the specified range of valid
+ * port values, which is between 0 and 65535, inclusive
+ */
+ public ServerString(String name, int port, String key) throws IOException {
+ super(name, port, key);
+ }
+
+ @Override
+ protected ConnectActionServer createConnectActionServer(Socket s) {
+ return new ConnectActionServerString(s, key) {
+ @Override
+ public void action(Version clientVersion) throws Exception {
+ long id = getNextId();
+ for (String data = rec(); data != null; data = rec()) {
+ String rep = null;
+ try {
+ rep = onRequest(this, clientVersion, data, id);
+ if (isClosing()) {
+ return;
+ }
+ } catch (Exception e) {
+ onError(e);
+ }
+
+ if (rep == null) {
+ rep = "";
+ }
+ send(rep);
+ }
+
+ onRequestDone(id, getBytesReceived(), getBytesSent());
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ ServerString.this.onError(e);
+ }
+ };
+ }
+
+ @Override
+ protected ConnectActionClient getConnectionToMe()
+ throws UnknownHostException, IOException {
+ return new ConnectActionClientString(new Socket((String) null,
+ getPort()), key);
+ }
+
+ /**
+ * This is the method that is called on each client request.
+ * <p>
+ * You are expected to react to it and return an answer (NULL will be
+ * converted to an empty {@link String}).
+ *
+ * @param action
+ * the client action
+ * @param clientVersion
+ * the client version
+ * @param data
+ * the data sent by the client
+ * @param id
+ * an ID to identify this request (will also be re-used for
+ * {@link ServerObject#onRequestDone(long, long, long)}.
+ *
+ * @return the answer to return to the client
+ *
+ * @throws Exception
+ * in case of an exception, the error will only be logged
+ */
+ protected String onRequest(ConnectActionServerString action,
+ Version clientVersion, String data,
+ @SuppressWarnings("unused") long id) throws Exception {
+ // TODO: change to abstract when deprecated method is removed
+ // Default implementation for compat
+ return onRequest(action, clientVersion, data);
+ }
+
+ // Deprecated //
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ServerString(int port, boolean ssl) throws IOException {
+ this(port, ssl ? "" : null);
+ }
+
+ /**
+ * @deprecated SSL support has been replaced by key-based encryption.
+ * <p>
+ * Please use the version with key encryption (this deprecated
+ * version uses an empty key when <tt>ssl</tt> is TRUE and no
+ * key (NULL) when <tt>ssl</tt> is FALSE).
+ */
+ @Deprecated
+ public ServerString(String name, int port, boolean ssl) throws IOException {
+ this(name, port, ssl ? "" : null);
+ }
+
+ /**
+ * Will be called if the correct version is not overrided.
+ *
+ * @deprecated use the version with the id.
+ *
+ * @param action
+ * the client action
+ * @param data
+ * the data sent by the client
+ *
+ * @return the answer to return to the client
+ *
+ * @throws Exception
+ * in case of an exception, the error will only be logged
+ */
+ @Deprecated
+ @SuppressWarnings("unused")
+ protected String onRequest(ConnectActionServerString action,
+ Version version, String data) throws Exception {
+ return null;
+ }
+}
+++ /dev/null
-package be.nikiroo.fanfix_swing;
-
-import java.awt.BorderLayout;
-import java.awt.Container;
-import java.awt.Window;
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-import java.net.UnknownHostException;
-
-import javax.swing.JDialog;
-import javax.swing.JLabel;
-import javax.swing.SwingWorker;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.bundles.StringIdGui;
-import be.nikiroo.fanfix.bundles.UiConfig;
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix.library.LocalLibrary;
-import be.nikiroo.fanfix.reader.BasicReader;
-import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
-import be.nikiroo.utils.Progress;
-
-public class Actions {
- static public void openExternal(final BasicLibrary lib, MetaData meta,
- final Container parent, final Runnable onDone) {
- Container parentWindow = parent;
- while (!(parentWindow instanceof Window) && parentWindow != null) {
- parentWindow = parentWindow.getParent();
- }
-
- // TODO: UI
- final JDialog wait = new JDialog((Window) parentWindow);
- wait.setTitle("Opening story");
- wait.setSize(400, 300);
- wait.setLayout(new BorderLayout());
- wait.add(new JLabel("Waiting..."));
-
- // TODO: pg?
-
- final Object waitLock = new Object();
- final Boolean[] waitScreen = new Boolean[] { false };
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- Thread.sleep(200);
- } catch (InterruptedException e) {
- }
-
- synchronized (waitLock) {
- if (!waitScreen[0]) {
- waitScreen[0] = true;
- wait.setVisible(true);
- }
- }
- }
- }).start();
-
- final String luid = meta.getLuid();
- final boolean isImageDocument = meta.isImageDocument();
-
- final SwingWorker<File, Void> worker = new SwingWorker<File, Void>() {
- private File target;
-
- @Override
- protected File doInBackground() throws Exception {
- target = lib.getFile(luid, null);
- return null;
- }
-
- @Override
- protected void done() {
- try {
- get();
- openExternal(target, isImageDocument);
- } catch (Exception e) {
- // TODO: i18n
- UiHelper.error(parent, e.getLocalizedMessage(),
- "Cannot open the story", e);
- }
-
- synchronized (waitLock) {
- if (waitScreen[0]) {
- wait.setVisible(false);
- }
- waitScreen[0] = true;
- }
-
- if (onDone != null) {
- onDone.run();
- }
- }
- };
-
- worker.execute();
- }
-
- /**
- * Open the {@link Story} with an external reader (the program will be
- * passed the given target file).
- *
- * @param target
- * the target {@link File}
- * @param isImageDocument
- * TRUE for image documents, FALSE for not-images documents
- *
- * @throws IOException
- * in case of I/O error
- */
- static public void openExternal(File target, boolean isImageDocument)
- throws IOException {
- String program = null;
- if (isImageDocument) {
- program = Instance.getInstance().getUiConfig()
- .getString(UiConfig.IMAGES_DOCUMENT_READER);
- } else {
- program = Instance.getInstance().getUiConfig()
- .getString(UiConfig.NON_IMAGES_DOCUMENT_READER);
- }
-
- if (program != null && program.trim().isEmpty()) {
- program = null;
- }
-
- start(target, program, false);
- }
-
- /**
- * Start a file and open it with the given program if given or the first
- * default system starter we can find.
- *
- * @param target
- * the target to open
- * @param program
- * the program to use or NULL for the default system starter
- * @param sync
- * execute the process synchronously (wait until it is terminated
- * before returning)
- *
- * @throws IOException
- * in case of I/O error
- */
- static protected void start(File target, String program, boolean sync)
- throws IOException {
- Process proc = null;
- if (program == null) {
- boolean ok = false;
- for (String starter : new String[] { "xdg-open", "open", "see",
- "start", "run" }) {
- try {
- Instance.getInstance().getTraceHandler()
- .trace("starting external program");
- proc = Runtime.getRuntime().exec(
- new String[] { starter, target.getAbsolutePath() });
- ok = true;
- break;
- } catch (IOException e) {
- }
- }
- if (!ok) {
- throw new IOException(
- "Cannot find a program to start the file");
- }
- } else {
- Instance.getInstance().getTraceHandler()
- .trace("starting external program");
- proc = Runtime.getRuntime()
- .exec(new String[] { program, target.getAbsolutePath() });
- }
-
- if (proc != null && sync) {
- try {
- proc.waitFor();
- } catch (InterruptedException e) {
- }
- }
- }
-
- /**
- * Actually import the {@link Story} into the main {@link LocalLibrary}.
- * <p>
- * Should be called inside the UI thread, will start a worker (i.e., this is
- * asynchronous).
- *
- * @param parent
- * a container we can use to show error messages if any
- * @param url
- * the {@link Story} to import by {@link URL}
- * @param pg
- * the optional progress reporter
- * @param onSuccess
- * Action to execute on success
- */
- static public void imprt(final Container parent, final String url,
- final Progress pg, final Runnable onSuccess) {
- final Progress fpg = pg;
- new SwingWorker<Void, Void>() {
- @Override
- protected Void doInBackground() throws Exception {
- Progress pg = fpg;
- if (pg == null)
- pg = new Progress();
-
- try {
- Instance.getInstance().getLibrary()
- .imprt(BasicReader.getUrl(url), fpg);
-
- fpg.done();
- if (onSuccess != null) {
- onSuccess.run();
- }
- } catch (IOException e) {
- fpg.done();
- if (e instanceof UnknownHostException) {
- UiHelper.error(parent,
- Instance.getInstance().getTransGui().getString(
- StringIdGui.ERROR_URL_NOT_SUPPORTED,
- url),
- Instance.getInstance().getTransGui().getString(
- StringIdGui.TITLE_ERROR),
- null);
- } else {
- UiHelper.error(parent,
- Instance.getInstance().getTransGui().getString(
- StringIdGui.ERROR_URL_IMPORT_FAILED,
- url, e.getMessage()),
- Instance.getInstance().getTransGui()
- .getString(StringIdGui.TITLE_ERROR),
- e);
- }
- }
-
- return null;
- }
- }.execute();
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing;
-
-import javax.swing.JFrame;
-
-import be.nikiroo.fanfix.DataLoader;
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.bundles.Config;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix.library.LocalLibrary;
-import be.nikiroo.fanfix_swing.gui.MainFrame;
-import be.nikiroo.utils.ui.UIUtils;
-
-public class Main {
- public static void main(String[] args) {
- UIUtils.setLookAndFeel();
-
- final String forceLib = null;
- // = "$HOME/Books/local";
-
- if (forceLib == null) {
- Instance.init();
- } else {
- Instance.init(new Instance() {
- private DataLoader cache;
- private BasicLibrary lib;
-
- @Override
- public DataLoader getCache() {
- if (cache == null) {
- cache = new DataLoader(getConfig()
- .getString(Config.NETWORK_USER_AGENT));
- }
-
- return cache;
- }
-
- @Override
- public BasicLibrary getLibrary() {
- if (lib == null) {
- lib = new LocalLibrary(getFile(forceLib), getConfig()) {
- };
- }
-
- return lib;
- }
- });
- }
-
- JFrame main = new MainFrame(true, true);
- main.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- main.setVisible(true);
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui;
-
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.Image;
-import java.awt.Point;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-
-import javax.swing.DefaultListModel;
-import javax.swing.JList;
-import javax.swing.JPopupMenu;
-import javax.swing.ListCellRenderer;
-import javax.swing.ListSelectionModel;
-import javax.swing.SwingWorker;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix_swing.Actions;
-import be.nikiroo.fanfix_swing.gui.book.BookBlock;
-import be.nikiroo.fanfix_swing.gui.book.BookInfo;
-import be.nikiroo.fanfix_swing.gui.book.BookLine;
-import be.nikiroo.fanfix_swing.gui.book.BookPopup;
-import be.nikiroo.fanfix_swing.gui.utils.DelayWorker;
-import be.nikiroo.fanfix_swing.gui.utils.ListenerPanel;
-import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
-
-public class BooksPanel extends ListenerPanel {
- private class ListModel extends DefaultListModel<BookInfo> {
- public void fireElementChanged(BookInfo element) {
- int index = indexOf(element);
- if (index >= 0) {
- fireContentsChanged(element, index, index);
- }
- }
- }
-
- static public final String INVALIDATE_CACHE = "invalidate_cache";
-
- private List<BookInfo> bookInfos = new ArrayList<BookInfo>();
- private Map<BookInfo, BookLine> books = new HashMap<BookInfo, BookLine>();
- private boolean seeWordCount;
- private boolean listMode;
-
- private JList<BookInfo> list;
- private int hoveredIndex = -1;
- private ListModel data = new ListModel();
- private DelayWorker bookCoverUpdater;
-
- private SearchBar searchBar;
-
- public BooksPanel(boolean listMode) {
- setLayout(new BorderLayout());
-
- searchBar = new SearchBar();
- add(searchBar, BorderLayout.NORTH);
-
- searchBar.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- filter(searchBar.getText());
- }
- });
-
- bookCoverUpdater = new DelayWorker(20);
- bookCoverUpdater.start();
- add(UiHelper.scroll(initList(listMode)), BorderLayout.CENTER);
- }
-
- // null or empty -> all sources
- // sources hierarchy supported ("source/" will includes all "source" and
- // "source/*")
- public void load(final List<String> sources, final List<String> authors,
- final List<String> tags) {
- new SwingWorker<List<BookInfo>, Void>() {
- @Override
- protected List<BookInfo> doInBackground() throws Exception {
- List<BookInfo> bookInfos = new ArrayList<BookInfo>();
- BasicLibrary lib = Instance.getInstance().getLibrary();
- for (MetaData meta : lib.getList().filter(sources, authors,
- tags)) {
- bookInfos.add(BookInfo.fromMeta(lib, meta));
- }
-
- return bookInfos;
- }
-
- @Override
- protected void done() {
- try {
- load(get());
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
- // TODO: error
- }
- }.execute();
- }
-
- public void load(List<BookInfo> bookInfos) {
- this.bookInfos.clear();
- this.bookInfos.addAll(bookInfos);
- bookCoverUpdater.clear();
-
- filter(searchBar.getText());
- }
-
- // cannot be NULL
- private void filter(String filter) {
- data.clear();
- for (BookInfo bookInfo : bookInfos) {
- if (bookInfo.getMainInfo() == null || filter.isEmpty()
- || bookInfo.getMainInfo().toLowerCase()
- .contains(filter.toLowerCase())) {
- data.addElement(bookInfo);
- }
- }
- list.repaint();
- }
-
- /**
- * The secondary value content: word count or author.
- *
- * @return TRUE to see word counts, FALSE to see authors
- */
- public boolean isSeeWordCount() {
- return seeWordCount;
- }
-
- /**
- * The secondary value content: word count or author.
- *
- * @param seeWordCount
- * TRUE to see word counts, FALSE to see authors
- */
- public void setSeeWordCount(boolean seeWordCount) {
- if (this.seeWordCount != seeWordCount) {
- if (books != null) {
- for (BookLine book : books.values()) {
- book.setSeeWordCount(seeWordCount);
- }
-
- list.repaint();
- }
- }
- }
-
- private JList<BookInfo> initList(boolean listMode) {
- final JList<BookInfo> list = new JList<BookInfo>(data);
-
- final JPopupMenu popup = new BookPopup(
- Instance.getInstance().getLibrary(), new BookPopup.Informer() {
- @Override
- public void setCached(BookInfo book, boolean cached) {
- book.setCached(cached);
- fireElementChanged(book);
- }
-
- public void fireElementChanged(BookInfo book) {
- data.fireElementChanged(book);
- }
-
- @Override
- public List<BookInfo> getSelected() {
- List<BookInfo> selected = new ArrayList<BookInfo>();
- for (int index : list.getSelectedIndices()) {
- selected.add(data.get(index));
- }
-
- return selected;
- }
-
- @Override
- public BookInfo getUniqueSelected() {
- List<BookInfo> selected = getSelected();
- if (selected.size() == 1) {
- return selected.get(0);
- }
- return null;
- }
-
- @Override
- public void invalidateCache() {
- // TODO: also reset the popup menu for sources/author
- fireActionPerformed(INVALIDATE_CACHE);
- }
- });
-
- list.addMouseMotionListener(new MouseAdapter() {
- @Override
- public void mouseMoved(MouseEvent me) {
- if (popup.isShowing())
- return;
-
- Point p = new Point(me.getX(), me.getY());
- int index = list.locationToIndex(p);
- if (index != hoveredIndex) {
- hoveredIndex = index;
- list.repaint();
- }
- }
- });
- list.addMouseListener(new MouseAdapter() {
- @Override
- public void mousePressed(MouseEvent e) {
- check(e);
- }
-
- @Override
- public void mouseReleased(MouseEvent e) {
- check(e);
- }
-
- @Override
- public void mouseExited(MouseEvent e) {
- if (popup.isShowing())
- return;
-
- if (hoveredIndex > -1) {
- hoveredIndex = -1;
- list.repaint();
- }
- }
-
- @Override
- public void mouseClicked(MouseEvent e) {
- super.mouseClicked(e);
- if (e.getClickCount() == 2) {
- int index = list.locationToIndex(e.getPoint());
- list.setSelectedIndex(index);
-
- final BookInfo book = data.get(index);
- BasicLibrary lib = Instance.getInstance().getLibrary();
-
- Actions.openExternal(lib, book.getMeta(), BooksPanel.this,
- new Runnable() {
- @Override
- public void run() {
- book.setCached(true);
- data.fireElementChanged(book);
- }
- });
- }
- }
-
- private void check(MouseEvent e) {
- if (e.isPopupTrigger()) {
- if (list.getSelectedIndices().length <= 1) {
- list.setSelectedIndex(
- list.locationToIndex(e.getPoint()));
- }
-
- popup.show(list, e.getX(), e.getY());
- }
- }
- });
-
- list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
- list.setSelectedIndex(0);
- list.setCellRenderer(generateRenderer());
- list.setVisibleRowCount(0);
-
- this.list = list;
- setListMode(listMode);
- return this.list;
- }
-
- private ListCellRenderer<BookInfo> generateRenderer() {
- return new ListCellRenderer<BookInfo>() {
- @Override
- public Component getListCellRendererComponent(
- JList<? extends BookInfo> list, BookInfo value, int index,
- boolean isSelected, boolean cellHasFocus) {
- BookLine book = books.get(value);
- if (book == null) {
- if (listMode) {
- book = new BookLine(value, seeWordCount);
- } else {
- book = new BookBlock(value, seeWordCount);
- startUpdateBookCover((BookBlock) book);
- }
- books.put(value, book);
- }
-
- book.setSelected(isSelected);
- book.setHovered(index == hoveredIndex);
- return book;
- }
- };
- }
-
- private void startUpdateBookCover(final BookBlock book) {
- bookCoverUpdater.delay(book.getInfo().getId(),
- new SwingWorker<Image, Void>() {
- @Override
- protected Image doInBackground() throws Exception {
- BasicLibrary lib = Instance.getInstance().getLibrary();
- return BookBlock.generateCoverImage(lib,
- book.getInfo());
- }
-
- protected void done() {
- try {
- book.setCoverImage(get());
- data.fireElementChanged(book.getInfo());
- } catch (Exception e) {
- // TODO ? probably just log
- }
- }
- });
- }
-
- public boolean isListMode() {
- return listMode;
- }
-
- public void setListMode(boolean listMode) {
- this.listMode = listMode;
- books.clear();
- list.setLayoutOrientation(
- listMode ? JList.VERTICAL : JList.HORIZONTAL_WRAP);
-
- StringBuilder longString = new StringBuilder();
- for (int i = 0; i < 20; i++) {
- longString.append(
- "Some long string, which is 50 chars long itself...");
- }
- if (listMode) {
- bookCoverUpdater.clear();
- Dimension sz = new BookLine(
- BookInfo.fromSource(null, longString.toString()), true)
- .getPreferredSize();
- list.setFixedCellHeight((int) sz.getHeight());
- list.setFixedCellWidth(list.getWidth());
- } else {
- Dimension sz = new BookBlock(
- BookInfo.fromSource(null, longString.toString()), true)
- .getPreferredSize();
- list.setFixedCellHeight((int) sz.getHeight());
- list.setFixedCellWidth((int) sz.getWidth());
- }
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui;
-
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.util.List;
-
-import javax.swing.JButton;
-import javax.swing.JPanel;
-import javax.swing.JTabbedPane;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix_swing.gui.book.BookInfo;
-import be.nikiroo.fanfix_swing.gui.browser.AuthorTab;
-import be.nikiroo.fanfix_swing.gui.browser.BasicTab;
-import be.nikiroo.fanfix_swing.gui.browser.SourceTab;
-import be.nikiroo.fanfix_swing.gui.browser.TagsTab;
-import be.nikiroo.fanfix_swing.gui.utils.ListenerPanel;
-import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
-
-/**
- * Panel dedicated to browse the stories through different means: by authors, by
- * tags or by sources.
- *
- * @author niki
- */
-public class BrowserPanel extends ListenerPanel {
- private static final long serialVersionUID = 1L;
-
- /**
- * The {@link ActionEvent} you receive from
- * {@link BrowserPanel#addActionListener(ActionListener)} can return this as
- * a command (see {@link ActionEvent#getActionCommand()}) if they were
- * created in the scope of a source.
- */
- static public final String SOURCE_SELECTION = "source_selection";
- /**
- * The {@link ActionEvent} you receive from
- * {@link BrowserPanel#addActionListener(ActionListener)} can return this as
- * a command (see {@link ActionEvent#getActionCommand()}) if they were
- * created in the scope of an author.
- */
- static public final String AUTHOR_SELECTION = "author_selection";
- /**
- * The {@link ActionEvent} you receive from
- * {@link BrowserPanel#addActionListener(ActionListener)} can return this as
- * a command (see {@link ActionEvent#getActionCommand()}) if they were
- * created in the scope of a tag.
- */
- static public final String TAGS_SELECTION = "tags_selection";
- /**
- * The {@link ActionEvent} you receive from
- * {@link BrowserPanel#addActionListener(ActionListener)} can return this as
- * a command (see {@link ActionEvent#getActionCommand()}) if they were
- * created in the scope of a tab change.
- */
- static public final String TAB_CHANGE = "tab_change";
-
- private JTabbedPane tabs;
- private SourceTab sourceTab;
- private AuthorTab authorTab;
- private TagsTab tagsTab;
-
- private boolean keepSelection;
-
- /**
- * Create a nesw {@link BrowserPanel}.
- */
- public BrowserPanel() {
- this.setPreferredSize(new Dimension(200, 800));
-
- this.setLayout(new BorderLayout());
- tabs = new JTabbedPane();
-
- int index = 0;
- tabs.add(sourceTab = new SourceTab(index++, SOURCE_SELECTION));
- tabs.add(authorTab = new AuthorTab(index++, AUTHOR_SELECTION));
- tabs.add(tagsTab = new TagsTab(index++, TAGS_SELECTION));
-
- configureTab(tabs, sourceTab, "Sources", "Tooltip for Sources");
- configureTab(tabs, authorTab, "Authors", "Tooltip for Authors");
- configureTab(tabs, tagsTab, "Tags", "Tooltip for Tags");
-
- JPanel options = new JPanel();
- options.setLayout(new BorderLayout());
-
- final JButton keep = new JButton("Keep selection");
- UiHelper.setButtonPressed(keep, keepSelection);
- keep.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- keepSelection = !keepSelection;
- UiHelper.setButtonPressed(keep, keepSelection);
- keep.setSelected(keepSelection);
- if (!keepSelection) {
- unselect();
- }
- }
- });
-
- options.add(keep, BorderLayout.CENTER);
-
- add(tabs, BorderLayout.CENTER);
- add(options, BorderLayout.SOUTH);
-
- tabs.addChangeListener(new ChangeListener() {
- @Override
- public void stateChanged(ChangeEvent e) {
- if (!keepSelection) {
- unselect();
- }
-
- fireActionPerformed(TAB_CHANGE);
- }
- });
- }
-
- @SuppressWarnings("rawtypes")
- private void unselect() {
- for (int i = 0; i < tabs.getTabCount(); i++) {
- if (i == tabs.getSelectedIndex())
- continue;
-
- BasicTab tab = (BasicTab) tabs.getComponent(i);
- tab.unselect();
- }
- }
-
- private void configureTab(JTabbedPane tabs,
- @SuppressWarnings("rawtypes") BasicTab tab, String name,
- String tooltip) {
- tab.setBaseTitle(name);
- tabs.setTitleAt(tab.getIndex(), tab.getTitle());
- tabs.setToolTipTextAt(tab.getIndex(), tooltip);
- listenTabs(tabs, tab);
- }
-
- private void listenTabs(final JTabbedPane tabs,
- @SuppressWarnings("rawtypes") final BasicTab tab) {
- tab.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- tabs.setTitleAt(tab.getIndex(), tab.getTitle());
- fireActionPerformed(e.getActionCommand());
- }
- });
- }
-
- /**
- * Get the {@link BookInfo} to highlight, even if none or more than one are
- * selected.
- * <p>
- * Return a special "all" {@link BookInfo} of the correct type when nothing
- * is selected.
- *
- * @return the {@link BookInfo} to highlight, can be NULL
- */
- public BookInfo getHighlight() {
- String selected1 = null;
- Component selectedTab = tabs.getSelectedComponent();
- if (selectedTab instanceof BasicTab) {
- @SuppressWarnings({ "unchecked", "rawtypes" })
- List<String> selectedAll = ((BasicTab) selectedTab)
- .getSelectedElements();
- if (!selectedAll.isEmpty()) {
- selected1 = selectedAll.get(0);
- }
- }
-
- BasicLibrary lib = Instance.getInstance().getLibrary();
- if (tabs.getSelectedComponent() == sourceTab) {
- return BookInfo.fromSource(lib, selected1);
- } else if (tabs.getSelectedComponent() == authorTab) {
- return BookInfo.fromAuthor(lib, selected1);
- } else if (tabs.getSelectedComponent() == tagsTab) {
- return BookInfo.fromTag(lib, selected1);
- }
-
- // ...what?
- return null;
- }
-
- /**
- * The currently selected sources, or an empty list.
- *
- * @return the sources (cannot be NULL)
- */
- public List<String> getSelectedSources() {
- return sourceTab.getSelectedElements();
- }
-
- /**
- * The currently selected authors, or an empty list.
- *
- * @return the sources (cannot be NULL)
- */
- public List<String> getSelectedAuthors() {
- return authorTab.getSelectedElements();
- }
-
- /**
- * The currently selected tags, or an empty list.
- *
- * @return the sources (cannot be NULL)
- */
- public List<String> getSelectedTags() {
- return tagsTab.getSelectedElements();
- }
-
- /**
- * Reload all the data from the 3 tabs.
- */
- public void reloadData() {
- sourceTab.reloadData();
- authorTab.reloadData();
- tagsTab.reloadData();
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Dimension;
-import java.awt.Image;
-import java.util.concurrent.ExecutionException;
-
-import javax.swing.ImageIcon;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.SwingWorker;
-import javax.swing.border.EmptyBorder;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix_swing.gui.book.BookBlock;
-import be.nikiroo.fanfix_swing.gui.book.BookInfo;
-
-/**
- * Display detailed informations about a {@link BookInfo}.
- * <p>
- * Actually, just its name, the number of stories it contains and a small image
- * if possible.
- *
- * @author niki
- */
-public class DetailsPanel extends JPanel {
- private static final long serialVersionUID = 1L;
-
- private JLabel icon;
- private JLabel name;
- private JLabel opt;
-
- private BookInfo info;
-
- /**
- * Create a new {@link DetailsPanel}.
- */
- public DetailsPanel() {
- this.setLayout(new BorderLayout());
-
- this.setPreferredSize(new Dimension(300, 300));
- this.setMinimumSize(new Dimension(200, 200));
-
- icon = config(new JLabel(), Color.black);
- name = config(new JLabel(), Color.black);
- opt = config(new JLabel(), Color.gray);
-
- JPanel panel = new JPanel(new BorderLayout());
- panel.add(name, BorderLayout.NORTH);
- panel.add(opt, BorderLayout.SOUTH);
- panel.setBorder(new EmptyBorder(0, 0, 10, 0));
-
- this.add(icon, BorderLayout.CENTER);
- this.add(panel, BorderLayout.SOUTH);
-
- setBook(null);
- }
-
- /**
- * Configure a {@link JLabel} with the given colour.
- *
- * @param label
- * the label to configure
- * @param color
- * the colour to use
- *
- * @return the (same) configured label
- */
- private JLabel config(JLabel label, Color color) {
- label.setAlignmentX(CENTER_ALIGNMENT);
- label.setHorizontalAlignment(JLabel.CENTER);
- label.setHorizontalTextPosition(JLabel.CENTER);
- label.setForeground(color);
- return label;
- }
-
- /**
- * Set the {@link BookInfo} you want to see displayed here.
- *
- * @param info
- * the {@link BookInfo} to display
- */
- public void setBook(final BookInfo info) {
- this.info = info;
-
- icon.setIcon(null);
- if (info == null) {
- name.setText(null);
- opt.setText(null);
- } else if (info.getMainInfo() == null) {
- name.setText(
- "All the " + info.getType().toString().toLowerCase() + "s");
- opt.setText(info.getSecondaryInfo(true));
- } else {
- final String myId = info.getId();
-
- name.setText(info.getMainInfo());
- opt.setText(info.getSecondaryInfo(true));
-
- new SwingWorker<Image, Void>() {
- @Override
- protected Image doInBackground() throws Exception {
- Thread.sleep(20);
-
- BookInfo current = DetailsPanel.this.info;
- if (current != null && current.getId().equals(myId)) {
- return BookBlock.generateCoverImage(
- Instance.getInstance().getLibrary(), info);
- }
-
- return null;
- }
-
- @Override
- protected void done() {
- BookInfo current = DetailsPanel.this.info;
- if (current != null && current.getId().equals(myId)) {
- try {
- Image img = get();
- if (img != null)
- icon.setIcon(new ImageIcon(img));
- } catch (InterruptedException e) {
- } catch (ExecutionException e) {
- }
- }
- }
- }.execute();
- }
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui;
-
-import java.awt.Container;
-import java.awt.Toolkit;
-import java.awt.datatransfer.DataFlavor;
-import java.io.File;
-import java.net.URL;
-
-import javax.swing.JFileChooser;
-import javax.swing.JFrame;
-import javax.swing.JOptionPane;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.bundles.StringIdGui;
-import be.nikiroo.fanfix.library.LocalLibrary;
-import be.nikiroo.fanfix_swing.Actions;
-import be.nikiroo.utils.Progress;
-
-public class ImporterFrame extends JFrame {
- public ImporterFrame() {
-
- }
-
- /**
- * Ask for and import an {@link URL} into the main {@link LocalLibrary}.
- * <p>
- * Should be called inside the UI thread.
- *
- * @param parent
- * a container we can use to display the {@link URL} chooser and
- * to show error messages if any
- * @param onSuccess
- * Action to execute on success
- */
- public void imprtUrl(final Container parent, final Runnable onSuccess) {
- String clipboard = "";
- try {
- clipboard = ("" + Toolkit.getDefaultToolkit().getSystemClipboard()
- .getData(DataFlavor.stringFlavor)).trim();
- } catch (Exception e) {
- // No data will be handled
- }
-
- if (clipboard == null || !(clipboard.startsWith("http://") || //
- clipboard.startsWith("https://"))) {
- clipboard = "";
- }
-
- Object url = JOptionPane.showInputDialog(parent,
- Instance.getInstance().getTransGui()
- .getString(StringIdGui.SUBTITLE_IMPORT_URL),
- Instance.getInstance().getTransGui()
- .getString(StringIdGui.TITLE_IMPORT_URL),
- JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
-
- Progress pg = null;
- if (url != null && !url.toString().isEmpty()) {
- Actions.imprt(parent, url.toString(), pg, onSuccess);
- }
- }
-
- /**
- * Ask for and import a {@link File} into the main {@link LocalLibrary}.
- * <p>
- * Should be called inside the UI thread.
- *
- * @param parent
- * a container we can use to display the {@link File} chooser and
- * to show error messages if any
- * @param onSuccess
- * Action to execute on success
- */
-
- public void imprtFile(final Container parent, final Runnable onSuccess) {
- JFileChooser fc = new JFileChooser();
-
- Progress pg = null;
- if (fc.showOpenDialog(parent) != JFileChooser.CANCEL_OPTION) {
- Object url = fc.getSelectedFile().getAbsolutePath();
- if (url != null && !url.toString().isEmpty()) {
- Actions.imprt(parent, url.toString(), pg, onSuccess);
- }
- }
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.KeyEvent;
-
-import javax.swing.JComponent;
-import javax.swing.JFrame;
-import javax.swing.JLabel;
-import javax.swing.JMenu;
-import javax.swing.JMenuBar;
-import javax.swing.JMenuItem;
-import javax.swing.JSplitPane;
-
-import be.nikiroo.utils.Version;
-
-public class MainFrame extends JFrame {
- private BooksPanel books;
- private DetailsPanel details;
- private BrowserPanel browser;
-
- public MainFrame(boolean sidePanel, boolean detailsPanel) {
- super("Fanfix " + Version.getCurrentVersion());
- setSize(800, 600);
- setJMenuBar(createMenuBar());
-
- sidePanel = true;
- detailsPanel = true;
-
- browser = new BrowserPanel();
- books = new BooksPanel(true);
-
- JComponent other = null;
- boolean orientationH = true;
- if (sidePanel && !detailsPanel) {
- other = browser;
- } else if (sidePanel && detailsPanel) {
- JComponent side = browser;
- details = new DetailsPanel();
- other = split(side, details, false, 0.5, 1);
- } else if (!sidePanel && !detailsPanel) {
- orientationH = false;
- other = new JLabel("<< Go back");
- } else if (!sidePanel && detailsPanel) {
- JComponent goBack = new JLabel("<< Go back");
- details = new DetailsPanel();
- other = split(goBack, details, false, 0.5, 1);
- }
-
- browser.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- books.load(browser.getSelectedSources(),
- browser.getSelectedAuthors(),
- browser.getSelectedTags());
- details.setBook(browser.getHighlight());
- }
- });
- books.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- if (BooksPanel.INVALIDATE_CACHE.equals(e.getActionCommand())) {
- browser.reloadData();
- }
- }
- });
-
- JSplitPane split = split(other, books, orientationH, 0.5, 0);
-
- this.add(split);
- }
-
- private JSplitPane split(JComponent leftTop, JComponent rightBottom,
- boolean horizontal, double ratio, double weight) {
- JSplitPane split = new JSplitPane(
- horizontal ? JSplitPane.HORIZONTAL_SPLIT
- : JSplitPane.VERTICAL_SPLIT,
- leftTop, rightBottom);
- split.setOneTouchExpandable(true);
- split.setResizeWeight(weight);
- split.setContinuousLayout(true);
- split.setDividerLocation(ratio);
-
- return split;
- }
-
- private JMenuBar createMenuBar() {
- JMenuBar bar = new JMenuBar();
-
- JMenu file = new JMenu("File");
- file.setMnemonic(KeyEvent.VK_F);
-
- JMenuItem item1 = new JMenuItem("Download", KeyEvent.VK_D);
- item1.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- // TODO: correctly use the importer (wip)
- new ImporterFrame().imprtUrl(MainFrame.this, new Runnable() {
- @Override
- public void run() {
- browser.reloadData();
- books.load(browser.getSelectedSources(),
- browser.getSelectedAuthors(),
- browser.getSelectedTags());
- details.setBook(browser.getHighlight());
- }
- });
- }
- });
-
- JMenuItem item2 = new JMenuItem("Import file", KeyEvent.VK_I);
- item2.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- // TODO: correctly use the importer (wip)
- new ImporterFrame().imprtFile(MainFrame.this, new Runnable() {
- @Override
- public void run() {
- browser.reloadData();
- books.load(browser.getSelectedSources(),
- browser.getSelectedAuthors(),
- browser.getSelectedTags());
- details.setBook(browser.getHighlight());
- }
- });
- }
- });
-
- file.add(item1);
- file.add(item2);
-
- JMenu edit = new JMenu("Edit");
- edit.setMnemonic(KeyEvent.VK_E);
-
- JMenu view = new JMenu("View");
- view.setMnemonic(KeyEvent.VK_V);
-
- JMenuItem listMode = new JMenuItem("List mode", KeyEvent.VK_L);
- listMode.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- books.setListMode(!books.isListMode());
- }
- });
-
- view.add(listMode);
-
- bar.add(file);
- bar.add(edit);
- bar.add(view);
-
- return bar;
- }
-}
+++ /dev/null
-
-package be.nikiroo.fanfix_swing.gui;
-
-import java.awt.BorderLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.KeyAdapter;
-import java.awt.event.KeyEvent;
-
-import javax.swing.JButton;
-import javax.swing.JTextField;
-import javax.swing.SwingUtilities;
-
-import be.nikiroo.fanfix_swing.gui.utils.ListenerPanel;
-import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
-import be.nikiroo.fanfix_swing.images.IconGenerator;
-import be.nikiroo.fanfix_swing.images.IconGenerator.Icon;
-import be.nikiroo.fanfix_swing.images.IconGenerator.Size;
-
-/**
- * A generic search/filter bar.
- *
- * @author niki
- */
-public class SearchBar extends ListenerPanel {
- static private final long serialVersionUID = 1L;
-
- private JButton search;
- private JTextField text;
- private JButton clear;
-
- private boolean realTime;
-
- /**
- * Create a new {@link SearchBar}.
- */
- public SearchBar() {
- setLayout(new BorderLayout());
-
- // TODO: option for this
- realTime = true;
-
- search = new JButton(IconGenerator.get(Icon.search, Size.x16));
- UiHelper.setButtonPressed(search, realTime);
- search.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- realTime = !realTime;
- UiHelper.setButtonPressed(search, realTime);
- text.requestFocus();
-
- if (realTime) {
- fireActionPerformed(getText());
- }
- }
- });
-
- text = new JTextField();
- text.addKeyListener(new KeyAdapter() {
- @Override
- public void keyTyped(final KeyEvent e) {
- super.keyTyped(e);
- SwingUtilities.invokeLater(new Runnable() {
- @Override
- public void run() {
- boolean empty = (text.getText().isEmpty());
- clear.setVisible(!empty);
-
- if (realTime) {
- fireActionPerformed(getText());
- }
- }
- });
- }
- });
- text.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- if (!realTime) {
- fireActionPerformed(getText());
- }
- }
- });
-
- clear = new JButton(IconGenerator.get(Icon.clear, Size.x16));
- clear.setBackground(text.getBackground());
- clear.setVisible(false);
- clear.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- text.setText("");
- clear.setVisible(false);
- text.requestFocus();
-
- fireActionPerformed(getText());
- }
- });
-
- add(search, BorderLayout.WEST);
- add(text, BorderLayout.CENTER);
- add(clear, BorderLayout.EAST);
- }
-
- /**
- * Return the current text displayed by this {@link SearchBar}.
- *
- * @return the text
- */
- public String getText() {
- return text.getText();
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.book;
-
-import java.awt.BorderLayout;
-import java.awt.Dimension;
-import java.awt.Graphics;
-import java.awt.Image;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix_swing.gui.BooksPanel;
-
-/**
- * A book item presented in a {@link BooksPanel}.
- * <p>
- * Can be a story, or a comic or... a group.
- *
- * @author niki
- */
-public class BookBlock extends BookLine {
- static private final long serialVersionUID = 1L;
- static private Image empty = BookCoverImager.generateCoverImage(null,
- (BookInfo) null);
-
- private JLabel title;
- private Image coverImage;
-
- /**
- * Create a new {@link BookBlock} item for the given {@link Story}.
- *
- * @param info
- * the information about the story to represent
- * @param seeWordCount
- * TRUE to see word counts, FALSE to see authors
- */
- public BookBlock(BookInfo info, boolean seeWordCount) {
- super(info, seeWordCount);
- }
-
- @Override
- protected void init() {
- coverImage = empty;
- title = new JLabel();
- updateMeta();
-
- JPanel filler = new JPanel();
- filler.setPreferredSize(new Dimension(BookCoverImager.getCoverWidth(),
- BookCoverImager.getCoverHeight()));
- filler.setOpaque(false);
-
- setLayout(new BorderLayout(10, 10));
- add(filler, BorderLayout.CENTER);
- add(title, BorderLayout.SOUTH);
- }
-
- /**
- * the cover image to use a base (see
- * {@link BookCoverImager#generateCoverImage(BasicLibrary, BookInfo)})
- *
- * @param coverImage
- * the image
- */
- public void setCoverImage(Image coverImage) {
- this.coverImage = coverImage;
- }
-
- @Override
- public void paint(Graphics g) {
- super.paint(g);
- g.drawImage(coverImage,
- BookCoverImager.TEXT_WIDTH - BookCoverImager.COVER_WIDTH, 0,
- null);
- BookCoverImager.paintOverlay(g, isEnabled(), isSelected(), isHovered(),
- getInfo().isCached());
- }
-
- @Override
- protected void updateMeta() {
- String main = getMainInfoDisplay();
- String optSecondary = getSecondaryInfoDisplay(isSeeWordCount());
- String color = String.format("#%X%X%X", AUTHOR_COLOR.getRed(),
- AUTHOR_COLOR.getGreen(), AUTHOR_COLOR.getBlue());
- title.setText(String.format("<html>"
- + "<body style='width: %d px; height: %d px; text-align: center;'>"
- + "%s" + "<br>" + "<span style='color: %s;'>" + "%s" + "</span>"
- + "</body>" + "</html>", BookCoverImager.TEXT_WIDTH,
- BookCoverImager.TEXT_HEIGHT, main, color, optSecondary));
-
- setBackground(BookCoverImager.getBackground(isEnabled(), isSelected(),
- isHovered()));
- }
-
- /**
- * Generate a cover icon based upon the given {@link BookInfo}.
- *
- * @param lib
- * the library the meta comes from
- * @param info
- * the {@link BookInfo}
- *
- * @return the image
- */
- static public java.awt.Image generateCoverImage(BasicLibrary lib,
- BookInfo info) {
- return BookCoverImager.generateCoverImage(lib, info);
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.book;
-
-import java.awt.Color;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.Polygon;
-import java.awt.Rectangle;
-import java.awt.image.BufferedImage;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-
-import javax.imageio.ImageIO;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix.reader.ui.GuiReaderBookInfo;
-import be.nikiroo.utils.Image;
-import be.nikiroo.utils.ui.ImageUtilsAwt;
-import be.nikiroo.utils.ui.UIUtils;
-
-/**
- * This class can create a cover icon ready to use for the graphical
- * application.
- *
- * @author niki
- */
-class BookCoverImager {
- // TODO: export some of the configuration options?
- static final int COVER_WIDTH = 100;
- static final int COVER_HEIGHT = 150;
- static final int SPINE_WIDTH = 5;
- static final int SPINE_HEIGHT = 5;
- static final int HOFFSET = 20;
- static final Color SPINE_COLOR_BOTTOM = new Color(180, 180, 180);
- static final Color SPINE_COLOR_RIGHT = new Color(100, 100, 100);
- static final Color BORDER = Color.black;
-
- public static final Color UNCACHED_ICON_COLOR = Color.green.darker();
- // new Color(0, 80, 220);
-
- public static final int TEXT_HEIGHT = 50;
- public static final int TEXT_WIDTH = COVER_WIDTH + 40;
-
- //
-
- static public Color getBackground(boolean enabled, boolean selected,
- boolean hovered) {
- Color color = new Color(255, 255, 255, 0);
- if (!enabled) {
- } else if (selected && !hovered) {
- color = new Color(80, 80, 100, 40);
- } else if (!selected && hovered) {
- color = new Color(230, 230, 255, 100);
- } else if (selected && hovered) {
- color = new Color(200, 200, 255, 100);
- }
-
- return color;
- }
-
- /**
- * Draw a partially transparent overlay if needed depending upon the
- * selection and mouse-hover states on top of the normal component, as well
- * as a possible "cached" icon if the item is cached.
- *
- * @param g
- * the {@link Graphics} to paint onto
- * @param enabled
- * draw an enabled overlay
- * @param selected
- * draw a selected overlay
- * @param hovered
- * draw a hovered overlay
- * @param cached
- * draw a non-cached overlay if needed
- */
- static public void paintOverlay(Graphics g, boolean enabled,
- boolean selected, boolean hovered, boolean cached) {
- Rectangle clip = g.getClipBounds();
- if (clip.getWidth() <= 0 || clip.getHeight() <= 0) {
- return;
- }
-
- int h = COVER_HEIGHT;
- int w = COVER_WIDTH;
- int xOffset = (TEXT_WIDTH - COVER_WIDTH) - 1;
- int yOffset = HOFFSET;
-
- if (BORDER != null) {
- if (BORDER != null) {
- g.setColor(BORDER);
- g.drawRect(xOffset, yOffset, COVER_WIDTH, COVER_HEIGHT);
- }
-
- xOffset++;
- yOffset++;
- }
-
- int[] xs = new int[] { xOffset, xOffset + SPINE_WIDTH,
- xOffset + w + SPINE_WIDTH, xOffset + w };
- int[] ys = new int[] { yOffset + h, yOffset + h + SPINE_HEIGHT,
- yOffset + h + SPINE_HEIGHT, yOffset + h };
- g.setColor(SPINE_COLOR_BOTTOM);
- g.fillPolygon(new Polygon(xs, ys, xs.length));
- xs = new int[] { xOffset + w, xOffset + w + SPINE_WIDTH,
- xOffset + w + SPINE_WIDTH, xOffset + w };
- ys = new int[] { yOffset, yOffset + SPINE_HEIGHT,
- yOffset + h + SPINE_HEIGHT, yOffset + h };
- g.setColor(SPINE_COLOR_RIGHT);
- g.fillPolygon(new Polygon(xs, ys, xs.length));
-
- Color color = getBackground(enabled, selected, hovered);
-
- g.setColor(color);
- g.fillRect(clip.x, clip.y, clip.width, clip.height);
-
- UIUtils.drawEllipse3D(g, UNCACHED_ICON_COLOR,
- COVER_WIDTH + HOFFSET + 30, 10, 20, 20, cached);
- }
-
- /**
- * The width of a cover image.
- *
- * @return the width
- */
- static public int getCoverWidth() {
- return SPINE_WIDTH + COVER_WIDTH;
- }
-
- /**
- * The height of a cover image.
- *
- * @return the height
- */
- static public int getCoverHeight() {
- return COVER_HEIGHT + HOFFSET;
- }
-
- /**
- * Generate a cover icon based upon the given {@link GuiReaderBookInfo}.
- *
- * @param lib
- * the library the meta comes from (can be NULL)
- * @param info
- * the {@link GuiReaderBookInfo}
- *
- * @return the image
- */
- static public java.awt.Image generateCoverImage(BasicLibrary lib,
- BookInfo info) {
- BufferedImage resizedImage = null;
- String id = getIconId(info);
-
- InputStream in = Instance.getInstance().getCache().getFromCache(id);
- if (in != null) {
- try {
- resizedImage = ImageUtilsAwt.fromImage(new Image(in));
- in.close();
- in = null;
- } catch (IOException e) {
- Instance.getInstance().getTraceHandler().error(e);
- }
- }
-
- if (resizedImage == null) {
- try {
- Image cover = null;
- if (info != null) {
- cover = info.getBaseImage(lib);
- }
-
- resizedImage = new BufferedImage(getCoverWidth(),
- getCoverHeight(), BufferedImage.TYPE_4BYTE_ABGR);
-
- Graphics2D g = resizedImage.createGraphics();
- try {
- if (info != null && info.supportsCover()) {
- g.setColor(Color.white);
- g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
-
- if (cover != null) {
- BufferedImage coverb = ImageUtilsAwt
- .fromImage(cover);
- g.drawImage(coverb, 0, HOFFSET, COVER_WIDTH,
- COVER_HEIGHT, null);
- } else {
- g.setColor(Color.black);
- g.drawLine(0, HOFFSET, COVER_WIDTH,
- HOFFSET + COVER_HEIGHT);
- g.drawLine(COVER_WIDTH, HOFFSET, 0,
- HOFFSET + COVER_HEIGHT);
- }
- }
- } finally {
- g.dispose();
- }
-
- // Only save image with a cover, not the X thing
- if (id != null && cover != null) {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- ImageIO.write(resizedImage, "png", out);
- byte[] imageBytes = out.toByteArray();
- in = new ByteArrayInputStream(imageBytes);
- Instance.getInstance().getCache().addToCache(in, id);
- in.close();
- in = null;
- }
- } catch (MalformedURLException e) {
- Instance.getInstance().getTraceHandler().error(e);
- } catch (IOException e) {
- Instance.getInstance().getTraceHandler().error(e);
- }
- }
-
- return resizedImage;
- }
-
- /**
- * Manually clear the icon set for this item.
- *
- * @param info
- * the info about the story or source/type or author
- */
- static public void clearIcon(BookInfo info) {
- String id = getIconId(info);
- Instance.getInstance().getCache().removeFromCache(id);
- }
-
- /**
- * Get a unique ID from this {@link GuiReaderBookInfo} (note that it can be
- * a story, a fake item for a source/type or a fake item for an author).
- *
- * @param info
- * the info or NULL for a generic (non unique!) ID
- * @return the unique ID
- */
- static private String getIconId(BookInfo info) {
- return (info == null ? "" : info.getId() + ".") + "book-thumb_"
- + SPINE_WIDTH + "x" + COVER_WIDTH + "+" + SPINE_HEIGHT + "+"
- + COVER_HEIGHT + "@" + HOFFSET;
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.book;
-
-import java.awt.print.Book;
-import java.io.IOException;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.bundles.StringIdGui;
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix.library.CacheLibrary;
-import be.nikiroo.utils.Image;
-import be.nikiroo.utils.StringUtils;
-
-/**
- * Some meta information related to a "book" (which can either be a
- * {@link Story}, a fake-story grouping some authors or a fake-story grouping
- * some sources/types).
- *
- * @author niki
- */
-public class BookInfo {
- /**
- * The type of {@link Book} (i.e., related to a story or to something else
- * that can encompass stories).
- *
- * @author niki
- */
- public enum Type {
- /** A normal story, which can be "read". */
- STORY,
- /**
- * A special, empty story that represents a source/type common to one or
- * more normal stories.
- */
- SOURCE,
- /** A special, empty story that represents an author. */
- AUTHOR,
- /** A special, empty story that represents a tag. **/
- TAG
- }
-
- private Type type;
- private String id;
- private String value;
- private String count;
-
- private boolean cached;
-
- private MetaData meta;
-
- /**
- * For private use; see the "fromXXX" constructors instead for public use.
- *
- * @param type
- * the type of book
- * @param id
- * the main id, which must uniquely identify this book and will
- * be used as a unique ID later on
- * @param value
- * the main value to show (see {@link BookInfo#getMainInfo()})
- */
- protected BookInfo(Type type, String id, String value) {
- this.type = type;
- this.id = id;
- this.value = value;
- }
-
- /**
- * The type of {@link BookInfo}.
- *
- * @return the type
- */
- public Type getType() {
- return type;
- }
-
- /**
- * Get the main info to display for this book (a title, an author, a
- * source/type name...).
- * <p>
- * Note that when {@link MetaData} about the book are present, the title
- * inside is returned instead of the actual value (that way, we can update
- * the {@link MetaData} and see the changes here).
- *
- * @return the main info, usually the title
- */
- public String getMainInfo() {
- if (meta != null) {
- return meta.getTitle();
- }
-
- return value;
- }
-
- /**
- * Get the secondary info, of the given type.
- *
- * @param seeCount
- * TRUE for word/image/story count, FALSE for author name
- *
- * @return the secondary info, never NULL
- */
- public String getSecondaryInfo(boolean seeCount) {
- String author = meta == null ? null : meta.getAuthor();
- String secondaryInfo = seeCount ? count : author;
-
- if (secondaryInfo != null && !secondaryInfo.trim().isEmpty()) {
- secondaryInfo = "(" + secondaryInfo + ")";
- } else {
- secondaryInfo = "";
- }
-
- return secondaryInfo;
- }
-
- /**
- * A unique ID for this {@link BookInfo}.
- *
- * @return the unique ID
- */
- public String getId() {
- return id;
- }
-
- /**
- * This item library cache state.
- *
- * @return TRUE if it is present in the {@link CacheLibrary} cache
- */
- public boolean isCached() {
- return cached;
- }
-
- /**
- * This item library cache state.
- *
- * @param cached
- * TRUE if it is present in the {@link CacheLibrary} cache
- */
- public void setCached(boolean cached) {
- this.cached = cached;
- }
-
- /**
- * The {@link MetaData} associated with this book, if this book is a
- * {@link Story}.
- * <p>
- * Can be NULL for non-story books (authors or sources/types).
- *
- * @return the {@link MetaData} or NULL
- */
- public MetaData getMeta() {
- return meta;
- }
-
- /**
- * Get the base image to use to represent this book.
- * <p>
- * The image is <b>NOT</b> resized in any way, this is the original version.
- * <p>
- * It can be NULL if no image can be found for this book.
- *
- * @param lib
- * the {@link BasicLibrary} to use to fetch the image (can be
- * NULL)
- *
- * @return the base image, or NULL if no library or no image
- *
- * @throws IOException
- * in case of I/O error
- */
- public Image getBaseImage(BasicLibrary lib) throws IOException {
- if (lib != null) {
- switch (type) {
- case STORY:
- if (meta.getCover() != null) {
- return meta.getCover();
- }
-
- if (meta.getLuid() != null) {
- return lib.getCover(meta.getLuid());
- }
-
- return null;
- case SOURCE:
- return lib.getSourceCover(value);
- case AUTHOR:
- return lib.getAuthorCover(value);
- case TAG:
- return null;
- }
- }
-
- return null;
- }
-
- /**
- * This {@link BookInfo} could have a cover (so we need to somehow represent
- * that to the user).
- *
- * @return TRUE if it does
- */
- public boolean supportsCover() {
- return type != Type.TAG;
- }
-
- /**
- * Create a new book describing the given {@link Story}.
- *
- * @param lib
- * the {@link BasicLibrary} to use to retrieve some more
- * information about the source
- * @param meta
- * the {@link MetaData} representing the {@link Story}
- *
- * @return the book
- */
- static public BookInfo fromMeta(BasicLibrary lib, MetaData meta) {
- String uid = meta.getUuid();
- if (uid == null || uid.trim().isEmpty()) {
- uid = meta.getLuid();
- }
- if (uid == null || uid.trim().isEmpty()) {
- uid = meta.getUrl();
- }
-
- BookInfo info = new BookInfo(Type.STORY, uid, meta.getTitle());
-
- info.meta = meta;
- info.count = StringUtils.formatNumber(meta.getWords());
- if (!info.count.isEmpty()) {
- info.count = Instance.getInstance().getTransGui().getString(
- meta.isImageDocument() ? StringIdGui.BOOK_COUNT_IMAGES
- : StringIdGui.BOOK_COUNT_WORDS,
- new Object[] { info.count });
- }
-
- if (lib instanceof CacheLibrary) {
- info.setCached(((CacheLibrary) lib).isCached(meta.getLuid()));
- } else {
- info.setCached(true);
- }
-
- return info;
- }
-
- /**
- * Create a new book describing the given source/type.
- *
- * @param lib
- * the {@link BasicLibrary} to use to retrieve some more
- * information about the source
- * @param source
- * the source name
- *
- * @return the book
- */
- static public BookInfo fromSource(BasicLibrary lib, String source) {
- BookInfo info = new BookInfo(Type.SOURCE,
- "source_" + (source == null ? "" : source), source);
-
- int size = 0;
- if (lib != null) {
- try {
- size = lib.getList().filter(source, null, null).size();
- } catch (IOException e) {
- }
- }
-
- info.count = StringUtils.formatNumber(size);
- if (!info.count.isEmpty()) {
- info.count = Instance.getInstance().getTransGui().getString(
- StringIdGui.BOOK_COUNT_STORIES,
- new Object[] { info.count });
- }
-
- return info;
- }
-
- /**
- * Create a new book describing the given author.
- *
- * @param lib
- * the {@link BasicLibrary} to use to retrieve some more
- * information about the author
- * @param author
- * the author name
- *
- * @return the book
- */
- static public BookInfo fromAuthor(BasicLibrary lib, String author) {
- BookInfo info = new BookInfo(Type.AUTHOR,
- "author_" + (author == null ? "" : author), author);
-
- int size = 0;
- if (lib != null) {
- try {
- size = lib.getList().filter(null, author, null).size();
- } catch (IOException e) {
- }
- }
-
- info.count = StringUtils.formatNumber(size);
- if (!info.count.isEmpty()) {
- info.count = Instance.getInstance().getTransGui().getString(
- StringIdGui.BOOK_COUNT_STORIES,
- new Object[] { info.count });
- }
-
- return info;
- }
-
- /**
- * Create a new book describing the given tag.
- *
- * @param lib
- * the {@link BasicLibrary} to use to retrieve some more
- * information about the tag
- * @param tag
- * the tag name
- *
- * @return the book
- */
- static public BookInfo fromTag(BasicLibrary lib, String tag) {
- BookInfo info = new BookInfo(Type.TAG,
- "tag_" + (tag == null ? "" : tag), tag);
-
- int size = 0;
- try {
- size = lib.getList().filter(null, null, tag).size();
- } catch (IOException e) {
- }
-
- info.count = StringUtils.formatNumber(size);
- if (!info.count.isEmpty()) {
- info.count = Instance.getInstance().getTransGui().getString(
- StringIdGui.BOOK_COUNT_STORIES,
- new Object[] { info.count });
- }
-
- return info;
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.book;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Graphics;
-
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.SwingConstants;
-
-import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix_swing.gui.BooksPanel;
-
-/**
- * A book item presented in a {@link BooksPanel}.
- * <p>
- * Can be a story, or a comic or... a group.
- *
- * @author niki
- */
-public class BookLine extends JPanel {
- private static final long serialVersionUID = 1L;
-
- private static final int MAX_DISPLAY_SIZE = 40;
-
- /** Colour used for the seconday item (author/word count). */
- protected static final Color AUTHOR_COLOR = new Color(128, 128, 128);
-
- private boolean selected;
- private boolean hovered;
-
- private BookInfo info;
- private boolean seeWordCount;
-
- private JLabel title;
- private JLabel secondary;
- private JLabel iconCached;
- private JLabel iconNotCached;
-
- /**
- * Create a new {@link BookLine} item for the given {@link Story}.
- *
- * @param info
- * the information about the story to represent
- * @param seeWordCount
- * TRUE to see word counts, FALSE to see authors
- */
- public BookLine(BookInfo info, boolean seeWordCount) {
- this.info = info;
- this.seeWordCount = seeWordCount;
-
- init();
- }
-
- /**
- * Initialise this {@link BookLine}.
- */
- protected void init() {
- iconCached = new JLabel(" ◉ ");
- iconNotCached = new JLabel(" ○ ");
-
- iconNotCached.setForeground(BookCoverImager.UNCACHED_ICON_COLOR);
- iconCached.setForeground(BookCoverImager.UNCACHED_ICON_COLOR);
- iconCached.setPreferredSize(iconNotCached.getPreferredSize());
-
- title = new JLabel();
- secondary = new JLabel();
- secondary.setForeground(AUTHOR_COLOR);
-
- String luid = null;
- if (info.getMeta() != null) {
- luid = info.getMeta().getLuid();
- }
- JLabel id = new JLabel(luid);
- id.setPreferredSize(new JLabel(" 999 ").getPreferredSize());
- id.setForeground(Color.gray);
- id.setHorizontalAlignment(SwingConstants.CENTER);
-
- JPanel idTitle = new JPanel(new BorderLayout());
- idTitle.setOpaque(false);
- idTitle.add(id, BorderLayout.WEST);
- idTitle.add(title, BorderLayout.CENTER);
-
- setLayout(new BorderLayout());
- add(idTitle, BorderLayout.CENTER);
- add(secondary, BorderLayout.EAST);
-
- updateMeta();
- }
-
- /**
- * The book current selection state.
- *
- * @return the selection state
- */
- public boolean isSelected() {
- return selected;
- }
-
- /**
- * The book current selection state,
- *
- * @param selected
- * TRUE if it is selected
- */
- public void setSelected(boolean selected) {
- if (this.selected != selected) {
- this.selected = selected;
- repaint();
- }
- }
-
- /**
- * The item mouse-hover state.
- *
- * @return TRUE if it is mouse-hovered
- */
- public boolean isHovered() {
- return this.hovered;
- }
-
- /**
- * The item mouse-hover state.
- *
- * @param hovered
- * TRUE if it is mouse-hovered
- */
- public void setHovered(boolean hovered) {
- if (this.hovered != hovered) {
- this.hovered = hovered;
- repaint();
- }
- }
-
- /**
- * The secondary value content: word count or author.
- *
- * @return TRUE to see word counts, FALSE to see authors
- */
- public boolean isSeeWordCount() {
- return seeWordCount;
- }
-
- /**
- * The secondary value content: word count or author.
- *
- * @param seeWordCount
- * TRUE to see word counts, FALSE to see authors
- */
- public void setSeeWordCount(boolean seeWordCount) {
- if (this.seeWordCount != seeWordCount) {
- this.seeWordCount = seeWordCount;
- repaint();
- }
- }
-
- /**
- * The information about the book represented by this item.
- *
- * @return the meta
- */
- public BookInfo getInfo() {
- return info;
- }
-
- /**
- * Update the title, paint the item.
- */
- @Override
- public void paint(Graphics g) {
- updateMeta();
- super.paint(g);
- }
-
- /**
- * Return a display-ready version of {@link BookInfo#getMainInfo()}.
- *
- * @return the main info in a ready-to-display version
- */
- protected String getMainInfoDisplay() {
- return toDisplay(getInfo().getMainInfo());
- }
-
- /**
- * Return a display-ready version of
- * {@link BookInfo#getSecondaryInfo(boolean)}.
- *
- * @param seeCount
- * TRUE for word/image/story count, FALSE for author name
- *
- * @return the main info in a ready-to-display version
- */
- protected String getSecondaryInfoDisplay(boolean seeCount) {
- return toDisplay(getInfo().getSecondaryInfo(seeCount));
- }
-
- /**
- * Update the title with the currently registered information.
- */
- protected void updateMeta() {
- String main = getMainInfoDisplay();
- String optSecondary = getSecondaryInfoDisplay(isSeeWordCount());
-
- title.setText(main);
- secondary.setText(optSecondary + " ");
-
- setBackground(BookCoverImager.getBackground(isEnabled(), isSelected(),
- isHovered()));
-
- remove(iconCached);
- remove(iconNotCached);
- add(getInfo().isCached() ? iconCached : iconNotCached,
- BorderLayout.WEST);
- validate();
- }
-
- /**
- * Make the given {@link String} display-ready (i.e., shorten it if it is
- * too long).
- *
- * @param value
- * the full value
- *
- * @return the display-ready value
- */
- private String toDisplay(String value) {
- if (value == null)
- value = "";
-
- if (value.length() > MAX_DISPLAY_SIZE) {
- value = value.substring(0, MAX_DISPLAY_SIZE - 3) + "...";
- }
-
- return value;
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.book;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.KeyEvent;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import javax.swing.JFileChooser;
-import javax.swing.JFrame;
-import javax.swing.JMenu;
-import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
-import javax.swing.JPopupMenu;
-import javax.swing.SwingWorker;
-import javax.swing.filechooser.FileFilter;
-import javax.swing.filechooser.FileNameExtensionFilter;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.bundles.Config;
-import be.nikiroo.fanfix.bundles.StringIdGui;
-import be.nikiroo.fanfix.bundles.UiConfig;
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix.library.BasicLibrary.Status;
-import be.nikiroo.fanfix.output.BasicOutput.OutputType;
-import be.nikiroo.fanfix_swing.Actions;
-import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
-import be.nikiroo.utils.Progress;
-import be.nikiroo.utils.ui.ConfigEditor;
-
-public class BookPopup extends JPopupMenu {
- public abstract interface Informer {
-
- // not null
- public List<BookInfo> getSelected();
-
- public void setCached(BookInfo book, boolean cached);
-
- public BookInfo getUniqueSelected();
-
- public void fireElementChanged(BookInfo book);
-
- public void invalidateCache();
- }
-
- /**
- * The different modification actions you can use on {@link Story} items.
- *
- * @author niki
- */
- private enum ChangeAction {
- /** Change the source/type, that is, move it to another source. */
- SOURCE,
- /** Change its name. */
- TITLE,
- /** Change its author. */
- AUTHOR
- }
-
- // be careful with that
- private BasicLibrary lib;
-
- private Informer informer;
-
- public BookPopup(BasicLibrary lib, Informer informer) {
- this.lib = lib;
- this.informer = informer;
-
- Status status = lib.getStatus();
- add(createMenuItemOpenBook());
- addSeparator();
- add(createMenuItemExport());
- if (status.isWritable()) {
- add(createMenuItemMoveTo());
- add(createMenuItemSetCoverForSource());
- add(createMenuItemSetCoverForAuthor());
- }
- add(createMenuItemDownloadToCache());
- add(createMenuItemClearCache());
- if (status.isWritable()) {
- add(createMenuItemRedownload());
- addSeparator();
- add(createMenuItemRename());
- add(createMenuItemSetAuthor());
- addSeparator();
- add(createMenuItemDelete());
- }
- addSeparator();
- add(createMenuItemProperties());
- }
-
- private String trans(StringIdGui id) {
- return Instance.getInstance().getTransGui().getString(id);
- }
-
- /**
- * Create the Fanfix Configuration menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemConfig() {
- final String title = trans(StringIdGui.TITLE_CONFIG);
- JMenuItem item = new JMenuItem(title);
- item.setMnemonic(KeyEvent.VK_F);
-
- item.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- ConfigEditor<Config> ed = new ConfigEditor<Config>(Config.class,
- Instance.getInstance().getConfig(),
- trans(StringIdGui.SUBTITLE_CONFIG));
- JFrame frame = new JFrame(title);
- frame.add(ed);
- frame.setSize(850, 600);
- frame.setVisible(true);
- }
- });
-
- return item;
- }
-
- /**
- * Create the UI Configuration menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemUiConfig() {
- final String title = trans(StringIdGui.TITLE_CONFIG_UI);
- JMenuItem item = new JMenuItem(title);
- item.setMnemonic(KeyEvent.VK_U);
-
- item.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- ConfigEditor<UiConfig> ed = new ConfigEditor<UiConfig>(
- UiConfig.class, Instance.getInstance().getUiConfig(),
- trans(StringIdGui.SUBTITLE_CONFIG_UI));
- JFrame frame = new JFrame(title);
- frame.add(ed);
- frame.setSize(800, 600);
- frame.setVisible(true);
- }
- });
-
- return item;
- }
-
- /**
- * Create the export menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemExport() {
-
- // TODO: allow dir for multiple selection?
-
- final JFileChooser fc = new JFileChooser();
- fc.setAcceptAllFileFilterUsed(false);
-
- // Add the "ALL" filters first, then the others
- final Map<FileFilter, OutputType> otherFilters = new HashMap<FileFilter, OutputType>();
- for (OutputType type : OutputType.values()) {
- String ext = type.getDefaultExtension(false);
- String desc = type.getDesc(false);
-
- if (ext == null || ext.isEmpty()) {
- fc.addChoosableFileFilter(createAllFilter(desc));
- } else {
- otherFilters.put(new FileNameExtensionFilter(desc, ext), type);
- }
- }
-
- for (Entry<FileFilter, OutputType> entry : otherFilters.entrySet()) {
- fc.addChoosableFileFilter(entry.getKey());
- }
- //
-
- JMenuItem export = new JMenuItem(trans(StringIdGui.MENU_FILE_EXPORT),
- KeyEvent.VK_S);
- export.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- final BookInfo book = informer.getUniqueSelected();
- if (book != null) {
- fc.showDialog(BookPopup.this.getParent(),
- trans(StringIdGui.TITLE_SAVE));
- if (fc.getSelectedFile() != null) {
- final OutputType type = otherFilters
- .get(fc.getFileFilter());
- final String path = fc.getSelectedFile()
- .getAbsolutePath()
- + type.getDefaultExtension(false);
- final Progress pg = new Progress();
-
- new SwingWorker<Void, Void>() {
- @Override
- protected Void doInBackground() throws Exception {
- lib.export(book.getMeta().getLuid(), type, path,
- pg);
- return null;
- }
-
- @Override
- protected void done() {
- try {
- get();
- } catch (Exception e) {
- UiHelper.error(BookPopup.this.getParent(),
- e.getLocalizedMessage(),
- "IOException", e);
- }
- }
- }.execute();
- }
- }
- }
- });
-
- return export;
- }
-
- /**
- * Create a {@link FileFilter} that accepts all files and return the given
- * description.
- *
- * @param desc
- * the description
- *
- * @return the filter
- */
- private FileFilter createAllFilter(final String desc) {
- return new FileFilter() {
- @Override
- public String getDescription() {
- return desc;
- }
-
- @Override
- public boolean accept(File f) {
- return true;
- }
- };
- }
-
- /**
- * Create the refresh (delete cache) menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemClearCache() {
- JMenuItem refresh = new JMenuItem(
- trans(StringIdGui.MENU_EDIT_CLEAR_CACHE), KeyEvent.VK_C);
- refresh.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- final List<BookInfo> selected = informer.getSelected();
- if (!selected.isEmpty()) {
- new SwingWorker<Void, Void>() {
- @Override
- protected Void doInBackground() throws Exception {
- for (BookInfo book : selected) {
- lib.clearFromCache(book.getMeta().getLuid());
- BookCoverImager.clearIcon(book);
- }
- return null;
- }
-
- @Override
- protected void done() {
- try {
- get();
- for (BookInfo book : selected) {
- informer.setCached(book, false);
- }
- } catch (Exception e) {
- UiHelper.error(BookPopup.this.getParent(),
- e.getLocalizedMessage(), "IOException",
- e);
- }
- }
- }.execute();
- }
- }
- });
-
- return refresh;
- }
-
- /**
- * Create the "move to" menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemMoveTo() {
- JMenu changeTo = new JMenu(trans(StringIdGui.MENU_FILE_MOVE_TO));
- changeTo.setMnemonic(KeyEvent.VK_M);
-
- Map<String, List<String>> groupedSources = new HashMap<String, List<String>>();
- try {
- groupedSources = lib.getSourcesGrouped();
- } catch (IOException e) {
- UiHelper.error(BookPopup.this.getParent(), e.getLocalizedMessage(),
- "IOException", e);
- }
-
- JMenuItem item = new JMenuItem(
- trans(StringIdGui.MENU_FILE_MOVE_TO_NEW_TYPE));
- item.addActionListener(createMoveAction(ChangeAction.SOURCE, null));
- changeTo.add(item);
- changeTo.addSeparator();
-
- for (final String type : groupedSources.keySet()) {
- List<String> list = groupedSources.get(type);
- if (list.size() == 1 && list.get(0).isEmpty()) {
- item = new JMenuItem(type);
- item.addActionListener(
- createMoveAction(ChangeAction.SOURCE, type));
- changeTo.add(item);
- } else {
- JMenu dir = new JMenu(type);
- for (String sub : list) {
- // " " instead of "" for the visual height
- String itemName = sub.isEmpty() ? " " : sub;
- String actualType = type;
- if (!sub.isEmpty()) {
- actualType += "/" + sub;
- }
-
- item = new JMenuItem(itemName);
- item.addActionListener(
- createMoveAction(ChangeAction.SOURCE, actualType));
- dir.add(item);
- }
- changeTo.add(dir);
- }
- }
-
- return changeTo;
- }
-
- /**
- * Create the "set author" menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemSetAuthor() {
- JMenu changeTo = new JMenu(trans(StringIdGui.MENU_FILE_SET_AUTHOR));
- changeTo.setMnemonic(KeyEvent.VK_A);
-
- // New author
- JMenuItem newItem = new JMenuItem(
- trans(StringIdGui.MENU_FILE_MOVE_TO_NEW_AUTHOR));
- changeTo.add(newItem);
- changeTo.addSeparator();
- newItem.addActionListener(createMoveAction(ChangeAction.AUTHOR, null));
-
- // Existing authors
- Map<String, List<String>> groupedAuthors;
-
- try {
- groupedAuthors = lib.getAuthorsGrouped();
- } catch (IOException e) {
- UiHelper.error(BookPopup.this.getParent(), e.getLocalizedMessage(),
- "IOException", e);
- groupedAuthors = new HashMap<String, List<String>>();
-
- }
-
- if (groupedAuthors.size() > 1) {
- for (String key : groupedAuthors.keySet()) {
- JMenu group = new JMenu(key);
- for (String value : groupedAuthors.get(key)) {
- JMenuItem item = new JMenuItem(value.isEmpty()
- ? trans(StringIdGui.MENU_AUTHORS_UNKNOWN)
- : value);
- item.addActionListener(
- createMoveAction(ChangeAction.AUTHOR, value));
- group.add(item);
- }
- changeTo.add(group);
- }
- } else if (groupedAuthors.size() == 1) {
- for (String value : groupedAuthors.values().iterator().next()) {
- JMenuItem item = new JMenuItem(value.isEmpty()
- ? trans(StringIdGui.MENU_AUTHORS_UNKNOWN)
- : value);
- item.addActionListener(
- createMoveAction(ChangeAction.AUTHOR, value));
- changeTo.add(item);
- }
- }
-
- return changeTo;
- }
-
- /**
- * Create the "rename" menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemRename() {
- JMenuItem changeTo = new JMenuItem(trans(StringIdGui.MENU_FILE_RENAME));
- changeTo.setMnemonic(KeyEvent.VK_R);
- changeTo.addActionListener(createMoveAction(ChangeAction.TITLE, null));
- return changeTo;
- }
-
- private ActionListener createMoveAction(final ChangeAction what,
- final String type) {
- return new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- final List<BookInfo> selected = informer.getSelected();
- if (!selected.isEmpty()) {
- String changeTo = type;
- if (type == null) {
- String init = "";
-
- if (selected.size() == 1) {
- MetaData meta = selected.get(0).getMeta();
- if (what == ChangeAction.SOURCE) {
- init = meta.getSource();
- } else if (what == ChangeAction.TITLE) {
- init = meta.getTitle();
- } else if (what == ChangeAction.AUTHOR) {
- init = meta.getAuthor();
- }
- }
-
- Object rep = JOptionPane.showInputDialog(
- BookPopup.this.getParent(),
- trans(StringIdGui.SUBTITLE_MOVE_TO),
- trans(StringIdGui.TITLE_MOVE_TO),
- JOptionPane.QUESTION_MESSAGE, null, null, init);
-
- if (rep == null) {
- return;
- }
-
- changeTo = rep.toString();
- }
-
- final String fChangeTo = changeTo;
- new SwingWorker<Void, Void>() {
- @Override
- protected Void doInBackground() throws Exception {
- for (BookInfo book : selected) {
- String luid = book.getMeta().getLuid();
- if (what == ChangeAction.SOURCE) {
- lib.changeSource(luid, fChangeTo, null);
- } else if (what == ChangeAction.TITLE) {
- lib.changeTitle(luid, fChangeTo, null);
- } else if (what == ChangeAction.AUTHOR) {
- lib.changeAuthor(luid, fChangeTo, null);
- }
- }
-
- return null;
- }
-
- @Override
- protected void done() {
- try {
- // this can create new sources/authors, so a
- // simple fireElementChanged is not
- // enough, we need to clear the whole cache (for
- // BrowserPanel for instance)
- informer.invalidateCache();
-
- // But we ALSO fire those, because they appear
- // before the whole refresh...
- for (BookInfo book : selected) {
- informer.fireElementChanged(book);
- }
-
- // TODO: also refresh the
- // Sources/Authors(/Tags?) list
-
- // Even if problems occurred, still invalidate
- // the cache
- get();
- } catch (Exception e) {
- UiHelper.error(BookPopup.this.getParent(),
- e.getLocalizedMessage(), "IOException",
- e);
- }
- }
- }.execute();
- }
- }
- };
- }
-
- /**
- * Create the re-download (then delete original) menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemRedownload() {
- JMenuItem refresh = new JMenuItem(
- trans(StringIdGui.MENU_EDIT_REDOWNLOAD), KeyEvent.VK_R);
- refresh.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- // final GuiReaderBook selectedBook =
- // mainPanel.getSelectedBook();
- // if (selectedBook != null) {
- // final MetaData meta = selectedBook.getInfo().getMeta();
- // mainPanel.imprt(meta.getUrl(), new MetaDataRunnable() {
- // @Override
- // public void run(MetaData newMeta) {
- // if (!newMeta.getSource().equals(meta.getSource())) {
- // reader.changeSource(newMeta.getLuid(), meta.getSource());
- // }
- // }
- // }, trans(StringIdGui.PROGRESS_CHANGE_SOURCE));
- // }
- }
- });
-
- return refresh;
- }
-
- /**
- * Create the download to cache menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemDownloadToCache() {
- JMenuItem refresh = new JMenuItem(
- trans(StringIdGui.MENU_EDIT_DOWNLOAD_TO_CACHE), KeyEvent.VK_T);
- refresh.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- final List<BookInfo> selected = informer.getSelected();
-
- new SwingWorker<Void, Void>() {
- @Override
- protected Void doInBackground() throws Exception {
-
- final List<String> luids = new LinkedList<String>();
- for (BookInfo book : selected) {
- switch (book.getType()) {
- case STORY:
- luids.add(book.getMeta().getLuid());
- break;
- case SOURCE:
- for (MetaData meta : lib.getList().filter(
- book.getMainInfo(), null, null)) {
- luids.add(meta.getLuid());
- }
- break;
- case AUTHOR:
- for (MetaData meta : lib.getList().filter(null,
- book.getMainInfo(), null)) {
- luids.add(meta.getLuid());
- }
- break;
- case TAG:
- for (MetaData meta : lib.getList().filter(null,
- null, book.getMainInfo())) {
- luids.add(meta.getLuid());
- }
- break;
- }
- }
-
- // TODO: do something with pg?
- final Progress pg = new Progress();
- pg.setMax(luids.size());
- for (String luid : luids) {
- Progress pgStep = new Progress();
- pg.addProgress(pgStep, 1);
-
- lib.getFile(luid, pgStep);
- }
-
- return null;
- }
-
- @Override
- protected void done() {
- try {
- get();
- for (BookInfo book : selected) {
- informer.setCached(book, true);
- }
- } catch (Exception e) {
- UiHelper.error(BookPopup.this.getParent(),
- e.getLocalizedMessage(), "IOException", e);
- }
- }
- }.execute();
- }
- });
-
- return refresh;
- }
-
- /**
- * Create the delete menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemDelete() {
- JMenuItem delete = new JMenuItem(trans(StringIdGui.MENU_EDIT_DELETE),
- KeyEvent.VK_D);
- delete.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- // final GuiReaderBook selectedBook =
- // mainPanel.getSelectedBook();
- // if (selectedBook != null && selectedBook.getInfo().getMeta()
- // != null) {
- //
- // final MetaData meta = selectedBook.getInfo().getMeta();
- // int rep = JOptionPane.showConfirmDialog(GuiReaderFrame.this,
- // trans(StringIdGui.SUBTITLE_DELETE, meta.getLuid(),
- // meta.getTitle()),
- // trans(StringIdGui.TITLE_DELETE),
- // JOptionPane.OK_CANCEL_OPTION);
- //
- // if (rep == JOptionPane.OK_OPTION) {
- // mainPanel.outOfUi(null, true, new Runnable() {
- // @Override
- // public void run() {
- // reader.delete(meta.getLuid());
- // mainPanel.unsetSelectedBook();
- // }
- // });
- // }
- // }
- }
- });
-
- return delete;
- }
-
- /**
- * Create the properties menu item.
- *
- * @return the item
- */
- private JMenuItem createMenuItemProperties() {
- JMenuItem delete = new JMenuItem(
- trans(StringIdGui.MENU_FILE_PROPERTIES), KeyEvent.VK_P);
- delete.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- // final GuiReaderBook selectedBook =
- // mainPanel.getSelectedBook();
- // if (selectedBook != null) {
- // mainPanel.outOfUi(null, false, new Runnable() {
- // @Override
- // public void run() {
- // new GuiReaderPropertiesFrame(lib,
- // selectedBook.getInfo().getMeta())
- // .setVisible(true);
- // }
- // });
- // }
- }
- });
-
- return delete;
- }
-
- /**
- * Create the open menu item for a book, a source/type or an author.
- *
- * @return the item
- */
- public JMenuItem createMenuItemOpenBook() {
- JMenuItem open = new JMenuItem(trans(StringIdGui.MENU_FILE_OPEN),
- KeyEvent.VK_O);
- open.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- final BookInfo book = informer.getUniqueSelected();
- if (book != null) {
- Actions.openExternal(lib, book.getMeta(),
- BookPopup.this.getParent(), new Runnable() {
- @Override
- public void run() {
- informer.setCached(book, true);
- }
- });
- }
- }
- });
-
- return open;
- }
-
- /**
- * Create the SetCover menu item for a book to change the linked source
- * cover.
- *
- * @return the item
- */
- private JMenuItem createMenuItemSetCoverForSource() {
- JMenuItem open = new JMenuItem(
- trans(StringIdGui.MENU_EDIT_SET_COVER_FOR_SOURCE),
- KeyEvent.VK_C);
- open.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent ae) {
- // final GuiReaderBook selectedBook =
- // mainPanel.getSelectedBook();
- // if (selectedBook != null) {
- // BasicLibrary lib = lib;
- // String luid = selectedBook.getInfo().getMeta().getLuid();
- // String source = selectedBook.getInfo().getMeta().getSource();
- //
- // try {
- // lib.setSourceCover(source, luid);
- // } catch (IOException e) {
- // error(e.getLocalizedMessage(), "IOException", e);
- // }
- //
- // GuiReaderBookInfo sourceInfo =
- // GuiReaderBookInfo.fromSource(lib, source);
- // GuiReaderCoverImager.clearIcon(sourceInfo);
- // }
- }
- });
-
- return open;
- }
-
- /**
- * Create the SetCover menu item for a book to change the linked source
- * cover.
- *
- * @return the item
- */
- private JMenuItem createMenuItemSetCoverForAuthor() {
- JMenuItem open = new JMenuItem(
- trans(StringIdGui.MENU_EDIT_SET_COVER_FOR_AUTHOR),
- KeyEvent.VK_A);
- open.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent ae) {
- // final GuiReaderBook selectedBook =
- // mainPanel.getSelectedBook();
- // if (selectedBook != null) {
- // String luid = selectedBook.getInfo().getMeta().getLuid();
- // String author = selectedBook.getInfo().getMeta().getAuthor();
- //
- // try {
- // lib.setAuthorCover(author, luid);
- // } catch (IOException e) {
- // error(e.getLocalizedMessage(), "IOException", e);
- // }
- //
- // GuiReaderBookInfo authorInfo =
- // GuiReaderBookInfo.fromAuthor(lib, author);
- // GuiReaderCoverImager.clearIcon(authorInfo);
- // }
- }
- });
-
- return open;
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.browser;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.swing.tree.DefaultMutableTreeNode;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.bundles.StringIdGui;
-
-public class AuthorTab extends BasicTab<List<String>> {
- public AuthorTab(int index, String listenerCommand) {
- super(index, listenerCommand);
- }
-
- @Override
- protected List<String> createEmptyData() {
- return new ArrayList<String>();
- }
-
- @Override
- protected void fillData(List<String> data) {
- data.clear();
- try {
- List<String> authors = Instance.getInstance().getLibrary()
- .getAuthors();
- for (String author : authors) {
- data.add(author);
- }
-
- sort(data);
- } catch (Exception e) {
- // TODO
- e.printStackTrace();
- }
- }
-
- @Override
- protected String keyToElement(String key) {
- return key;
- }
-
- @Override
- protected String keyToDisplay(String key) {
- if (key.trim().isEmpty()) {
- key = Instance.getInstance().getTransGui()
- .getString(StringIdGui.MENU_AUTHORS_UNKNOWN);
- }
-
- return key;
- }
-
- @Override
- protected int loadData(DefaultMutableTreeNode root, List<String> authors,
- String filter) {
- for (String author : authors) {
- if (checkFilter(filter, author)) {
- DefaultMutableTreeNode sourceNode = new DefaultMutableTreeNode(
- author);
- root.add(sourceNode);
- }
- }
-
- return authors.size();
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.browser;
-
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-
-import javax.swing.JTree;
-import javax.swing.SwingWorker;
-import javax.swing.event.TreeSelectionEvent;
-import javax.swing.event.TreeSelectionListener;
-import javax.swing.plaf.basic.BasicTreeUI;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.DefaultTreeCellRenderer;
-import javax.swing.tree.DefaultTreeModel;
-import javax.swing.tree.TreeCellRenderer;
-import javax.swing.tree.TreePath;
-
-import be.nikiroo.fanfix_swing.gui.SearchBar;
-import be.nikiroo.fanfix_swing.gui.utils.ListenerPanel;
-import be.nikiroo.fanfix_swing.gui.utils.TreeCellSpanner;
-import be.nikiroo.fanfix_swing.gui.utils.TreeSnapshot;
-import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
-import be.nikiroo.fanfix_swing.images.IconGenerator;
-import be.nikiroo.fanfix_swing.images.IconGenerator.Icon;
-import be.nikiroo.fanfix_swing.images.IconGenerator.Size;
-
-public abstract class BasicTab<T> extends ListenerPanel {
- private int totalCount = 0;
- private List<String> selectedElements = new ArrayList<String>();
- private T data;
- private String baseTitle;
- private String listenerCommand;
- private int index;
-
- private JTree tree;
- private DefaultMutableTreeNode root;
- private SearchBar searchBar;
-
- public BasicTab(int index, String listenerCommand) {
- setLayout(new BorderLayout());
-
- this.index = index;
- this.listenerCommand = listenerCommand;
-
- data = createEmptyData();
- totalCount = 0;
-
- root = new DefaultMutableTreeNode();
-
- tree = new JTree(root);
- tree.setUI(new BasicTreeUI());
- TreeCellSpanner spanner = new TreeCellSpanner(tree,
- generateCellRenderer());
- tree.setCellRenderer(spanner);
- tree.setRootVisible(false);
- tree.setShowsRootHandles(false);
-
- tree.addTreeSelectionListener(new TreeSelectionListener() {
- @Override
- public void valueChanged(TreeSelectionEvent e) {
- List<String> elements = new ArrayList<String>();
- TreePath[] paths = tree.getSelectionPaths();
- if (paths != null) {
- for (TreePath path : paths) {
- String key = path.getLastPathComponent().toString();
- elements.add(keyToElement(key));
- }
- }
-
- List<String> selectedElements = new ArrayList<String>();
- for (String element : elements) {
- if (!selectedElements.contains(element)) {
- selectedElements.add(element);
- }
- }
-
- BasicTab.this.selectedElements = selectedElements;
-
- fireActionPerformed(BasicTab.this.listenerCommand);
- }
- });
-
- add(UiHelper.scroll(tree), BorderLayout.CENTER);
-
- searchBar = new SearchBar();
- add(searchBar, BorderLayout.NORTH);
- searchBar.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent e) {
- reloadData();
- }
- });
-
- reloadData();
- }
-
- public void reloadData() {
- final TreeSnapshot snapshot = new TreeSnapshot(tree) {
- @Override
- protected boolean isSamePath(TreePath oldPath, TreePath newPath) {
- String oldString = oldPath.toString();
- if (oldString.endsWith("/]"))
- oldString = oldString.substring(0, oldString.length() - 2)
- + "]";
-
- String newString = newPath.toString();
- if (newString.endsWith("/]"))
- newString = newString.substring(0, newString.length() - 2)
- + "]";
-
- return oldString.equals(newString);
- }
- };
- SwingWorker<Map<String, List<String>>, Integer> worker = new SwingWorker<Map<String, List<String>>, Integer>() {
- @Override
- protected Map<String, List<String>> doInBackground()
- throws Exception {
- fillData(data);
- return null;
- }
-
- @Override
- protected void done() {
- try {
- get();
- } catch (Exception e) {
- // TODO: error
- }
-
- root.removeAllChildren();
- totalCount = loadData(root, data, searchBar.getText());
- ((DefaultTreeModel) tree.getModel()).reload();
-
- snapshot.apply();
-
- fireActionPerformed(listenerCommand);
- }
- };
- worker.execute();
- }
-
- /**
- * The currently selected elements, or an empty list.
- *
- * @return the sources (cannot be NULL)
- */
- public List<String> getSelectedElements() {
- return selectedElements;
- }
-
- public int getTotalCount() {
- return totalCount;
- }
-
- public String getBaseTitle() {
- return baseTitle;
- }
-
- public void setBaseTitle(String baseTitle) {
- this.baseTitle = baseTitle;
- }
-
- public String getTitle() {
- String title = getBaseTitle();
- String count = "";
- if (totalCount > 0) {
- int selected = selectedElements.size();
- count = " (" + (selected > 0 ? selected + "/" : "") + totalCount
- + ")";
- }
-
- return title + count;
- }
-
- public int getIndex() {
- return index;
- }
-
- public void unselect() {
- tree.clearSelection();
- }
-
- protected boolean checkFilter(String filter, String value) {
- return (filter == null || filter.isEmpty()
- || value.toLowerCase().contains(filter.toLowerCase()));
- }
-
- protected boolean checkFilter(String filter, List<String> list) {
- for (String value : list) {
- if (checkFilter(filter, value))
- return true;
- }
- return false;
- }
-
- protected abstract T createEmptyData();
-
- // beware: you should update it OR clean/re-add it, but previous data may
- // still be there
- protected abstract void fillData(T data);
-
- protected abstract String keyToElement(String key);
-
- protected abstract String keyToDisplay(String key);
-
- protected abstract int loadData(DefaultMutableTreeNode root, T data,
- String filter);
-
- protected void sort(List<String> values) {
- Collections.sort(values, new Comparator<String>() {
- @Override
- public int compare(String o1, String o2) {
- return ("" + o1).compareToIgnoreCase("" + o2);
- }
- });
- }
-
- private TreeCellRenderer generateCellRenderer() {
- DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer() {
- @Override
- public Component getTreeCellRendererComponent(JTree tree,
- Object value, boolean selected, boolean expanded,
- boolean leaf, int row, boolean hasFocus) {
- if (value instanceof DefaultMutableTreeNode) {
- if (((DefaultMutableTreeNode) value).getLevel() > 1) {
- setLeafIcon(null);
- setLeafIcon(IconGenerator.get(Icon.empty, Size.x4));
- } else {
- setLeafIcon(IconGenerator.get(Icon.empty, Size.x16));
- }
- }
-
- String display = value == null ? "" : value.toString();
- display = keyToDisplay(display);
-
- return super.getTreeCellRendererComponent(tree, display,
- selected, expanded, leaf, row, hasFocus);
- }
- };
-
- renderer.setClosedIcon(IconGenerator.get(Icon.arrow_right, Size.x16));
- renderer.setOpenIcon(IconGenerator.get(Icon.arrow_down, Size.x16));
- renderer.setLeafIcon(IconGenerator.get(Icon.empty, Size.x16));
-
- return renderer;
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.browser;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.swing.tree.DefaultMutableTreeNode;
-
-import be.nikiroo.fanfix.Instance;
-
-public class SourceTab extends BasicTab<Map<String, List<String>>> {
- public SourceTab(int index, String listenerCommand) {
- super(index, listenerCommand);
- }
-
- @Override
- protected Map<String, List<String>> createEmptyData() {
- return new HashMap<String, List<String>>();
- }
-
- @Override
- protected void fillData(Map<String, List<String>> data) {
- data.clear();
- try {
- Map<String, List<String>> sourcesGrouped = Instance.getInstance()
- .getLibrary().getSourcesGrouped();
- for (String group : sourcesGrouped.keySet()) {
- data.put(group, sourcesGrouped.get(group));
- }
- } catch (Exception e) {
- // TODO
- e.printStackTrace();
- }
- }
-
- @Override
- protected String keyToElement(String key) {
- return key.substring(1);
- }
-
- @Override
- protected String keyToDisplay(String key) {
- if (key.trim().isEmpty()) {
- return "[*]"; // Root node
- }
-
- // Get and remove type
- String type = key.substring(0, 1);
- key = key.substring(1);
-
- if (!type.equals(">")) {
- // Only display the final name
- int pos = key.toString().lastIndexOf("/");
- if (pos >= 0) {
- key = key.toString().substring(pos + 1);
- }
- }
-
- if (key.toString().isEmpty()) {
- key = " ";
- }
-
- return key;
- }
-
- @Override
- protected int loadData(DefaultMutableTreeNode root,
- Map<String, List<String>> sourcesGrouped, String filter) {
- int count = 0;
- List<String> sources = new ArrayList<String>(sourcesGrouped.keySet());
- sort(sources);
- for (String source : sources) {
- if (checkFilter(filter, source)
- || checkFilter(filter, sourcesGrouped.get(source))) {
- List<String> children = sourcesGrouped.get(source);
- boolean hasChildren = (children.size() > 1)
- || (children.size() == 1
- && !children.get(0).trim().isEmpty());
- DefaultMutableTreeNode sourceNode = new DefaultMutableTreeNode(
- ">" + source + (hasChildren ? "/" : ""));
- root.add(sourceNode);
- sort(children);
- for (String subSource : children) {
- if (checkFilter(filter, source)
- || checkFilter(filter, subSource)) {
- count = count + 1;
- if (subSource.isEmpty()
- && sourcesGrouped.get(source).size() > 1) {
- sourceNode.add(
- new DefaultMutableTreeNode(" " + source));
- } else if (!subSource.isEmpty()) {
- sourceNode.add(new DefaultMutableTreeNode(
- " " + source + "/" + subSource));
- }
- }
- }
- }
- }
-
- return count;
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.browser;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import javax.swing.tree.DefaultMutableTreeNode;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.bundles.StringIdGui;
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.library.MetaResultList;
-
-public class TagsTab extends BasicTab<List<String>> {
- public TagsTab(int index, String listenerCommand) {
- super(index, listenerCommand);
- }
-
- @Override
- protected List<String> createEmptyData() {
- return new ArrayList<String>();
- }
-
- @Override
- protected void fillData(List<String> data) {
- data.clear();
- try {
- MetaResultList metas = Instance.getInstance().getLibrary()
- .getList();
- // TODO: getTagList, getAuthorList... ?
- for (MetaData meta : metas.getMetas()) {
- List<String> tags = meta.getTags();
- if (tags != null) {
- for (String tag : tags) {
- if (!data.contains(tag)) {
- data.add(tag);
- }
- }
- }
- }
-
- sort(data);
- } catch (Exception e) {
- // TODO
- e.printStackTrace();
- }
- }
-
- @Override
- protected String keyToElement(String key) {
- return key;
- }
-
- @Override
- protected String keyToDisplay(String key) {
- if (key.trim().isEmpty()) {
- // TODO: new TAG_UNKNOWN needed
- key = Instance.getInstance().getTransGui()
- .getString(StringIdGui.MENU_AUTHORS_UNKNOWN);
- }
-
- return key;
- }
-
- @Override
- protected int loadData(DefaultMutableTreeNode root, List<String> tags,
- String filter) {
- for (String tag : tags) {
- if (checkFilter(filter, tag)) {
- DefaultMutableTreeNode sourceNode = new DefaultMutableTreeNode(
- tag);
- root.add(sourceNode);
- }
- }
-
- return tags.size();
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.utils;
-
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.TreeSet;
-
-import javax.swing.SwingWorker;
-
-/**
- * This class helps you delay some graphical actions and execute the most recent
- * ones when under contention.
- * <p>
- * How does it work?
- * <ul>
- * <li>it takes an ID and an associated {@link SwingWorker} and will call
- * {@link SwingWorker#execute()} after a small delay (see
- * {@link DelayWorker#DelayWorker(int)})</li>
- * <li>if a second call to {@link DelayWorker#delay(String, SwingWorker)} comes
- * with the same ID before the first one is done, it will be put on a waiting
- * queue</li>
- * <li>if a third call still with the same ID comes, its associated worker will
- * <b>replace</b> the one in the queue (only one worker per ID in the queue,
- * always the latest one)</li>
- * <li>when the first worker is done, it will check the waiting queue and
- * execute that latest worker if any</li>
- * </ul>
- *
- * @author niki
- *
- */
-@SuppressWarnings("rawtypes")
-public class DelayWorker {
- private Map<String, SwingWorker> lazyEnCours;
- private Object lazyEnCoursLock;
-
- private TreeSet<String> wip;
-
- private Object waiter;
-
- private boolean cont;
- private boolean paused;
- private Thread loop;
-
- /**
- * Create a new {@link DelayWorker} with the given delay (in milliseconds)
- * before each drain of the queue.
- *
- * @param delayMs
- * the delay in milliseconds (can be 0, cannot be negative)
- */
- public DelayWorker(final int delayMs) {
- if (delayMs < 0) {
- throw new IllegalArgumentException(
- "A waiting delay cannot be negative");
- }
-
- lazyEnCours = new HashMap<String, SwingWorker>();
- lazyEnCoursLock = new Object();
- wip = new TreeSet<String>();
- waiter = new Object();
- cont = true;
- paused = false;
-
- loop = new Thread(new Runnable() {
- @Override
- public void run() {
- while (cont) {
- try {
- Thread.sleep(delayMs);
- } catch (InterruptedException e) {
- }
-
- Map<String, SwingWorker> workers = new HashMap<String, SwingWorker>();
- synchronized (lazyEnCoursLock) {
- for (String key : new ArrayList<String>(
- lazyEnCours.keySet())) {
- if (!wip.contains(key)) {
- workers.put(key, lazyEnCours.remove(key));
- }
- }
- }
-
- for (final String key : workers.keySet()) {
- SwingWorker worker = workers.get(key);
-
- synchronized (lazyEnCoursLock) {
- wip.add(key);
- }
-
- worker.addPropertyChangeListener(
- new PropertyChangeListener() {
- @Override
- public void propertyChange(
- PropertyChangeEvent evt) {
- synchronized (lazyEnCoursLock) {
- wip.remove(key);
- }
- wakeup();
- }
- });
-
- // Start it, at last
- worker.execute();
- }
-
- synchronized (waiter) {
- do {
- try {
- if (cont)
- waiter.wait();
- } catch (InterruptedException e) {
- }
- } while (cont && paused);
- }
- }
- }
- });
-
- loop.setDaemon(true);
- loop.setName("Loop for DelayWorker");
- }
-
- /**
- * Start the internal loop that will drain the processing queue. <b>MUST
- * NOT</b> be started twice (but see {@link DelayWorker#pause()} and
- * {@link DelayWorker#resume()} instead).
- */
- public void start() {
- loop.start();
- }
-
- /**
- * Pause the system until {@link DelayWorker#resume()} is called -- note
- * that it will still continue on the processes currently scheduled to run,
- * but will pause after that.
- * <p>
- * Can be called even if already paused, will just do nothing in that
- * context.
- */
- public void pause() {
- paused = true;
- }
-
- /**
- * Check if the {@link DelayWorker} is currently paused.
- *
- * @return TRUE if it is
- */
- public boolean isPaused() {
- return paused;
- }
-
- /**
- * Resume the system after a pause.
- * <p>
- * Can be called even if already running, will just do nothing in that
- * context.
- */
- public void resume() {
- synchronized (waiter) {
- paused = false;
- wakeup();
- }
- }
-
- /**
- * Stop the system.
- * <p>
- * Note: this is final, you <b>MUST NOT</b> call {@link DelayWorker#start()}
- * a second time (but see {@link DelayWorker#pause()} and
- * {@link DelayWorker#resume()} instead).
- */
- public void stop() {
- synchronized (waiter) {
- cont = false;
- wakeup();
- }
- }
-
- /**
- * Clear all the processes that were put on the queue but not yet scheduled
- * to be executed -- note that it will still continue on the processes
- * currently scheduled to run.
- */
- public void clear() {
- synchronized (lazyEnCoursLock) {
- lazyEnCours.clear();
- wip.clear();
- }
- }
-
- /**
- * Put a new process in the delay queue.
- *
- * @param id
- * the ID of this process (if you want to skip workers when they
- * are superseded by a new one, you need to use the same ID key)
- * @param worker
- * the process to delay
- */
- public void delay(final String id, final SwingWorker worker) {
- synchronized (lazyEnCoursLock) {
- lazyEnCours.put(id, worker);
- }
-
- wakeup();
- }
-
- /**
- * Wake up the loop thread.
- */
- private void wakeup() {
- synchronized (waiter) {
- waiter.notifyAll();
- }
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.utils;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.util.LinkedList;
-import java.util.Queue;
-
-import javax.swing.JPanel;
-
-import be.nikiroo.fanfix_swing.gui.SearchBar;
-
-/**
- * A {@link JPanel} with the default {@link ActionListener} add/remove/fire
- * methods.
- * <p>
- * Note that it will queue all events until at least one listener comes (or
- * comes back!); this first (or at least currently unique) listener will drain
- * the queue.
- *
- * @author niki
- */
-public class ListenerPanel extends JPanel {
- private static final long serialVersionUID = 1L;
-
- /** Waiting queue until at least one listener is here to get the events. */
- private final Queue<ActionEvent> waitingQueue;
-
- /**
- * Create a new {@link ListenerPanel}.
- */
- public ListenerPanel() {
- waitingQueue = new LinkedList<ActionEvent>();
- }
-
- /**
- * Check that this {@link ListenerPanel} currently has
- * {@link ActionListener}s that listen on it.
- *
- * @return TRUE if it has
- */
- public synchronized boolean hasListeners() {
- return listenerList.getListenerList().length > 1;
- }
-
- /**
- * Check how many events are currently waiting for an
- * {@link ActionListener}.
- *
- * @return the number of waiting events (can be 0)
- */
- public synchronized int getWaitingEventCount() {
- return waitingQueue.size();
- }
-
- /**
- * Adds the specified action listener to receive action events from this
- * {@link SearchBar}.
- *
- * @param listener
- * the action listener to be added
- */
- public synchronized void addActionListener(ActionListener listener) {
- if (!hasListeners()) {
- while (!waitingQueue.isEmpty()) {
- listener.actionPerformed(waitingQueue.remove());
- }
- }
-
- listenerList.add(ActionListener.class, listener);
- }
-
- /**
- * Removes the specified action listener so that it no longer receives
- * action events from this {@link SearchBar}.
- *
- * @param listener
- * the action listener to be removed
- */
- public synchronized void removeActionListener(ActionListener listener) {
- listenerList.remove(ActionListener.class, listener);
- }
-
- /**
- * Notify the listeners of an action.
- *
- * @param listenerCommand
- * A string that may specify a command (possibly one of several)
- * associated with the event
- */
- protected synchronized void fireActionPerformed(String listenerCommand) {
- ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
- listenerCommand);
-
- ActionListener[] listeners = getListeners(ActionListener.class);
- if (listeners.length > 0) {
- for (ActionListener action : listeners) {
- action.actionPerformed(e);
- }
- } else {
- waitingQueue.add(e);
- }
- }
-}
+++ /dev/null
-/*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-// Can be found at: https://code.google.com/archive/p/aephyr/source/default/source
-// package aephyr.swing;
-package be.nikiroo.fanfix_swing.gui.utils;
-
-import java.awt.*;
-import java.awt.event.*;
-
-import javax.swing.*;
-import javax.swing.tree.*;
-
-import java.util.*;
-
-public class TreeCellSpanner extends Container implements TreeCellRenderer, ComponentListener {
-
- public TreeCellSpanner(JTree tree, TreeCellRenderer renderer) {
- if (tree == null || renderer == null)
- throw new NullPointerException();
- this.tree = tree;
- this.renderer = renderer;
- treeParent = tree.getParent();
- if (treeParent != null && treeParent instanceof JViewport) {
- treeParent.addComponentListener(this);
- } else {
- treeParent = null;
- tree.addComponentListener(this);
- }
- }
-
- protected final JTree tree;
-
- private TreeCellRenderer renderer;
-
- private Component rendererComponent;
-
- private Container treeParent;
-
- private Map<TreePath,Integer> offsets = new HashMap<TreePath,Integer>();
-
- private TreePath path;
-
- public TreeCellRenderer getRenderer() {
- return renderer;
- }
-
- @Override
- public Component getTreeCellRendererComponent(JTree tree, Object value,
- boolean selected, boolean expanded, boolean leaf, int row,
- boolean hasFocus) {
- path = tree.getPathForRow(row);
- if (path != null && path.getLastPathComponent() != value)
- path = null;
- rendererComponent = renderer.getTreeCellRendererComponent(
- tree, value, selected, expanded, leaf, row, hasFocus);
- if (getComponentCount() < 1 || getComponent(0) != rendererComponent) {
- removeAll();
- add(rendererComponent);
- }
- return this;
- }
-
- @Override
- public void doLayout() {
- int x = getX();
- if (x < 0)
- return;
- if (path != null) {
- Integer offset = offsets.get(path);
- if (offset == null || offset.intValue() != x) {
- offsets.put(path, x);
- fireTreePathChanged(path);
- }
- }
- rendererComponent.setBounds(getX(), getY(), getWidth(), getHeight());
- }
-
- @Override
- public void paint(Graphics g) {
- if (rendererComponent != null)
- rendererComponent.paint(g);
- }
-
- @Override
- public Dimension getPreferredSize() {
- Dimension s = rendererComponent.getPreferredSize();
- // check if path count is greater than 1 to exclude the root
- if (path != null && path.getPathCount() > 1) {
- Integer offset = offsets.get(path);
- if (offset != null) {
- int width;
- if (tree.getParent() == treeParent) {
- width = treeParent.getWidth();
- } else {
- if (treeParent != null) {
- treeParent.removeComponentListener(this);
- tree.addComponentListener(this);
- treeParent = null;
- }
- if (tree.getParent() instanceof JViewport) {
- treeParent = tree.getParent();
- tree.removeComponentListener(this);
- treeParent.addComponentListener(this);
- width = treeParent.getWidth();
- } else {
- width = tree.getWidth();
- }
- }
- s.width = width - offset;
- }
- }
- return s;
- }
-
-
- protected void fireTreePathChanged(TreePath path) {
- if (path.getPathCount() > 1) {
- // this cannot be used for the root node or else
- // the entire tree will keep being revalidated ad infinitum
- TreeModel model = tree.getModel();
- Object node = path.getLastPathComponent();
- if (node instanceof TreeNode && (model instanceof DefaultTreeModel
- || (model instanceof TreeModelTransformer<?> &&
- (model=((TreeModelTransformer<?>)model).getModel()) instanceof DefaultTreeModel))) {
- ((DefaultTreeModel)model).nodeChanged((TreeNode)node);
- } else {
- model.valueForPathChanged(path, node.toString());
- }
- } else {
- // root!
-
- }
- }
-
-
- private int lastWidth;
-
- @Override
- public void componentHidden(ComponentEvent e) {}
-
- @Override
- public void componentMoved(ComponentEvent e) {}
-
- @Override
- public void componentResized(ComponentEvent e) {
- if (e.getComponent().getWidth() != lastWidth) {
- lastWidth = e.getComponent().getWidth();
- for (int row=tree.getRowCount(); --row>=0;) {
- fireTreePathChanged(tree.getPathForRow(row));
- }
- }
- }
-
- @Override
- public void componentShown(ComponentEvent e) {}
-
-}
+++ /dev/null
-/*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-// Can be found at: https://code.google.com/archive/p/aephyr/source/default/source
-// package aephyr.swing;
-package be.nikiroo.fanfix_swing.gui.utils;
-
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import javax.swing.JTree;
-import javax.swing.SortOrder;
-import javax.swing.event.EventListenerList;
-import javax.swing.event.TreeExpansionEvent;
-import javax.swing.event.TreeExpansionListener;
-import javax.swing.event.TreeModelEvent;
-import javax.swing.event.TreeModelListener;
-import javax.swing.event.TreeWillExpandListener;
-import javax.swing.tree.ExpandVetoException;
-import javax.swing.tree.TreeModel;
-import javax.swing.tree.TreePath;
-
-
-public class TreeModelTransformer<N> implements TreeModel {
-
- public TreeModelTransformer(JTree tree, TreeModel model) {
- if (tree == null)
- throw new IllegalArgumentException();
- if (model == null)
- throw new IllegalArgumentException();
- this.tree = tree;
- this.model = model;
- handler = createHandler();
- addListeners();
- }
-
- private JTree tree;
-
- private TreeModel model;
-
- private Handler handler;
-
- private Filter<N> filter;
-
- private TreePath filterStartPath;
-
- private int filterDepthLimit;
-
- private SortOrder sortOrder = SortOrder.UNSORTED;
-
- private Map<Object,Converter> converters;
-
- protected EventListenerList listenerList = new EventListenerList();
-
- protected Handler createHandler() {
- return new Handler();
- }
-
- protected void addListeners() {
- tree.addTreeExpansionListener(handler);
- model.addTreeModelListener(handler);
- }
-
- protected void removeListeners() {
- tree.removeTreeExpansionListener(handler);
- model.removeTreeModelListener(handler);
- }
-
- public void dispose() {
- removeListeners();
- }
-
- public TreeModel getModel() {
- return model;
- }
-
- private Converter getConverter(Object node) {
- return converters == null ? null : converters.get(node);
- }
-
- int convertRowIndexToView(Object parent, int index) {
- Converter converter = getConverter(parent);
- if (converter != null)
- return converter.convertRowIndexToView(index);
- return index;
- }
-
- int convertRowIndexToModel(Object parent, int index) {
- Converter converter = getConverter(parent);
- if (converter != null)
- return converter.convertRowIndexToModel(index);
- return index;
- }
-
- @Override
- public Object getChild(Object parent, int index) {
- return model.getChild(parent, convertRowIndexToModel(parent, index));
- }
-
- @Override
- public int getChildCount(Object parent) {
- Converter converter = getConverter(parent);
- if (converter != null)
- return converter.getChildCount();
- return model.getChildCount(parent);
- }
-
- @Override
- public int getIndexOfChild(Object parent, Object child) {
- int index = model.getIndexOfChild(parent, child);
- if (index < 0)
- return -1;
- return convertRowIndexToView(parent, index);
- }
-
- @Override
- public Object getRoot() {
- return model.getRoot();
- }
-
- @Override
- public boolean isLeaf(Object node) {
- return model.isLeaf(node);
- }
-
- @Override
- public void valueForPathChanged(TreePath path, Object newValue) {
- model.valueForPathChanged(path, newValue);
- }
-
- @Override
- public void addTreeModelListener(TreeModelListener l) {
- listenerList.add(TreeModelListener.class, l);
- }
-
- @Override
- public void removeTreeModelListener(TreeModelListener l) {
- listenerList.remove(TreeModelListener.class, l);
- }
-
- /**
- * Set the comparator that compares nodes in sorting.
- * @param comparator
- * @see #getComparator()
- */
- public void setComparator(Comparator<N> comparator) {
- handler.setComparator(comparator);
- }
-
- /**
- * @return comparator that compares nodes
- * @see #setComparator(Comparator)
- */
- public Comparator<N> getComparator() {
- return handler.getComparator();
- }
-
- public void setSortOrder(SortOrder newOrder) {
- SortOrder oldOrder = sortOrder;
- if (oldOrder == newOrder)
- return;
- sortOrder = newOrder;
- ArrayList<TreePath> paths = null;
- switch (newOrder) {
- case ASCENDING:
- if (oldOrder == SortOrder.DESCENDING) {
- flip();
- } else {
- paths = sort();
- }
- break;
- case DESCENDING:
- if (oldOrder == SortOrder.ASCENDING) {
- flip();
- } else {
- paths = sort();
- }
- break;
- case UNSORTED:
- unsort();
- break;
- }
- fireTreeStructureChangedAndExpand(new TreePath(getRoot()), paths, true);
- }
-
- public SortOrder getSortOrder() {
- return sortOrder;
- }
-
- public void toggleSortOrder() {
- setSortOrder(sortOrder == SortOrder.ASCENDING ?
- SortOrder.DESCENDING : SortOrder.ASCENDING);
- }
-
-
- /**
- * Flip all sorted paths.
- */
- private void flip() {
- for (Converter c : converters.values()) {
- flip(c.viewToModel);
- }
- }
-
- /**
- * Flip array.
- * @param array
- */
- private static void flip(int[] array) {
- for (int left=0, right=array.length-1;
- left<right; left++, right--) {
- int tmp = array[left];
- array[left] = array[right];
- array[right] = tmp;
- }
- }
-
- private void unsort() {
- if (filter == null) {
- converters = null;
- } else {
- Iterator<Converter> cons = converters.values().iterator();
- while (cons.hasNext()) {
- Converter converter = cons.next();
- if (!converter.isFiltered()) {
- cons.remove();
- } else {
- Arrays.sort(converter.viewToModel);
- }
- }
- }
- }
-
- /**
- * Sort root and expanded descendants.
- * @return list of paths that were sorted
- */
- private ArrayList<TreePath> sort() {
- if (converters == null)
- converters = createConvertersMap(); //new IdentityHashMap<Object,Converter>();
- return sortHierarchy(new TreePath(model.getRoot()));
- }
-
- /**
- * Sort path and expanded descendants.
- * @param path
- * @return list of paths that were sorted
- */
- private ArrayList<TreePath> sortHierarchy(TreePath path) {
- ValueIndexPair<N>[] pairs = createValueIndexPairArray(20);
- ArrayList<TreePath> list = new ArrayList<TreePath>();
- pairs = sort(path.getLastPathComponent(), pairs);
- list.add(path);
- Enumeration<TreePath> paths = tree.getExpandedDescendants(path);
- if (paths != null)
- while (paths.hasMoreElements()) {
- path = paths.nextElement();
- pairs = sort(path.getLastPathComponent(), pairs);
- list.add(path);
- }
- return list;
- }
-
- private ValueIndexPair<N>[] sort(Object node, ValueIndexPair<N>[] pairs) {
- Converter converter = getConverter(node);
- TreeModel mdl = model;
- int[] vtm;
- if (converter != null) {
- vtm = converter.viewToModel;
- if (pairs.length < vtm.length)
- pairs = createValueIndexPairArray(vtm.length);
- for (int i=vtm.length; --i>=0;) {
- int idx = vtm[i];
- pairs[i].index = idx;
- pairs[i].value = (N)mdl.getChild(node, idx);
- }
- } else {
- int count = mdl.getChildCount(node);
- if (count <= 0)
- return pairs;
- if (pairs.length < count)
- pairs = createValueIndexPairArray(count);
- for (int i=count; --i>=0;) {
- pairs[i].index = i;
- pairs[i].value = (N)mdl.getChild(node, i);
- }
- vtm = new int[count];
- }
- Arrays.sort(pairs, 0, vtm.length, handler);
- for (int i=vtm.length; --i>=0;)
- vtm[i] = pairs[i].index;
- if (converter == null) {
- converters.put(node, new Converter(vtm, false));
- }
- if (sortOrder == SortOrder.DESCENDING)
- flip(vtm);
- return pairs;
- }
-
- private ValueIndexPair<N>[] createValueIndexPairArray(int len) {
- ValueIndexPair<N>[] pairs = new ValueIndexPair[len];
- for (int i=len; --i>=0;)
- pairs[i] = new ValueIndexPair<N>();
- return pairs;
- }
-
- public void setFilter(Filter<N> filter) {
- setFilter(filter, null);
- }
-
- public void setFilter(Filter<N> filter, TreePath startingPath) {
- setFilter(filter, null, -1);
- }
-
- public void setFilter(Filter<N> filter, TreePath startingPath, int depthLimit) {
- if (filter == null && startingPath != null)
- throw new IllegalArgumentException();
- if (startingPath != null && startingPath.getPathCount() == 1)
- startingPath = null;
- Filter<N> oldFilter = this.filter;
- TreePath oldStartPath = filterStartPath;
- this.filter = filter;
- filterStartPath = startingPath;
- filterDepthLimit = depthLimit;
- applyFilter(oldFilter, oldStartPath, null, true);
- }
-
- public Filter<N> getFilter() {
- return filter;
- }
-
- public TreePath getFilterStartPath() {
- return filterStartPath;
- }
-
- private void applyFilter(Filter<N> oldFilter, TreePath oldStartPath, Collection<TreePath> expanded, boolean sort) {
- TreePath startingPath = filterStartPath;
- ArrayList<TreePath> expand = null;
- if (filter == null) {
- converters = null;
- } else {
- if (converters == null || startingPath == null) {
- converters = createConvertersMap();
- } else if (oldFilter != null) {
- // unfilter the oldStartPath if oldStartPath isn't descendant of startingPath
- if (oldStartPath == null) {
- converters = createConvertersMap();
- fireTreeStructureChangedAndExpand(new TreePath(getRoot()), null, true);
- } else if (!startingPath.isDescendant(oldStartPath)) {
- Object node = oldStartPath.getLastPathComponent();
- handler.removeConverter(getConverter(node), node);
- fireTreeStructureChangedAndExpand(oldStartPath, null, true);
- }
- }
- expand = new ArrayList<TreePath>();
- TreePath path = startingPath != null ? startingPath : new TreePath(getRoot());
- if (!applyFilter(filter, path, expand, filterDepthLimit)) {
- converters.put(path.getLastPathComponent(), new Converter(Converter.EMPTY, true));
- }
- }
- if (startingPath == null)
- startingPath = new TreePath(getRoot());
- fireTreeStructureChanged(startingPath);
- if (expanded != null)
- expand.retainAll(expanded);
- expandPaths(expand);
- if (sort && sortOrder != SortOrder.UNSORTED) {
- if (filter == null)
- converters = createConvertersMap();
- if (startingPath.getPathCount() > 1 && oldFilter != null) {
- // upgrade startingPath or sort oldStartPath
- if (oldStartPath == null) {
- startingPath = new TreePath(getRoot());
- } else if (oldStartPath.isDescendant(startingPath)) {
- startingPath = oldStartPath;
- } else if (!startingPath.isDescendant(oldStartPath)) {
- expand = sortHierarchy(oldStartPath);
- fireTreeStructureChanged(oldStartPath);
- expandPaths(expand);
- }
- }
- expand = sortHierarchy(startingPath);
- fireTreeStructureChanged(startingPath);
- expandPaths(expand);
- }
-
- }
-
- private boolean applyFilter(Filter<N> filter, TreePath path, ArrayList<TreePath> expand) {
- int depthLimit = filterDepthLimit;
- if (depthLimit >= 0) {
- depthLimit -= filterStartPath.getPathCount() - path.getPathCount();
- if (depthLimit <= 0)
- return false;
- }
- return applyFilter(filter, path, expand, depthLimit);
- }
-
- private boolean applyFilter(Filter<N> filter, TreePath path, ArrayList<TreePath> expand, int depthLimit) {
- Object node = path.getLastPathComponent();
- int count = model.getChildCount(node);
- int[] viewToModel = null;
- int viewIndex = 0;
- boolean needsExpand = false;
- boolean isExpanded = false;
- if (depthLimit > 0)
- depthLimit--;
- for (int i=0; i<count; i++) {
- Object child = model.getChild(node, i);
- boolean leaf = model.isLeaf(child);
- if (filter.acceptNode(path, (N)child, leaf)) {
- if (viewToModel == null)
- viewToModel = new int[count-i];
- viewToModel[viewIndex++] = i;
- needsExpand = true;
- } else if (depthLimit != 0 && !leaf) {
- if (applyFilter(filter, path.pathByAddingChild(child), expand, depthLimit)) {
- if (viewToModel == null)
- viewToModel = new int[count-i];
- viewToModel[viewIndex++] = i;
- isExpanded = true;
- }
- }
- }
- if (needsExpand && expand != null && !isExpanded && path.getPathCount() > 1) {
- expand.add(path);
- }
- if (viewToModel != null) {
- if (viewIndex < viewToModel.length)
- viewToModel = Arrays.copyOf(viewToModel, viewIndex);
- // a node must have a converter to signify that tree modifications
- // need to query the filter, so have to put in converter
- // even if viewIndex == viewToModel.length
- converters.put(node, new Converter(viewToModel, true));
- return true;
- }
- return false;
- }
-
-
- private void expandPaths(ArrayList<TreePath> paths) {
- if (paths == null || paths.isEmpty())
- return;
- JTree tre = tree;
- for (TreePath path : paths)
- tre.expandPath(path);
- }
-
-
- private void fireTreeStructureChangedAndExpand(TreePath path, ArrayList<TreePath> list, boolean retainSelection) {
- Enumeration<TreePath> paths = list != null ?
- Collections.enumeration(list) : tree.getExpandedDescendants(path);
- TreePath[] sel = retainSelection ? tree.getSelectionPaths() : null;
- fireTreeStructureChanged(path);
- if (paths != null)
- while (paths.hasMoreElements())
- tree.expandPath(paths.nextElement());
- if (sel != null)
- tree.setSelectionPaths(sel);
- }
-
-
-
- protected void fireTreeStructureChanged(TreePath path) {
- Object[] listeners = listenerList.getListenerList();
- TreeModelEvent e = null;
- for (int i = listeners.length-2; i>=0; i-=2) {
- if (listeners[i]==TreeModelListener.class) {
- if (e == null)
- e = new TreeModelEvent(this, path, null, null);
- ((TreeModelListener)listeners[i+1]).treeStructureChanged(e);
- }
- }
- }
-
- protected void fireTreeNodesChanged(TreePath path, int[] childIndices, Object[] childNodes) {
- Object[] listeners = listenerList.getListenerList();
- TreeModelEvent e = null;
- for (int i = listeners.length-2; i>=0; i-=2) {
- if (listeners[i]==TreeModelListener.class) {
- if (e == null)
- e = new TreeModelEvent(this, path, childIndices, childNodes);
- ((TreeModelListener)listeners[i+1]).treeNodesChanged(e);
- }
- }
- }
-
- protected void fireTreeNodesInserted(TreePath path, int[] childIndices, Object[] childNodes) {
- Object[] listeners = listenerList.getListenerList();
- TreeModelEvent e = null;
- for (int i = listeners.length-2; i>=0; i-=2) {
- if (listeners[i]==TreeModelListener.class) {
- if (e == null)
- e = new TreeModelEvent(this, path, childIndices, childNodes);
- ((TreeModelListener)listeners[i+1]).treeNodesInserted(e);
- }
- }
- }
-
- protected void fireTreeNodesRemoved(TreePath path, int[] childIndices, Object[] childNodes) {
- Object[] listeners = listenerList.getListenerList();
- TreeModelEvent e = null;
- for (int i = listeners.length-2; i>=0; i-=2) {
- if (listeners[i]==TreeModelListener.class) {
- if (e == null)
- e = new TreeModelEvent(this, path, childIndices, childNodes);
- ((TreeModelListener)listeners[i+1]).treeNodesRemoved(e);
- }
- }
- }
-
-
- protected class Handler implements Comparator<ValueIndexPair<N>>,
- TreeModelListener, TreeExpansionListener {
-
- private Comparator<N> comparator;
-
- private Collator collator = Collator.getInstance();
-
- void setComparator(Comparator<N> cmp) {
- comparator = cmp;
- collator = cmp == null ? Collator.getInstance() : null;
- }
-
- Comparator<N> getComparator() {
- return comparator;
- }
-
- // TODO, maybe switch to TreeWillExpandListener?
- // TreeExpansionListener was used in case an expanded node
- // had children that would also be expanded, but it is impossible
- // for hidden nodes' expansion state to survive a SortOrder change
- // since they are all erased when the tree structure change event
- // is fired after changing the SortOrder.
-
- @Override
- public void treeCollapsed(TreeExpansionEvent e) {}
-
- @Override
- public void treeExpanded(TreeExpansionEvent e) {
- if (sortOrder != SortOrder.UNSORTED) {
- TreePath path = e.getPath();
- Converter converter = getConverter(path.getLastPathComponent());
- if (converter == null) {
- ArrayList<TreePath> paths = sortHierarchy(path);
- fireTreeStructureChangedAndExpand(path, paths, false);
- }
- }
- }
-
- private boolean isFiltered(Object node) {
- Converter c = getConverter(node);
- return c == null ? false : c.isFiltered();
- }
-
- private boolean acceptable(TreePath path, Object[] childNodes, int index, ArrayList<TreePath> expand) {
- return acceptable(path, childNodes, index) ||
- applyFilter(filter, path.pathByAddingChild(childNodes[index]), expand);
- }
-
- @Override
- public void treeNodesChanged(TreeModelEvent e) {
- treeNodesChanged(e.getTreePath(), e.getChildIndices(), e.getChildren());
- }
-
- protected void treeNodesChanged(TreePath path, int[] childIndices, Object[] childNodes) {
- if (childIndices == null) {
- // path should be root path
- // reapply filter
- if (filter != null)
- applyFilter(null, null, null, true);
- return;
- }
- Converter converter = getConverter(path.getLastPathComponent());
- ArrayList<TreePath> expand = null;
- if (converter != null) {
- expand = new ArrayList<TreePath>();
- int childIndex = 0;
- for (int i=0; i<childIndices.length; i++) {
- int idx = converter.convertRowIndexToView(childIndices[i]);
- if (idx >= 0) {
- // see if the filter dislikes the nodes new state
- if (converter.isFiltered() &&
- !isFiltered(childNodes[i]) &&
- !acceptable(path, childNodes, i)) {
- // maybe it likes a child nodes state
- if (!applyFilter(filter, path.pathByAddingChild(childNodes[i]), expand))
- remove(path, childNodes[i]);
- continue;
- }
- childNodes[childIndex] = childNodes[i];
- childIndices[childIndex++] = idx;
- } else if (acceptable(path, childNodes, i, expand)) {
- int viewIndex = insert(path.getLastPathComponent(),
- childNodes[i], childIndices[i], converter);
- fireTreeNodesInserted(path, indices(viewIndex), nodes(childNodes[i]));
- }
- }
- if (childIndex == 0) {
- maybeFireStructureChange(path, expand);
- return;
- }
- if (sortOrder != SortOrder.UNSORTED && converter.getChildCount() > 1) {
- sort(path.getLastPathComponent(), createValueIndexPairArray(converter.getChildCount()));
- fireTreeStructureChangedAndExpand(path, null, true);
- expandPaths(expand);
- return;
- }
- if (childIndex != childIndices.length) {
- childIndices = Arrays.copyOf(childIndices, childIndex);
- childNodes = Arrays.copyOf(childNodes, childIndex);
- }
- } else if (filter != null && isFilteredOut(path)) {
- // see if the filter likes the nodes new states
- expand = new ArrayList<TreePath>();
- int[] vtm = null;
- int idx = 0;
- for (int i=0; i<childIndices.length; i++) {
- if (acceptable(path, childNodes, i, expand)) {
- if (vtm == null)
- vtm = new int[childIndices.length-i];
- vtm[idx++] = childIndices[i];
- }
- }
- // filter in path if appropriate
- if (vtm != null)
- filterIn(vtm, idx, path, expand);
- return;
- }
- // must fire tree nodes changed even if a
- // structure change will be fired because the
- // expanded paths need to be updated first
- fireTreeNodesChanged(path, childIndices, childNodes);
- maybeFireStructureChange(path, expand);
- }
-
- /**
- * Helper method for treeNodesChanged...
- * @param path
- * @param expand
- */
- private void maybeFireStructureChange(TreePath path, ArrayList<TreePath> expand) {
- if (expand != null && !expand.isEmpty()) {
- Enumeration<TreePath> expanded = tree.getExpandedDescendants(path);
- fireTreeStructureChanged(path);
- if (expanded != null)
- while (expanded.hasMoreElements())
- tree.expandPath(expanded.nextElement());
- expandPaths(expand);
- }
- }
-
- @Override
- public void treeNodesInserted(TreeModelEvent e) {
- treeNodesInserted(e.getTreePath(), e.getChildIndices(), e.getChildren());
- }
-
- protected void treeNodesInserted(TreePath path, int[] childIndices, Object[] childNodes) {
- Object parent = path.getLastPathComponent();
- Converter converter = getConverter(parent);
- ArrayList<TreePath> expand = null;
- if (converter != null) {
-// if (childIndices.length > 3 && !converter.isFiltered()
-// && childIndices.length > converter.getChildCount()/10) {
-// TreePath expand = sortHierarchy(path);
-// fireTreeStructureChangedAndExpand(expand);
-// return;
-// }
- int childIndex = 0;
- for (int i=0; i<childIndices.length; i++) {
- if (converter.isFiltered()) {
- // path hasn't met the filter criteria, so childNodes must be filtered
- if (expand == null)
- expand = new ArrayList<TreePath>();
- if (!applyFilter(filter, path.pathByAddingChild(childNodes[i]), expand))
- continue;
- }
- // shift the appropriate cached modelIndices
- int[] vtm = converter.viewToModel;
- int modelIndex = childIndices[i];
- for (int j=vtm.length; --j>=0;) {
- if (vtm[j] >= modelIndex)
- vtm[j] += 1;
- }
- // insert modelIndex to converter
- int viewIndex = insert(parent, childNodes[i], modelIndex, converter);
- childNodes[childIndex] = childNodes[i];
- childIndices[childIndex++] = viewIndex;
- }
- if (childIndex == 0)
- return;
- if (childIndex != childIndices.length) {
- childIndices = Arrays.copyOf(childIndices, childIndex);
- childNodes = Arrays.copyOf(childNodes, childIndex);
- }
- if (childIndex > 1 && sortOrder != SortOrder.UNSORTED) {
- sort(childIndices, childNodes);
- }
- } else if (filter != null && isFilteredOut(path)) {
- // apply filter to inserted nodes
- int[] vtm = null;
- int idx = 0;
- expand = new ArrayList<TreePath>();
- for (int i=0; i<childIndices.length; i++) {
- if (acceptable(path, childNodes, i, expand)) {
- if (vtm == null)
- vtm = new int[childIndices.length-i];
- vtm[idx++] = childIndices[i];
- }
- }
- // filter in path if appropriate
- if (vtm != null)
- filterIn(vtm, idx, path, expand);
- return;
- }
- fireTreeNodesInserted(path, childIndices, childNodes);
- expandPaths(expand);
- }
-
- @Override
- public void treeNodesRemoved(TreeModelEvent e) {
- treeNodesRemoved(e.getTreePath(), e.getChildIndices(), e.getChildren());
- }
-
-
- private boolean isFilterStartPath(TreePath path) {
- if (filterStartPath == null)
- return path.getPathCount() == 1;
- return filterStartPath.equals(path);
- }
-
- protected void treeNodesRemoved(TreePath path, int[] childIndices, Object[] childNodes) {
- Object parent = path.getLastPathComponent();
- Converter converter = getConverter(parent);
- if (converter != null) {
- int len = 0;
- for (int i=0; i<childNodes.length; i++) {
- removeConverter(childNodes[i]);
- int viewIndex = converter.convertRowIndexToView(childIndices[i]);
- if (viewIndex >= 0) {
- childNodes[len] = childNodes[i];
- childIndices[len++] = viewIndex;
- }
- }
- if (len == 0)
- return;
- if (converter.isFiltered() && converter.getChildCount() == len) {
- ArrayList<TreePath> expand = new ArrayList<TreePath>();
- if (applyFilter(filter, path, expand)) {
- expand.retainAll(getExpandedPaths(path));
- if (sortOrder != SortOrder.UNSORTED)
- sortHierarchy(path);
- fireTreeStructureChangedAndExpand(path, expand, true);
- } else if (isFilterStartPath(path)) {
- converters.put(parent, new Converter(Converter.EMPTY, true));
- fireTreeStructureChanged(path);
- } else {
- remove(path.getParentPath(), parent);
- }
- return;
- }
- if (len != childIndices.length) {
- childIndices = Arrays.copyOf(childIndices, len);
- childNodes = Arrays.copyOf(childNodes, len);
- }
- if (len > 1 && sortOrder != SortOrder.UNSORTED) {
- sort(childIndices, childNodes);
- }
- if (childIndices.length == 1) {
- converter.remove(converter.convertRowIndexToModel(childIndices[0]));
- } else {
- converter.remove(childIndices);
- }
- } else if (filter != null && isFilteredOut(path)) {
- return;
- }
- fireTreeNodesRemoved(path, childIndices, childNodes);
- }
-
- private Collection<TreePath> getExpandedPaths(TreePath path) {
- Enumeration<TreePath> en = tree.getExpandedDescendants(path);
- if (en == null)
- return Collections.emptySet();
- HashSet<TreePath> expanded = new HashSet<TreePath>();
- while (en.hasMoreElements())
- expanded.add(en.nextElement());
- return expanded;
- }
-
- @Override
- public void treeStructureChanged(TreeModelEvent e) {
- if (converters != null) {
- // not enough information to properly clean up
- // reapply filter/sort
- converters = createConvertersMap();
- TreePath[] sel = tree.getSelectionPaths();
- if (filter != null) {
- applyFilter(null, null, getExpandedPaths(new TreePath(getRoot())), false);
- }
- if (sortOrder != SortOrder.UNSORTED) {
- TreePath path = new TreePath(getRoot());
- ArrayList<TreePath> expand = sortHierarchy(path);
- fireTreeStructureChangedAndExpand(path, expand, false);
- }
- if (sel != null) {
- tree.clearSelection();
- TreePath changedPath = e.getTreePath();
- for (TreePath path : sel) {
- if (!changedPath.isDescendant(path))
- tree.addSelectionPath(path);
- }
- }
- } else {
- fireTreeStructureChanged(e.getTreePath());
- }
- }
-
-
- @Override
- public final int compare(ValueIndexPair<N> a, ValueIndexPair<N> b) {
- return compareNodes(a.value, b.value);
- }
-
-
- protected int compareNodes(N a, N b) {
- if (comparator != null)
- return comparator.compare(a, b);
- return collator.compare(a.toString(), b.toString());
- }
-
- private void removeConverter(Object node) {
- Converter c = getConverter(node);
- if (c != null)
- removeConverter(c, node);
- }
-
- private void removeConverter(Converter converter, Object node) {
- for (int i=converter.getChildCount(); --i>=0;) {
- int index = converter.convertRowIndexToModel(i);
- Object child = model.getChild(node, index);
- Converter c = getConverter(child);
- if (c != null)
- removeConverter(c, child);
- }
- converters.remove(node);
- }
-
- private boolean isFilteredOut(TreePath path) {
- if (filterStartPath != null && !filterStartPath.isDescendant(path))
- return false;
- TreePath parent = path.getParentPath();
- // root should always have a converter if filter is non-null,
- // so if parent is ever null, there is a bug somewhere else
- Converter c = getConverter(parent.getLastPathComponent());
- if (c != null) {
- return getIndexOfChild(
- parent.getLastPathComponent(),
- path.getLastPathComponent()) < 0;
- }
- return isFilteredOut(parent);
- }
-
- private void filterIn(int[] vtm, int vtmLength, TreePath path, ArrayList<TreePath> expand) {
- Object node = path.getLastPathComponent();
- if (vtmLength != vtm.length)
- vtm = Arrays.copyOf(vtm, vtmLength);
- Converter converter = new Converter(vtm, true);
- converters.put(node, converter);
- insert(path.getParentPath(), node);
- tree.expandPath(path);
- expandPaths(expand);
- }
-
- private boolean acceptable(TreePath path, Object[] nodes, int index) {
- Object node = nodes[index];
- return filter.acceptNode(path, (N)node, model.isLeaf(node));
- }
-
- private int ascInsertionIndex(int[] vtm, Object parent, N node, int idx) {
- for (int i=vtm.length; --i>=0;) {
- int cmp = compareNodes(node, (N)model.getChild(parent, vtm[i]));
- if (cmp > 0 || (cmp == 0 && idx > vtm[i])) {
- return i+1;
- }
- }
- return 0;
- }
-
-
- private int dscInsertionIndex(int[] vtm, Object parent, N node, int idx) {
- for (int i=vtm.length; --i>=0;) {
- int cmp = compareNodes(node, (N)model.getChild(parent, vtm[i]));
- if (cmp < 0) {
- return i+1;
- } else if (cmp == 0 && idx < vtm[i]) {
- return i;
- }
- }
- return 0;
- }
-
-
- /**
- * Inserts the specified path and node and any parent paths as necessary.
- * <p>
- * Fires appropriate event.
- * @param path
- * @param node
- */
- private void insert(TreePath path, Object node) {
- Object parent = path.getLastPathComponent();
- Converter converter = converters.get(parent);
- int modelIndex = model.getIndexOfChild(parent, node);
- if (converter == null) {
- converter = new Converter(indices(modelIndex), true);
- converters.put(parent, converter);
- insert(path.getParentPath(), parent);
- } else {
- int viewIndex = insert(parent, node, modelIndex, converter);
- fireTreeNodesInserted(path, indices(viewIndex), nodes(node));
- }
- }
-
- /**
- * Inserts node into parent in correct sort order.
- * <p>
- * Responsibility of caller to fire appropriate event with the returned viewIndex.
- * @param path
- * @param node
- * @param modelIndex
- * @param converter
- * @return viewIndex
- */
- private int insert(Object parent, Object node, int modelIndex, Converter converter) {
- int[] vtm = converter.viewToModel;
- int viewIndex;
- switch (sortOrder) {
- case ASCENDING:
- viewIndex = ascInsertionIndex(vtm, parent, (N)node, modelIndex);
- break;
- case DESCENDING:
- viewIndex = dscInsertionIndex(vtm, parent, (N)node, modelIndex);
- break;
- default: case UNSORTED:
- viewIndex = unsortedInsertionIndex(vtm, modelIndex);
- break;
- }
- int[] a = new int[vtm.length+1];
- System.arraycopy(vtm, 0, a, 0, viewIndex);
- System.arraycopy(vtm, viewIndex, a, viewIndex+1, vtm.length-viewIndex);
- a[viewIndex] = modelIndex;
- converter.viewToModel = a;
- return viewIndex;
- }
-
- private void remove(TreePath path, Object node) {
- Object parent = path.getLastPathComponent();
- if (path.getPathCount() == 1 || (filterStartPath != null && filterStartPath.equals(path))) {
- removeConverter(node);
- converters.put(parent, new Converter(Converter.EMPTY, true));
- fireTreeNodesRemoved(path, indices(0), nodes(node));
- return;
- }
- Converter converter = converters.get(parent);
- int modelIndex = model.getIndexOfChild(parent, node);
- int viewIndex = converter.remove(modelIndex);
- switch (viewIndex) {
- default:
- removeConverter(node);
- fireTreeNodesRemoved(path, indices(viewIndex), nodes(node));
- break;
- case Converter.ONLY_INDEX:
-// if (path.getParentPath() == null) {
-// // reached filter root
-// removeConverter(node);
-// converters.put(parent, new Converter(Converter.EMPTY, true));
-// fireTreeNodesRemoved(path, indices(0), nodes(node));
-// return;
-// }
- remove(path.getParentPath(), parent);
- break;
- case Converter.INDEX_NOT_FOUND:
- removeConverter(node);
- }
- }
-
-
-
- }
-
-
-
- private static int unsortedInsertionIndex(int[] vtm, int idx) {
- for (int i=vtm.length; --i>=0;)
- if (vtm[i] < idx)
- return i+1;
- return 0;
- }
-
- private static void sort(int[] childIndices, Object[] childNodes) {
- int len = childIndices.length;
- ValueIndexPair[] pairs = new ValueIndexPair[len];
- for (int i=len; --i>=0;)
- pairs[i] = new ValueIndexPair<Object>(childIndices[i], childNodes[i]);
- Arrays.sort(pairs);
- for (int i=len; --i>=0;) {
- childIndices[i] = pairs[i].index;
- childNodes[i] = pairs[i].value;
- }
- }
-
- private static int[] indices(int...indices) {
- return indices;
- }
-
- private static Object[] nodes(Object...nodes) {
- return nodes;
- }
-
-
-
-
- /**
- * This class has a dual purpose, both related to comparing/sorting.
- * <p>
- * The Handler class sorts an array of ValueIndexPair based on the value.
- * Used for sorting the view.
- * <p>
- * ValueIndexPair sorts itself based on the index.
- * Used for sorting childIndices for fire* methods.
- */
- private static class ValueIndexPair<N> implements Comparable<ValueIndexPair<N>> {
- ValueIndexPair() {}
-
- ValueIndexPair(int idx, N val) {
- index = idx;
- value = val;
- }
-
- N value;
-
- int index;
-
- public int compareTo(ValueIndexPair<N> o) {
- return index - o.index;
- }
- }
-
- private static class Converter {
-
- static final int[] EMPTY = new int[0];
-
- static final int ONLY_INDEX = -2;
-
- static final int INDEX_NOT_FOUND = -1;
-
- Converter(int[] vtm, boolean filtered) {
- viewToModel = vtm;
- isFiltered = filtered;
- }
-
- private int[] viewToModel;
-
- private boolean isFiltered;
-
-// public boolean equals(Converter conv) {
-// if (conv == null)
-// return false;
-// if (isFiltered != conv.isFiltered)
-// return false;
-// return Arrays.equals(viewToModel, conv.viewToModel);
-// }
-
- boolean isFiltered() {
- return isFiltered;
- }
-
- void remove(int viewIndices[]) {
- int len = viewToModel.length - viewIndices.length;
- if (len == 0) {
- viewToModel = EMPTY;
- } else {
- int[] oldVTM = viewToModel;
- int[] newVTM = new int[len];
- for (int oldIndex=0, newIndex=0, removeIndex=0;
- newIndex<newVTM.length;
- newIndex++, oldIndex++) {
- while (removeIndex < viewIndices.length && oldIndex == viewIndices[removeIndex]) {
- int idx = oldVTM[oldIndex];
- removeIndex++;
- oldIndex++;
- for (int i=newIndex; --i>=0;)
- if (newVTM[i] > idx)
- newVTM[i]--;
- for (int i=oldIndex; i<oldVTM.length; i++)
- if (oldVTM[i] > idx)
- oldVTM[i]--;
- }
- newVTM[newIndex] = oldVTM[oldIndex];
- }
- viewToModel = newVTM;
- }
- }
-
- /**
- * @param modelIndex
- * @return viewIndex that was removed<br>
- * or <code>ONLY_INDEX</code> if the modelIndex is the only one in the view<br>
- * or <code>INDEX_NOT_FOUND</code> if the modelIndex is not in the view
- */
- int remove(int modelIndex) {
- int[] vtm = viewToModel;
- for (int i=vtm.length; --i>=0;) {
- if (vtm[i] > modelIndex) {
- vtm[i] -= 1;
- } else if (vtm[i] == modelIndex) {
- if (vtm.length == 1) {
- viewToModel = EMPTY;
- return ONLY_INDEX;
- }
- int viewIndex = i;
- while (--i>=0) {
- if (vtm[i] > modelIndex)
- vtm[i] -= 1;
- }
- int[] a = new int[vtm.length-1];
- if (viewIndex > 0)
- System.arraycopy(vtm, 0, a, 0, viewIndex);
- int len = a.length-viewIndex;
- if (len > 0)
- System.arraycopy(vtm, viewIndex+1, a, viewIndex, len);
- viewToModel = a;
- return viewIndex;
- }
- }
- return INDEX_NOT_FOUND;
- }
-
-
- int getChildCount() {
- return viewToModel.length;
- }
-
- /**
- * @param modelIndex
- * @return viewIndex corresponding to modelIndex<br>
- * or <code>INDEX_NOT_FOUND</code> if the modelIndex is not in the view
- */
- int convertRowIndexToView(int modelIndex) {
- int[] vtm = viewToModel;
- for (int i=vtm.length; --i>=0;) {
- if (vtm[i] == modelIndex)
- return i;
- }
- return INDEX_NOT_FOUND;
- }
-
- int convertRowIndexToModel(int viewIndex) {
- return viewToModel[viewIndex];
- }
- }
-
- public interface Filter<N> {
- boolean acceptNode(TreePath parent, N node, boolean leaf);
- }
-
- public static class RegexFilter<N> implements Filter<N> {
-
- public RegexFilter(Pattern pattern, boolean leaf) {
- matcher = pattern.matcher("");
- leafOnly = leaf;
- }
-
- private Matcher matcher;
-
- private boolean leafOnly;
-
- public boolean acceptNode(TreePath parent, N node, boolean leaf) {
- if (leafOnly && !leaf)
- return false;
- matcher.reset(getStringValue(node));
- return matcher.find();
- }
-
- protected String getStringValue(N node) {
- return node.toString();
- }
- }
-
-
- private static Map<Object,Converter> createConvertersMap() {
- return new HashMap<Object,Converter>();
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.utils;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-
-import javax.swing.JTree;
-import javax.swing.tree.TreeModel;
-import javax.swing.tree.TreeNode;
-import javax.swing.tree.TreePath;
-
-public class TreeSnapshot {
- private interface NodeAction {
- public void run(TreeNode node);
- }
-
- private JTree tree;
- private TreePath[] selectionPaths;
- private List<TreePath> expanded;
-
- public TreeSnapshot(JTree tree) {
- this.tree = tree;
-
- selectionPaths = tree.getSelectionPaths();
- if (selectionPaths == null) {
- selectionPaths = new TreePath[0];
- }
-
- expanded = new ArrayList<TreePath>();
- forEach(tree, new NodeAction() {
- @Override
- public void run(TreeNode node) {
- TreePath path = nodeToPath(node);
- if (path != null) {
- if (TreeSnapshot.this.tree.isExpanded(path)) {
- expanded.add(path);
- }
- }
- }
- });
- }
-
- public void apply() {
- applyTo(tree);
- }
-
- public void applyTo(JTree tree) {
- final List<TreePath> newExpanded = new ArrayList<TreePath>();
- final List<TreePath> newSlectionPaths = new ArrayList<TreePath>();
-
- forEach(tree, new NodeAction() {
- @Override
- public void run(TreeNode newNode) {
- TreePath newPath = nodeToPath(newNode);
- if (newPath != null) {
- for (TreePath path : selectionPaths) {
- if (isSamePath(path, newPath)) {
- newSlectionPaths.add(newPath);
- if (expanded.contains(path)) {
- newExpanded.add(newPath);
- }
- }
- }
- }
- }
- });
-
- for (TreePath newPath : newExpanded) {
- tree.expandPath(newPath);
- }
-
- tree.setSelectionPaths(newSlectionPaths.toArray(new TreePath[0]));
- }
-
- // You can override this
- protected boolean isSamePath(TreePath oldPath, TreePath newPath) {
- return newPath.toString().equals(oldPath.toString());
- }
-
- private void forEach(JTree tree, NodeAction action) {
- forEach(tree.getModel(), tree.getModel().getRoot(), action);
- }
-
- private void forEach(TreeModel model, Object parent, NodeAction action) {
- if (!(parent instanceof TreeNode))
- return;
-
- TreeNode node = (TreeNode) parent;
-
- action.run(node);
- int count = model.getChildCount(node);
- for (int i = 0; i < count; i++) {
- Object child = model.getChild(node, i);
- forEach(model, child, action);
- }
- }
-
- private static TreePath nodeToPath(TreeNode node) {
- List<Object> nodes = new LinkedList<Object>();
- if (node != null) {
- nodes.add(node);
- node = node.getParent();
- while (node != null) {
- nodes.add(0, node);
- node = node.getParent();
- }
- }
-
- return nodes.isEmpty() ? null : new TreePath(nodes.toArray());
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append("Tree Snapshot of: ").append(tree).append("\n");
- builder.append("Selected paths:\n");
- for (TreePath path : selectionPaths) {
- builder.append("\t").append(path).append("\n");
- }
- builder.append("Expanded paths:\n");
- for (TreePath epath : expanded) {
- builder.append("\t").append(epath).append("\n");
- }
-
- return builder.toString();
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.gui.utils;
-
-import java.awt.Color;
-import java.awt.Component;
-
-import javax.swing.JButton;
-import javax.swing.JComponent;
-import javax.swing.JOptionPane;
-import javax.swing.JScrollPane;
-import javax.swing.JTree;
-import javax.swing.SwingUtilities;
-
-import be.nikiroo.fanfix.Instance;
-
-public class UiHelper {
- static private Color buttonNormal;
- static private Color buttonPressed;
-
- static public void setButtonPressed(JButton button, boolean pressed) {
- if (buttonNormal == null) {
- JButton defButton = new JButton(" ");
- buttonNormal = defButton.getBackground();
- if (buttonNormal.getBlue() >= 128) {
- buttonPressed = new Color( //
- Math.max(buttonNormal.getRed() - 100, 0), //
- Math.max(buttonNormal.getGreen() - 100, 0), //
- Math.max(buttonNormal.getBlue() - 100, 0));
- } else {
- buttonPressed = new Color( //
- Math.min(buttonNormal.getRed() + 100, 255), //
- Math.min(buttonNormal.getGreen() + 100, 255), //
- Math.min(buttonNormal.getBlue() + 100, 255));
- }
- }
-
- button.setSelected(pressed);
- button.setBackground(pressed ? buttonPressed : buttonNormal);
- }
-
- static public JComponent scroll(JComponent pane) {
- JScrollPane scroll = new JScrollPane(pane);
- scroll.getVerticalScrollBar().setUnitIncrement(16);
- scroll.setHorizontalScrollBarPolicy(
- JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
- return scroll;
- }
-
- /**
- * Display an error message and log the linked {@link Exception}.
- *
- * @param owner
- * the owner of the error (to link the messagebox to it)
- * @param message
- * the message
- * @param title
- * the title of the error message
- * @param e
- * the exception to log if any
- */
- static public void error(final Component owner, final String message,
- final String title, Exception e) {
- Instance.getInstance().getTraceHandler().error(title + ": " + message);
- if (e != null) {
- Instance.getInstance().getTraceHandler().error(e);
- }
-
- SwingUtilities.invokeLater(new Runnable() {
- @Override
- public void run() {
- JOptionPane.showMessageDialog(owner, message, title,
- JOptionPane.ERROR_MESSAGE);
- }
- });
- }
-}
+++ /dev/null
-package be.nikiroo.fanfix_swing.images;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.swing.ImageIcon;
-
-import be.nikiroo.utils.IOUtils;
-
-/**
- * Icons generator for this project.
- *
- * @author niki
- */
-public class IconGenerator {
- /**
- * The available icons.
- *
- * @author niki
- */
- public enum Icon {
- /** Icon used to clear text fields */
- clear,
- /** Search icon (magnifying glass) */
- search,
- /** An interrogation point */
- unknown,
- /** A small, left-pointed arrow */
- arrow_left,
- /** A small, right-pointed arrow */
- arrow_right,
- /** A small, up-pointed arrow */
- arrow_up,
- /** A small, down-pointed arrow */
- arrow_down,
- /** An empty (transparent) icon */
- empty,
- }
-
- /**
- * The available sizes.
- *
- * @author niki
- */
- public enum Size {
- /** 4x4 pixels, only for {@link Icon#empty} */
- x4(4),
- /** 8x8 pixels, only for {@link Icon#empty} */
- x8(8),
- /** 16x16 pixels */
- x16(16),
- /** 24x24 pixels */
- x24(24),
- /** 32x32 pixels */
- x32(32),
- /** 64x64 pixels */
- x64(64);
-
- private int size;
-
- private Size(int size) {
- this.size = size;
- }
-
- /**
- * Return the size in pixels.
- *
- * @return the size
- */
- public int getSize() {
- return size;
- }
- }
-
- static private Map<String, ImageIcon> map = new HashMap<String, ImageIcon>();
-
- /**
- * Generate a new image.
- *
- * @param name
- * the name of the resource
- * @param size
- * the requested size
- *
- * @return the image, or NULL if it does not exist or does not exist at that
- * size
- */
- static public ImageIcon get(Icon name, Size size) {
- String key = String.format("%s-%dx%d.png", name.name(), size.getSize(),
- size.getSize());
- if (!map.containsKey(key)) {
- map.put(key, generate(key));
- }
-
- return map.get(key);
- }
-
- /**
- * Generate a new image.
- *
- * @param filename
- * the file name of the resource (no directory)
- *
- * @return the image, or NULL if it does not exist or does not exist at that
- * size
- */
- static private ImageIcon generate(String filename) {
- try {
- InputStream in = IOUtils.openResource(IconGenerator.class,
- filename);
- if (in != null) {
- try {
- return new ImageIcon(IOUtils.toByteArray(in));
- } finally {
- in.close();
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- return null;
- }
-}
+++ /dev/null
-#!/bin/bash
-
-if [ "$1" = "" ]; then
- echo Syntax: "$0 file1.png file2.png..." >&2
- exit 1
-fi
-
-while [ "$1" != "" ]; do
- name="`basename "$1" .png`"
- for S in 8 16 24 32 64; do
- convert -resize ${S}x${S} "$name".png "$name"-${S}x${S}.png
- done
- shift
-done
-
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Changes (@author niki):
+ * - default charset -> UTF-8
+ */
+
+package be.nikiroo.utils.streams;
+
+import java.io.UnsupportedEncodingException;
+
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * Utilities for encoding and decoding the Base64 representation of
+ * binary data. See RFCs <a
+ * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
+ * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
+ */
+class Base64 {
+ /**
+ * Default values for encoder/decoder flags.
+ */
+ public static final int DEFAULT = 0;
+
+ /**
+ * Encoder flag bit to omit the padding '=' characters at the end
+ * of the output (if any).
+ */
+ public static final int NO_PADDING = 1;
+
+ /**
+ * Encoder flag bit to omit all line terminators (i.e., the output
+ * will be on one long line).
+ */
+ public static final int NO_WRAP = 2;
+
+ /**
+ * Encoder flag bit to indicate lines should be terminated with a
+ * CRLF pair instead of just an LF. Has no effect if {@code
+ * NO_WRAP} is specified as well.
+ */
+ public static final int CRLF = 4;
+
+ /**
+ * Encoder/decoder flag bit to indicate using the "URL and
+ * filename safe" variant of Base64 (see RFC 3548 section 4) where
+ * {@code -} and {@code _} are used in place of {@code +} and
+ * {@code /}.
+ */
+ public static final int URL_SAFE = 8;
+
+ /**
+ * Flag to pass to {@link Base64OutputStream} to indicate that it
+ * should not close the output stream it is wrapping when it
+ * itself is closed.
+ */
+ public static final int NO_CLOSE = 16;
+
+ // --------------------------------------------------------
+ // shared code
+ // --------------------------------------------------------
+
+ /* package */ static abstract class Coder {
+ public byte[] output;
+ public int op;
+
+ /**
+ * Encode/decode another block of input data. this.output is
+ * provided by the caller, and must be big enough to hold all
+ * the coded data. On exit, this.opwill be set to the length
+ * of the coded data.
+ *
+ * @param finish true if this is the final call to process for
+ * this object. Will finalize the coder state and
+ * include any final bytes in the output.
+ *
+ * @return true if the input so far is good; false if some
+ * error has been detected in the input stream..
+ */
+ public abstract boolean process(byte[] input, int offset, int len, boolean finish);
+
+ /**
+ * @return the maximum number of bytes a call to process()
+ * could produce for the given number of input bytes. This may
+ * be an overestimate.
+ */
+ public abstract int maxOutputSize(int len);
+ }
+
+ // --------------------------------------------------------
+ // decoding
+ // --------------------------------------------------------
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param str the input String to decode, which is converted to
+ * bytes using the default charset
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(String str, int flags) {
+ return decode(StringUtils.getBytes(str), flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the input array to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int flags) {
+ return decode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the data to decode
+ * @param offset the position within the input array at which to start
+ * @param len the number of bytes of input to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int offset, int len, int flags) {
+ // Allocate space for the most data the input could represent.
+ // (It could contain less if it contains whitespace, etc.)
+ Decoder decoder = new Decoder(flags, new byte[len*3/4]);
+
+ if (!decoder.process(input, offset, len, true)) {
+ throw new IllegalArgumentException("bad base-64");
+ }
+
+ // Maybe we got lucky and allocated exactly enough output space.
+ if (decoder.op == decoder.output.length) {
+ return decoder.output;
+ }
+
+ // Need to shorten the array, so allocate a new one of the
+ // right size and copy.
+ byte[] temp = new byte[decoder.op];
+ System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
+ return temp;
+ }
+
+ /* package */ static class Decoder extends Coder {
+ /**
+ * Lookup table for turning bytes into their position in the
+ * Base64 alphabet.
+ */
+ private static final int DECODE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /**
+ * Decode lookup table for the "web safe" variant (RFC 3548
+ * sec. 4) where - and _ replace + and /.
+ */
+ private static final int DECODE_WEBSAFE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /** Non-data values in the DECODE arrays. */
+ private static final int SKIP = -1;
+ private static final int EQUALS = -2;
+
+ /**
+ * States 0-3 are reading through the next input tuple.
+ * State 4 is having read one '=' and expecting exactly
+ * one more.
+ * State 5 is expecting no more data or padding characters
+ * in the input.
+ * State 6 is the error state; an error has been detected
+ * in the input and no future input can "fix" it.
+ */
+ private int state; // state number (0 to 6)
+ private int value;
+
+ final private int[] alphabet;
+
+ public Decoder(int flags, byte[] output) {
+ this.output = output;
+
+ alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
+ state = 0;
+ value = 0;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could decode to.
+ */
+ @Override
+ public int maxOutputSize(int len) {
+ return len * 3/4 + 10;
+ }
+
+ /**
+ * Decode another block of input data.
+ *
+ * @return true if the state machine is still healthy. false if
+ * bad base-64 data has been detected in the input stream.
+ */
+ @Override
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ if (this.state == 6) return false;
+
+ int p = offset;
+ len += offset;
+
+ // Using local variables makes the decoder about 12%
+ // faster than if we manipulate the member variables in
+ // the loop. (Even alphabet makes a measurable
+ // difference, which is somewhat surprising to me since
+ // the member variable is final.)
+ int state = this.state;
+ int value = this.value;
+ int op = 0;
+ final byte[] output = this.output;
+ final int[] alphabet = this.alphabet;
+
+ while (p < len) {
+ // Try the fast path: we're starting a new tuple and the
+ // next four bytes of the input stream are all data
+ // bytes. This corresponds to going through states
+ // 0-1-2-3-0. We expect to use this method for most of
+ // the data.
+ //
+ // If any of the next four bytes of input are non-data
+ // (whitespace, etc.), value will end up negative. (All
+ // the non-data values in decode are small negative
+ // numbers, so shifting any of them up and or'ing them
+ // together will result in a value with its top bit set.)
+ //
+ // You can remove this whole block and the output should
+ // be the same, just slower.
+ if (state == 0) {
+ while (p+4 <= len &&
+ (value = ((alphabet[input[p] & 0xff] << 18) |
+ (alphabet[input[p+1] & 0xff] << 12) |
+ (alphabet[input[p+2] & 0xff] << 6) |
+ (alphabet[input[p+3] & 0xff]))) >= 0) {
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ p += 4;
+ }
+ if (p >= len) break;
+ }
+
+ // The fast path isn't available -- either we've read a
+ // partial tuple, or the next four input bytes aren't all
+ // data, or whatever. Fall back to the slower state
+ // machine implementation.
+
+ int d = alphabet[input[p++] & 0xff];
+
+ switch (state) {
+ case 0:
+ if (d >= 0) {
+ value = d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 1:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 2:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect exactly one more padding character.
+ output[op++] = (byte) (value >> 4);
+ state = 4;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 3:
+ if (d >= 0) {
+ // Emit the output triple and return to state 0.
+ value = (value << 6) | d;
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ state = 0;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect no further data or padding characters.
+ output[op+1] = (byte) (value >> 2);
+ output[op] = (byte) (value >> 10);
+ op += 2;
+ state = 5;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 4:
+ if (d == EQUALS) {
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 5:
+ if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (!finish) {
+ // We're out of input, but a future call could provide
+ // more.
+ this.state = state;
+ this.value = value;
+ this.op = op;
+ return true;
+ }
+
+ // Done reading input. Now figure out where we are left in
+ // the state machine and finish up.
+
+ switch (state) {
+ case 0:
+ // Output length is a multiple of three. Fine.
+ break;
+ case 1:
+ // Read one extra input byte, which isn't enough to
+ // make another output byte. Illegal.
+ this.state = 6;
+ return false;
+ case 2:
+ // Read two extra input bytes, enough to emit 1 more
+ // output byte. Fine.
+ output[op++] = (byte) (value >> 4);
+ break;
+ case 3:
+ // Read three extra input bytes, enough to emit 2 more
+ // output bytes. Fine.
+ output[op++] = (byte) (value >> 10);
+ output[op++] = (byte) (value >> 2);
+ break;
+ case 4:
+ // Read one padding '=' when we expected 2. Illegal.
+ this.state = 6;
+ return false;
+ case 5:
+ // Read all the padding '='s we expected and no more.
+ // Fine.
+ break;
+ }
+
+ this.state = state;
+ this.op = op;
+ return true;
+ }
+ }
+
+ // --------------------------------------------------------
+ // encoding
+ // --------------------------------------------------------
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int flags) {
+ try {
+ return new String(encode(input, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int offset, int len, int flags) {
+ try {
+ return new String(encode(input, offset, len, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int flags) {
+ return encode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int offset, int len, int flags) {
+ Encoder encoder = new Encoder(flags, null);
+
+ // Compute the exact length of the array we will produce.
+ int output_len = len / 3 * 4;
+
+ // Account for the tail of the data and the padding bytes, if any.
+ if (encoder.do_padding) {
+ if (len % 3 > 0) {
+ output_len += 4;
+ }
+ } else {
+ switch (len % 3) {
+ case 0: break;
+ case 1: output_len += 2; break;
+ case 2: output_len += 3; break;
+ }
+ }
+
+ // Account for the newlines, if any.
+ if (encoder.do_newline && len > 0) {
+ output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
+ (encoder.do_cr ? 2 : 1);
+ }
+
+ encoder.output = new byte[output_len];
+ encoder.process(input, offset, len, true);
+
+ assert encoder.op == output_len;
+
+ return encoder.output;
+ }
+
+ /* package */ static class Encoder extends Coder {
+ /**
+ * Emit a new line every this many output tuples. Corresponds to
+ * a 76-character line length (the maximum allowable according to
+ * <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
+ */
+ public static final int LINE_GROUPS = 19;
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
+ };
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE_WEBSAFE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
+ };
+
+ final private byte[] tail;
+ /* package */ int tailLen;
+ private int count;
+
+ final public boolean do_padding;
+ final public boolean do_newline;
+ final public boolean do_cr;
+ final private byte[] alphabet;
+
+ public Encoder(int flags, byte[] output) {
+ this.output = output;
+
+ do_padding = (flags & NO_PADDING) == 0;
+ do_newline = (flags & NO_WRAP) == 0;
+ do_cr = (flags & CRLF) != 0;
+ alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
+
+ tail = new byte[2];
+ tailLen = 0;
+
+ count = do_newline ? LINE_GROUPS : -1;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could encode to.
+ */
+ @Override
+ public int maxOutputSize(int len) {
+ return len * 8/5 + 10;
+ }
+
+ @Override
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ // Using local variables makes the encoder about 9% faster.
+ final byte[] alphabet = this.alphabet;
+ final byte[] output = this.output;
+ int op = 0;
+ int count = this.count;
+
+ int p = offset;
+ len += offset;
+ int v = -1;
+
+ // First we need to concatenate the tail of the previous call
+ // with any input bytes available now and see if we can empty
+ // the tail.
+
+ switch (tailLen) {
+ case 0:
+ // There was no tail.
+ break;
+
+ case 1:
+ if (p+2 <= len) {
+ // A 1-byte tail with at least 2 bytes of
+ // input available now.
+ v = ((tail[0] & 0xff) << 16) |
+ ((input[p++] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+
+ case 2:
+ if (p+1 <= len) {
+ // A 2-byte tail with at least 1 byte of input.
+ v = ((tail[0] & 0xff) << 16) |
+ ((tail[1] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+ }
+
+ if (v != -1) {
+ output[op++] = alphabet[(v >> 18) & 0x3f];
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ // At this point either there is no tail, or there are fewer
+ // than 3 bytes of input available.
+
+ // The main loop, turning 3 input bytes into 4 output bytes on
+ // each iteration.
+ while (p+3 <= len) {
+ v = ((input[p] & 0xff) << 16) |
+ ((input[p+1] & 0xff) << 8) |
+ (input[p+2] & 0xff);
+ output[op] = alphabet[(v >> 18) & 0x3f];
+ output[op+1] = alphabet[(v >> 12) & 0x3f];
+ output[op+2] = alphabet[(v >> 6) & 0x3f];
+ output[op+3] = alphabet[v & 0x3f];
+ p += 3;
+ op += 4;
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ if (finish) {
+ // Finish up the tail of the input. Note that we need to
+ // consume any bytes in tail before any bytes
+ // remaining in input; there should be at most two bytes
+ // total.
+
+ if (p-tailLen == len-1) {
+ int t = 0;
+ v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (p-tailLen == len-2) {
+ int t = 0;
+ v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
+ (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (do_newline && op > 0 && count != LINE_GROUPS) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+
+ assert tailLen == 0;
+ assert p == len;
+ } else {
+ // Save the leftovers in tail to be consumed on the next
+ // call to encodeInternal.
+
+ if (p == len-1) {
+ tail[tailLen++] = input[p];
+ } else if (p == len-2) {
+ tail[tailLen++] = input[p];
+ tail[tailLen++] = input[p+1];
+ }
+ }
+
+ this.op = op;
+ this.count = count;
+
+ return true;
+ }
+ }
+
+ private Base64() { } // don't instantiate
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package be.nikiroo.utils.streams;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An InputStream that does Base64 decoding on the data read through
+ * it.
+ */
+public class Base64InputStream extends FilterInputStream {
+ private final Base64.Coder coder;
+
+ private static byte[] EMPTY = new byte[0];
+
+ private static final int BUFFER_SIZE = 2048;
+ private boolean eof;
+ private byte[] inputBuffer;
+ private int outputStart;
+ private int outputEnd;
+
+ /**
+ * An InputStream that performs Base64 decoding on the data read
+ * from the wrapped stream.
+ *
+ * @param in the InputStream to read the source data from
+ */
+ public Base64InputStream(InputStream in) {
+ this(in, false);
+ }
+
+ /**
+ * Performs Base64 encoding or decoding on the data read from the
+ * wrapped InputStream.
+ *
+ * @param in the InputStream to read the source data from
+ * @param flags bit flags for controlling the decoder; see the
+ * constants in {@link Base64}
+ * @param encode true to encode, false to decode
+ *
+ * @hide
+ */
+ public Base64InputStream(InputStream in, boolean encode) {
+ super(in);
+ eof = false;
+ inputBuffer = new byte[BUFFER_SIZE];
+ if (encode) {
+ coder = new Base64.Encoder(Base64.NO_WRAP, null);
+ } else {
+ coder = new Base64.Decoder(Base64.NO_WRAP, null);
+ }
+ coder.output = new byte[coder.maxOutputSize(BUFFER_SIZE)];
+ outputStart = 0;
+ outputEnd = 0;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ @SuppressWarnings("sync-override")
+ @Override
+ public void mark(int readlimit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("sync-override")
+ @Override
+ public void reset() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() throws IOException {
+ in.close();
+ inputBuffer = null;
+ }
+
+ @Override
+ public int available() {
+ return outputEnd - outputStart;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (outputStart >= outputEnd) {
+ refill();
+ }
+ if (outputStart >= outputEnd) {
+ return 0;
+ }
+ long bytes = Math.min(n, outputEnd-outputStart);
+ outputStart += bytes;
+ return bytes;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (outputStart >= outputEnd) {
+ refill();
+ }
+ if (outputStart >= outputEnd) {
+ return -1;
+ }
+
+ return coder.output[outputStart++] & 0xff;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (outputStart >= outputEnd) {
+ refill();
+ }
+ if (outputStart >= outputEnd) {
+ return -1;
+ }
+ int bytes = Math.min(len, outputEnd-outputStart);
+ System.arraycopy(coder.output, outputStart, b, off, bytes);
+ outputStart += bytes;
+ return bytes;
+ }
+
+ /**
+ * Read data from the input stream into inputBuffer, then
+ * decode/encode it into the empty coder.output, and reset the
+ * outputStart and outputEnd pointers.
+ */
+ private void refill() throws IOException {
+ if (eof) return;
+ int bytesRead = in.read(inputBuffer);
+ boolean success;
+ if (bytesRead == -1) {
+ eof = true;
+ success = coder.process(EMPTY, 0, 0, true);
+ } else {
+ success = coder.process(inputBuffer, 0, bytesRead, false);
+ }
+ if (!success) {
+ throw new IOException("bad base-64");
+ }
+ outputEnd = coder.op;
+ outputStart = 0;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package be.nikiroo.utils.streams;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An OutputStream that does Base64 encoding on the data written to
+ * it, writing the resulting data to another OutputStream.
+ */
+public class Base64OutputStream extends FilterOutputStream {
+ private final Base64.Coder coder;
+ private final int flags;
+
+ private byte[] buffer = null;
+ private int bpos = 0;
+
+ private static byte[] EMPTY = new byte[0];
+
+ /**
+ * Performs Base64 encoding on the data written to the stream,
+ * writing the encoded data to another OutputStream.
+ *
+ * @param out the OutputStream to write the encoded data to
+ */
+ public Base64OutputStream(OutputStream out) {
+ this(out, true);
+ }
+
+ /**
+ * Performs Base64 encoding or decoding on the data written to the
+ * stream, writing the encoded/decoded data to another
+ * OutputStream.
+ *
+ * @param out the OutputStream to write the encoded data to
+ * @param encode true to encode, false to decode
+ *
+ * @hide
+ */
+ public Base64OutputStream(OutputStream out, boolean encode) {
+ super(out);
+ this.flags = Base64.NO_WRAP;
+ if (encode) {
+ coder = new Base64.Encoder(flags, null);
+ } else {
+ coder = new Base64.Decoder(flags, null);
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ // To avoid invoking the encoder/decoder routines for single
+ // bytes, we buffer up calls to write(int) in an internal
+ // byte array to transform them into writes of decently-sized
+ // arrays.
+
+ if (buffer == null) {
+ buffer = new byte[1024];
+ }
+ if (bpos >= buffer.length) {
+ // internal buffer full; write it out.
+ internalWrite(buffer, 0, bpos, false);
+ bpos = 0;
+ }
+ buffer[bpos++] = (byte) b;
+ }
+
+ /**
+ * Flush any buffered data from calls to write(int). Needed
+ * before doing a write(byte[], int, int) or a close().
+ */
+ private void flushBuffer() throws IOException {
+ if (bpos > 0) {
+ internalWrite(buffer, 0, bpos, false);
+ bpos = 0;
+ }
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (len <= 0) return;
+ flushBuffer();
+ internalWrite(b, off, len, false);
+ }
+
+ @Override
+ public void close() throws IOException {
+ IOException thrown = null;
+ try {
+ flushBuffer();
+ internalWrite(EMPTY, 0, 0, true);
+ } catch (IOException e) {
+ thrown = e;
+ }
+
+ try {
+ if ((flags & Base64.NO_CLOSE) == 0) {
+ out.close();
+ } else {
+ out.flush();
+ }
+ } catch (IOException e) {
+ if (thrown != null) {
+ thrown = e;
+ }
+ }
+
+ if (thrown != null) {
+ throw thrown;
+ }
+ }
+
+ /**
+ * Write the given bytes to the encoder/decoder.
+ *
+ * @param finish true if this is the last batch of input, to cause
+ * encoder/decoder state to be finalized.
+ */
+ private void internalWrite(byte[] b, int off, int len, boolean finish) throws IOException {
+ coder.output = embiggen(coder.output, coder.maxOutputSize(len));
+ if (!coder.process(b, off, len, finish)) {
+ throw new IOException("bad base-64");
+ }
+ out.write(coder.output, 0, coder.op);
+ }
+
+ /**
+ * If b.length is at least len, return b. Otherwise return a new
+ * byte array of length len.
+ */
+ private byte[] embiggen(byte[] b, int len) {
+ if (b == null || b.length < len) {
+ return new byte[len];
+ }
+ return b;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * A simple {@link InputStream} that is buffered with a bytes array.
+ * <p>
+ * It is mostly intended to be used as a base class to create new
+ * {@link InputStream}s with special operation modes, and to give some default
+ * methods.
+ *
+ * @author niki
+ */
+public class BufferedInputStream extends InputStream {
+ /**
+ * The size of the internal buffer (can be different if you pass your own
+ * buffer, of course).
+ * <p>
+ * A second buffer of twice the size can sometimes be created as needed for
+ * the {@link BufferedInputStream#startsWith(byte[])} search operation.
+ */
+ static private final int BUFFER_SIZE = 4096;
+
+ /** The current position in the buffer. */
+ protected int start;
+ /** The index of the last usable position of the buffer. */
+ protected int stop;
+ /** The buffer itself. */
+ protected byte[] buffer;
+ /** An End-Of-File (or {@link InputStream}, here) marker. */
+ protected boolean eof;
+
+ private boolean closed;
+ private InputStream in;
+ private int openCounter;
+
+ // special use, prefetched next buffer
+ private byte[] buffer2;
+ private int pos2;
+ private int len2;
+ private byte[] originalBuffer;
+
+ private long bytesRead;
+
+ /**
+ * Create a new {@link BufferedInputStream} that wraps the given
+ * {@link InputStream}.
+ *
+ * @param in
+ * the {@link InputStream} to wrap
+ */
+ public BufferedInputStream(InputStream in) {
+ this.in = in;
+
+ this.buffer = new byte[BUFFER_SIZE];
+ this.originalBuffer = this.buffer;
+ this.start = 0;
+ this.stop = 0;
+ }
+
+ /**
+ * Create a new {@link BufferedInputStream} that wraps the given bytes array
+ * as a data source.
+ *
+ * @param in
+ * the array to wrap, cannot be NULL
+ */
+ public BufferedInputStream(byte[] in) {
+ this(in, 0, in.length);
+ }
+
+ /**
+ * Create a new {@link BufferedInputStream} that wraps the given bytes array
+ * as a data source.
+ *
+ * @param in
+ * the array to wrap, cannot be NULL
+ * @param offset
+ * the offset to start the reading at
+ * @param length
+ * the number of bytes to take into account in the array,
+ * starting from the offset
+ *
+ * @throws NullPointerException
+ * if the array is NULL
+ * @throws IndexOutOfBoundsException
+ * if the offset and length do not correspond to the given array
+ */
+ public BufferedInputStream(byte[] in, int offset, int length) {
+ if (in == null) {
+ throw new NullPointerException();
+ } else if (offset < 0 || length < 0 || length > in.length - offset) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ this.in = null;
+
+ this.buffer = in;
+ this.originalBuffer = this.buffer;
+ this.start = offset;
+ this.stop = length;
+ }
+
+ /**
+ * The internal buffer size (can be useful to know for search methods).
+ *
+ * @return the size of the internal buffer, in bytes.
+ */
+ public int getInternalBufferSize() {
+ return originalBuffer.length;
+ }
+
+ /**
+ * Return this very same {@link BufferedInputStream}, but keep a counter of
+ * how many streams were open this way. When calling
+ * {@link BufferedInputStream#close()}, decrease this counter if it is not
+ * already zero instead of actually closing the stream.
+ * <p>
+ * You are now responsible for it — you <b>must</b> close it.
+ * <p>
+ * This method allows you to use a wrapping stream around this one and still
+ * close the wrapping stream.
+ *
+ * @return the same stream, but you are now responsible for closing it
+ *
+ * @throws IOException
+ * in case of I/O error or if the stream is closed
+ */
+ public synchronized InputStream open() throws IOException {
+ checkClose();
+ openCounter++;
+ return this;
+ }
+
+ /**
+ * Check if the current content (until eof) is equal to the given search
+ * term.
+ * <p>
+ * Note: the search term size <b>must</b> be smaller or equal the internal
+ * buffer size.
+ *
+ * @param search
+ * the term to search for
+ *
+ * @return TRUE if the content that will be read starts with it
+ *
+ * @throws IOException
+ * in case of I/O error or if the size of the search term is
+ * greater than the internal buffer
+ */
+ public boolean is(String search) throws IOException {
+ return is(StringUtils.getBytes(search));
+ }
+
+ /**
+ * Check if the current content (until eof) is equal to the given search
+ * term.
+ * <p>
+ * Note: the search term size <b>must</b> be smaller or equal the internal
+ * buffer size.
+ *
+ * @param search
+ * the term to search for
+ *
+ * @return TRUE if the content that will be read starts with it
+ *
+ * @throws IOException
+ * in case of I/O error or if the size of the search term is
+ * greater than the internal buffer
+ */
+ public boolean is(byte[] search) throws IOException {
+ if (startsWith(search)) {
+ return (stop - start) == search.length;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the current content (what will be read next) starts with the
+ * given search term.
+ * <p>
+ * Note: the search term size <b>must</b> be smaller or equal the internal
+ * buffer size.
+ *
+ * @param search
+ * the term to search for
+ *
+ * @return TRUE if the content that will be read starts with it
+ *
+ * @throws IOException
+ * in case of I/O error or if the size of the search term is
+ * greater than the internal buffer
+ */
+ public boolean startsWith(String search) throws IOException {
+ return startsWith(StringUtils.getBytes(search));
+ }
+
+ /**
+ * Check if the current content (what will be read next) starts with the
+ * given search term.
+ * <p>
+ * An empty string will always return true (unless the stream is closed,
+ * which would throw an {@link IOException}).
+ * <p>
+ * Note: the search term size <b>must</b> be smaller or equal the internal
+ * buffer size.
+ *
+ * @param search
+ * the term to search for
+ *
+ * @return TRUE if the content that will be read starts with it
+ *
+ * @throws IOException
+ * in case of I/O error or if the size of the search term is
+ * greater than the internal buffer
+ */
+ public boolean startsWith(byte[] search) throws IOException {
+ if (search.length > originalBuffer.length) {
+ throw new IOException(
+ "This stream does not support searching for more than "
+ + buffer.length + " bytes");
+ }
+
+ checkClose();
+
+ if (available() < search.length) {
+ preRead();
+ }
+
+ if (available() >= search.length) {
+ // Easy path
+ return StreamUtils.startsWith(search, buffer, start, stop);
+ } else if (in != null && !eof) {
+ // Harder path
+ if (buffer2 == null && buffer.length == originalBuffer.length) {
+ buffer2 = Arrays.copyOf(buffer, buffer.length * 2);
+
+ pos2 = buffer.length;
+ len2 = read(in, buffer2, pos2, buffer.length);
+ if (len2 > 0) {
+ bytesRead += len2;
+ }
+
+ // Note: here, len/len2 = INDEX of last good byte
+ len2 += pos2;
+ }
+
+ return StreamUtils.startsWith(search, buffer2, pos2, len2);
+ }
+
+ return false;
+ }
+
+ /**
+ * The number of bytes read from the under-laying {@link InputStream}.
+ *
+ * @return the number of bytes
+ */
+ public long getBytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Check if this stream is spent (no more data to read or to
+ * process).
+ *
+ * @return TRUE if it is
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public boolean eof() throws IOException {
+ if (closed) {
+ return true;
+ }
+
+ preRead();
+ return !hasMoreData();
+ }
+
+ /**
+ * Read the whole {@link InputStream} until the end and return the number of
+ * bytes read.
+ *
+ * @return the number of bytes read
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public long end() throws IOException {
+ long skipped = 0;
+ while (hasMoreData()) {
+ skipped += skip(buffer.length);
+ }
+
+ return skipped;
+ }
+
+ @Override
+ public int read() throws IOException {
+ checkClose();
+
+ preRead();
+ if (eof) {
+ return -1;
+ }
+
+ return buffer[start++];
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public int read(byte[] b, int boff, int blen) throws IOException {
+ checkClose();
+
+ if (b == null) {
+ throw new NullPointerException();
+ } else if (boff < 0 || blen < 0 || blen > b.length - boff) {
+ throw new IndexOutOfBoundsException();
+ } else if (blen == 0) {
+ return 0;
+ }
+
+ int done = 0;
+ while (hasMoreData() && done < blen) {
+ preRead();
+ if (hasMoreData()) {
+ int now = Math.min(blen - done, stop - start);
+ if (now > 0) {
+ System.arraycopy(buffer, start, b, boff + done, now);
+ start += now;
+ done += now;
+ }
+ }
+ }
+
+ return done > 0 ? done : -1;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (n <= 0) {
+ return 0;
+ }
+
+ long skipped = 0;
+ while (hasMoreData() && n > 0) {
+ preRead();
+
+ long inBuffer = Math.min(n, available());
+ start += inBuffer;
+ n -= inBuffer;
+ skipped += inBuffer;
+ }
+
+ return skipped;
+ }
+
+ @Override
+ public int available() {
+ if (closed) {
+ return 0;
+ }
+
+ return Math.max(0, stop - start);
+ }
+
+ /**
+ * Closes this stream and releases any system resources associated with the
+ * stream.
+ * <p>
+ * Including the under-laying {@link InputStream}.
+ * <p>
+ * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
+ * prior to this one, it will just decrease the internal count of how many
+ * open streams it held and do nothing else. The stream will actually be
+ * closed when you have called {@link BufferedInputStream#close()} once more
+ * than {@link BufferedInputStream#open()}.
+ *
+ * @exception IOException
+ * in case of I/O error
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ close(true);
+ }
+
+ /**
+ * Closes this stream and releases any system resources associated with the
+ * stream.
+ * <p>
+ * Including the under-laying {@link InputStream} if
+ * <tt>incudingSubStream</tt> is true.
+ * <p>
+ * You can call this method multiple times, it will not cause an
+ * {@link IOException} for subsequent calls.
+ * <p>
+ * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
+ * prior to this one, it will just decrease the internal count of how many
+ * open streams it held and do nothing else. The stream will actually be
+ * closed when you have called {@link BufferedInputStream#close()} once more
+ * than {@link BufferedInputStream#open()}.
+ *
+ * @param includingSubStream
+ * also close the under-laying stream
+ *
+ * @exception IOException
+ * in case of I/O error
+ */
+ public synchronized void close(boolean includingSubStream)
+ throws IOException {
+ if (!closed) {
+ if (openCounter > 0) {
+ openCounter--;
+ } else {
+ closed = true;
+ if (includingSubStream && in != null) {
+ in.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if we still have some data in the buffer and, if not, fetch some.
+ *
+ * @return TRUE if we fetched some data, FALSE if there are still some in
+ * the buffer
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ protected boolean preRead() throws IOException {
+ boolean hasRead = false;
+ if (in != null && !eof && start >= stop) {
+ start = 0;
+ if (buffer2 != null) {
+ buffer = buffer2;
+ start = pos2;
+ stop = len2;
+
+ buffer2 = null;
+ pos2 = 0;
+ len2 = 0;
+ } else {
+ buffer = originalBuffer;
+
+ stop = read(in, buffer, 0, buffer.length);
+ if (stop > 0) {
+ bytesRead += stop;
+ }
+ }
+
+ hasRead = true;
+ }
+
+ if (start >= stop) {
+ eof = true;
+ }
+
+ return hasRead;
+ }
+
+ /**
+ * Read the under-laying stream into the local buffer.
+ *
+ * @param in
+ * the under-laying {@link InputStream}
+ * @param buffer
+ * the buffer we use in this {@link BufferedInputStream}
+ * @param off
+ * the offset
+ * @param len
+ * the length in bytes
+ *
+ * @return the number of bytes read
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ protected int read(InputStream in, byte[] buffer, int off, int len)
+ throws IOException {
+ return in.read(buffer, off, len);
+ }
+
+ /**
+ * We have more data available in the buffer <b>or</b> we can, maybe, fetch
+ * more.
+ *
+ * @return TRUE if it is the case, FALSE if not
+ */
+ protected boolean hasMoreData() {
+ if (closed) {
+ return false;
+ }
+
+ return (start < stop) || !eof;
+ }
+
+ /**
+ * Check that the stream was not closed, and throw an {@link IOException} if
+ * it was.
+ *
+ * @throws IOException
+ * if it was closed
+ */
+ protected void checkClose() throws IOException {
+ if (closed) {
+ throw new IOException(
+ "This BufferedInputStream was closed, you cannot use it anymore.");
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A simple {@link OutputStream} that is buffered with a bytes array.
+ * <p>
+ * It is mostly intended to be used as a base class to create new
+ * {@link OutputStream}s with special operation modes, and to give some default
+ * methods.
+ *
+ * @author niki
+ */
+public class BufferedOutputStream extends OutputStream {
+ /** The current position in the buffer. */
+ protected int start;
+ /** The index of the last usable position of the buffer. */
+ protected int stop;
+ /** The buffer itself. */
+ protected byte[] buffer;
+ /** An End-Of-File (or buffer, here) marker. */
+ protected boolean eof;
+ /** The actual under-laying stream. */
+ protected OutputStream out;
+ /** The number of bytes written to the under-laying stream. */
+ protected long bytesWritten;
+ /**
+ * Can bypass the flush process for big writes (will directly write to the
+ * under-laying buffer if the array to write is > the internal buffer
+ * size).
+ * <p>
+ * By default, this is true.
+ */
+ protected boolean bypassFlush = true;
+
+ private boolean closed;
+ private int openCounter;
+ private byte[] b1;
+
+ /**
+ * Create a new {@link BufferedInputStream} that wraps the given
+ * {@link InputStream}.
+ *
+ * @param out
+ * the {@link OutputStream} to wrap
+ */
+ public BufferedOutputStream(OutputStream out) {
+ this.out = out;
+
+ this.buffer = new byte[4096];
+ this.b1 = new byte[1];
+ this.start = 0;
+ this.stop = 0;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ b1[0] = (byte) b;
+ write(b1, 0, 1);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(byte[] source, int sourceOffset, int sourceLength)
+ throws IOException {
+
+ checkClose();
+
+ if (source == null) {
+ throw new NullPointerException();
+ } else if ((sourceOffset < 0) || (sourceOffset > source.length)
+ || (sourceLength < 0)
+ || ((sourceOffset + sourceLength) > source.length)
+ || ((sourceOffset + sourceLength) < 0)) {
+ throw new IndexOutOfBoundsException();
+ } else if (sourceLength == 0) {
+ return;
+ }
+
+ if (bypassFlush && sourceLength >= buffer.length) {
+ /*
+ * If the request length exceeds the size of the output buffer,
+ * flush the output buffer and then write the data directly. In this
+ * way buffered streams will cascade harmlessly.
+ */
+ flush(false);
+ out.write(source, sourceOffset, sourceLength);
+ bytesWritten += (sourceLength - sourceOffset);
+ return;
+ }
+
+ int done = 0;
+ while (done < sourceLength) {
+ if (available() <= 0) {
+ flush(false);
+ }
+
+ int now = Math.min(sourceLength - done, available());
+ System.arraycopy(source, sourceOffset + done, buffer, stop, now);
+ stop += now;
+ done += now;
+ }
+ }
+
+ /**
+ * The available space in the buffer.
+ *
+ * @return the space in bytes
+ */
+ private int available() {
+ if (closed) {
+ return 0;
+ }
+
+ return Math.max(0, buffer.length - stop - 1);
+ }
+
+ /**
+ * The number of bytes written to the under-laying {@link OutputStream}.
+ *
+ * @return the number of bytes
+ */
+ public long getBytesWritten() {
+ return bytesWritten;
+ }
+
+ /**
+ * Return this very same {@link BufferedInputStream}, but keep a counter of
+ * how many streams were open this way. When calling
+ * {@link BufferedInputStream#close()}, decrease this counter if it is not
+ * already zero instead of actually closing the stream.
+ * <p>
+ * You are now responsible for it — you <b>must</b> close it.
+ * <p>
+ * This method allows you to use a wrapping stream around this one and still
+ * close the wrapping stream.
+ *
+ * @return the same stream, but you are now responsible for closing it
+ *
+ * @throws IOException
+ * in case of I/O error or if the stream is closed
+ */
+ public synchronized OutputStream open() throws IOException {
+ checkClose();
+ openCounter++;
+ return this;
+ }
+
+ /**
+ * Check that the stream was not closed, and throw an {@link IOException} if
+ * it was.
+ *
+ * @throws IOException
+ * if it was closed
+ */
+ protected void checkClose() throws IOException {
+ if (closed) {
+ throw new IOException(
+ "This BufferedInputStream was closed, you cannot use it anymore.");
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ flush(true);
+ }
+
+ /**
+ * Flush the {@link BufferedOutputStream}, write the current buffered data
+ * to (and optionally also flush) the under-laying stream.
+ * <p>
+ * If {@link BufferedOutputStream#bypassFlush} is false, all writes to the
+ * under-laying stream are done in this method.
+ * <p>
+ * This can be used if you want to write some data in the under-laying
+ * stream yourself (in that case, flush this {@link BufferedOutputStream}
+ * with or without flushing the under-laying stream, then you can write to
+ * the under-laying stream).
+ *
+ * @param includingSubStream
+ * also flush the under-laying stream
+ * @throws IOException
+ * in case of I/O error
+ */
+ public void flush(boolean includingSubStream) throws IOException {
+ if (stop > start) {
+ out.write(buffer, start, stop - start);
+ bytesWritten += (stop - start);
+ }
+ start = 0;
+ stop = 0;
+
+ if (includingSubStream) {
+ out.flush();
+ }
+ }
+
+ /**
+ * Closes this stream and releases any system resources associated with the
+ * stream.
+ * <p>
+ * Including the under-laying {@link InputStream}.
+ * <p>
+ * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
+ * prior to this one, it will just decrease the internal count of how many
+ * open streams it held and do nothing else. The stream will actually be
+ * closed when you have called {@link BufferedInputStream#close()} once more
+ * than {@link BufferedInputStream#open()}.
+ *
+ * @exception IOException
+ * in case of I/O error
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ close(true);
+ }
+
+ /**
+ * Closes this stream and releases any system resources associated with the
+ * stream.
+ * <p>
+ * Including the under-laying {@link InputStream} if
+ * <tt>incudingSubStream</tt> is true.
+ * <p>
+ * You can call this method multiple times, it will not cause an
+ * {@link IOException} for subsequent calls.
+ * <p>
+ * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
+ * prior to this one, it will just decrease the internal count of how many
+ * open streams it held and do nothing else. The stream will actually be
+ * closed when you have called {@link BufferedInputStream#close()} once more
+ * than {@link BufferedInputStream#open()}.
+ *
+ * @param includingSubStream
+ * also close the under-laying stream
+ *
+ * @exception IOException
+ * in case of I/O error
+ */
+ public synchronized void close(boolean includingSubStream)
+ throws IOException {
+ if (!closed) {
+ if (openCounter > 0) {
+ openCounter--;
+ } else {
+ closed = true;
+ flush(includingSubStream);
+ if (includingSubStream && out != null) {
+ out.close();
+ }
+ }
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+
+/**
+ * This is a markable (and thus reset-able) stream that you can create from a
+ * FileInputStream.
+ *
+ * @author niki
+ */
+public class MarkableFileInputStream extends FilterInputStream {
+ private FileChannel channel;
+ private long mark = 0;
+
+ /**
+ * Create a new {@link MarkableFileInputStream} from this file.
+ *
+ * @param file
+ * the {@link File} to wrap
+ *
+ * @throws FileNotFoundException
+ * if the {@link File} cannot be found
+ */
+ public MarkableFileInputStream(File file) throws FileNotFoundException {
+ this(new FileInputStream(file));
+ }
+
+ /**
+ * Create a new {@link MarkableFileInputStream} from this stream.
+ *
+ * @param in
+ * the original {@link FileInputStream} to wrap
+ */
+ public MarkableFileInputStream(FileInputStream in) {
+ super(in);
+ channel = in.getChannel();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ try {
+ mark = channel.position();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ mark = -1;
+ }
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ if (mark < 0) {
+ throw new IOException("mark position not valid");
+ }
+ channel.position(mark);
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+/**
+ * This {@link InputStream} can be separated into sub-streams (you can process
+ * it as a normal {@link InputStream} but, when it is spent, you can call
+ * {@link NextableInputStream#next()} on it to unlock new data).
+ * <p>
+ * The separation in sub-streams is done via {@link NextableInputStreamStep}.
+ *
+ * @author niki
+ */
+public class NextableInputStream extends BufferedInputStream {
+ private NextableInputStreamStep step;
+ private boolean started;
+ private boolean stopped;
+
+ /**
+ * Create a new {@link NextableInputStream} that wraps the given
+ * {@link InputStream}.
+ *
+ * @param in
+ * the {@link InputStream} to wrap
+ * @param step
+ * how to separate it into sub-streams (can be NULL, but in that
+ * case it will behave as a normal {@link InputStream})
+ */
+ public NextableInputStream(InputStream in, NextableInputStreamStep step) {
+ super(in);
+ this.step = step;
+ }
+
+ /**
+ * Create a new {@link NextableInputStream} that wraps the given bytes array
+ * as a data source.
+ *
+ * @param in
+ * the array to wrap, cannot be NULL
+ * @param step
+ * how to separate it into sub-streams (can be NULL, but in that
+ * case it will behave as a normal {@link InputStream})
+ */
+ public NextableInputStream(byte[] in, NextableInputStreamStep step) {
+ this(in, step, 0, in.length);
+ }
+
+ /**
+ * Create a new {@link NextableInputStream} that wraps the given bytes array
+ * as a data source.
+ *
+ * @param in
+ * the array to wrap, cannot be NULL
+ * @param step
+ * how to separate it into sub-streams (can be NULL, but in that
+ * case it will behave as a normal {@link InputStream})
+ * @param offset
+ * the offset to start the reading at
+ * @param length
+ * the number of bytes to take into account in the array,
+ * starting from the offset
+ *
+ * @throws NullPointerException
+ * if the array is NULL
+ * @throws IndexOutOfBoundsException
+ * if the offset and length do not correspond to the given array
+ */
+ public NextableInputStream(byte[] in, NextableInputStreamStep step,
+ int offset, int length) {
+ super(in, offset, length);
+ this.step = step;
+ checkBuffer(true);
+ }
+
+ /**
+ * Unblock the processing of the next sub-stream.
+ * <p>
+ * It can only be called when the "current" stream is spent (i.e., you must
+ * first process the stream until it is spent).
+ * <p>
+ * {@link IOException}s can happen when we have no data available in the
+ * buffer; in that case, we fetch more data to know if we can have a next
+ * sub-stream or not.
+ * <p>
+ * This is can be a blocking call when data need to be fetched.
+ *
+ * @return TRUE if we unblocked the next sub-stream, FALSE if not (i.e.,
+ * FALSE when there are no more sub-streams to fetch)
+ *
+ * @throws IOException
+ * in case of I/O error or if the stream is closed
+ */
+ public boolean next() throws IOException {
+ return next(false);
+ }
+
+ /**
+ * Unblock the next sub-stream as would have done
+ * {@link NextableInputStream#next()}, but disable the sub-stream systems.
+ * <p>
+ * That is, the next stream, if any, will be the last one and will not be
+ * subject to the {@link NextableInputStreamStep}.
+ * <p>
+ * This is can be a blocking call when data need to be fetched.
+ *
+ * @return TRUE if we unblocked the next sub-stream, FALSE if not
+ *
+ * @throws IOException
+ * in case of I/O error or if the stream is closed
+ */
+ public boolean nextAll() throws IOException {
+ return next(true);
+ }
+
+ /**
+ * Check if this stream is totally spent (no more data to read or to
+ * process).
+ * <p>
+ * Note: when the stream is divided into sub-streams, each sub-stream will
+ * report its own eof when spent.
+ *
+ * @return TRUE if it is
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ @Override
+ public boolean eof() throws IOException {
+ return super.eof();
+ }
+
+ /**
+ * Check if we still have some data in the buffer and, if not, fetch some.
+ *
+ * @return TRUE if we fetched some data, FALSE if there are still some in
+ * the buffer
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ @Override
+ protected boolean preRead() throws IOException {
+ if (!stopped) {
+ boolean bufferChanged = super.preRead();
+ checkBuffer(bufferChanged);
+ return bufferChanged;
+ }
+
+ if (start >= stop) {
+ eof = true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected boolean hasMoreData() {
+ return started && super.hasMoreData();
+ }
+
+ /**
+ * Check that the buffer didn't overshot to the next item, and fix
+ * {@link NextableInputStream#stop} if needed.
+ * <p>
+ * If {@link NextableInputStream#stop} is fixed,
+ * {@link NextableInputStream#eof} and {@link NextableInputStream#stopped}
+ * are set to TRUE.
+ *
+ * @param newBuffer
+ * we changed the buffer, we need to clear some information in
+ * the {@link NextableInputStreamStep}
+ */
+ private void checkBuffer(boolean newBuffer) {
+ if (step != null && stop >= 0) {
+ if (newBuffer) {
+ step.clearBuffer();
+ }
+
+ int stopAt = step.stop(buffer, start, stop, eof);
+ if (stopAt >= 0) {
+ stop = stopAt;
+ eof = true;
+ stopped = true;
+ }
+ }
+ }
+
+ /**
+ * The implementation of {@link NextableInputStream#next()} and
+ * {@link NextableInputStream#nextAll()}.
+ * <p>
+ * This is can be a blocking call when data need to be fetched.
+ *
+ * @param all
+ * TRUE for {@link NextableInputStream#nextAll()}, FALSE for
+ * {@link NextableInputStream#next()}
+ *
+ * @return TRUE if we unblocked the next sub-stream, FALSE if not (i.e.,
+ * FALSE when there are no more sub-streams to fetch)
+ *
+ * @throws IOException
+ * in case of I/O error or if the stream is closed
+ */
+ private boolean next(boolean all) throws IOException {
+ checkClose();
+
+ if (!started) {
+ // First call before being allowed to read
+ started = true;
+
+ if (all) {
+ step = null;
+ }
+
+ return true;
+ }
+
+ // If started, must be stopped and no more data to continue
+ // i.e., sub-stream must be spent
+ if (!stopped || hasMoreData()) {
+ return false;
+ }
+
+ if (step != null) {
+ stop = step.getResumeLen();
+ start += step.getResumeSkip();
+ eof = step.getResumeEof();
+ stopped = false;
+
+ if (all) {
+ step = null;
+ }
+
+ checkBuffer(false);
+
+ return true;
+ }
+
+ return false;
+
+ // // consider that if EOF, there is no next
+ // if (start >= stop) {
+ // // Make sure, block if necessary
+ // preRead();
+ //
+ // return hasMoreData();
+ // }
+ //
+ // return true;
+ }
+
+ /**
+ * Display a DEBUG {@link String} representation of this object.
+ * <p>
+ * Do <b>not</b> use for release code.
+ */
+ @Override
+ public String toString() {
+ String data = "";
+ if (stop > 0) {
+ try {
+ data = new String(Arrays.copyOfRange(buffer, 0, stop), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ }
+ if (data.length() > 200) {
+ data = data.substring(0, 197) + "...";
+ }
+ }
+ String rep = String.format(
+ "Nextable %s: %d -> %d [eof: %s] [more data: %s]: %s",
+ (stopped ? "stopped" : "running"), start, stop, "" + eof, ""
+ + hasMoreData(), data);
+
+ return rep;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import java.io.InputStream;
+
+/**
+ * Divide an {@link InputStream} into sub-streams.
+ *
+ * @author niki
+ */
+public class NextableInputStreamStep {
+ private int stopAt;
+ private int last = -1;
+ private int resumeLen;
+ private int resumeSkip;
+ private boolean resumeEof;
+
+ /**
+ * Create a new divider that will separate the sub-streams each time it sees
+ * this byte.
+ * <p>
+ * Note that the byte will be bypassed by the {@link InputStream} as far as
+ * the consumers will be aware.
+ *
+ * @param byt
+ * the byte at which to separate two sub-streams
+ */
+ public NextableInputStreamStep(int byt) {
+ stopAt = byt;
+ }
+
+ /**
+ * Check if we need to stop the {@link InputStream} reading at some point in
+ * the current buffer.
+ * <p>
+ * If we do, return the index at which to stop; if not, return -1.
+ * <p>
+ * This method will <b>not</b> return the same index a second time (unless
+ * we cleared the buffer).
+ *
+ * @param buffer
+ * the buffer to check
+ * @param pos
+ * the current position of what was read in the buffer
+ * @param len
+ * the maximum index to use in the buffer (anything above that is
+ * not to be used)
+ * @param eof
+ * the current state of the under-laying stream
+ *
+ * @return the index at which to stop, or -1
+ */
+ public int stop(byte[] buffer, int pos, int len, boolean eof) {
+ for (int i = pos; i < len; i++) {
+ if (buffer[i] == stopAt) {
+ if (i > this.last) {
+ // we skip the sep
+ this.resumeSkip = 1;
+
+ this.resumeLen = len;
+ this.resumeEof = eof;
+ this.last = i;
+ return i;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Get the maximum index to use in the buffer used in
+ * {@link NextableInputStreamStep#stop(byte[], int, int, boolean)} at resume
+ * time.
+ *
+ * @return the index
+ */
+ public int getResumeLen() {
+ return resumeLen;
+ }
+
+ /**
+ * Get the number of bytes to skip at resume time.
+ *
+ * @return the number of bytes to skip
+ */
+ public int getResumeSkip() {
+ return resumeSkip;
+ }
+
+ /**
+ * Get the under-laying stream state at resume time.
+ *
+ * @return the EOF state
+ */
+ public boolean getResumeEof() {
+ return resumeEof;
+ }
+
+ /**
+ * Clear the information we may have kept about the current buffer
+ * <p>
+ * You should call this method each time you change the content of the
+ * buffer used in
+ * {@link NextableInputStreamStep#stop(byte[], int, int, boolean)}.
+ */
+ public void clearBuffer() {
+ this.last = -1;
+ this.resumeSkip = 0;
+ this.resumeLen = 0;
+ this.resumeEof = false;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * This {@link InputStream} will change some of its content by replacing it with
+ * something else.
+ *
+ * @author niki
+ */
+public class ReplaceInputStream extends BufferedInputStream {
+ /**
+ * The minimum size of the internal buffer (could be more if at least one of
+ * the 'FROM' bytes arrays is > 2048 bytes — in that case the
+ * buffer will be twice the largest size of the 'FROM' bytes arrays).
+ * <p>
+ * This is a different buffer than the one from the inherited class.
+ */
+ static private final int MIN_BUFFER_SIZE = 4096;
+
+ private byte[][] froms;
+ private byte[][] tos;
+ private int maxFromSize;
+ private int maxToSize;
+
+ private byte[] source;
+ private int spos;
+ private int slen;
+
+ /**
+ * Create a {@link ReplaceInputStream} that will replace <tt>from</tt> with
+ * <tt>to</tt>.
+ *
+ * @param in
+ * the under-laying {@link InputStream}
+ * @param from
+ * the {@link String} to replace
+ * @param to
+ * the {@link String} to replace with
+ */
+ public ReplaceInputStream(InputStream in, String from, String to) {
+ this(in, StringUtils.getBytes(from), StringUtils.getBytes(to));
+ }
+
+ /**
+ * Create a {@link ReplaceInputStream} that will replace <tt>from</tt> with
+ * <tt>to</tt>.
+ *
+ * @param in
+ * the under-laying {@link InputStream}
+ * @param from
+ * the value to replace
+ * @param to
+ * the value to replace with
+ */
+ public ReplaceInputStream(InputStream in, byte[] from, byte[] to) {
+ this(in, new byte[][] { from }, new byte[][] { to });
+ }
+
+ /**
+ * Create a {@link ReplaceInputStream} that will replace all <tt>froms</tt>
+ * with <tt>tos</tt>.
+ * <p>
+ * Note that they will be replaced in order, and that for each <tt>from</tt>
+ * a <tt>to</tt> must correspond.
+ *
+ * @param in
+ * the under-laying {@link InputStream}
+ * @param froms
+ * the values to replace
+ * @param tos
+ * the values to replace with
+ */
+ public ReplaceInputStream(InputStream in, String[] froms, String[] tos) {
+ this(in, StreamUtils.getBytes(froms), StreamUtils.getBytes(tos));
+ }
+
+ /**
+ * Create a {@link ReplaceInputStream} that will replace all <tt>froms</tt>
+ * with <tt>tos</tt>.
+ * <p>
+ * Note that they will be replaced in order, and that for each <tt>from</tt>
+ * a <tt>to</tt> must correspond.
+ *
+ * @param in
+ * the under-laying {@link InputStream}
+ * @param froms
+ * the values to replace
+ * @param tos
+ * the values to replace with
+ */
+ public ReplaceInputStream(InputStream in, byte[][] froms, byte[][] tos) {
+ super(in);
+
+ if (froms.length != tos.length) {
+ throw new IllegalArgumentException(
+ "For replacing, each FROM must have a corresponding TO");
+ }
+
+ this.froms = froms;
+ this.tos = tos;
+
+ maxFromSize = 0;
+ for (int i = 0; i < froms.length; i++) {
+ maxFromSize = Math.max(maxFromSize, froms[i].length);
+ }
+
+ maxToSize = 0;
+ for (int i = 0; i < tos.length; i++) {
+ maxToSize = Math.max(maxToSize, tos[i].length);
+ }
+
+ // We need at least maxFromSize so we can iterate and replace
+ source = new byte[Math.max(2 * maxFromSize, MIN_BUFFER_SIZE)];
+ spos = 0;
+ slen = 0;
+ }
+
+ @Override
+ protected int read(InputStream in, byte[] buffer, int off, int len)
+ throws IOException {
+ if (len < maxToSize || source.length < maxToSize * 2) {
+ throw new IOException(
+ "An underlaying buffer is too small for these replace values");
+ }
+
+ // We need at least one byte of data to process
+ if (available() < Math.max(maxFromSize, 1) && !eof) {
+ spos = 0;
+ slen = in.read(source);
+ }
+
+ // Note: very simple, not efficient implementation; sorry.
+ int count = 0;
+ while (spos < slen && count < len - maxToSize) {
+ boolean replaced = false;
+ for (int i = 0; i < froms.length; i++) {
+ if (froms[i] != null && froms[i].length > 0
+ && StreamUtils.startsWith(froms[i], source, spos, slen)) {
+ if (tos[i] != null && tos[i].length > 0) {
+ System.arraycopy(tos[i], 0, buffer, off + spos,
+ tos[i].length);
+ count += tos[i].length;
+ }
+
+ spos += froms[i].length;
+ replaced = true;
+ break;
+ }
+ }
+
+ if (!replaced) {
+ buffer[off + count++] = source[spos++];
+ }
+ }
+
+ return count;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * This {@link OutputStream} will change some of its content by replacing it
+ * with something else.
+ *
+ * @author niki
+ */
+public class ReplaceOutputStream extends BufferedOutputStream {
+ private byte[][] froms;
+ private byte[][] tos;
+
+ /**
+ * Create a {@link ReplaceOutputStream} that will replace <tt>from</tt> with
+ * <tt>to</tt>.
+ *
+ * @param out
+ * the under-laying {@link OutputStream}
+ * @param from
+ * the {@link String} to replace
+ * @param to
+ * the {@link String} to replace with
+ */
+ public ReplaceOutputStream(OutputStream out, String from, String to) {
+ this(out, StringUtils.getBytes(from), StringUtils.getBytes(to));
+ }
+
+ /**
+ * Create a {@link ReplaceOutputStream} that will replace <tt>from</tt> with
+ * <tt>to</tt>.
+ *
+ * @param out
+ * the under-laying {@link OutputStream}
+ * @param from
+ * the value to replace
+ * @param to
+ * the value to replace with
+ */
+ public ReplaceOutputStream(OutputStream out, byte[] from, byte[] to) {
+ this(out, new byte[][] { from }, new byte[][] { to });
+ }
+
+ /**
+ * Create a {@link ReplaceOutputStream} that will replace all <tt>froms</tt>
+ * with <tt>tos</tt>.
+ * <p>
+ * Note that they will be replaced in order, and that for each <tt>from</tt>
+ * a <tt>to</tt> must correspond.
+ *
+ * @param out
+ * the under-laying {@link OutputStream}
+ * @param froms
+ * the values to replace
+ * @param tos
+ * the values to replace with
+ */
+ public ReplaceOutputStream(OutputStream out, String[] froms, String[] tos) {
+ this(out, StreamUtils.getBytes(froms), StreamUtils.getBytes(tos));
+ }
+
+ /**
+ * Create a {@link ReplaceOutputStream} that will replace all <tt>froms</tt>
+ * with <tt>tos</tt>.
+ * <p>
+ * Note that they will be replaced in order, and that for each <tt>from</tt>
+ * a <tt>to</tt> must correspond.
+ *
+ * @param out
+ * the under-laying {@link OutputStream}
+ * @param froms
+ * the values to replace
+ * @param tos
+ * the values to replace with
+ */
+ public ReplaceOutputStream(OutputStream out, byte[][] froms, byte[][] tos) {
+ super(out);
+ bypassFlush = false;
+
+ if (froms.length != tos.length) {
+ throw new IllegalArgumentException(
+ "For replacing, each FROM must have a corresponding TO");
+ }
+
+ this.froms = froms;
+ this.tos = tos;
+ }
+
+ /**
+ * Flush the {@link BufferedOutputStream}, write the current buffered data
+ * to (and optionally also flush) the under-laying stream.
+ * <p>
+ * If {@link BufferedOutputStream#bypassFlush} is false, all writes to the
+ * under-laying stream are done in this method.
+ * <p>
+ * This can be used if you want to write some data in the under-laying
+ * stream yourself (in that case, flush this {@link BufferedOutputStream}
+ * with or without flushing the under-laying stream, then you can write to
+ * the under-laying stream).
+ * <p>
+ * <b>But be careful!</b> If a replacement could be done with the end o the
+ * currently buffered data and the start of the data to come, we obviously
+ * will not be able to do it.
+ *
+ * @param includingSubStream
+ * also flush the under-laying stream
+ * @throws IOException
+ * in case of I/O error
+ */
+ @Override
+ public void flush(boolean includingSubStream) throws IOException {
+ // Note: very simple, not efficient implementation; sorry.
+ while (start < stop) {
+ boolean replaced = false;
+ for (int i = 0; i < froms.length; i++) {
+ if (froms[i] != null
+ && froms[i].length > 0
+ && StreamUtils
+ .startsWith(froms[i], buffer, start, stop)) {
+ if (tos[i] != null && tos[i].length > 0) {
+ out.write(tos[i]);
+ bytesWritten += tos[i].length;
+ }
+
+ start += froms[i].length;
+ replaced = true;
+ break;
+ }
+ }
+
+ if (!replaced) {
+ out.write(buffer[start++]);
+ bytesWritten++;
+ }
+ }
+
+ start = 0;
+ stop = 0;
+
+ if (includingSubStream) {
+ out.flush();
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.streams;
+
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * Some non-public utilities used in the stream classes.
+ *
+ * @author niki
+ */
+class StreamUtils {
+ /**
+ * Check if the buffer starts with the given search term (given as an array,
+ * a start position and an end position).
+ * <p>
+ * Note: the parameter <tt>stop</tt> is the <b>index</b> of the last
+ * position, <b>not</b> the length.
+ * <p>
+ * Note: the search term size <b>must</b> be smaller or equal the internal
+ * buffer size.
+ *
+ * @param search
+ * the term to search for
+ * @param buffer
+ * the buffer to look into
+ * @param start
+ * the offset at which to start the search
+ * @param stop
+ * the maximum index of the data to check (this is <b>not</b> a
+ * length, but an index)
+ *
+ * @return TRUE if the search content is present at the given location and
+ * does not exceed the <tt>len</tt> index
+ */
+ static public boolean startsWith(byte[] search, byte[] buffer, int start,
+ int stop) {
+
+ // Check if there even is enough space for it
+ if (search.length > (stop - start)) {
+ return false;
+ }
+
+ boolean same = true;
+ for (int i = 0; i < search.length; i++) {
+ if (search[i] != buffer[start + i]) {
+ same = false;
+ break;
+ }
+ }
+
+ return same;
+ }
+
+ /**
+ * Return the bytes array representation of the given {@link String} in
+ * UTF-8.
+ *
+ * @param strs
+ * the {@link String}s to transform into bytes
+ * @return the content in bytes
+ */
+ static public byte[][] getBytes(String[] strs) {
+ byte[][] bytes = new byte[strs.length][];
+ for (int i = 0; i < strs.length; i++) {
+ bytes[i] = StringUtils.getBytes(strs[i]);
+ }
+
+ return bytes;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import be.nikiroo.utils.IOUtils;
+
+/**
+ * A {@link TestCase} that can be run with {@link TestLauncher}.
+ *
+ * @author niki
+ */
+abstract public class TestCase {
+ /**
+ * The type of {@link Exception} used to signal a failed assertion or a
+ * force-fail.
+ *
+ * @author niki
+ */
+ class AssertException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public AssertException(String reason, Exception source) {
+ super(reason, source);
+ }
+
+ public AssertException(String reason) {
+ super(reason);
+ }
+ }
+
+ private String name;
+
+ /**
+ * Create a new {@link TestCase}.
+ *
+ * @param name
+ * the test name
+ */
+ public TestCase(String name) {
+ this.name = name;
+ }
+
+ /**
+ * This constructor can be used if you require a no-param constructor. In
+ * this case, you are allowed to set the name manually via
+ * {@link TestCase#setName}.
+ */
+ protected TestCase() {
+ this("no name");
+ }
+
+ /**
+ * Setup the test (called before the test is run).
+ *
+ * @throws Exception
+ * in case of error
+ */
+ public void setUp() throws Exception {
+ }
+
+ /**
+ * Tear-down the test (called when the test has been ran).
+ *
+ * @throws Exception
+ * in case of error
+ */
+ public void tearDown() throws Exception {
+ }
+
+ /**
+ * The test name.
+ *
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * The test name.
+ *
+ * @param name
+ * the new name (internal use only)
+ *
+ * @return this (so we can chain and so we can initialize it in a member
+ * variable if this is an anonymous inner class)
+ */
+ protected TestCase setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Actually do the test.
+ *
+ * @throws Exception
+ * in case of error
+ */
+ abstract public void test() throws Exception;
+
+ /**
+ * Force a failure.
+ *
+ * @throws AssertException
+ * every time
+ */
+ public void fail() throws AssertException {
+ fail(null);
+ }
+
+ /**
+ * Force a failure.
+ *
+ * @param reason
+ * the failure reason
+ *
+ * @throws AssertException
+ * every time
+ */
+ public void fail(String reason) throws AssertException {
+ fail(reason, null);
+ }
+
+ /**
+ * Force a failure.
+ *
+ * @param reason
+ * the failure reason
+ * @param e
+ * the exception that caused the failure (can be NULL)
+ *
+ * @throws AssertException
+ * every time
+ */
+ public void fail(String reason, Exception e) throws AssertException {
+ throw new AssertException("Failed!" + //
+ reason != null ? "\n" + reason : "", e);
+ }
+
+ /**
+ * Check that 2 {@link Object}s are equals.
+ *
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(Object expected, Object actual)
+ throws AssertException {
+ assertEquals(null, expected, actual);
+ }
+
+ /**
+ * Check that 2 {@link Object}s are equals.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(String errorMessage, Object expected, Object actual)
+ throws AssertException {
+ if ((expected == null && actual != null)
+ || (expected != null && !expected.equals(actual))) {
+ if (errorMessage == null) {
+ throw new AssertException(generateAssertMessage(expected,
+ actual));
+ }
+
+ throw new AssertException(errorMessage, new AssertException(
+ generateAssertMessage(expected, actual)));
+ }
+ }
+
+ /**
+ * Check that 2 longs are equals.
+ *
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(long expected, long actual) throws AssertException {
+ assertEquals(Long.valueOf(expected), Long.valueOf(actual));
+ }
+
+ /**
+ * Check that 2 longs are equals.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(String errorMessage, long expected, long actual)
+ throws AssertException {
+ assertEquals(errorMessage, Long.valueOf(expected), Long.valueOf(actual));
+ }
+
+ /**
+ * Check that 2 booleans are equals.
+ *
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(boolean expected, boolean actual)
+ throws AssertException {
+ assertEquals(Boolean.valueOf(expected), Boolean.valueOf(actual));
+ }
+
+ /**
+ * Check that 2 booleans are equals.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(String errorMessage, boolean expected,
+ boolean actual) throws AssertException {
+ assertEquals(errorMessage, Boolean.valueOf(expected),
+ Boolean.valueOf(actual));
+ }
+
+ /**
+ * Check that 2 doubles are equals.
+ *
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(double expected, double actual)
+ throws AssertException {
+ assertEquals(Double.valueOf(expected), Double.valueOf(actual));
+ }
+
+ /**
+ * Check that 2 doubles are equals.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(String errorMessage, double expected, double actual)
+ throws AssertException {
+ assertEquals(errorMessage, Double.valueOf(expected),
+ Double.valueOf(actual));
+ }
+
+ /**
+ * Check that 2 {@link List}s are equals.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(List<?> expected, List<?> actual)
+ throws AssertException {
+ assertEquals("Assertion failed", expected, actual);
+ }
+
+ /**
+ * Check that 2 {@link List}s are equals.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(String errorMessage, List<?> expected,
+ List<?> actual) throws AssertException {
+
+ if (expected.size() != actual.size()) {
+ assertEquals(errorMessage + ": not same number of items",
+ list(expected), list(actual));
+ }
+
+ int size = expected.size();
+ for (int i = 0; i < size; i++) {
+ assertEquals(errorMessage + ": item " + i
+ + " (0-based) is not correct", expected.get(i),
+ actual.get(i));
+ }
+ }
+
+ /**
+ * Check that 2 {@link File}s are equals, by doing a line-by-line
+ * comparison.
+ *
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ * @param errorMessage
+ * the error message to display if they differ
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(File expected, File actual) throws AssertException {
+ assertEquals(generateAssertMessage(expected, actual), expected, actual);
+ }
+
+ /**
+ * Check that 2 {@link File}s are equals, by doing a line-by-line
+ * comparison.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(String errorMessage, File expected, File actual)
+ throws AssertException {
+ assertEquals(errorMessage, expected, actual, null);
+ }
+
+ /**
+ * Check that 2 {@link File}s are equals, by doing a line-by-line
+ * comparison.
+ *
+ * @param errorMessage
+ * the error message to display if they differ
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ * @param skipCompare
+ * skip the lines starting with some values for the given files
+ * (relative path from base directory in recursive mode)
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertEquals(String errorMessage, File expected, File actual,
+ Map<String, List<String>> skipCompare) throws AssertException {
+ assertEquals(errorMessage, expected, actual, skipCompare, null);
+ }
+
+ private void assertEquals(String errorMessage, File expected, File actual,
+ Map<String, List<String>> skipCompare, String removeFromName)
+ throws AssertException {
+
+ if (expected.isDirectory() || actual.isDirectory()) {
+ assertEquals(errorMessage + ": type mismatch: expected a "
+ + (expected.isDirectory() ? "directory" : "file")
+ + ", received a "
+ + (actual.isDirectory() ? "directory" : "file"),
+ expected.isDirectory(), actual.isDirectory());
+
+ List<String> expectedFiles = Arrays.asList(expected.list());
+ Collections.sort(expectedFiles);
+ List<String> actualFiles = Arrays.asList(actual.list());
+ Collections.sort(actualFiles);
+
+ assertEquals(errorMessage, expectedFiles, actualFiles);
+ for (int i = 0; i < actualFiles.size(); i++) {
+ File expectedFile = new File(expected, expectedFiles.get(i));
+ File actualFile = new File(actual, actualFiles.get(i));
+
+ assertEquals(errorMessage, expectedFile, actualFile,
+ skipCompare, expected.getAbsolutePath());
+ }
+ } else {
+ try {
+ List<String> expectedLines = Arrays.asList(IOUtils
+ .readSmallFile(expected).split("\n"));
+ List<String> resultLines = Arrays.asList(IOUtils.readSmallFile(
+ actual).split("\n"));
+
+ String name = expected.getAbsolutePath();
+ if (removeFromName != null && name.startsWith(removeFromName)) {
+ name = expected.getName()
+ + name.substring(removeFromName.length());
+ }
+
+ assertEquals(errorMessage + ": " + name
+ + ": the number of lines is not the same",
+ expectedLines.size(), resultLines.size());
+
+ for (int j = 0; j < expectedLines.size(); j++) {
+ String expectedLine = expectedLines.get(j);
+ String resultLine = resultLines.get(j);
+
+ boolean skip = false;
+ if (skipCompare != null) {
+ for (Entry<String, List<String>> skipThose : skipCompare
+ .entrySet()) {
+ for (String skipStart : skipThose.getValue()) {
+ if (name.endsWith(skipThose.getKey())
+ && expectedLine.startsWith(skipStart)
+ && resultLine.startsWith(skipStart)) {
+ skip = true;
+ }
+ }
+ }
+ }
+
+ if (skip) {
+ continue;
+ }
+
+ assertEquals(errorMessage + ": line " + (j + 1)
+ + " is not the same in file " + name, expectedLine,
+ resultLine);
+ }
+ } catch (Exception e) {
+ throw new AssertException(errorMessage, e);
+ }
+ }
+ }
+
+ /**
+ * Check that given {@link Object} is not NULL.
+ *
+ * @param errorMessage
+ * the error message to display if it is NULL
+ * @param actual
+ * the actual value
+ *
+ * @throws AssertException
+ * in case they differ
+ */
+ public void assertNotNull(String errorMessage, Object actual)
+ throws AssertException {
+ if (actual == null) {
+ String defaultReason = String.format("" //
+ + "Assertion failed!%n" //
+ + "Object should not have been NULL");
+
+ if (errorMessage == null) {
+ throw new AssertException(defaultReason);
+ }
+
+ throw new AssertException(errorMessage, new AssertException(
+ defaultReason));
+ }
+ }
+
+ /**
+ * Generate the default assert message for 2 different values that were
+ * supposed to be equals.
+ *
+ * @param expected
+ * the expected value
+ * @param actual
+ * the actual value
+ *
+ * @return the message
+ */
+ public static String generateAssertMessage(Object expected, Object actual) {
+ return String.format("" //
+ + "Assertion failed!%n" //
+ + "Expected value: [%s]%n" //
+ + "Actual value: [%s]", expected, actual);
+ }
+
+ private static String list(List<?> items) {
+ StringBuilder builder = new StringBuilder();
+ for (Object item : items) {
+ if (builder.length() == 0) {
+ builder.append(items.size() + " item(s): ");
+ } else {
+ builder.append(", ");
+ }
+
+ builder.append("" + item);
+
+ if (builder.length() > 60) {
+ builder.setLength(57);
+ builder.append("...");
+ break;
+ }
+ }
+
+ return builder.toString();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link TestLauncher} starts a series of {@link TestCase}s and displays the
+ * result to the user.
+ *
+ * @author niki
+ */
+public class TestLauncher {
+ /**
+ * {@link Exception} happening during the setup process.
+ *
+ * @author niki
+ */
+ private class SetupException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SetupException(Throwable e) {
+ super(e);
+ }
+ }
+
+ /**
+ * {@link Exception} happening during the tear-down process.
+ *
+ * @author niki
+ */
+ private class TearDownException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public TearDownException(Throwable e) {
+ super(e);
+ }
+ }
+
+ private List<TestLauncher> series;
+ private List<TestCase> tests;
+ private TestLauncher parent;
+
+ private int columns;
+ private String okString;
+ private String koString;
+ private String name;
+ private boolean cont;
+
+ protected int executed;
+ protected int total;
+
+ private int currentSeries = 0;
+ private boolean details = false;
+
+ /**
+ * Create a new {@link TestLauncher} with default parameters.
+ *
+ * @param name
+ * the test suite name
+ * @param args
+ * the arguments to configure the number of columns and the ok/ko
+ * {@link String}s
+ */
+ public TestLauncher(String name, String[] args) {
+ this.name = name;
+
+ int cols = 80;
+ if (args != null && args.length >= 1) {
+ try {
+ cols = Integer.parseInt(args[0]);
+ } catch (NumberFormatException e) {
+ System.err.println("Test configuration: given number "
+ + "of columns is not parseable: " + args[0]);
+ }
+ }
+
+ setColumns(cols);
+
+ String okString = "[ ok ]";
+ String koString = "[ !! ]";
+ if (args != null && args.length >= 3) {
+ okString = args[1];
+ koString = args[2];
+ }
+
+ setOkString(okString);
+ setKoString(koString);
+
+ series = new ArrayList<TestLauncher>();
+ tests = new ArrayList<TestCase>();
+ cont = true;
+ }
+
+ /**
+ * Display the details of the errors
+ *
+ * @return TRUE to display them, false to simply mark the test as failed
+ */
+ public boolean isDetails() {
+ if (parent != null) {
+ return parent.isDetails();
+ }
+
+ return details;
+ }
+
+ /**
+ * Display the details of the errors
+ *
+ * @param details
+ * TRUE to display them, false to simply mark the test as failed
+ */
+ public void setDetails(boolean details) {
+ if (parent != null) {
+ parent.setDetails(details);
+ }
+
+ this.details = details;
+ }
+
+ /**
+ * Called before actually starting the tests themselves.
+ *
+ * @throws Exception
+ * in case of error
+ */
+ protected void start() throws Exception {
+ }
+
+ /**
+ * Called when the tests are passed (or failed to do so).
+ *
+ * @throws Exception
+ * in case of error
+ */
+ protected void stop() throws Exception {
+ }
+
+ protected void addTest(TestCase test) {
+ tests.add(test);
+ }
+
+ protected void addSeries(TestLauncher series) {
+ this.series.add(series);
+ series.parent = this;
+ }
+
+ /**
+ * Launch the series of {@link TestCase}s and the {@link TestCase}s.
+ *
+ * @return the number of errors
+ */
+ public int launch() {
+ return launch(0);
+ }
+
+ /**
+ * Launch the series of {@link TestCase}s and the {@link TestCase}s.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ *
+ * @return the number of errors
+ */
+ public int launch(int depth) {
+ int errors = 0;
+ executed = 0;
+ total = tests.size();
+
+ print(depth);
+
+ try {
+ start();
+
+ errors += launchTests(depth);
+ if (tests.size() > 0 && depth == 0) {
+ System.out.println("");
+ }
+
+ currentSeries = 0;
+ for (TestLauncher serie : series) {
+ errors += serie.launch(depth + 1);
+ executed += serie.executed;
+ total += serie.total;
+ currentSeries++;
+ }
+ } catch (Exception e) {
+ print(depth, "__start");
+ print(depth, e);
+ } finally {
+ try {
+ stop();
+ } catch (Exception e) {
+ print(depth, "__stop");
+ print(depth, e);
+ }
+ }
+
+ print(depth, executed, errors, total);
+
+ return errors;
+ }
+
+ /**
+ * Launch the {@link TestCase}s.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ *
+ * @return the number of errors
+ */
+ protected int launchTests(int depth) {
+ int errors = 0;
+ for (TestCase test : tests) {
+ print(depth, test.getName());
+
+ Throwable ex = null;
+ try {
+ try {
+ test.setUp();
+ } catch (Throwable e) {
+ throw new SetupException(e);
+ }
+ test.test();
+ try {
+ test.tearDown();
+ } catch (Throwable e) {
+ throw new TearDownException(e);
+ }
+ } catch (Throwable e) {
+ ex = e;
+ }
+
+ if (ex != null) {
+ errors++;
+ }
+
+ print(depth, ex);
+
+ executed++;
+
+ if (ex != null && !cont) {
+ break;
+ }
+ }
+
+ return errors;
+ }
+
+ /**
+ * Specify a custom number of columns to use for the display of messages.
+ *
+ * @param columns
+ * the number of columns
+ */
+ public void setColumns(int columns) {
+ this.columns = columns;
+ }
+
+ /**
+ * Continue to run the tests when an error is detected.
+ *
+ * @param cont
+ * yes or no
+ */
+ public void setContinueAfterFail(boolean cont) {
+ this.cont = cont;
+ }
+
+ /**
+ * Set a custom "[ ok ]" {@link String} when a test passed.
+ *
+ * @param okString
+ * the {@link String} to display at the end of a success
+ */
+ public void setOkString(String okString) {
+ this.okString = okString;
+ }
+
+ /**
+ * Set a custom "[ !! ]" {@link String} when a test failed.
+ *
+ * @param koString
+ * the {@link String} to display at the end of a failure
+ */
+ public void setKoString(String koString) {
+ this.koString = koString;
+ }
+
+ /**
+ * Print the test suite header.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ */
+ protected void print(int depth) {
+ if (depth == 0) {
+ System.out.println("[ Test suite: " + name + " ]");
+ System.out.println("");
+ } else {
+ System.out.println(prefix(depth, false) + name + ":");
+ }
+ }
+
+ /**
+ * Print the name of the {@link TestCase} we will start immediately after.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ * @param name
+ * the {@link TestCase} name
+ */
+ protected void print(int depth, String name) {
+ name = prefix(depth, false)
+ + (name == null ? "" : name).replace("\t", " ");
+
+ StringBuilder dots = new StringBuilder();
+ while ((name.length() + dots.length()) < columns - 11) {
+ dots.append('.');
+ }
+
+ System.out.print(name + dots.toString());
+ }
+
+ /**
+ * Print the result of the {@link TestCase} we just ran.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ * @param error
+ * the {@link Exception} it ran into if any
+ */
+ private void print(int depth, Throwable error) {
+ if (error != null) {
+ System.out.println(" " + koString);
+ if (isDetails()) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ error.printStackTrace(pw);
+ String lines = sw.toString();
+ for (String line : lines.split("\n")) {
+ System.out.println(prefix(depth, false) + "\t\t" + line);
+ }
+ }
+ } else {
+ System.out.println(" " + okString);
+ }
+ }
+
+ /**
+ * Print the total result for this test suite.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ * @param executed
+ * the number of tests actually ran
+ * @param errors
+ * the number of errors encountered
+ * @param total
+ * the total number of tests in the suite
+ */
+ private void print(int depth, int executed, int errors, int total) {
+ int ok = executed - errors;
+ int pc = (int) ((100.0 * ok) / executed);
+ if (pc == 0 && ok > 0) {
+ pc = 1;
+ }
+ int pcTotal = (int) ((100.0 * ok) / total);
+ if (pcTotal == 0 && ok > 0) {
+ pcTotal = 1;
+ }
+
+ String resume = "Tests passed: " + ok + "/" + executed + " (" + pc
+ + "%) on a total of " + total + " (" + pcTotal + "% total)";
+ if (depth == 0) {
+ System.out.println(resume);
+ } else {
+ String arrow = "┗▶ ";
+ System.out.println(prefix(depth, currentSeries == 0) + arrow
+ + resume);
+ System.out.println(prefix(depth, currentSeries == 0));
+ }
+ }
+
+ private int last = -1;
+
+ /**
+ * Return the prefix to print before the current line.
+ *
+ * @param depth
+ * the current depth
+ * @param first
+ * this line is the first of its tabulation level
+ *
+ * @return the prefix
+ */
+ private String prefix(int depth, boolean first) {
+ String space = tabs(depth - 1);
+
+ String line = "";
+ if (depth > 0) {
+ if (depth > 1) {
+ if (depth != last && first) {
+ line = "╻"; // first line
+ } else {
+ line = "┃"; // continuation
+ }
+ }
+
+ space += line + tabs(1);
+ }
+
+ last = depth;
+ return space;
+ }
+
+ /**
+ * Return the given number of space-converted tabs in a {@link String}.
+ *
+ * @param depth
+ * the number of tabs to return
+ *
+ * @return the string
+ */
+ private String tabs(int depth) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ builder.append(" ");
+ }
+ return builder.toString();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.BufferedInputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class BufferedInputStreamTest extends TestLauncher {
+ public BufferedInputStreamTest(String[] args) {
+ super("BufferedInputStream test", args);
+
+ addTest(new TestCase("Simple InputStream reading") {
+ @Override
+ public void test() throws Exception {
+ byte[] expected = new byte[] { 42, 12, 0, 127 };
+ BufferedInputStream in = new BufferedInputStream(
+ new ByteArrayInputStream(expected));
+ checkArrays(this, "FIRST", in, expected);
+ }
+ });
+
+ addTest(new TestCase("Simple byte array reading") {
+ @Override
+ public void test() throws Exception {
+ byte[] expected = new byte[] { 42, 12, 0, 127 };
+ BufferedInputStream in = new BufferedInputStream(expected);
+ checkArrays(this, "FIRST", in, expected);
+ }
+ });
+
+ addTest(new TestCase("Byte array is(byte[])") {
+ @Override
+ public void test() throws Exception {
+ byte[] expected = new byte[] { 42, 12, 0, 127 };
+ BufferedInputStream in = new BufferedInputStream(expected);
+ assertEquals(
+ "The array should be considered identical to its source",
+ true, in.is(expected));
+ assertEquals(
+ "The array should be considered different to that one",
+ false, in.is(new byte[] { 42, 12, 0, 121 }));
+ in.close();
+ }
+ });
+
+ addTest(new TestCase("InputStream is(byte[])") {
+ @Override
+ public void test() throws Exception {
+ byte[] expected = new byte[] { 42, 12, 0, 127 };
+ BufferedInputStream in = new BufferedInputStream(
+ new ByteArrayInputStream(expected));
+ assertEquals(
+ "The array should be considered identical to its source",
+ true, in.is(expected));
+ assertEquals(
+ "The array should be considered different to that one",
+ false, in.is(new byte[] { 42, 12, 0, 121 }));
+ in.close();
+ }
+ });
+
+ addTest(new TestCase("Byte array is(String)") {
+ @Override
+ public void test() throws Exception {
+ String expected = "Testy";
+ BufferedInputStream in = new BufferedInputStream(
+ expected.getBytes("UTF-8"));
+ assertEquals(
+ "The array should be considered identical to its source",
+ true, in.is(expected));
+ assertEquals(
+ "The array should be considered different to that one",
+ false, in.is("Autre"));
+ assertEquals(
+ "The array should be considered different to that one",
+ false, in.is("Test"));
+ in.close();
+ }
+ });
+
+ addTest(new TestCase("InputStream is(String)") {
+ @Override
+ public void test() throws Exception {
+ String expected = "Testy";
+ BufferedInputStream in = new BufferedInputStream(
+ new ByteArrayInputStream(expected.getBytes("UTF-8")));
+ assertEquals(
+ "The array should be considered identical to its source",
+ true, in.is(expected));
+ assertEquals(
+ "The array should be considered different to that one",
+ false, in.is("Autre"));
+ assertEquals(
+ "The array should be considered different to that one",
+ false, in.is("Testy."));
+ in.close();
+ }
+ });
+ }
+
+ static void checkArrays(TestCase test, String prefix, InputStream in,
+ byte[] expected) throws Exception {
+ byte[] actual = IOUtils.toByteArray(in);
+ test.assertEquals("The " + prefix
+ + " resulting array has not the correct number of items",
+ expected.length, actual.length);
+ for (int i = 0; i < actual.length; i++) {
+ test.assertEquals(prefix + ": item " + i
+ + " (0-based) is not the same", expected[i], actual[i]);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import be.nikiroo.utils.streams.BufferedOutputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class BufferedOutputStreamTest extends TestLauncher {
+ public BufferedOutputStreamTest(String[] args) {
+ super("BufferedOutputStream test", args);
+
+ addTest(new TestCase("Single write") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ BufferedOutputStream out = new BufferedOutputStream(bout);
+
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+
+ out.write(data);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, data);
+ }
+ });
+
+ addTest(new TestCase("Single write of 5000 bytes") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ BufferedOutputStream out = new BufferedOutputStream(bout);
+
+ byte[] data = new byte[5000];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (i % 255);
+ }
+
+ out.write(data);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, data);
+ }
+ });
+
+ addTest(new TestCase("Multiple writes") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ BufferedOutputStream out = new BufferedOutputStream(bout);
+
+ byte[] data1 = new byte[] { 42, 12, 0, 127 };
+ byte[] data2 = new byte[] { 15, 55 };
+ byte[] data3 = new byte[] {};
+
+ byte[] dataAll = new byte[] { 42, 12, 0, 127, 15, 55 };
+
+ out.write(data1);
+ out.write(data2);
+ out.write(data3);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, dataAll);
+ }
+ });
+
+ addTest(new TestCase("Multiple writes for a 5000 bytes total") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ BufferedOutputStream out = new BufferedOutputStream(bout);
+
+ byte[] data = new byte[] { 42, 12, 0, 127, 51, 2, 32, 66, 7, 87 };
+
+ List<Byte> bytes = new ArrayList<Byte>();
+
+ // write 400 * 10 + 1000 bytes = 5000
+ for (int i = 0; i < 400; i++) {
+ for (int j = 0; j < data.length; j++) {
+ bytes.add(data[j]);
+ }
+ out.write(data);
+ }
+
+ for (int i = 0; i < 1000; i++) {
+ for (int j = 0; j < data.length; j++) {
+ bytes.add(data[j]);
+ }
+ out.write(data);
+ }
+
+ out.close();
+
+ byte[] abytes = new byte[bytes.size()];
+ for (int i = 0; i < bytes.size(); i++) {
+ abytes[i] = bytes.get(i);
+ }
+
+ checkArrays(this, "FIRST", bout, abytes);
+ }
+ });
+ }
+
+ static void checkArrays(TestCase test, String prefix,
+ ByteArrayOutputStream bout, byte[] expected) throws Exception {
+ byte[] actual = bout.toByteArray();
+
+ if (false) {
+ System.out.print("\nExpected data: [ ");
+ for (int i = 0; i < expected.length; i++) {
+ if (i > 0)
+ System.out.print(", ");
+ System.out.print(expected[i]);
+ }
+ System.out.println(" ]");
+
+ System.out.print("Actual data : [ ");
+ for (int i = 0; i < actual.length; i++) {
+ if (i > 0)
+ System.out.print(", ");
+ System.out.print(actual[i]);
+ }
+ System.out.println(" ]");
+ }
+
+ test.assertEquals("The " + prefix
+ + " resulting array has not the correct number of items",
+ expected.length, actual.length);
+ for (int i = 0; i < actual.length; i++) {
+ test.assertEquals(prefix + ": item " + i
+ + " (0-based) is not the same", expected[i], actual[i]);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.Bundles;
+import be.nikiroo.utils.resources.Meta;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class BundleTest extends TestLauncher {
+ private File tmp;
+ private B b = new B();
+
+ public BundleTest(String[] args) {
+ this("Bundle test", args);
+ }
+
+ protected BundleTest(String name, String[] args) {
+ super(name, args);
+
+ for (TestCase test : getSimpleTests()) {
+ addTest(test);
+ }
+
+ addSeries(new TestLauncher("After saving/reloading the resources", args) {
+ {
+ for (TestCase test : getSimpleTests()) {
+ addTest(test);
+ }
+ }
+
+ @Override
+ protected void start() throws Exception {
+ tmp = File.createTempFile("nikiroo-utils", ".test");
+ tmp.delete();
+ tmp.mkdir();
+ b.updateFile(tmp.getAbsolutePath());
+ Bundles.setDirectory(tmp.getAbsolutePath());
+ b.reload(false);
+ }
+
+ @Override
+ protected void stop() {
+ IOUtils.deltree(tmp);
+ }
+ });
+
+ addSeries(new TestLauncher("Read/Write support", args) {
+ {
+ addTest(new TestCase("Reload") {
+ @Override
+ public void test() throws Exception {
+ String def = b.getString(E.ONE);
+ String val = "Something";
+
+ b.setString(E.ONE, val);
+ b.updateFile();
+ b.reload(true);
+
+ assertEquals("We should have reset the bundle", def,
+ b.getString(E.ONE));
+
+ b.reload(false);
+
+ assertEquals("We should have reloaded the same files",
+ val, b.getString(E.ONE));
+
+ // reset values for next tests
+ b.reload(true);
+ b.updateFile();
+ }
+ });
+
+ addTest(new TestCase("Set/Get") {
+ @Override
+ public void test() throws Exception {
+ String val = "Newp";
+ b.setString(E.ONE, val);
+ String setGet = b.getString(E.ONE);
+
+ assertEquals(val, setGet);
+
+ // reset values for next tests
+ b.restoreSnapshot(null);
+ }
+ });
+
+ addTest(new TestCase("Snapshots") {
+ @Override
+ public void test() throws Exception {
+ String val = "Newp";
+ String def = b.getString(E.ONE);
+
+ b.setString(E.ONE, val);
+ Object snap = b.takeSnapshot();
+
+ b.restoreSnapshot(null);
+ assertEquals(
+ "restoreChanges(null) should clear the changes",
+ def, b.getString(E.ONE));
+ b.restoreSnapshot(snap);
+ assertEquals(
+ "restoreChanges(snapshot) should restore the changes",
+ val, b.getString(E.ONE));
+
+ // reset values for next tests
+ b.restoreSnapshot(null);
+ }
+ });
+
+ addTest(new TestCase("updateFile with changes") {
+ @Override
+ public void test() throws Exception {
+ String val = "Go to disk! (UTF-8 test: 日本語)";
+
+ String def = b.getString(E.ONE);
+ b.setString(E.ONE, val);
+ b.updateFile(tmp.getAbsolutePath());
+ b.reload(false);
+
+ assertEquals(val, b.getString(E.ONE));
+
+ // reset values for next tests
+ b.setString(E.ONE, def);
+ b.updateFile(tmp.getAbsolutePath());
+ b.reload(false);
+ }
+ });
+ }
+
+ @Override
+ protected void start() throws Exception {
+ tmp = File.createTempFile("nikiroo-utils", ".test");
+ tmp.delete();
+ tmp.mkdir();
+ b.updateFile(tmp.getAbsolutePath());
+ Bundles.setDirectory(tmp.getAbsolutePath());
+ b.reload(false);
+ }
+
+ @Override
+ protected void stop() {
+ IOUtils.deltree(tmp);
+ }
+ });
+ }
+
+ private List<TestCase> getSimpleTests() {
+ String pre = "";
+
+ List<TestCase> list = new ArrayList<TestCase>();
+
+ list.add(new TestCase(pre + "getString simple") {
+ @Override
+ public void test() throws Exception {
+ assertEquals("un", b.getString(E.ONE));
+ }
+ });
+
+ list.add(new TestCase(pre + "getStringX with null suffix") {
+ @Override
+ public void test() throws Exception {
+ assertEquals("un", b.getStringX(E.ONE, null));
+ }
+ });
+
+ list.add(new TestCase(pre + "getStringX with empty suffix") {
+ @Override
+ public void test() throws Exception {
+ assertEquals(null, b.getStringX(E.ONE, ""));
+ }
+ });
+
+ list.add(new TestCase(pre + "getStringX with existing suffix") {
+ @Override
+ public void test() throws Exception {
+ assertEquals("un + suffix", b.getStringX(E.ONE, "suffix"));
+ }
+ });
+
+ list.add(new TestCase(pre + "getStringX with not existing suffix") {
+ @Override
+ public void test() throws Exception {
+ assertEquals(null, b.getStringX(E.ONE, "fake"));
+ }
+ });
+
+ list.add(new TestCase(pre + "getString with UTF-8 content") {
+ @Override
+ public void test() throws Exception {
+ assertEquals("日本語 Nihongo", b.getString(E.JAPANESE));
+ }
+ });
+
+ return list;
+ }
+
+ /**
+ * {@link Bundle}.
+ *
+ * @author niki
+ */
+ private class B extends Bundle<E> {
+ protected B() {
+ super(E.class, N.bundle_test, null);
+ }
+
+ @Override
+ // ...and make it public
+ public Object takeSnapshot() {
+ return super.takeSnapshot();
+ }
+
+ @Override
+ // ...and make it public
+ public void restoreSnapshot(Object snap) {
+ super.restoreSnapshot(snap);
+ }
+ }
+
+ /**
+ * Key enum for the {@link Bundle}.
+ *
+ * @author niki
+ */
+ private enum E {
+ @Meta
+ ONE, //
+ @Meta
+ ONE_SUFFIX, //
+ @Meta
+ TWO, //
+ @Meta
+ JAPANESE
+ }
+
+ /**
+ * Name enum for the {@link Bundle}.
+ *
+ * @author niki
+ */
+ private enum N {
+ bundle_test
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.CryptUtils;
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class CryptUtilsTest extends TestLauncher {
+ private String key;
+ private CryptUtils crypt;
+
+ public CryptUtilsTest(String[] args) {
+ super("CryptUtils test", args);
+
+ String longKey = "some long string with more than 128 bits (=32 bytes) of data";
+
+ addSeries(new CryptUtilsTest(args, "Manual input wuth NULL key", null,
+ 1));
+ addSeries(new CryptUtilsTest(args, "Streams with NULL key", null, true));
+
+ addSeries(new CryptUtilsTest(args, "Manual input with emptykey", "", 1));
+ addSeries(new CryptUtilsTest(args, "Streams with empty key", "", true));
+
+ addSeries(new CryptUtilsTest(args, "Manual input with long key",
+ longKey, 1));
+ addSeries(new CryptUtilsTest(args, "Streams with long key", longKey,
+ true));
+ }
+
+ @Override
+ protected void addTest(final TestCase test) {
+ super.addTest(new TestCase(test.getName()) {
+ @Override
+ public void test() throws Exception {
+ test.test();
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ crypt = new CryptUtils(key);
+ test.setUp();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ test.tearDown();
+ crypt = null;
+ }
+ });
+ }
+
+ private CryptUtilsTest(String[] args, String title, String key,
+ @SuppressWarnings("unused") int dummy) {
+ super(title, args);
+ this.key = key;
+
+ final String longData = "Le premier jour, Le Grand Barbu dans le cloud fit la lumière, et il vit que c'était bien. Ou quelque chose comme ça. Je préfère la Science-Fiction en général, je trouve ça plus sain :/";
+
+ addTest(new TestCase("Short") {
+ @Override
+ public void test() throws Exception {
+ String orig = "data";
+ byte[] encrypted = crypt.encrypt(orig);
+ String decrypted = crypt.decrypts(encrypted);
+
+ assertEquals(orig, decrypted);
+ }
+ });
+
+ addTest(new TestCase("Short, base64") {
+ @Override
+ public void test() throws Exception {
+ String orig = "data";
+ String encrypted = crypt.encrypt64(orig);
+ String decrypted = crypt.decrypt64s(encrypted);
+
+ assertEquals(orig, decrypted);
+ }
+ });
+
+ addTest(new TestCase("Empty") {
+ @Override
+ public void test() throws Exception {
+ String orig = "";
+ byte[] encrypted = crypt.encrypt(orig);
+ String decrypted = crypt.decrypts(encrypted);
+
+ assertEquals(orig, decrypted);
+ }
+ });
+
+ addTest(new TestCase("Empty, base64") {
+ @Override
+ public void test() throws Exception {
+ String orig = "";
+ String encrypted = crypt.encrypt64(orig);
+ String decrypted = crypt.decrypt64s(encrypted);
+
+ assertEquals(orig, decrypted);
+ }
+ });
+
+ addTest(new TestCase("Long") {
+ @Override
+ public void test() throws Exception {
+ String orig = longData;
+ byte[] encrypted = crypt.encrypt(orig);
+ String decrypted = crypt.decrypts(encrypted);
+
+ assertEquals(orig, decrypted);
+ }
+ });
+
+ addTest(new TestCase("Long, base64") {
+ @Override
+ public void test() throws Exception {
+ String orig = longData;
+ String encrypted = crypt.encrypt64(orig);
+ String decrypted = crypt.decrypt64s(encrypted);
+
+ assertEquals(orig, decrypted);
+ }
+ });
+ }
+
+ private CryptUtilsTest(String[] args, String title, String key,
+ @SuppressWarnings("unused") boolean dummy) {
+ super(title, args);
+ this.key = key;
+
+ addTest(new TestCase("Simple test") {
+ @Override
+ public void test() throws Exception {
+ InputStream in = new ByteArrayInputStream(new byte[] { 42, 127,
+ 12 });
+ crypt.encrypt(in);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IOUtils.write(in, out);
+ byte[] result = out.toByteArray();
+
+ assertEquals(
+ "We wrote 3 bytes, we expected 3 bytes back but got: "
+ + result.length, result.length, result.length);
+
+ assertEquals(42, result[0]);
+ assertEquals(127, result[1]);
+ assertEquals(12, result[2]);
+ }
+ });
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class IOUtilsTest extends TestLauncher {
+ public IOUtilsTest(String[] args) {
+ super("IOUtils test", args);
+
+ addTest(new TestCase("openResource") {
+ @Override
+ public void test() throws Exception {
+ InputStream in = IOUtils.openResource("VERSION");
+ assertNotNull(
+ "The VERSION file is supposed to be present in the binaries",
+ in);
+ in.close();
+ }
+ });
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+public class NextableInputStreamTest extends TestLauncher {
+ public NextableInputStreamTest(String[] args) {
+ super("NextableInputStream test", args);
+
+ addTest(new TestCase("Simple byte array reading") {
+ @Override
+ public void test() throws Exception {
+ byte[] expected = new byte[] { 42, 12, 0, 127 };
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(expected), null);
+ checkNext(this, "READ", in, expected);
+ }
+ });
+
+ addTest(new TestCase("Stop at 12") {
+ @Override
+ public void test() throws Exception {
+ byte[] expected = new byte[] { 42, 12, 0, 127 };
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(expected),
+ new NextableInputStreamStep(12));
+
+ checkNext(this, "FIRST", in, new byte[] { 42 });
+ }
+ });
+
+ addTest(new TestCase("Stop at 12, resume, stop again, resume") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(data),
+ new NextableInputStreamStep(12));
+
+ checkNext(this, "FIRST", in, new byte[] { 42 });
+ checkNext(this, "SECOND", in, new byte[] { 0, 127 });
+ checkNext(this, "THIRD", in, new byte[] { 51, 11 });
+ }
+ });
+
+ addTest(new TestCase("Encapsulation") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 4, 127, 12, 5 };
+ NextableInputStream in4 = new NextableInputStream(
+ new ByteArrayInputStream(data),
+ new NextableInputStreamStep(4));
+ NextableInputStream subIn12 = new NextableInputStream(in4,
+ new NextableInputStreamStep(12));
+
+ in4.next();
+ checkNext(this, "SUB FIRST", subIn12, new byte[] { 42 });
+ checkNext(this, "SUB SECOND", subIn12, new byte[] { 0 });
+
+ assertEquals("The subIn still has some data", false,
+ subIn12.next());
+
+ checkNext(this, "MAIN LAST", in4, new byte[] { 127, 12, 5 });
+ }
+ });
+
+ addTest(new TestCase("UTF-8 text lines test") {
+ @Override
+ public void test() throws Exception {
+ String ln1 = "Ligne première";
+ String ln2 = "Ligne la deuxième du nom";
+ byte[] data = (ln1 + "\n" + ln2).getBytes("UTF-8");
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(data),
+ new NextableInputStreamStep('\n'));
+
+ checkNext(this, "FIRST", in, ln1.getBytes("UTF-8"));
+ checkNext(this, "SECOND", in, ln2.getBytes("UTF-8"));
+ }
+ });
+
+ addTest(new TestCase("nextAll()") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(data),
+ new NextableInputStreamStep(12));
+
+ checkNext(this, "FIRST", in, new byte[] { 42 });
+ checkNextAll(this, "REST", in, new byte[] { 0, 127, 12, 51, 11,
+ 12 });
+ assertEquals("The stream still has some data", false, in.next());
+ }
+ });
+
+ addTest(new TestCase("getBytesRead()") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(data),
+ new NextableInputStreamStep(12));
+
+ in.nextAll();
+ IOUtils.toByteArray(in);
+
+ assertEquals("The number of bytes read is not correct",
+ data.length, in.getBytesRead());
+ }
+ });
+
+ addTest(new TestCase("bytes array input") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+ NextableInputStream in = new NextableInputStream(data,
+ new NextableInputStreamStep(12));
+
+ checkNext(this, "FIRST", in, new byte[] { 42 });
+ checkNext(this, "SECOND", in, new byte[] { 0, 127 });
+ checkNext(this, "THIRD", in, new byte[] { 51, 11 });
+ }
+ });
+
+ addTest(new TestCase("Skip data") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+ NextableInputStream in = new NextableInputStream(data, null);
+ in.next();
+
+ byte[] rest = new byte[] { 12, 51, 11, 12 };
+
+ in.skip(4);
+ assertEquals("STARTS_WITH OK_1", true, in.startsWith(rest));
+ assertEquals("STARTS_WITH KO_1", false,
+ in.startsWith(new byte[] { 0 }));
+ assertEquals("STARTS_WITH KO_2", false, in.startsWith(data));
+ assertEquals("STARTS_WITH KO_3", false,
+ in.startsWith(new byte[] { 1, 2, 3 }));
+ assertEquals("STARTS_WITH OK_2", true, in.startsWith(rest));
+ assertEquals("READ REST", IOUtils.readSmallStream(in),
+ new String(rest));
+ in.close();
+ }
+ });
+
+ addTest(new TestCase("Starts with") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+ NextableInputStream in = new NextableInputStream(data, null);
+ in.next();
+
+ // yes
+ assertEquals("It actually starts with that", true,
+ in.startsWith(new byte[] { 42 }));
+ assertEquals("It actually starts with that", true,
+ in.startsWith(new byte[] { 42, 12 }));
+ assertEquals("It actually is the same array", true,
+ in.startsWith(data));
+
+ // no
+ assertEquals("It actually does not start with that", false,
+ in.startsWith(new byte[] { 12 }));
+ assertEquals(
+ "It actually does not start with that",
+ false,
+ in.startsWith(new byte[] { 42, 12, 0, 127, 12, 51, 11,
+ 11 }));
+
+ // too big
+ try {
+ in.startsWith(new byte[] { 42, 12, 0, 127, 12, 51, 11, 12,
+ 0 });
+ fail("Searching a prefix bigger than the array should throw an IOException");
+ } catch (IOException e) {
+ }
+
+ in.close();
+ }
+ });
+
+ addTest(new TestCase("Starts with strings") {
+ @Override
+ public void test() throws Exception {
+ String text = "Fanfan et Toto vont à la mer";
+ byte[] data = text.getBytes("UTF-8");
+ NextableInputStream in = new NextableInputStream(data, null);
+ in.next();
+
+ // yes
+ assertEquals("It actually starts with that", true,
+ in.startsWith("F"));
+ assertEquals("It actually starts with that", true,
+ in.startsWith("Fanfan et"));
+ assertEquals("It actually is the same text", true,
+ in.startsWith(text));
+
+ // no
+ assertEquals("It actually does not start with that", false,
+ in.startsWith("Toto"));
+ assertEquals("It actually does not start with that", false,
+ in.startsWith("Fanfan et Toto vont à la mee"));
+
+ // too big
+ try {
+ in.startsWith("Fanfan et Toto vont à la mer.");
+ fail("Searching a prefix bigger than the array should throw an IOException");
+ } catch (IOException e) {
+ }
+
+ in.close();
+ }
+ });
+
+ addTest(new TestCase("Starts With strings + steps") {
+ @Override
+ public void test() throws Exception {
+ String data = "{\nREF: fanfan\n}";
+ NextableInputStream in = new NextableInputStream(
+ data.getBytes("UTF-8"), new NextableInputStreamStep(
+ '\n'));
+ in.next();
+
+ assertEquals("STARTS_WITH OK", true, in.startsWith("{"));
+ in.skip(1);
+ assertEquals("STARTS_WITH WHEN SPENT", false,
+ in.startsWith("{"));
+
+ checkNext(this, "PARTIAL CONTENT", in,
+ "REF: fanfan".getBytes("UTF-8"));
+ }
+ });
+
+ addTest(new TestCase("InputStream is(String)") {
+ @Override
+ public void test() throws Exception {
+ String data = "{\nREF: fanfan\n}";
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(data.getBytes("UTF-8")),
+ new NextableInputStreamStep('\n'));
+
+ in.next();
+ assertEquals("Item 1 OK", true, in.is("{"));
+ assertEquals("Item 1 KO_1", false, in.is("|"));
+ assertEquals("Item 1 KO_2", false, in.is("{}"));
+ in.skip(1);
+ in.next();
+ assertEquals("Item 2 OK", true, in.is("REF: fanfan"));
+ assertEquals("Item 2 KO", false, in.is("REF: fanfan."));
+ IOUtils.readSmallStream(in);
+ in.next();
+ assertEquals("Item 3 OK", true, in.is("}"));
+
+ in.close();
+ }
+ });
+
+ addTest(new TestCase("Bytes NextAll test") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 };
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(data),
+ new NextableInputStreamStep(12));
+
+ checkNext(this, "FIRST", in, new byte[] { 42 });
+ checkNextAll(this, "SECOND", in, new byte[] { 0, 127, 12, 51,
+ 11, 12 });
+ }
+ });
+
+ addTest(new TestCase("String NextAll test") {
+ @Override
+ public void test() throws Exception {
+ String d1 = "^java.lang.String";
+ String d2 = "\"http://example.com/query.html\"";
+ String data = d1 + ":" + d2;
+ NextableInputStream in = new NextableInputStream(
+ new ByteArrayInputStream(data.getBytes("UTF-8")),
+ new NextableInputStreamStep(':'));
+
+ checkNext(this, "FIRST", in, d1.getBytes("UTF-8"));
+ checkNextAll(this, "SECOND", in, d2.getBytes("UTF-8"));
+ }
+ });
+
+ addTest(new TestCase("NextAll in Next test") {
+ @Override
+ public void test() throws Exception {
+ String line1 = "première ligne";
+ String d1 = "^java.lang.String";
+ String d2 = "\"http://example.com/query.html\"";
+ String line3 = "end of lines";
+ String data = line1 + "\n" + d1 + ":" + d2 + "\n" + line3;
+
+ NextableInputStream inL = new NextableInputStream(
+ new ByteArrayInputStream(data.getBytes("UTF-8")),
+ new NextableInputStreamStep('\n'));
+
+ checkNext(this, "Line 1", inL, line1.getBytes("UTF-8"));
+ inL.next();
+
+ NextableInputStream in = new NextableInputStream(inL,
+ new NextableInputStreamStep(':'));
+
+ checkNext(this, "Line 2 FIRST", in, d1.getBytes("UTF-8"));
+ checkNextAll(this, "Line 2 SECOND", in, d2.getBytes("UTF-8"));
+ }
+ });
+ }
+
+ static void checkNext(TestCase test, String prefix, NextableInputStream in,
+ byte[] expected) throws Exception {
+ test.assertEquals("Cannot get " + prefix + " entry", true, in.next());
+ checkArrays(test, prefix, in, expected);
+ }
+
+ static void checkNextAll(TestCase test, String prefix,
+ NextableInputStream in, byte[] expected) throws Exception {
+ test.assertEquals("Cannot get " + prefix + " entries", true,
+ in.nextAll());
+ checkArrays(test, prefix, in, expected);
+ }
+
+ static void checkArrays(TestCase test, String prefix,
+ NextableInputStream in, byte[] expected) throws Exception {
+ byte[] actual = IOUtils.toByteArray(in);
+ test.assertEquals("The " + prefix
+ + " resulting array has not the correct number of items",
+ expected.length, actual.length);
+ for (int i = 0; i < actual.length; i++) {
+ test.assertEquals("Item " + i + " (0-based) is not the same",
+ expected[i], actual[i]);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ProgressTest extends TestLauncher {
+ public ProgressTest(String[] args) {
+ super("Progress reporting", args);
+
+ addSeries(new TestLauncher("Simple progress", args) {
+ {
+ addTest(new TestCase("Relative values and direct values") {
+ @Override
+ public void test() throws Exception {
+ Progress p = new Progress();
+ assertEquals(0, p.getProgress());
+ assertEquals(0, p.getRelativeProgress());
+ p.setProgress(33);
+ assertEquals(33, p.getProgress());
+ assertEquals(0.33, p.getRelativeProgress());
+ p.setMax(3);
+ p.setProgress(1);
+ assertEquals(1, p.getProgress());
+ assertEquals(
+ generateAssertMessage("0.33..",
+ p.getRelativeProgress()), true,
+ p.getRelativeProgress() >= 0.332);
+ assertEquals(
+ generateAssertMessage("0.33..",
+ p.getRelativeProgress()), true,
+ p.getRelativeProgress() <= 0.334);
+ }
+ });
+
+ addTest(new TestCase("Listeners at first level") {
+ int pg;
+
+ @Override
+ public void test() throws Exception {
+ Progress p = new Progress();
+ p.addProgressListener(new Progress.ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ pg = progress.getProgress();
+ }
+ });
+
+ p.setProgress(42);
+ assertEquals(42, pg);
+ p.setProgress(0);
+ assertEquals(0, pg);
+ }
+ });
+ }
+ });
+
+ addSeries(new TestLauncher("Progress with children", args) {
+ {
+ addTest(new TestCase("One child") {
+ @Override
+ public void test() throws Exception {
+ Progress p = new Progress();
+ Progress child = new Progress();
+
+ p.addProgress(child, 100);
+
+ child.setProgress(42);
+ assertEquals(42, p.getProgress());
+ }
+ });
+
+ addTest(new TestCase("Multiple children") {
+ @Override
+ public void test() throws Exception {
+ Progress p = new Progress();
+ Progress child1 = new Progress();
+ Progress child2 = new Progress();
+ Progress child3 = new Progress();
+
+ p.addProgress(child1, 20);
+ p.addProgress(child2, 60);
+ p.addProgress(child3, 20);
+
+ child1.setProgress(50);
+ assertEquals(10, p.getProgress());
+ child2.setProgress(100);
+ assertEquals(70, p.getProgress());
+ child3.setProgress(100);
+ assertEquals(90, p.getProgress());
+ child1.setProgress(100);
+ assertEquals(100, p.getProgress());
+ }
+ });
+
+ addTest(new TestCase("Listeners with children") {
+ int pg;
+
+ @Override
+ public void test() throws Exception {
+ final Progress p = new Progress();
+ Progress child1 = new Progress();
+ Progress child2 = new Progress();
+ p.addProgress(child1, 50);
+ p.addProgress(child2, 50);
+
+ p.addProgressListener(new Progress.ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ pg = p.getProgress();
+ }
+ });
+
+ child1.setProgress(50);
+ assertEquals(25, pg);
+ child2.setProgress(100);
+ assertEquals(75, pg);
+ child1.setProgress(100);
+ assertEquals(100, pg);
+ }
+ });
+
+ addTest(new TestCase("Listeners with children, not 1-100") {
+ int pg;
+
+ @Override
+ public void test() throws Exception {
+ final Progress p = new Progress();
+ p.setMax(1000);
+
+ Progress child1 = new Progress();
+ child1.setMax(2);
+
+ Progress child2 = new Progress();
+ p.addProgress(child1, 500);
+ p.addProgress(child2, 500);
+
+ p.addProgressListener(new Progress.ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ pg = p.getProgress();
+ }
+ });
+
+ child1.setProgress(1);
+ assertEquals(250, pg);
+ child2.setProgress(100);
+ assertEquals(750, pg);
+ child1.setProgress(2);
+ assertEquals(1000, pg);
+ }
+ });
+
+ addTest(new TestCase(
+ "Listeners with children, not 1-100, local progress") {
+ int pg;
+
+ @Override
+ public void test() throws Exception {
+ final Progress p = new Progress();
+ p.setMax(1000);
+
+ Progress child1 = new Progress();
+ child1.setMax(2);
+
+ Progress child2 = new Progress();
+ p.addProgress(child1, 400);
+ p.addProgress(child2, 400);
+ // 200 = local progress
+
+ p.addProgressListener(new Progress.ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ pg = p.getProgress();
+ }
+ });
+
+ child1.setProgress(1);
+ assertEquals(200, pg);
+ child2.setProgress(100);
+ assertEquals(600, pg);
+ p.setProgress(100);
+ assertEquals(700, pg);
+ child1.setProgress(2);
+ assertEquals(900, pg);
+ p.setProgress(200);
+ assertEquals(1000, pg);
+ }
+ });
+
+ addTest(new TestCase("Listeners with 5+ children, 4+ depth") {
+ int pg;
+
+ @Override
+ public void test() throws Exception {
+ final Progress p = new Progress();
+ Progress child1 = new Progress();
+ Progress child2 = new Progress();
+ p.addProgress(child1, 50);
+ p.addProgress(child2, 50);
+ Progress child11 = new Progress();
+ child1.addProgress(child11, 100);
+ Progress child111 = new Progress();
+ child11.addProgress(child111, 100);
+ Progress child1111 = new Progress();
+ child111.addProgress(child1111, 20);
+ Progress child1112 = new Progress();
+ child111.addProgress(child1112, 20);
+ Progress child1113 = new Progress();
+ child111.addProgress(child1113, 20);
+ Progress child1114 = new Progress();
+ child111.addProgress(child1114, 20);
+ Progress child1115 = new Progress();
+ child111.addProgress(child1115, 20);
+
+ p.addProgressListener(new Progress.ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ pg = p.getProgress();
+ }
+ });
+
+ child1111.setProgress(100);
+ child1112.setProgress(50);
+ child1113.setProgress(25);
+ child1114.setProgress(25);
+ child1115.setProgress(50);
+ assertEquals(25, pg);
+ child2.setProgress(100);
+ assertEquals(75, pg);
+ child1111.setProgress(100);
+ child1112.setProgress(100);
+ child1113.setProgress(100);
+ child1114.setProgress(100);
+ child1115.setProgress(100);
+ assertEquals(100, pg);
+ }
+ });
+
+ addTest(new TestCase("Listeners with children, multi-thread") {
+ int pg;
+ boolean decrease;
+ Object lock1 = new Object();
+ Object lock2 = new Object();
+ int currentStep1;
+ int currentStep2;
+
+ @Override
+ public void test() throws Exception {
+ final Progress p = new Progress(0, 200);
+
+ final Progress child1 = new Progress();
+ final Progress child2 = new Progress();
+ p.addProgress(child1, 100);
+ p.addProgress(child2, 100);
+
+ p.addProgressListener(new Progress.ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ int now = p.getProgress();
+ if (now < pg) {
+ decrease = true;
+ }
+ pg = now;
+ }
+ });
+
+ // Run 200 concurrent threads, 2 at a time allowed to
+ // make progress (each on a different child)
+ for (int i = 0; i <= 100; i++) {
+ final int step = i;
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (lock1) {
+ if (step > currentStep1) {
+ currentStep1 = step;
+ child1.setProgress(step);
+ }
+ }
+ }
+ }).start();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (lock2) {
+ if (step > currentStep2) {
+ currentStep2 = step;
+ child2.setProgress(step);
+ }
+ }
+ }
+ }).start();
+ }
+
+ int i;
+ int timeout = 20; // in 1/10th of seconds
+ for (i = 0; i < timeout
+ && (currentStep1 + currentStep2) < 200; i++) {
+ Thread.sleep(100);
+ }
+
+ assertEquals("The test froze at step " + currentStep1
+ + " + " + currentStep2, true, i < timeout);
+ assertEquals(
+ "There should not have any decresing steps",
+ decrease, false);
+ assertEquals("The progress should have reached 200",
+ 200, p.getProgress());
+ assertEquals(
+ "The progress should have reached completion",
+ true, p.isDone());
+ }
+ });
+ }
+ });
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ReplaceInputStreamTest extends TestLauncher {
+ public ReplaceInputStreamTest(String[] args) {
+ super("ReplaceInputStream test", args);
+
+ addTest(new TestCase("Empty replace") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+ ReplaceInputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data), new byte[0],
+ new byte[0]);
+
+ checkArrays(this, "FIRST", in, data);
+ }
+ });
+
+ addTest(new TestCase("Simple replace") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+ ReplaceInputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data), new byte[] { 0 },
+ new byte[] { 10 });
+
+ checkArrays(this, "FIRST", in, new byte[] { 42, 12, 10, 127 });
+ }
+ });
+
+ addTest(new TestCase("3/4 replace") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+ ReplaceInputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data),
+ new byte[] { 12, 0, 127 }, new byte[] { 10, 10, 10 });
+
+ checkArrays(this, "FIRST", in, new byte[] { 42, 10, 10, 10 });
+ }
+ });
+
+ addTest(new TestCase("Lnger replace") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+ ReplaceInputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data), new byte[] { 0 },
+ new byte[] { 10, 10, 10 });
+
+ checkArrays(this, "FIRST", in, new byte[] { 42, 12, 10, 10, 10,
+ 127 });
+ }
+ });
+
+ addTest(new TestCase("Shorter replace") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+ ReplaceInputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data),
+ new byte[] { 42, 12, 0 }, new byte[] { 10 });
+
+ checkArrays(this, "FIRST", in, new byte[] { 10, 127 });
+ }
+ });
+
+ addTest(new TestCase("String replace") {
+ @Override
+ public void test() throws Exception {
+ byte[] data = "I like red".getBytes("UTF-8");
+ ReplaceInputStream in = new ReplaceInputStream(
+ new ByteArrayInputStream(data),
+ "red".getBytes("UTF-8"), "blue".getBytes("UTF-8"));
+
+ checkArrays(this, "FIRST", in, "I like blue".getBytes("UTF-8"));
+
+ data = "I like blue".getBytes("UTF-8");
+ in = new ReplaceInputStream(new ByteArrayInputStream(data),
+ "blue".getBytes("UTF-8"), "red".getBytes("UTF-8"));
+
+ checkArrays(this, "FIRST", in, "I like red".getBytes("UTF-8"));
+ }
+ });
+ }
+
+ static void checkArrays(TestCase test, String prefix, InputStream in,
+ byte[] expected) throws Exception {
+ byte[] actual = IOUtils.toByteArray(in);
+ test.assertEquals("The " + prefix
+ + " resulting array has not the correct number of items",
+ expected.length, actual.length);
+ for (int i = 0; i < actual.length; i++) {
+ test.assertEquals("Item " + i + " (0-based) is not the same",
+ expected[i], actual[i]);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayOutputStream;
+
+import be.nikiroo.utils.streams.ReplaceOutputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ReplaceOutputStreamTest extends TestLauncher {
+ public ReplaceOutputStreamTest(String[] args) {
+ super("ReplaceOutputStream test", args);
+
+ addTest(new TestCase("Single write, empty bytes replaces") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ReplaceOutputStream out = new ReplaceOutputStream(bout,
+ new byte[0], new byte[0]);
+
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+
+ out.write(data);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, data);
+ }
+ });
+
+ addTest(new TestCase("Multiple writes, empty Strings replaces") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ReplaceOutputStream out = new ReplaceOutputStream(bout, "", "");
+
+ byte[] data1 = new byte[] { 42, 12, 0, 127 };
+ byte[] data2 = new byte[] { 15, 55 };
+ byte[] data3 = new byte[] {};
+
+ byte[] dataAll = new byte[] { 42, 12, 0, 127, 15, 55 };
+
+ out.write(data1);
+ out.write(data2);
+ out.write(data3);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, dataAll);
+ }
+ });
+
+ addTest(new TestCase("Single write, bytes replaces") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ReplaceOutputStream out = new ReplaceOutputStream(bout,
+ new byte[] { 12 }, new byte[] { 55 });
+
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+
+ out.write(data);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 0, 127 });
+ }
+ });
+
+ addTest(new TestCase("Multiple writes, Strings replaces") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ReplaceOutputStream out = new ReplaceOutputStream(bout, "(-)",
+ "(.)");
+
+ byte[] data1 = "un mot ".getBytes("UTF-8");
+ byte[] data2 = "(-) of twee ".getBytes("UTF-8");
+ byte[] data3 = "(-) makes the difference".getBytes("UTF-8");
+
+ out.write(data1);
+ out.write(data2);
+ out.write(data3);
+ out.close();
+
+ checkArrays(this, "FIRST", bout,
+ "un mot (.) of twee (.) makes the difference"
+ .getBytes("UTF-8"));
+ }
+ });
+
+ addTest(new TestCase("Single write, longer bytes replaces") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ReplaceOutputStream out = new ReplaceOutputStream(bout,
+ new byte[] { 12 }, new byte[] { 55, 55, 66 });
+
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+
+ out.write(data);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 55, 66,
+ 0, 127 });
+ }
+ });
+
+ addTest(new TestCase("Single write, shorter bytes replaces") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ReplaceOutputStream out = new ReplaceOutputStream(bout,
+ new byte[] { 12, 0 }, new byte[] { 55 });
+
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+
+ out.write(data);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 127 });
+ }
+ });
+
+ addTest(new TestCase("Single write, remove bytes replaces") {
+ @Override
+ public void test() throws Exception {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ReplaceOutputStream out = new ReplaceOutputStream(bout,
+ new byte[] { 12 }, new byte[] {});
+
+ byte[] data = new byte[] { 42, 12, 0, 127 };
+
+ out.write(data);
+ out.close();
+
+ checkArrays(this, "FIRST", bout, new byte[] { 42, 0, 127 });
+ }
+ });
+ }
+
+ static void checkArrays(TestCase test, String prefix,
+ ByteArrayOutputStream bout, byte[] expected) throws Exception {
+ byte[] actual = bout.toByteArray();
+
+ if (false) {
+ System.out.print("\nExpected data: [ ");
+ for (int i = 0; i < expected.length; i++) {
+ if (i > 0)
+ System.out.print(", ");
+ System.out.print(expected[i]);
+ }
+ System.out.println(" ]");
+
+ System.out.print("Actual data : [ ");
+ for (int i = 0; i < actual.length; i++) {
+ if (i > 0)
+ System.out.print(", ");
+ System.out.print(actual[i]);
+ }
+ System.out.println(" ]");
+ }
+
+ test.assertEquals("The " + prefix
+ + " resulting array has not the correct number of items",
+ expected.length, actual.length);
+ for (int i = 0; i < actual.length; i++) {
+ test.assertEquals(prefix + ": item " + i
+ + " (0-based) is not the same", expected[i], actual[i]);
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.net.URL;
+
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.serial.server.ConnectActionClientObject;
+import be.nikiroo.utils.serial.server.ConnectActionClientString;
+import be.nikiroo.utils.serial.server.ConnectActionServerObject;
+import be.nikiroo.utils.serial.server.ConnectActionServerString;
+import be.nikiroo.utils.serial.server.ServerBridge;
+import be.nikiroo.utils.serial.server.ServerObject;
+import be.nikiroo.utils.serial.server.ServerString;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class SerialServerTest extends TestLauncher {
+ public SerialServerTest(String[] args) {
+ super("SerialServer test", args);
+
+ for (String key : new String[] { null,
+ "some super secret encryption key" }) {
+ for (boolean bridge : new Boolean[] { false, true }) {
+ final String skey = (key != null ? "(encrypted)"
+ : "(plain text)");
+ final String sbridge = (bridge ? " with bridge" : "");
+
+ addSeries(new SerialServerTest(args, key, skey, bridge,
+ sbridge, "ServerString"));
+
+ addSeries(new SerialServerTest(args, key, skey, bridge,
+ sbridge, new Object() {
+ @Override
+ public String toString() {
+ return "ServerObject";
+ }
+ }));
+ }
+ }
+ }
+
+ private SerialServerTest(final String[] args, final String key,
+ final String skey, final boolean bridge, final String sbridge,
+ final String title) {
+
+ super(title + " " + skey + sbridge, args);
+
+ addTest(new TestCase("Simple connection " + skey) {
+ @Override
+ public void test() throws Exception {
+ final String[] rec = new String[1];
+
+ ServerString server = new ServerString(this.getName(), 0, key) {
+ @Override
+ protected String onRequest(
+ ConnectActionServerString action, Version version,
+ String data, long id) throws Exception {
+ return null;
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ }
+ };
+
+ int port = server.getPort();
+ assertEquals("A port should have been assigned", true, port > 0);
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+
+ port = br.getPort();
+ assertEquals(
+ "A port should have been assigned to the bridge",
+ true, port > 0);
+
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientString(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ rec[0] = "ok";
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ assertNotNull("The client action was not run", rec[0]);
+ assertEquals("ok", rec[0]);
+ }
+ });
+
+ addTest(new TestCase("Simple exchange " + skey) {
+ final String[] sent = new String[1];
+ final String[] recd = new String[1];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ ServerString server = new ServerString(this.getName(), 0, key) {
+ @Override
+ protected String onRequest(
+ ConnectActionServerString action, Version version,
+ String data, long id) throws Exception {
+ sent[0] = data;
+ return "pong";
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ err[0] = e;
+ }
+ };
+
+ int port = server.getPort();
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientString(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ recd[0] = send("ping");
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
+ }
+
+ assertEquals("ping", sent[0]);
+ assertEquals("pong", recd[0]);
+ }
+ });
+
+ addTest(new TestCase("Multiple exchanges " + skey) {
+ final String[] sent = new String[3];
+ final String[] recd = new String[3];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ ServerString server = new ServerString(this.getName(), 0, key) {
+ @Override
+ protected String onRequest(
+ ConnectActionServerString action, Version version,
+ String data, long id) throws Exception {
+ sent[0] = data;
+ action.send("pong");
+ sent[1] = action.rec();
+ return "pong2";
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ err[0] = e;
+ }
+ };
+
+ int port = server.getPort();
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientString(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ recd[0] = send("ping");
+ recd[1] = send("ping2");
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
+ }
+
+ assertEquals("ping", sent[0]);
+ assertEquals("pong", recd[0]);
+ assertEquals("ping2", sent[1]);
+ assertEquals("pong2", recd[1]);
+ }
+ });
+
+ addTest(new TestCase("Multiple call from client " + skey) {
+ final String[] sent = new String[3];
+ final String[] recd = new String[3];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ ServerString server = new ServerString(this.getName(), 0, key) {
+ @Override
+ protected String onRequest(
+ ConnectActionServerString action, Version version,
+ String data, long id) throws Exception {
+ sent[Integer.parseInt(data)] = data;
+ return "" + (Integer.parseInt(data) * 2);
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ err[0] = e;
+ }
+ };
+
+ int port = server.getPort();
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientString(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ for (int i = 0; i < 3; i++) {
+ recd[i] = send("" + i);
+ }
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
+ }
+
+ assertEquals("0", sent[0]);
+ assertEquals("0", recd[0]);
+ assertEquals("1", sent[1]);
+ assertEquals("2", recd[1]);
+ assertEquals("2", sent[2]);
+ assertEquals("4", recd[2]);
+ }
+ });
+ }
+
+ private SerialServerTest(final String[] args, final String key,
+ final String skey, final boolean bridge, final String sbridge,
+ final Object title) {
+
+ super(title + " " + skey + sbridge, args);
+
+ addTest(new TestCase("Simple connection " + skey) {
+ @Override
+ public void test() throws Exception {
+ final Object[] rec = new Object[1];
+
+ ServerObject server = new ServerObject(this.getName(), 0, key) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServerObject action, Version version,
+ Object data, long id) throws Exception {
+ return null;
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ }
+ };
+
+ int port = server.getPort();
+ assertEquals("A port should have been assigned", true, port > 0);
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientObject(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ rec[0] = true;
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ assertNotNull("The client action was not run", rec[0]);
+ assertEquals(true, (boolean) ((Boolean) rec[0]));
+ }
+ });
+
+ addTest(new TestCase("Simple exchange " + skey) {
+ final Object[] sent = new Object[1];
+ final Object[] recd = new Object[1];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ ServerObject server = new ServerObject(this.getName(), 0, key) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServerObject action, Version version,
+ Object data, long id) throws Exception {
+ sent[0] = data;
+ return "pong";
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ err[0] = e;
+ }
+ };
+
+ int port = server.getPort();
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientObject(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ recd[0] = send("ping");
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
+ }
+
+ assertEquals("ping", sent[0]);
+ assertEquals("pong", recd[0]);
+ }
+ });
+
+ addTest(new TestCase("Multiple exchanges " + skey) {
+ final Object[] sent = new Object[3];
+ final Object[] recd = new Object[3];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ ServerObject server = new ServerObject(this.getName(), 0, key) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServerObject action, Version version,
+ Object data, long id) throws Exception {
+ sent[0] = data;
+ action.send("pong");
+ sent[1] = action.rec();
+ return "pong2";
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ err[0] = e;
+ }
+ };
+
+ int port = server.getPort();
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientObject(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ recd[0] = send("ping");
+ recd[1] = send("ping2");
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
+ }
+
+ assertEquals("ping", sent[0]);
+ assertEquals("pong", recd[0]);
+ assertEquals("ping2", sent[1]);
+ assertEquals("pong2", recd[1]);
+ }
+ });
+
+ addTest(new TestCase("Object array of URLs " + skey) {
+ final Object[] sent = new Object[1];
+ final Object[] recd = new Object[1];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ ServerObject server = new ServerObject(this.getName(), 0, key) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServerObject action, Version version,
+ Object data, long id) throws Exception {
+ sent[0] = data;
+ return new Object[] { "ACK" };
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ err[0] = e;
+ }
+ };
+
+ int port = server.getPort();
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientObject(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ recd[0] = send(new Object[] {
+ "key",
+ new URL(
+ "https://example.com/from_client"),
+ "https://example.com/from_client" });
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
+ }
+
+ Object[] sento = (Object[]) (sent[0]);
+ Object[] recdo = (Object[]) (recd[0]);
+
+ assertEquals("key", sento[0]);
+ assertEquals("https://example.com/from_client",
+ ((URL) sento[1]).toString());
+ assertEquals("https://example.com/from_client", sento[2]);
+ assertEquals("ACK", recdo[0]);
+ }
+ });
+
+ addTest(new TestCase("Multiple call from client " + skey) {
+ final Object[] sent = new Object[3];
+ final Object[] recd = new Object[3];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ ServerObject server = new ServerObject(this.getName(), 0, key) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServerObject action, Version version,
+ Object data, long id) throws Exception {
+ sent[(Integer) data] = data;
+ return ((Integer) data) * 2;
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ err[0] = e;
+ }
+ };
+
+ int port = server.getPort();
+
+ server.start();
+
+ ServerBridge br = null;
+ if (bridge) {
+ br = new ServerBridge(0, key, "", port, key);
+ br.setTraceHandler(null);
+ port = br.getPort();
+ br.start();
+ }
+
+ try {
+ try {
+ new ConnectActionClientObject(null, port, key) {
+ @Override
+ public void action(Version version)
+ throws Exception {
+ for (int i = 0; i < 3; i++) {
+ recd[i] = send(i);
+ }
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+ } finally {
+ if (br != null) {
+ br.stop();
+ }
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
+ }
+
+ assertEquals(0, sent[0]);
+ assertEquals(0, recd[0]);
+ assertEquals(1, sent[1]);
+ assertEquals(2, recd[1]);
+ assertEquals(2, sent[2]);
+ assertEquals(4, recd[2]);
+ }
+ });
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.NotSerializableException;
+import java.net.URL;
+import java.util.Arrays;
+
+import be.nikiroo.utils.serial.Exporter;
+import be.nikiroo.utils.serial.Importer;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class SerialTest extends TestLauncher {
+ /**
+ * Required for Import/Export of objects.
+ */
+ public SerialTest() {
+ this(null);
+ }
+
+ private void encodeRecodeTest(TestCase test, Object data) throws Exception {
+ byte[] encoded = toBytes(data, true);
+ Object redata = fromBytes(toBytes(data, false));
+ byte[] reencoded = toBytes(redata, true);
+
+ // We suppose text mode
+ if (encoded.length < 256 && reencoded.length < 256) {
+ test.assertEquals("Different data after encode/decode/encode",
+ new String(encoded, "UTF-8"),
+ new String(reencoded, "UTF-8"));
+ } else {
+ test.assertEquals("Different data after encode/decode/encode",
+ true, Arrays.equals(encoded, reencoded));
+ }
+ }
+
+ // try to remove pointer addresses
+ private byte[] toBytes(Object data, boolean clearRefs)
+ throws NotSerializableException, IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new Exporter(out).append(data);
+ out.flush();
+
+ if (clearRefs) {
+ String tmp = new String(out.toByteArray(), "UTF-8");
+ tmp = tmp.replaceAll("@[0-9]*", "@REF");
+ return tmp.getBytes("UTF-8");
+ }
+
+ return out.toByteArray();
+ }
+
+ private Object fromBytes(byte[] data) throws NoSuchFieldException,
+ NoSuchMethodException, ClassNotFoundException,
+ NullPointerException, IOException {
+
+ InputStream in = new ByteArrayInputStream(data);
+ try {
+ return new Importer().read(in).getValue();
+ } finally {
+ in.close();
+ }
+ }
+
+ 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);
+ encodeRecodeTest(this, data);
+ }
+ });
+
+ addTest(new TestCase() {
+ @SuppressWarnings("unused")
+ private TestCase me = setName("Anonymous inner class");
+
+ @Override
+ public void test() throws Exception {
+ Data data = new Data() {
+ @SuppressWarnings("unused")
+ int value = 42;
+ };
+ encodeRecodeTest(this, data);
+ }
+ });
+ addTest(new TestCase() {
+ @SuppressWarnings("unused")
+ private TestCase me = setName("Array of anonymous inner classes");
+
+ @Override
+ public void test() throws Exception {
+ Data[] data = new Data[] { new Data() {
+ @SuppressWarnings("unused")
+ int value = 42;
+ } };
+
+ byte[] encoded = toBytes(data, false);
+ Object redata = fromBytes(encoded);
+
+ // Comparing the 2 arrays won't be useful, because the @REFs
+ // will be ZIP-encoded; so we parse and re-encode each object
+
+ byte[] encoded1 = toBytes(data[0], true);
+ byte[] reencoded1 = toBytes(((Object[]) redata)[0], true);
+
+ assertEquals("Different data after encode/decode/encode", true,
+ Arrays.equals(encoded1, reencoded1));
+ }
+ });
+ addTest(new TestCase("URL Import/Export") {
+ @Override
+ public void test() throws Exception {
+ URL data = new URL("https://fanfan.be/");
+ encodeRecodeTest(this, data);
+ }
+ });
+ addTest(new TestCase("URL-String Import/Export") {
+ @Override
+ public void test() throws Exception {
+ String data = new URL("https://fanfan.be/").toString();
+ encodeRecodeTest(this, data);
+ }
+ });
+ addTest(new TestCase("URL/URL-String arrays Import/Export") {
+ @Override
+ public void test() throws Exception {
+ final String url = "https://fanfan.be/";
+ Object[] data = new Object[] { new URL(url), url };
+
+ byte[] encoded = toBytes(data, false);
+ Object redata = fromBytes(encoded);
+
+ // Comparing the 2 arrays won't be useful, because the @REFs
+ // will be ZIP-encoded; so we parse and re-encode each object
+
+ byte[] encoded1 = toBytes(data[0], true);
+ byte[] reencoded1 = toBytes(((Object[]) redata)[0], true);
+ byte[] encoded2 = toBytes(data[1], true);
+ byte[] reencoded2 = toBytes(((Object[]) redata)[1], true);
+
+ assertEquals("Different data 1 after encode/decode/encode",
+ true, Arrays.equals(encoded1, reencoded1));
+ assertEquals("Different data 2 after encode/decode/encode",
+ true, Arrays.equals(encoded2, reencoded2));
+ }
+ });
+ addTest(new TestCase("Import/Export with nested objects") {
+ @Override
+ public void test() throws Exception {
+ Data data = new DataObject(new Data(21));
+ encodeRecodeTest(this, data);
+ }
+ });
+ addTest(new TestCase("Import/Export String in object") {
+ @Override
+ public void test() throws Exception {
+ Data data = new DataString("fanfan");
+ encodeRecodeTest(this, data);
+ data = new DataString("http://example.com/query.html");
+ encodeRecodeTest(this, data);
+ data = new DataString("Test|Ché|http://|\"\\\"Pouch\\");
+ encodeRecodeTest(this, data);
+ data = new DataString("Test|Ché\\n|\nhttp://|\"\\\"Pouch\\");
+ encodeRecodeTest(this, data);
+ }
+ });
+ 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;
+ encodeRecodeTest(this, data);
+ }
+ });
+ addTest(new TestCase("Array in Object Import/Export") {
+ @Override
+ public void test() throws Exception {
+ Object data = new DataArray();// new String[] { "un", "deux" };
+ encodeRecodeTest(this, data);
+ }
+ });
+ addTest(new TestCase("Array Import/Export") {
+ @Override
+ public void test() throws Exception {
+ Object data = new String[] { "un", "deux" };
+ encodeRecodeTest(this, data);
+ }
+ });
+ addTest(new TestCase("Enum Import/Export") {
+ @Override
+ public void test() throws Exception {
+ Object data = EnumToSend.FANFAN;
+ encodeRecodeTest(this, data);
+ }
+ });
+ }
+
+ class DataArray {
+ public String[] data = new String[] { "un", "deux" };
+ }
+
+ class Data {
+ private int value;
+
+ private Data() {
+ }
+
+ public Data(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Data) {
+ Data other = (Data) obj;
+ return other.value == this.value;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return new Integer(value).hashCode();
+ }
+ }
+
+ @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 DataString extends Data {
+ private String data;
+
+ @SuppressWarnings("synthetic-access")
+ private DataString() {
+ }
+
+ @SuppressWarnings("synthetic-access")
+ public DataString(String 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;
+ }
+ }
+
+ enum EnumToSend {
+ FANFAN, TULIPE,
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.StringUtils.Alignment;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class StringUtilsTest extends TestLauncher {
+ public StringUtilsTest(String[] args) {
+ super("StringUtils test", args);
+
+ addTest(new TestCase("Time serialisation") {
+ @Override
+ public void test() throws Exception {
+ for (long fullTime : new Long[] { 0l, 123456l, 123456000l,
+ new Date().getTime() }) {
+ // precise to the second, no more
+ long time = (fullTime / 1000) * 1000;
+
+ String displayTime = StringUtils.fromTime(time);
+ assertNotNull("The stringified time for " + time
+ + " should not be null", displayTime);
+ assertEquals("The stringified time for " + time
+ + " should not be empty", false, displayTime.trim()
+ .isEmpty());
+
+ assertEquals("The time " + time
+ + " should be loop-convertable", time,
+ StringUtils.toTime(displayTime));
+
+ assertEquals("The time " + displayTime
+ + " should be loop-convertable", displayTime,
+ StringUtils.fromTime(StringUtils
+ .toTime(displayTime)));
+ }
+ }
+ });
+
+ addTest(new TestCase("MD5") {
+ @Override
+ public void test() throws Exception {
+ String mess = "The String we got is not what 'md5sum' said it should heve been";
+ assertEquals(mess, "34ded48fcff4221d644be9a37e1cb1d9",
+ StringUtils.getMd5Hash("fanfan la tulipe"));
+ assertEquals(mess, "7691b0cb74ed0f94b4d8cd858abe1165",
+ StringUtils.getMd5Hash("je te do-o-o-o-o-o-nne"));
+ }
+ });
+
+ addTest(new TestCase("Padding") {
+ @Override
+ public void test() throws Exception {
+ for (String data : new String[] { "fanfan", "la tulipe",
+ "1234567890", "12345678901234567890", "1", "" }) {
+ String result = StringUtils.padString(data, -1);
+ assertEquals("A size of -1 is expected to produce a noop",
+ true, data.equals(result));
+ for (int size : new Integer[] { 0, 1, 5, 10, 40 }) {
+ result = StringUtils.padString(data, size);
+ assertEquals(
+ "Padding a String at a certain size should give a String of the given size",
+ size, result.length());
+ assertEquals(
+ "Padding a String should not change the content",
+ true, data.trim().startsWith(result.trim()));
+
+ result = StringUtils.padString(data, size, false, null);
+ assertEquals(
+ "Padding a String without cutting should not shorten the String",
+ true, data.length() <= result.length());
+ assertEquals(
+ "Padding a String without cutting should keep the whole content",
+ true, data.trim().equals(result.trim()));
+
+ result = StringUtils.padString(data, size, false,
+ Alignment.RIGHT);
+ if (size > data.length()) {
+ assertEquals(
+ "Padding a String to the end should work as expected",
+ true, result.endsWith(data));
+ }
+
+ result = StringUtils.padString(data, size, false,
+ Alignment.JUSTIFY);
+ if (size > data.length()) {
+ String unspacedData = data.trim();
+ String unspacedResult = result.trim();
+ for (int i = 0; i < size; i++) {
+ unspacedData = unspacedData.replace(" ", " ");
+ unspacedResult = unspacedResult.replace(" ",
+ " ");
+ }
+
+ assertEquals(
+ "Justified text trimmed with all spaces collapsed "
+ + "sould be identical to original text "
+ + "trimmed with all spaces collapsed",
+ unspacedData, unspacedResult);
+ }
+
+ result = StringUtils.padString(data, size, false,
+ Alignment.CENTER);
+ if (size > data.length()) {
+ int before = 0;
+ for (int i = 0; i < result.length()
+ && result.charAt(i) == ' '; i++) {
+ before++;
+ }
+
+ int after = 0;
+ for (int i = result.length() - 1; i >= 0
+ && result.charAt(i) == ' '; i--) {
+ after++;
+ }
+
+ if (result.trim().isEmpty()) {
+ after = before / 2;
+ if (before > (2 * after)) {
+ before = after + 1;
+ } else {
+ before = after;
+ }
+ }
+
+ assertEquals(
+ "Padding a String on center should work as expected",
+ result.length(), before + data.length()
+ + after);
+ assertEquals(
+ "Padding a String on center should not uncenter the content",
+ true, Math.abs(before - after) <= 1);
+ }
+ }
+ }
+ }
+ });
+
+ addTest(new TestCase("Justifying") {
+ @Override
+ public void test() throws Exception {
+ Map<String, Map<Integer, Entry<Alignment, List<String>>>> source = new HashMap<String, Map<Integer, Entry<Alignment, List<String>>>>();
+ addValue(source, Alignment.LEFT, "testy", -1, "testy");
+ addValue(source, Alignment.RIGHT, "testy", -1, "testy");
+ addValue(source, Alignment.CENTER, "testy", -1, "testy");
+ addValue(source, Alignment.JUSTIFY, "testy", -1, "testy");
+ addValue(source, Alignment.LEFT, "testy", 5, "testy");
+ addValue(source, Alignment.LEFT, "testy", 3, "te-", "sty");
+ addValue(source, Alignment.LEFT,
+ "Un petit texte qui se mettra sur plusieurs lignes",
+ 10, "Un petit", "texte qui", "se mettra", "sur",
+ "plusieurs", "lignes");
+ addValue(source, Alignment.LEFT,
+ "Un petit texte qui se mettra sur plusieurs lignes", 7,
+ "Un", "petit", "texte", "qui se", "mettra", "sur",
+ "plusie-", "urs", "lignes");
+ addValue(source, Alignment.RIGHT,
+ "Un petit texte qui se mettra sur plusieurs lignes", 7,
+ " Un", " petit", " texte", " qui se", " mettra",
+ " sur", "plusie-", " urs", " lignes");
+ addValue(source, Alignment.CENTER,
+ "Un petit texte qui se mettra sur plusieurs lignes", 7,
+ " Un ", " petit ", " texte ", "qui se ", "mettra ",
+ " sur ", "plusie-", " urs ", "lignes ");
+ addValue(source, Alignment.JUSTIFY,
+ "Un petit texte qui se mettra sur plusieurs lignes", 7,
+ "Un pet-", "it tex-", "te qui", "se met-", "tra sur",
+ "plusie-", "urs li-", "gnes");
+ addValue(source, Alignment.JUSTIFY,
+ "Un petit texte qui se mettra sur plusieurs lignes",
+ 14, "Un petit", "texte qui se",
+ "mettra sur", "plusieurs lig-", "nes");
+ addValue(source, Alignment.JUSTIFY, "le dash-test", 9,
+ "le dash-", "test");
+
+ for (String data : source.keySet()) {
+ for (int size : source.get(data).keySet()) {
+ Alignment align = source.get(data).get(size).getKey();
+ List<String> values = source.get(data).get(size)
+ .getValue();
+
+ List<String> result = StringUtils.justifyText(data,
+ size, align);
+
+ // System.out.println("[" + data + " (" + size + ")" +
+ // "] -> [");
+ // for (int i = 0; i < result.size(); i++) {
+ // String resultLine = result.get(i);
+ // System.out.println(i + ": " + resultLine);
+ // }
+ // System.out.println("]");
+
+ assertEquals(values, result);
+ }
+ }
+ }
+ });
+
+ addTest(new TestCase("unhtml") {
+ @Override
+ public void test() throws Exception {
+ Map<String, String> data = new HashMap<String, String>();
+ data.put("aa", "aa");
+ data.put("test with spaces ", "test with spaces ");
+ data.put("<a href='truc://target/'>link</a>", "link");
+ data.put("<html>Digimon</html>", "Digimon");
+ data.put("", "");
+ data.put(" ", " ");
+
+ for (Entry<String, String> entry : data.entrySet()) {
+ String result = StringUtils.unhtml(entry.getKey());
+ assertEquals("Result is not what we expected",
+ entry.getValue(), result);
+ }
+ }
+ });
+
+ addTest(new TestCase("zip64") {
+ @Override
+ public void test() throws Exception {
+ String orig = "test";
+ String zipped = StringUtils.zip64(orig);
+ String unzipped = StringUtils.unzip64s(zipped);
+ assertEquals(orig, unzipped);
+ }
+ });
+
+ addTest(new TestCase("format/toNumber simple") {
+ @Override
+ public void test() throws Exception {
+ assertEquals(263l, StringUtils.toNumber("263"));
+ assertEquals(21200l, StringUtils.toNumber("21200"));
+ assertEquals(0l, StringUtils.toNumber("0"));
+ assertEquals("263", StringUtils.formatNumber(263l));
+ assertEquals("21 k", StringUtils.formatNumber(21000l));
+ assertEquals("0", StringUtils.formatNumber(0l));
+ }
+ });
+
+ addTest(new TestCase("format/toNumber not 000") {
+ @Override
+ public void test() throws Exception {
+ assertEquals(263200l, StringUtils.toNumber("263.2 k"));
+ assertEquals(42000l, StringUtils.toNumber("42.0 k"));
+ assertEquals(12000000l, StringUtils.toNumber("12 M"));
+ assertEquals(2000000000l, StringUtils.toNumber("2 G"));
+ assertEquals("263 k", StringUtils.formatNumber(263012l));
+ assertEquals("42 k", StringUtils.formatNumber(42012l));
+ assertEquals("12 M", StringUtils.formatNumber(12012121l));
+ assertEquals("7 G", StringUtils.formatNumber(7364635928l));
+ }
+ });
+
+ addTest(new TestCase("format/toNumber decimals") {
+ @Override
+ public void test() throws Exception {
+ assertEquals(263200l, StringUtils.toNumber("263.2 k"));
+ assertEquals(1200l, StringUtils.toNumber("1.2 k"));
+ assertEquals(42700000l, StringUtils.toNumber("42.7 M"));
+ assertEquals(1220l, StringUtils.toNumber("1.22 k"));
+ assertEquals(1432l, StringUtils.toNumber("1.432 k"));
+ assertEquals(6938l, StringUtils.toNumber("6.938 k"));
+ assertEquals("1.3 k", StringUtils.formatNumber(1300l, 1));
+ assertEquals("263.2020 k", StringUtils.formatNumber(263202l, 4));
+ assertEquals("1.26 k", StringUtils.formatNumber(1267l, 2));
+ assertEquals("42.7 M", StringUtils.formatNumber(42712121l, 1));
+ assertEquals("5.09 G", StringUtils.formatNumber(5094837485l, 2));
+ }
+ });
+ }
+
+ static private void addValue(
+ Map<String, Map<Integer, Entry<Alignment, List<String>>>> source,
+ final Alignment align, String input, int size,
+ final String... result) {
+ if (!source.containsKey(input)) {
+ source.put(input,
+ new HashMap<Integer, Entry<Alignment, List<String>>>());
+ }
+
+ source.get(input).put(size, new Entry<Alignment, List<String>>() {
+ @Override
+ public Alignment getKey() {
+ return align;
+ }
+
+ @Override
+ public List<String> getValue() {
+ return Arrays.asList(result);
+ }
+
+ @Override
+ public List<String> setValue(List<String> value) {
+ return null;
+ }
+ });
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import java.io.File;
+import java.io.IOException;
+
+import be.nikiroo.utils.TempFiles;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class TempFilesTest extends TestLauncher {
+ public TempFilesTest(String[] args) {
+ super("TempFiles", args);
+
+ addTest(new TestCase("Name is correct") {
+ @Override
+ public void test() throws Exception {
+ RootTempFiles files = new RootTempFiles("testy");
+ try {
+ assertEquals("The root was not created", true, files
+ .getRoot().exists());
+ assertEquals(
+ "The root is not prefixed with the expected name",
+ true, files.getRoot().getName().startsWith("testy"));
+
+ } finally {
+ files.close();
+ }
+ }
+ });
+
+ addTest(new TestCase("Clean after itself no use") {
+ @Override
+ public void test() throws Exception {
+ RootTempFiles files = new RootTempFiles("testy2");
+ try {
+ assertEquals("The root was not created", true, files
+ .getRoot().exists());
+ } finally {
+ files.close();
+ assertEquals("The root was not deleted", false, files
+ .getRoot().exists());
+ }
+ }
+ });
+
+ addTest(new TestCase("Clean after itself after usage") {
+ @Override
+ public void test() throws Exception {
+ RootTempFiles files = new RootTempFiles("testy3");
+ try {
+ assertEquals("The root was not created", true, files
+ .getRoot().exists());
+ files.createTempFile("test");
+ } finally {
+ files.close();
+ assertEquals("The root was not deleted", false, files
+ .getRoot().exists());
+ assertEquals("The main root in /tmp was not deleted",
+ false, files.getRoot().getParentFile().exists());
+ }
+ }
+ });
+
+ addTest(new TestCase("Temporary directories") {
+ @Override
+ public void test() throws Exception {
+ RootTempFiles files = new RootTempFiles("testy4");
+ File file = null;
+ try {
+ File dir = files.createTempDir("test");
+ file = new File(dir, "fanfan");
+ file.createNewFile();
+ assertEquals(
+ "Cannot create a file in a temporary directory",
+ true, file.exists());
+ } finally {
+ files.close();
+ if (file != null) {
+ assertEquals(
+ "A file created in a temporary directory should be deleted on close",
+ false, file.exists());
+ }
+ assertEquals("The root was not deleted", false, files
+ .getRoot().exists());
+ }
+ }
+ });
+ }
+
+ private class RootTempFiles extends TempFiles {
+ private File root = null;
+
+ public RootTempFiles(String name) throws IOException {
+ super(name);
+ }
+
+ public File getRoot() {
+ if (root != null)
+ return root;
+ return super.root;
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ root = super.root;
+ super.close();
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import be.nikiroo.utils.Cache;
+import be.nikiroo.utils.CacheMemory;
+import be.nikiroo.utils.Downloader;
+import be.nikiroo.utils.Proxy;
+import be.nikiroo.utils.main.bridge;
+import be.nikiroo.utils.main.img2aa;
+import be.nikiroo.utils.main.justify;
+import be.nikiroo.utils.test.TestLauncher;
+import be.nikiroo.utils.ui.UIUtils;
+
+/**
+ * Tests for nikiroo-utils.
+ *
+ * @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);
+
+ // setDetails(true);
+
+ addSeries(new ProgressTest(args));
+ addSeries(new BundleTest(args));
+ addSeries(new IOUtilsTest(args));
+ addSeries(new VersionTest(args));
+ addSeries(new SerialTest(args));
+ addSeries(new SerialServerTest(args));
+ addSeries(new StringUtilsTest(args));
+ addSeries(new TempFilesTest(args));
+ addSeries(new CryptUtilsTest(args));
+ addSeries(new BufferedInputStreamTest(args));
+ addSeries(new NextableInputStreamTest(args));
+ addSeries(new ReplaceInputStreamTest(args));
+ addSeries(new BufferedOutputStreamTest(args));
+ addSeries(new ReplaceOutputStreamTest(args));
+
+ // TODO: test cache and downloader
+ Cache cache = null;
+ CacheMemory memcache = null;
+ Downloader downloader = null;
+
+ // To include the sources:
+ img2aa siu;
+ justify ssu;
+ bridge aa;
+ Proxy proxy;
+ UIUtils uiUtils;
+ }
+
+ /**
+ * Main entry point of the program.
+ *
+ * @param args
+ * the arguments passed to the {@link TestLauncher}s.
+ */
+ static public void main(String[] args) {
+ System.exit(new Test(args).launch());
+ }
+}
--- /dev/null
+package be.nikiroo.utils.test_code;
+
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class VersionTest extends TestLauncher {
+ public VersionTest(String[] args) {
+ super("Version test", args);
+
+ addTest(new TestCase("String <-> int") {
+ @Override
+ public void test() throws Exception {
+ assertEquals("Cannot parse version 1.2.3 from int to String",
+ "1.2.3", new Version(1, 2, 3).toString());
+ assertEquals(
+ "Cannot parse major version \"1.2.3\" from String to int",
+ 1, new Version("1.2.3").getMajor());
+ assertEquals(
+ "Cannot parse minor version \"1.2.3\" from String to int",
+ 2, new Version("1.2.3").getMinor());
+ assertEquals(
+ "Cannot parse patch version \"1.2.3\" from String to int",
+ 3, new Version("1.2.3").getPatch());
+ }
+ });
+
+ addTest(new TestCase("Bad input") {
+ @Override
+ public void test() throws Exception {
+ assertEquals(
+ "Bad input should return an empty version",
+ true,
+ new Version(
+ "Doors 98 SE Special Deluxe Edition Pro++ Not-Home")
+ .isEmpty());
+
+ assertEquals(
+ "Bad input should return [unknown]",
+ "[unknown]",
+ new Version(
+ "Doors 98 SE Special Deluxe Edition Pro++ Not-Home")
+ .toString());
+ }
+ });
+
+ addTest(new TestCase("Read current version") {
+ @Override
+ public void test() throws Exception {
+ assertNotNull("The version should not be NULL (in any case!)",
+ Version.getCurrentVersion());
+ assertEquals("The current version should not be empty", false,
+ Version.getCurrentVersion().isEmpty());
+ }
+ });
+
+ addTest(new TestCase("Tag version") {
+ @Override
+ public void test() throws Exception {
+ Version version = new Version("1.0.0-debian0");
+ assertEquals("debian", version.getTag());
+ assertEquals(0, version.getTagVersion());
+ version = new Version("1.0.0-debian.0");
+ assertEquals("debian.", version.getTag());
+ assertEquals(0, version.getTagVersion());
+ version = new Version("1.0.0-debian-0");
+ assertEquals("debian-", version.getTag());
+ assertEquals(0, version.getTagVersion());
+ version = new Version("1.0.0-debian-12");
+ assertEquals("debian-", version.getTag());
+ assertEquals(12, version.getTagVersion());
+
+ // tag with no tag version
+ version = new Version("1.0.0-dev");
+ assertEquals(1, version.getMajor());
+ assertEquals(0, version.getMinor());
+ assertEquals(0, version.getPatch());
+ assertEquals("dev", version.getTag());
+ assertEquals(-1, version.getTagVersion());
+ }
+ });
+
+ addTest(new TestCase("Comparing versions") {
+ @Override
+ public void test() throws Exception {
+ assertEquals(true,
+ new Version(1, 1, 1).isNewerThan(new Version(1, 1, 0)));
+ assertEquals(true,
+ new Version(2, 0, 0).isNewerThan(new Version(1, 1, 1)));
+ assertEquals(true,
+ new Version(10, 7, 8).isNewerThan(new Version(9, 9, 9)));
+ assertEquals(true,
+ new Version(0, 0, 0).isOlderThan(new Version(0, 0, 1)));
+ assertEquals(1,
+ new Version(1, 1, 1).compareTo(new Version(0, 1, 1)));
+ assertEquals(-1,
+ new Version(0, 0, 1).compareTo(new Version(0, 1, 1)));
+ assertEquals(0,
+ new Version(0, 0, 1).compareTo(new Version(0, 0, 1)));
+ assertEquals(true,
+ new Version(0, 0, 1).equals(new Version(0, 0, 1)));
+ assertEquals(false,
+ new Version(0, 2, 1).equals(new Version(0, 0, 1)));
+
+ assertEquals(true,
+ new Version(1, 0, 1, "my.tag.", 2).equals(new Version(
+ 1, 0, 1, "my.tag.", 2)));
+ assertEquals(false,
+ new Version(1, 0, 1, "my.tag.", 2).equals(new Version(
+ 1, 0, 0, "my.tag.", 2)));
+ assertEquals(false,
+ new Version(1, 0, 1, "my.tag.", 2).equals(new Version(
+ 1, 0, 1, "not-my.tag.", 2)));
+ }
+ });
+
+ addTest(new TestCase("toString") {
+ @Override
+ public void test() throws Exception {
+ // Check leading 0s:
+ Version version = new Version("01.002.4");
+ assertEquals("Leading 0s not working", "1.2.4",
+ version.toString());
+
+ // Check spacing
+ version = new Version("1 . 2.4 ");
+ assertEquals("Additional spaces not working", "1.2.4",
+ version.toString());
+
+ String[] tests = new String[] { "1.0.0", "1.2.3", "1.0.0-dev",
+ "1.1.2-niki0" };
+ for (String test : tests) {
+ version = new Version(test);
+ assertEquals("toString and back conversion failed", test,
+ version.toString());
+ }
+ }
+ });
+ }
+}
--- /dev/null
+ONE = un
+ONE_SUFFIX = un + suffix
+JAPANESE = 日本語 Nihongo
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.MetaInfo;
+
+/**
+ * A configuration panel for a {@link Bundle}.
+ * <p>
+ * All the items in the given {@link Bundle} will be displayed in editable
+ * controls, with options to Save, Reset and/or Reset to the application default
+ * values.
+ *
+ * @author niki
+ *
+ * @param <E>
+ * the type of {@link Bundle} to edit
+ */
+public class ConfigEditor<E extends Enum<E>> extends JPanel {
+ private static final long serialVersionUID = 1L;
+ private List<MetaInfo<E>> items;
+
+ /**
+ * Create a new {@link ConfigEditor} for this {@link Bundle}.
+ *
+ * @param type
+ * a class instance of the item type to work on
+ * @param bundle
+ * the {@link Bundle} to sort through
+ * @param title
+ * the title to display before the options
+ */
+ public ConfigEditor(Class<E> type, final Bundle<E> bundle, String title) {
+ this.setLayout(new BorderLayout());
+
+ JPanel main = new JPanel();
+ main.setLayout(new BoxLayout(main, BoxLayout.PAGE_AXIS));
+ main.setBorder(new EmptyBorder(5, 5, 5, 5));
+
+ main.add(new JLabel(title));
+
+ items = new ArrayList<MetaInfo<E>>();
+ List<MetaInfo<E>> groupedItems = MetaInfo.getItems(type, bundle);
+ for (MetaInfo<E> item : groupedItems) {
+ // will init this.items
+ addItem(main, item, 0);
+ }
+
+ JPanel buttons = new JPanel();
+ buttons.setLayout(new BoxLayout(buttons, BoxLayout.PAGE_AXIS));
+ buttons.setBorder(new EmptyBorder(5, 5, 5, 5));
+
+ buttons.add(createButton("Reset", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ for (MetaInfo<E> item : items) {
+ item.reload();
+ }
+ }
+ }));
+
+ buttons.add(createButton("Default", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Object snap = bundle.takeSnapshot();
+ bundle.reload(true);
+ for (MetaInfo<E> item : items) {
+ item.reload();
+ }
+ bundle.reload(false);
+ bundle.restoreSnapshot(snap);
+ }
+ }));
+
+ buttons.add(createButton("Save", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ for (MetaInfo<E> item : items) {
+ item.save(true);
+ }
+
+ try {
+ bundle.updateFile();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }));
+
+ JScrollPane scroll = new JScrollPane(main);
+ scroll.getVerticalScrollBar().setUnitIncrement(16);
+
+ this.add(scroll, BorderLayout.CENTER);
+ this.add(buttons, BorderLayout.SOUTH);
+ }
+
+ private void addItem(JPanel main, MetaInfo<E> item, int nhgap) {
+ if (item.isGroup()) {
+ JPanel bpane = new JPanel(new BorderLayout());
+ bpane.setBorder(new TitledBorder(item.getName()));
+ JPanel pane = new JPanel();
+ pane.setBorder(new EmptyBorder(5, 5, 5, 5));
+ pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
+
+ String info = item.getDescription();
+ info = StringUtils.justifyTexts(info, 100);
+ if (!info.isEmpty()) {
+ info = info + "\n";
+ JTextArea text = new JTextArea(info);
+ text.setWrapStyleWord(true);
+ text.setOpaque(false);
+ text.setForeground(new Color(100, 100, 180));
+ text.setEditable(false);
+ pane.add(text);
+ }
+
+ for (MetaInfo<E> subitem : item) {
+ addItem(pane, subitem, nhgap + 11);
+ }
+ bpane.add(pane, BorderLayout.CENTER);
+ main.add(bpane);
+ } else {
+ items.add(item);
+ main.add(ConfigItem.createItem(item, nhgap));
+ }
+ }
+
+ /**
+ * Add an action button for this action.
+ *
+ * @param title
+ * the action title
+ * @param listener
+ * the action
+ */
+ private JComponent createButton(String title, ActionListener listener) {
+ JButton button = new JButton(title);
+ button.addActionListener(listener);
+
+ JPanel panel = new JPanel();
+ panel.setLayout(new BorderLayout());
+ panel.setBorder(new EmptyBorder(2, 10, 2, 10));
+ panel.add(button, BorderLayout.CENTER);
+
+ return panel;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.StringUtils.Alignment;
+import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.MetaInfo;
+
+/**
+ * A graphical item that reflect a configuration option from the given
+ * {@link Bundle}.
+ * <p>
+ * This graphical item can be edited, and the result will be saved back into the
+ * linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should
+ * you wish to, of course.
+ *
+ * @author niki
+ *
+ * @param <E>
+ * the type of {@link Bundle} to edit
+ */
+public abstract class ConfigItem<E extends Enum<E>> extends JPanel {
+ private static final long serialVersionUID = 1L;
+
+ private static int minimumHeight = -1;
+
+ /** A small 16x16 "?" blue in PNG, base64 encoded. */
+ private static String img64info = //
+ ""
+ + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
+ + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wURFRg6IrtcdgAAATdJREFUOMvtkj8sQ1EUxr9z/71G"
+ + "m1RDogYxq7WDDYMYTSajSG4n6YRYzSaSLibWbiaDIGwdiLIYDFKDNJEgKu969xi8UNHy7H7LPcN3"
+ + "v/Odcy+hG9oOIeIcBCJS9MAvlZtOMtHxsrFrJHGqe0RVGnHAHpcIbPlng8BS3HmKBJYzabGUzcrJ"
+ + "XK+ckIrqANYR2JEv2nYDEVck0WKGfHzyq82Go+btxoX3XAcAIqTj8wPqOH6mtMeM4bGCLhyfhTMA"
+ + "qlLhKHqujCfaweCAmV0p50dPzsNpEKpK01V/n55HIvTnfDC2odKlfeYadZN/T+AqDACUsnkhqaU1"
+ + "LRIVuX1x7ciuSWQxVIrunONrfq3dI6oh+T94Z8453vEem/HTqT8ZpFJ0qDXtGkPbAGAMeSRngQCA"
+ + "eUvgn195AwlZWyvjtQdhAAAAAElFTkSuQmCC";
+
+ /** A small 16x16 "+" image with colours */
+ private static String img64add = //
+ ""
+ + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
+ + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeES0QBFvvnAAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
+ + "YXRlZCB3aXRoIEdJTVBkLmUHAAACH0lEQVQ4y42Tz0sVURTHP+fMmC7CQMpH1EjgIimCsEVBEIg/"
+ + "qIbcBAW2Uai1m/oH2rlJXLQpeRJt2gQhTO0iTTKC1I2JBf5gKCJCRPvhPOed22LmvV70Fn7hwr3c"
+ + "+z3ne+73HCFHEClxaASRHgduA91AW369BkwDI3Foy0GkEofmACQnSxyaCyItAkMClMzYdeCAJgVP"
+ + "tJJrPA7tVoUjNZlngXMAiRmXClfoK/Tjq09x7T6LW+8RxOVJ5+LQzgSRojm5WCEDlMrQVbjIQNtN"
+ + "rh0d5FTzaTLBmWKgM4h0Ig4NzWseohYCJUuqx123Sx0MBpF2+MAdyWUnlqX4lf4bIDHjR+rwJJPR"
+ + "qNCgCjDsA10lM/oKIRcO9lByCYklnG/pqQa4euQ6J5tPoKI0yD6ef33Ku40Z80R7CSJNWyZxT+Ki"
+ + "2ytGP911hyZxQaRp1RtPPPYKD4+sGJwPrDUp7Q9Xxnj9fYrUUnaszEAwQHfrZQAerT/g7cYMiuCp"
+ + "z8LmLI0qBqz6wLQn2v5he57FrXkAtlPH2ZZOuskCzG2+4dnnx3iSuSgCKqLAlAIjmXPiVIRsgYjU"
+ + "usrfO0Gq7cA9jUNbBsZrmiQnac1e6n3FeBzakpf39OSBG9IPHAZwzlFoagVg5edHXn57wZed9dpA"
+ + "C3FoYRDpf8M0AQwKwu9yubxjeA7Y72ENqlp3mOqMcwcwDPQCx8gGchV4BYzGoS1V3gL8AVA5C5/0"
+ + "oRFoAAAAAElFTkSuQmCC";
+
+ /** A small 32x32 "-" image with colours */
+ private static String img64remove = //
+ ""
+ + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI"
+ + "WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUeESw5X/JGsQAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl"
+ + "YXRlZCB3aXRoIEdJTVBkLmUHAAACKUlEQVQ4y5WTO2iTYRSG3+//v/+SJrG5SSABh1JQBHFJNUNR"
+ + "YodCLoMoTkK0YKhQtBmsl01wKVZRBwcrgosg3SwFW9Cippe0VmlpB6uYqYIaNSZtbv/lOKRx0iR9"
+ + "4YOzvOc8vOd8wLbG4nYGAKP9tshKr3Pq0zFXORt0UzbopvUeZ2ml1/niUcIWAYBzwwqr+xgAjCSt"
+ + "wpXjWzx105Ha+1XsMgT8U6IJfPAacyfO50OXJi3VwbtbxMbidtZ3tiClbzi/eAuCmxgai4AfNvNn"
+ + "KJn3X5xWKgwA0lHHYud3MdDUXMcmIOMx0oGJXJCN9tuiJ98p4//DbtTk2cFKhB/OSBcMgQHVMkir"
+ + "AqwJBhGYrIIkCQc2eJK3aewI9Crko2FIh0K1Jo0mcwmV6XFUlmfRXhK7eXuRKaRVIYdiUGKnW8Kn"
+ + "0ia0t6/hKHJVqCcLzncQgLhtIvBfbWbZZahq+cl96AuvQLre2Mw59NUlkCwjZ6USL0uYgSj26B/X"
+ + "oK+vtkYgMAhMRF4x5oWlPdod0UQtfUFo7YEBBKz59BEGAAtRx1xHVgzu5AYyHmMmMJHrZolhhU3t"
+ + "05XJe7s2PJuCq9k1MgKyNjOXiBf8kWW5JDy4XKHBl2ql6+pvX8ZjzDOqrcWsFQAAE/T3H3z2GG/6"
+ + "zhT8sfdKeehWkUQAeJ7WcH23xTz1uPBwf1hclA3mBZjPojFOIOSsVPpmN1OznfpA+Gn+2kCHqg/d"
+ + "LhIA/AFU5d0V6gTjtQAAAABJRU5ErkJggg==";
+
+ /** The code base */
+ private final ConfigItemBase<JComponent, E> base;
+
+ /** The main panel with all the fields in it. */
+ private JPanel main;
+
+ /**
+ * Prepare a new {@link ConfigItem} instance, linked to the given
+ * {@link MetaInfo}.
+ *
+ * @param info
+ * the info
+ * @param autoDirtyHandling
+ * TRUE to automatically manage the setDirty/Save operations,
+ * FALSE if you want to do it yourself via
+ * {@link ConfigItem#setDirtyItem(int)}
+ */
+ protected ConfigItem(MetaInfo<E> info, boolean autoDirtyHandling) {
+ base = new ConfigItemBase<JComponent, E>(info, autoDirtyHandling) {
+ @Override
+ protected JComponent createEmptyField(int item) {
+ return ConfigItem.this.createEmptyField(item);
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ return ConfigItem.this.getFromInfo(item);
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ ConfigItem.this.setToInfo(value, item);
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ return ConfigItem.this.getFromField(item);
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ ConfigItem.this.setToField(value, item);
+ }
+
+ @Override
+ public JComponent createField(int item) {
+ JComponent field = super.createField(item);
+
+ int height = Math.max(getMinimumHeight(),
+ field.getMinimumSize().height);
+ field.setPreferredSize(new Dimension(200, height));
+
+ return field;
+ }
+
+ @Override
+ public List<JComponent> reload() {
+ List<JComponent> removed = base.reload();
+ if (!removed.isEmpty()) {
+ for (JComponent c : removed) {
+ main.remove(c);
+ }
+ main.revalidate();
+ main.repaint();
+ }
+
+ return removed;
+ }
+
+ @Override
+ protected JComponent removeItem(int item) {
+ JComponent removed = super.removeItem(item);
+ main.remove(removed);
+ main.revalidate();
+ main.repaint();
+
+ return removed;
+ }
+ };
+ }
+
+ /**
+ * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
+ *
+ * @param nhgap
+ * negative horisontal gap in pixel to use for the label, i.e.,
+ * the step lock sized labels will start smaller by that amount
+ * (the use case would be to align controls that start at a
+ * different horisontal position)
+ */
+ public void init(int nhgap) {
+ if (getInfo().isArray()) {
+ this.setLayout(new BorderLayout());
+ add(label(nhgap), BorderLayout.WEST);
+
+ main = new JPanel();
+
+ main.setLayout(new BoxLayout(main, BoxLayout.Y_AXIS));
+ int size = getInfo().getListSize(false);
+ for (int i = 0; i < size; i++) {
+ addItemWithMinusPanel(i);
+ }
+ main.revalidate();
+ main.repaint();
+
+ final JButton add = new JButton();
+ setImage(add, img64add, "+");
+
+ add.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ addItemWithMinusPanel(base.getFieldsSize());
+ main.revalidate();
+ main.repaint();
+ }
+ });
+
+ JPanel tmp = new JPanel(new BorderLayout());
+ tmp.add(add, BorderLayout.WEST);
+
+ JPanel mainPlus = new JPanel(new BorderLayout());
+ mainPlus.add(main, BorderLayout.CENTER);
+ mainPlus.add(tmp, BorderLayout.SOUTH);
+
+ add(mainPlus, BorderLayout.CENTER);
+ } else {
+ this.setLayout(new BorderLayout());
+ add(label(nhgap), BorderLayout.WEST);
+
+ JComponent field = base.createField(-1);
+ add(field, BorderLayout.CENTER);
+ }
+ }
+
+ /** The {@link MetaInfo} linked to the field. */
+ public MetaInfo<E> getInfo() {
+ return base.getInfo();
+ }
+
+ /**
+ * Retrieve the associated graphical component that was created with
+ * {@link ConfigItemBase#createEmptyField(int)}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the graphical component
+ */
+ protected JComponent getField(int item) {
+ return base.getField(item);
+ }
+
+ /**
+ * Manually specify that the given item is "dirty" and thus should be saved
+ * when asked.
+ * <p>
+ * Has no effect if the class is using automatic dirty handling (see
+ * {@link ConfigItemBase#ConfigItem(MetaInfo, boolean)}).
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ protected void setDirtyItem(int item) {
+ base.setDirtyItem(item);
+ }
+
+ /**
+ * Check if the value changed since the last load/save into the linked
+ * {@link MetaInfo}.
+ * <p>
+ * Note that we consider NULL and an Empty {@link String} to be equals.
+ *
+ * @param value
+ * the value to test
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return TRUE if it has
+ */
+ protected boolean hasValueChanged(Object value, int item) {
+ return base.hasValueChanged(value, item);
+ }
+
+ private void addItemWithMinusPanel(int item) {
+ JPanel minusPanel = createMinusPanel(item);
+ JComponent field = base.addItem(item, minusPanel);
+ minusPanel.add(field, BorderLayout.CENTER);
+ }
+
+ private JPanel createMinusPanel(final int item) {
+ JPanel minusPanel = new JPanel(new BorderLayout());
+
+ final JButton remove = new JButton();
+ setImage(remove, img64remove, "-");
+
+ remove.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ base.removeItem(item);
+ }
+ });
+
+ minusPanel.add(remove, BorderLayout.EAST);
+
+ main.add(minusPanel);
+ main.revalidate();
+ main.repaint();
+
+ return minusPanel;
+ }
+
+ /**
+ * Create an empty graphical component to be used later by
+ * {@link ConfigItem#createField(int)}.
+ * <p>
+ * Note that {@link ConfigItem#reload(int)} will be called after it was
+ * created by {@link ConfigItem#createField(int)}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the graphical component
+ */
+ abstract protected JComponent createEmptyField(int item);
+
+ /**
+ * Get the information from the {@link MetaInfo} in the subclass preferred
+ * format.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the information in the subclass preferred format
+ */
+ abstract protected Object getFromInfo(int item);
+
+ /**
+ * Set the value to the {@link MetaInfo}.
+ *
+ * @param value
+ * the value in the subclass preferred format
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ abstract protected void setToInfo(Object value, int item);
+
+ /**
+ * The value present in the given item's related field in the subclass
+ * preferred format.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the value present in the given item's related field in the
+ * subclass preferred format
+ */
+ abstract protected Object getFromField(int item);
+
+ /**
+ * Set the value (in the subclass preferred format) into the field.
+ *
+ * @param value
+ * the value in the subclass preferred format
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ abstract protected void setToField(Object value, int item);
+
+ /**
+ * Create a label which width is constrained in lock steps.
+ *
+ * @param nhgap
+ * negative horisontal gap in pixel to use for the label, i.e.,
+ * the step lock sized labels will start smaller by that amount
+ * (the use case would be to align controls that start at a
+ * different horisontal position)
+ *
+ * @return the label
+ */
+ protected JComponent label(int nhgap) {
+ final JLabel label = new JLabel(getInfo().getName());
+
+ Dimension ps = label.getPreferredSize();
+ if (ps == null) {
+ ps = label.getSize();
+ }
+
+ ps.height = Math.max(ps.height, getMinimumHeight());
+
+ int w = ps.width;
+ int step = 150;
+ for (int i = 2 * step - nhgap; i < 10 * step; i += step) {
+ if (w < i) {
+ w = i;
+ break;
+ }
+ }
+
+ final Runnable showInfo = new Runnable() {
+ @Override
+ public void run() {
+ StringBuilder builder = new StringBuilder();
+ String text = (getInfo().getDescription().replace("\\n", "\n"))
+ .trim();
+ for (String line : StringUtils.justifyText(text, 80,
+ Alignment.LEFT)) {
+ if (builder.length() > 0) {
+ builder.append("\n");
+ }
+ builder.append(line);
+ }
+ text = builder.toString();
+ JOptionPane.showMessageDialog(ConfigItem.this, text, getInfo()
+ .getName(), JOptionPane.INFORMATION_MESSAGE);
+ }
+ };
+
+ JLabel help = new JLabel("");
+ help.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ setImage(help, img64info, "?");
+
+ help.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ showInfo.run();
+ }
+ });
+
+ JPanel pane2 = new JPanel(new BorderLayout());
+ pane2.add(help, BorderLayout.WEST);
+ pane2.add(new JLabel(" "), BorderLayout.CENTER);
+
+ JPanel contentPane = new JPanel(new BorderLayout());
+ contentPane.add(label, BorderLayout.WEST);
+ contentPane.add(pane2, BorderLayout.CENTER);
+
+ ps.width = w + 30; // 30 for the (?) sign
+ contentPane.setSize(ps);
+ contentPane.setPreferredSize(ps);
+
+ JPanel pane = new JPanel(new BorderLayout());
+ pane.add(contentPane, BorderLayout.NORTH);
+
+ return pane;
+ }
+
+ /**
+ * Create a new {@link ConfigItem} for the given {@link MetaInfo}.
+ *
+ * @param <E>
+ * the type of {@link Bundle} to edit
+ *
+ * @param info
+ * the {@link MetaInfo}
+ * @param nhgap
+ * negative horisontal gap in pixel to use for the label, i.e.,
+ * the step lock sized labels will start smaller by that amount
+ * (the use case would be to align controls that start at a
+ * different horisontal position)
+ *
+ * @return the new {@link ConfigItem}
+ */
+ static public <E extends Enum<E>> ConfigItem<E> createItem(
+ MetaInfo<E> info, int nhgap) {
+
+ ConfigItem<E> configItem;
+ switch (info.getFormat()) {
+ case BOOLEAN:
+ configItem = new ConfigItemBoolean<E>(info);
+ break;
+ case COLOR:
+ configItem = new ConfigItemColor<E>(info);
+ break;
+ case FILE:
+ configItem = new ConfigItemBrowse<E>(info, false);
+ break;
+ case DIRECTORY:
+ configItem = new ConfigItemBrowse<E>(info, true);
+ break;
+ case COMBO_LIST:
+ configItem = new ConfigItemCombobox<E>(info, true);
+ break;
+ case FIXED_LIST:
+ configItem = new ConfigItemCombobox<E>(info, false);
+ break;
+ case INT:
+ configItem = new ConfigItemInteger<E>(info);
+ break;
+ case PASSWORD:
+ configItem = new ConfigItemPassword<E>(info);
+ break;
+ case LOCALE:
+ configItem = new ConfigItemLocale<E>(info);
+ break;
+ case STRING:
+ default:
+ configItem = new ConfigItemString<E>(info);
+ break;
+ }
+
+ configItem.init(nhgap);
+ return configItem;
+ }
+
+ /**
+ * Set an image to the given {@link JButton}, with a fallback text if it
+ * fails.
+ *
+ * @param button
+ * the button to set
+ * @param image64
+ * the image in BASE64 (should be PNG or similar)
+ * @param fallbackText
+ * text to use in case the image cannot be created
+ */
+ static protected void setImage(JLabel button, String image64,
+ String fallbackText) {
+ try {
+ Image img = new Image(image64);
+ try {
+ BufferedImage bImg = ImageUtilsAwt.fromImage(img);
+ button.setIcon(new ImageIcon(bImg));
+ } finally {
+ img.close();
+ }
+ } catch (IOException e) {
+ // This is an hard-coded image, should not happen
+ button.setText(fallbackText);
+ }
+ }
+
+ /**
+ * Set an image to the given {@link JButton}, with a fallback text if it
+ * fails.
+ *
+ * @param button
+ * the button to set
+ * @param image64
+ * the image in BASE64 (should be PNG or similar)
+ * @param fallbackText
+ * text to use in case the image cannot be created
+ */
+ static protected void setImage(JButton button, String image64,
+ String fallbackText) {
+ try {
+ Image img = new Image(image64);
+ try {
+ BufferedImage bImg = ImageUtilsAwt.fromImage(img);
+ button.setIcon(new ImageIcon(bImg));
+ } finally {
+ img.close();
+ }
+ } catch (IOException e) {
+ // This is an hard-coded image, should not happen
+ button.setText(fallbackText);
+ }
+ }
+
+ static private int getMinimumHeight() {
+ if (minimumHeight < 0) {
+ minimumHeight = new JTextField("Test").getMinimumSize().height;
+ }
+
+ return minimumHeight;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.MetaInfo;
+
+/**
+ * A graphical item that reflect a configuration option from the given
+ * {@link Bundle}.
+ * <p>
+ * This graphical item can be edited, and the result will be saved back into the
+ * linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should
+ * you wish to, of course.
+ *
+ * @author niki
+ *
+ * @param <T>
+ * the graphical base type to use (i.e., T or TWidget)
+ * @param <E>
+ * the type of {@link Bundle} to edit
+ */
+public abstract class ConfigItemBase<T, E extends Enum<E>> {
+ /** The original value before current changes. */
+ private Object orig;
+ private List<Object> origs = new ArrayList<Object>();
+ private List<Integer> dirtyBits;
+
+ /** The fields (one for non-array, a list for arrays). */
+ private T field;
+ private List<T> fields = new ArrayList<T>();
+
+ /** The fields to panel map to get the actual item added to 'main'. */
+ private Map<Integer, T> itemFields = new HashMap<Integer, T>();
+
+ /** The {@link MetaInfo} linked to the field. */
+ private MetaInfo<E> info;
+
+ /** The {@link MetaInfo} linked to the field. */
+ public MetaInfo<E> getInfo() {
+ return info;
+ }
+
+ /**
+ * The number of fields, for arrays.
+ *
+ * @return
+ */
+ public int getFieldsSize() {
+ return fields.size();
+ }
+
+ /**
+ * The number of fields to panel map to get the actual item added to 'main'.
+ */
+ public int getItemFieldsSize() {
+ return itemFields.size();
+ }
+
+ /**
+ * Add a new item in an array-value {@link MetaInfo}.
+ *
+ * @param item
+ * the index of the new item
+ * @param panel
+ * a linked T, if we want to link it into the itemFields (can be
+ * NULL) -- that way, we can get it back later on
+ * {@link ConfigItemBase#removeItem(int)}
+ *
+ * @return the newly created graphical field
+ */
+ public T addItem(final int item, T panel) {
+ if (panel != null) {
+ itemFields.put(item, panel);
+ }
+ return createField(item);
+ }
+
+ /**
+ * The counter-part to {@link ConfigItemBase#addItem(int, Object)}, to
+ * remove a specific item of an array-values {@link MetaInfo}; all the
+ * remaining items will be shifted as required (so, always the last
+ * graphical object will be removed).
+ *
+ * @param item
+ * the index of the item to remove
+ *
+ * @return the linked graphical T to remove if any (always the latest
+ * graphical object if any)
+ */
+ protected T removeItem(int item) {
+ int last = itemFields.size() - 1;
+
+ for (int i = item; i <= last; i++) {
+ Object value = null;
+ if (i < last) {
+ value = getFromField(i + 1);
+ }
+ setToField(value, i);
+ setToInfo(value, i);
+ setDirtyItem(i);
+ }
+
+ return itemFields.remove(last);
+ }
+
+ /**
+ * Prepare a new {@link ConfigItemBase} instance, linked to the given
+ * {@link MetaInfo}.
+ *
+ * @param info
+ * the info
+ * @param autoDirtyHandling
+ * TRUE to automatically manage the setDirty/Save operations,
+ * FALSE if you want to do it yourself via
+ * {@link ConfigItemBase#setDirtyItem(int)}
+ */
+ protected ConfigItemBase(MetaInfo<E> info, boolean autoDirtyHandling) {
+ this.info = info;
+ if (!autoDirtyHandling) {
+ dirtyBits = new ArrayList<Integer>();
+ }
+ }
+
+ /**
+ * Create an empty graphical component to be used later by
+ * {@link ConfigItemBase#createField(int)}.
+ * <p>
+ * Note that {@link ConfigItemBase#reload(int)} will be called after it was
+ * created by {@link ConfigItemBase#createField(int)}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the graphical component
+ */
+ abstract protected T createEmptyField(int item);
+
+ /**
+ * Get the information from the {@link MetaInfo} in the subclass preferred
+ * format.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the information in the subclass preferred format
+ */
+ abstract protected Object getFromInfo(int item);
+
+ /**
+ * Set the value to the {@link MetaInfo}.
+ *
+ * @param value
+ * the value in the subclass preferred format
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ abstract protected void setToInfo(Object value, int item);
+
+ /**
+ * The value present in the given item's related field in the subclass
+ * preferred format.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the value present in the given item's related field in the
+ * subclass preferred format
+ */
+ abstract protected Object getFromField(int item);
+
+ /**
+ * Set the value (in the subclass preferred format) into the field.
+ *
+ * @param value
+ * the value in the subclass preferred format
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ abstract protected void setToField(Object value, int item);
+
+ /**
+ * Create a new field for the given graphical component at the given index
+ * (note that the component is usually created by
+ * {@link ConfigItemBase#createEmptyField(int)}).
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param field
+ * the graphical component
+ */
+ private void setField(int item, T field) {
+ if (item < 0) {
+ this.field = field;
+ return;
+ }
+
+ for (int i = fields.size(); i <= item; i++) {
+ fields.add(null);
+ }
+
+ fields.set(item, field);
+ }
+
+ /**
+ * Retrieve the associated graphical component that was created with
+ * {@link ConfigItemBase#createEmptyField(int)}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the graphical component
+ */
+ public T getField(int item) {
+ if (item < 0) {
+ return field;
+ }
+
+ if (item < fields.size()) {
+ return fields.get(item);
+ }
+
+ return null;
+ }
+
+ /**
+ * The original value (before any changes to the {@link MetaInfo}) for this
+ * item.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the original value
+ */
+ private Object getOrig(int item) {
+ if (item < 0) {
+ return orig;
+ }
+
+ if (item < origs.size()) {
+ return origs.get(item);
+ }
+
+ return null;
+ }
+
+ /**
+ * The original value (before any changes to the {@link MetaInfo}) for this
+ * item.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param value
+ * the new original value
+ */
+ private void setOrig(Object value, int item) {
+ if (item < 0) {
+ orig = value;
+ } else {
+ while (item >= origs.size()) {
+ origs.add(null);
+ }
+
+ origs.set(item, value);
+ }
+ }
+
+ /**
+ * Manually specify that the given item is "dirty" and thus should be saved
+ * when asked.
+ * <p>
+ * Has no effect if the class is using automatic dirty handling (see
+ * {@link ConfigItemBase#ConfigItem(MetaInfo, boolean)}).
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setDirtyItem(int item) {
+ if (dirtyBits != null) {
+ dirtyBits.add(item);
+ }
+ }
+
+ /**
+ * Check if the value changed since the last load/save into the linked
+ * {@link MetaInfo}.
+ * <p>
+ * Note that we consider NULL and an Empty {@link String} to be equals.
+ *
+ * @param value
+ * the value to test
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return TRUE if it has
+ */
+ public boolean hasValueChanged(Object value, int item) {
+ // We consider "" and NULL to be equals
+ Object orig = getOrig(item);
+ if (orig == null) {
+ orig = "";
+ }
+ return !orig.equals(value == null ? "" : value);
+ }
+
+ /**
+ * Reload the values to what they currently are in the {@link MetaInfo}.
+ *
+ * @return for arrays, the list of graphical T objects we don't need any
+ * more (never NULL, but can be empty)
+ */
+ public List<T> reload() {
+ List<T> removed = new ArrayList<T>();
+ if (info.isArray()) {
+ while (!itemFields.isEmpty()) {
+ removed.add(itemFields.remove(itemFields.size() - 1));
+ }
+ for (int item = 0; item < info.getListSize(false); item++) {
+ reload(item);
+ }
+ } else {
+ reload(-1);
+ }
+
+ return removed;
+ }
+
+ /**
+ * Reload the values to what they currently are in the {@link MetaInfo}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ private void reload(int item) {
+ if (item >= 0 && !itemFields.containsKey(item)) {
+ addItem(item, null);
+ }
+
+ Object value = getFromInfo(item);
+ setToField(value, item);
+ setOrig(value == null ? "" : value, item);
+ }
+
+ /**
+ * If the item has been modified, set the {@link MetaInfo} to dirty then
+ * modify it to, reflect the changes so it can be saved later.
+ * <p>
+ * This method does <b>not</b> call {@link MetaInfo#save(boolean)}.
+ */
+ private void save() {
+ if (info.isArray()) {
+ boolean dirty = itemFields.size() != info.getListSize(false);
+ for (int item = 0; item < itemFields.size(); item++) {
+ if (getDirtyBit(item)) {
+ dirty = true;
+ }
+ }
+
+ if (dirty) {
+ info.setDirty();
+ info.setString(null, -1);
+
+ for (int item = 0; item < itemFields.size(); item++) {
+ Object value = null;
+ if (getField(item) != null) {
+ value = getFromField(item);
+ if ("".equals(value)) {
+ value = null;
+ }
+ }
+
+ setToInfo(value, item);
+ setOrig(value, item);
+ }
+ }
+ } else {
+ if (getDirtyBit(-1)) {
+ Object value = getFromField(-1);
+
+ info.setDirty();
+ setToInfo(value, -1);
+ setOrig(value, -1);
+ }
+ }
+ }
+
+ /**
+ * Check if the item is dirty, and clear the dirty bit if set.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return TRUE if it was dirty, FALSE if not
+ */
+ private boolean getDirtyBit(int item) {
+ if (dirtyBits != null) {
+ return dirtyBits.remove((Integer) item);
+ }
+
+ Object value = null;
+ if (getField(item) != null) {
+ value = getFromField(item);
+ }
+
+ return hasValueChanged(value, item);
+ }
+
+ /**
+ * Create a new field for the given item.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the newly created field
+ */
+ public T createField(final int item) {
+ T field = createEmptyField(item);
+ setField(item, field);
+ reload(item);
+
+ info.addReloadedListener(new Runnable() {
+ @Override
+ public void run() {
+ reload();
+ }
+ });
+ info.addSaveListener(new Runnable() {
+ @Override
+ public void run() {
+ save();
+ }
+ });
+
+ return field;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemBoolean<E extends Enum<E>> extends ConfigItem<E> {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new {@link ConfigItemBoolean} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ */
+ public ConfigItemBoolean(MetaInfo<E> info) {
+ super(info, true);
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ JCheckBox field = (JCheckBox) getField(item);
+ if (field != null) {
+ return field.isSelected();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ return getInfo().getBoolean(item, true);
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ JCheckBox field = (JCheckBox) getField(item);
+ if (field != null) {
+ // Should not happen if config enum is correct
+ // (but this is not enforced)
+ if (value == null) {
+ value = false;
+ }
+
+ field.setSelected((Boolean) value);
+ }
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ getInfo().setBoolean((Boolean) value, item);
+ }
+
+ @Override
+ protected JComponent createEmptyField(int item) {
+ // Should not happen!
+ if (getFromInfo(item) == null) {
+ System.err
+ .println("No default value given for BOOLEAN parameter \""
+ + getInfo().getName()
+ + "\", we consider it is FALSE");
+ }
+
+ return new JCheckBox();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFileChooser;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemBrowse<E extends Enum<E>> extends ConfigItem<E> {
+ private static final long serialVersionUID = 1L;
+
+ private boolean dir;
+ private Map<JComponent, JTextField> fields = new HashMap<JComponent, JTextField>();
+
+ /**
+ * Create a new {@link ConfigItemBrowse} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ * @param dir
+ * TRUE for directory browsing, FALSE for file browsing
+ */
+ public ConfigItemBrowse(MetaInfo<E> info, boolean dir) {
+ super(info, false);
+ this.dir = dir;
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ JTextField field = fields.get(getField(item));
+ if (field != null) {
+ return new File(field.getText());
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ String path = getInfo().getString(item, false);
+ if (path != null && !path.isEmpty()) {
+ return new File(path);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ JTextField field = fields.get(getField(item));
+ if (field != null) {
+ field.setText(value == null ? "" : ((File) value).getPath());
+ }
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ getInfo().setString(((File) value).getPath(), item);
+ }
+
+ @Override
+ protected JComponent createEmptyField(final int item) {
+ final JPanel pane = new JPanel(new BorderLayout());
+ final JTextField field = new JTextField();
+ field.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyTyped(KeyEvent e) {
+ File file = null;
+ if (!field.getText().isEmpty()) {
+ file = new File(field.getText());
+ }
+
+ if (hasValueChanged(file, item)) {
+ setDirtyItem(item);
+ }
+ }
+ });
+
+ final JButton browseButton = new JButton("...");
+ browseButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ JFileChooser chooser = new JFileChooser();
+ chooser.setCurrentDirectory((File) getFromInfo(item));
+ chooser.setFileSelectionMode(dir ? JFileChooser.DIRECTORIES_ONLY
+ : JFileChooser.FILES_ONLY);
+ if (chooser.showOpenDialog(ConfigItemBrowse.this) == JFileChooser.APPROVE_OPTION) {
+ File file = chooser.getSelectedFile();
+ if (file != null) {
+ setToField(file, item);
+ if (hasValueChanged(file, item)) {
+ setDirtyItem(item);
+ }
+ }
+ }
+ }
+ });
+
+ pane.add(browseButton, BorderLayout.WEST);
+ pane.add(field, BorderLayout.CENTER);
+
+ fields.put(pane, field);
+ return pane;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.image.BufferedImage;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JColorChooser;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemColor<E extends Enum<E>> extends ConfigItem<E> {
+ private static final long serialVersionUID = 1L;
+
+ private Map<JComponent, JTextField> fields = new HashMap<JComponent, JTextField>();
+ private Map<JComponent, JButton> panels = new HashMap<JComponent, JButton>();
+
+ /**
+ * Create a new {@link ConfigItemColor} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ */
+ public ConfigItemColor(MetaInfo<E> info) {
+ super(info, true);
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ JTextField field = fields.get(getField(item));
+ if (field != null) {
+ return field.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ return getInfo().getString(item, true);
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ JTextField field = fields.get(getField(item));
+ if (field != null) {
+ field.setText(value == null ? "" : value.toString());
+ }
+
+ JButton colorWheel = panels.get(getField(item));
+ if (colorWheel != null) {
+ colorWheel.setIcon(getIcon(17, getFromInfoColor(item)));
+ }
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ getInfo().setString((String) value, item);
+ }
+
+ /**
+ * Get the colour currently present in the linked info for the given item.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return a colour
+ */
+ private int getFromInfoColor(int item) {
+ Integer color = getInfo().getColor(item, true);
+ if (color == null) {
+ return new Color(255, 255, 255, 255).getRGB();
+ }
+
+ return color;
+ }
+
+ @Override
+ protected JComponent createEmptyField(final int item) {
+ final JPanel pane = new JPanel(new BorderLayout());
+ final JTextField field = new JTextField();
+
+ final JButton colorWheel = new JButton();
+ colorWheel.setIcon(getIcon(17, getFromInfoColor(item)));
+ colorWheel.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int icol = getFromInfoColor(item);
+ Color initialColor = new Color(icol, true);
+ Color newColor = JColorChooser.showDialog(ConfigItemColor.this,
+ getInfo().getName(), initialColor);
+ if (newColor != null) {
+ getInfo().setColor(newColor.getRGB(), item);
+ field.setText(getInfo().getString(item, false));
+ colorWheel.setIcon(getIcon(17,
+ getInfo().getColor(item, true)));
+ }
+ }
+ });
+
+ field.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyTyped(KeyEvent e) {
+ getInfo().setString(field.getText() + e.getKeyChar(), item);
+ int color = getFromInfoColor(item);
+ colorWheel.setIcon(getIcon(17, color));
+ }
+ });
+
+ pane.add(colorWheel, BorderLayout.WEST);
+ pane.add(field, BorderLayout.CENTER);
+
+ fields.put(pane, field);
+ panels.put(pane, colorWheel);
+ return pane;
+ }
+
+ /**
+ * Return an {@link Icon} to use as a colour badge for the colour field
+ * controls.
+ *
+ * @param size
+ * the size of the badge
+ * @param color
+ * the colour of the badge, which can be NULL (will return
+ * transparent white)
+ *
+ * @return the badge
+ */
+ static private Icon getIcon(int size, Integer color) {
+ // Allow null values
+ if (color == null) {
+ color = new Color(255, 255, 255, 255).getRGB();
+ }
+
+ Color c = new Color(color, true);
+ int avg = (c.getRed() + c.getGreen() + c.getBlue()) / 3;
+ Color border = (avg >= 128 ? Color.BLACK : Color.WHITE);
+
+ BufferedImage img = new BufferedImage(size, size,
+ BufferedImage.TYPE_4BYTE_ABGR);
+
+ Graphics2D g = img.createGraphics();
+ try {
+ g.setColor(c);
+ g.fillRect(0, 0, img.getWidth(), img.getHeight());
+ g.setColor(border);
+ g.drawRect(0, 0, img.getWidth() - 1, img.getHeight() - 1);
+ } finally {
+ g.dispose();
+ }
+
+ return new ImageIcon(img);
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemCombobox<E extends Enum<E>> extends ConfigItem<E> {
+ private static final long serialVersionUID = 1L;
+
+ private boolean editable;
+ private String[] allowedValues;
+
+ /**
+ * Create a new {@link ConfigItemCombobox} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ * @param editable
+ * allows the user to type in another value not in the list
+ */
+ public ConfigItemCombobox(MetaInfo<E> info, boolean editable) {
+ super(info, true);
+ this.editable = editable;
+ this.allowedValues = info.getAllowedValues();
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ // rawtypes for Java 1.6 (and 1.7 ?) support
+ @SuppressWarnings("rawtypes")
+ JComboBox field = (JComboBox) getField(item);
+ if (field != null) {
+ return field.getSelectedItem();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ return getInfo().getString(item, false);
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ // rawtypes for Java 1.6 (and 1.7 ?) support
+ @SuppressWarnings("rawtypes")
+ JComboBox field = (JComboBox) getField(item);
+ if (field != null) {
+ field.setSelectedItem(value);
+ }
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ getInfo().setString((String) value, item);
+ }
+
+ // rawtypes for Java 1.6 (and 1.7 ?) support
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ protected JComponent createEmptyField(int item) {
+ JComboBox field = new JComboBox(allowedValues);
+ field.setEditable(editable);
+ return field;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import javax.swing.JComponent;
+import javax.swing.JSpinner;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemInteger<E extends Enum<E>> extends ConfigItem<E> {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new {@link ConfigItemInteger} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ */
+ public ConfigItemInteger(MetaInfo<E> info) {
+ super(info, true);
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ JSpinner field = (JSpinner) getField(item);
+ if (field != null) {
+ return field.getValue();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ return getInfo().getInteger(item, true);
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ JSpinner field = (JSpinner) getField(item);
+ if (field != null) {
+ field.setValue(value == null ? 0 : (Integer) value);
+ }
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ getInfo().setInteger((Integer) value, item);
+ }
+
+ @Override
+ protected JComponent createEmptyField(int item) {
+ return new JSpinner();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.Component;
+import java.util.Locale;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JList;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemLocale<E extends Enum<E>> extends ConfigItemCombobox<E> {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new {@link ConfigItemLocale} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ */
+ public ConfigItemLocale(MetaInfo<E> info) {
+ super(info, true);
+ }
+
+ // rawtypes for Java 1.6 (and 1.7 ?) support
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ protected JComponent createEmptyField(int item) {
+ JComboBox field = (JComboBox) super.createEmptyField(item);
+ field.setRenderer(new DefaultListCellRenderer() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Component getListCellRendererComponent(JList list,
+ Object value, int index, boolean isSelected,
+ boolean cellHasFocus) {
+
+ String svalue = value == null ? "" : value.toString();
+ String[] tab = svalue.split("-");
+ Locale locale = null;
+ if (tab.length == 1) {
+ locale = new Locale(tab[0]);
+ } else if (tab.length == 2) {
+ locale = new Locale(tab[0], tab[1]);
+ } else if (tab.length == 3) {
+ locale = new Locale(tab[0], tab[1], tab[2]);
+ }
+
+ String displayValue = svalue;
+ if (locale != null) {
+ displayValue = locale.getDisplayName();
+ }
+
+ return super.getListCellRendererComponent(list, displayValue,
+ index, isSelected, cellHasFocus);
+ }
+ });
+
+ return field;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemPassword<E extends Enum<E>> extends ConfigItem<E> {
+ private static final long serialVersionUID = 1L;
+ /** A small 16x16 pass-protecet icon in PNG, base64 encoded. */
+ private static String img64passProtected = //
+ ""
+ + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAnNCSVQICFXsRgQAAAD5SURBVCjP"
+ + "ndG9LoNxGIbxHxJTG9U0IsJAdCSNqZEa9BR87BaHYfW5ESYkmjQh4giwIU00MWFwAPWRSmpgaf6G"
+ + "6ts36eZ+xuu+lvuhlTGjOFHAsXldWVDRa82WhE9pZFxrtmBeUY87+yqCH3UzMh4E1VYhp2ZVVfi7"
+ + "C0PuBc9G2v6KoOlIQUoyhovyLb+uZla/TbsRHnOgJkfSi4YpbDiXjuwJDS+SlASLYC9mw5KgxJlg"
+ + "CWJ4OyqckvKkIWswwmXrmPbl0QBkHcbsHRv6Fbz6MNnesWMnpMw51vRmphuXo7FujHf+cCt4NGza"
+ + "lbp3l5b1xR/1rWrYf/MLWpplWwswQpMAAAAASUVORK5CYII=";
+
+ /** A small 16x16 pass-unprotecet icon in PNG, base64 encoded. */
+ private static String img64passUnprotected = //
+ ""
+ + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAA"
+ + "CxMAAAsTAQCanBgAAAAHdElNRQfjBR8MIilwhCwdAAABK0lEQVQoz5XQv0uUAQCH8c/7qod4nect"
+ + "gop3BIKDFBIiRyiKtATmcEiBDW7+Ae5ODt5gW0SLigouKTg6SJvkjw4Co8mcNeWgc+o839dBBXPz"
+ + "+Y7PM33r3NCpWcWKM1lfHapJq0B4G/TbEDoyZlyHQxuGtdw6eSMC33yyJxa79MW+wIj8TdDrxJSS"
+ + "+N5KppQNEchrkrMosmzRT0/0eGdSaFrob6DXloSqgu9mNWlUNqPPpmYNJkg5UvEMResystYVpbwW"
+ + "qWpjVWwcfNQqLS1rAXwQOw4N4SWoqZeUVFMGuzgg65/IqIw5a3LarZnDcxd+ScMrkcikhB8+m1eU"
+ + "MODUua67q967EttR0KHFoCVX/nhxp1N4o/rfUTueekC332KRM9veqnuoAwQyHs81DiddylUvrecA"
+ + "AAAASUVORK5CYII=";
+
+ private Map<JComponent, JPasswordField> fields = new HashMap<JComponent, JPasswordField>();
+
+ /**
+ * Create a new {@link ConfigItemPassword} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ */
+ public ConfigItemPassword(MetaInfo<E> info) {
+ super(info, true);
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ JPasswordField field = fields.get(getField(item));
+ if (field != null) {
+ return new String(field.getPassword());
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ return getInfo().getString(item, false);
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ JPasswordField field = fields.get(getField(item));
+ if (field != null) {
+ field.setText(value == null ? "" : value.toString());
+ }
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ getInfo().setString((String) value, item);
+ }
+
+ @Override
+ protected JComponent createEmptyField(int item) {
+ JPanel pane = new JPanel(new BorderLayout());
+ final JPasswordField field = new JPasswordField();
+ field.setEchoChar('*');
+
+ final JButton show = new JButton();
+ final Boolean[] visible = new Boolean[] { false };
+ setImage(show, img64passProtected, "/");
+ show.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ visible[0] = !visible[0];
+ if (visible[0]) {
+ field.setEchoChar((char) 0);
+ setImage(show, img64passUnprotected, "o");
+ } else {
+ field.setEchoChar('*');
+ setImage(show, img64passProtected, "/");
+ }
+ }
+ });
+
+ pane.add(field, BorderLayout.CENTER);
+ pane.add(show, BorderLayout.EAST);
+
+ fields.put(pane, field);
+ return pane;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import javax.swing.JComponent;
+import javax.swing.JTextField;
+
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemString<E extends Enum<E>> extends ConfigItem<E> {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new {@link ConfigItemString} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ */
+ public ConfigItemString(MetaInfo<E> info) {
+ super(info, true);
+ }
+
+ @Override
+ protected Object getFromField(int item) {
+ JTextField field = (JTextField) getField(item);
+ if (field != null) {
+ return field.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Object getFromInfo(int item) {
+ return getInfo().getString(item, false);
+ }
+
+ @Override
+ protected void setToField(Object value, int item) {
+ JTextField field = (JTextField) getField(item);
+ if (field != null) {
+ field.setText(value == null ? "" : value.toString());
+ }
+ }
+
+ @Override
+ protected void setToInfo(Object value, int item) {
+ getInfo().setString((String) value, item);
+ }
+
+ @Override
+ protected JComponent createEmptyField(int item) {
+ return new JTextField();
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.awt.image.ImageObserver;
+
+/**
+ * This class converts an {@link Image} into a textual representation that can
+ * be displayed to the user in a TUI.
+ *
+ * @author niki
+ */
+public class ImageTextAwt {
+ private Image image;
+ private Dimension size;
+ private String text;
+ private boolean ready;
+ private Mode mode;
+ private boolean invert;
+
+ /**
+ * The rendering modes supported by this {@link ImageTextAwt} to convert
+ * {@link Image}s into text.
+ *
+ * @author niki
+ *
+ */
+ public enum Mode {
+ /**
+ * Use 5 different "colours" which are actually Unicode
+ * {@link Character}s representing
+ * <ul>
+ * <li>space (blank)</li>
+ * <li>low shade (░)</li>
+ * <li>medium shade (▒)</li>
+ * <li>high shade (▓)</li>
+ * <li>full block (█)</li>
+ * </ul>
+ */
+ DITHERING,
+ /**
+ * Use "block" Unicode {@link Character}s up to quarter blocks, thus in
+ * effect doubling the resolution both in vertical and horizontal space.
+ * Note that since 2 {@link Character}s next to each other are square,
+ * we will use 4 blocks per 2 blocks for w/h resolution.
+ */
+ DOUBLE_RESOLUTION,
+ /**
+ * Use {@link Character}s from both {@link Mode#DOUBLE_RESOLUTION} and
+ * {@link Mode#DITHERING}.
+ */
+ DOUBLE_DITHERING,
+ /**
+ * Only use ASCII {@link Character}s.
+ */
+ ASCII,
+ }
+
+ /**
+ * Create a new {@link ImageTextAwt} with the given parameters. Defaults to
+ * {@link Mode#DOUBLE_DITHERING} and no colour inversion.
+ *
+ * @param image
+ * the source {@link Image}
+ * @param size
+ * the final text size to target
+ */
+ public ImageTextAwt(Image image, Dimension size) {
+ this(image, size, Mode.DOUBLE_DITHERING, false);
+ }
+
+ /**
+ * Create a new {@link ImageTextAwt} with the given parameters.
+ *
+ * @param image
+ * the source {@link Image}
+ * @param size
+ * the final text size to target
+ * @param mode
+ * the mode of conversion
+ * @param invert
+ * TRUE to invert colours rendering
+ */
+ public ImageTextAwt(Image image, Dimension size, Mode mode, boolean invert) {
+ setImage(image);
+ setSize(size);
+ setMode(mode);
+ setColorInvert(invert);
+ }
+
+ /**
+ * Change the source {@link Image}.
+ *
+ * @param image
+ * the new {@link Image}
+ */
+ public void setImage(Image image) {
+ this.text = null;
+ this.ready = false;
+ this.image = image;
+ }
+
+ /**
+ * Change the target size of this {@link ImageTextAwt}.
+ *
+ * @param size
+ * the new size
+ */
+ public void setSize(Dimension size) {
+ this.text = null;
+ this.ready = false;
+ this.size = size;
+ }
+
+ /**
+ * Change the image-to-text mode.
+ *
+ * @param mode
+ * the new {@link Mode}
+ */
+ public void setMode(Mode mode) {
+ this.mode = mode;
+ this.text = null;
+ this.ready = false;
+ }
+
+ /**
+ * Set the colour-invert mode.
+ *
+ * @param invert
+ * TRUE to inverse the colours
+ */
+ public void setColorInvert(boolean invert) {
+ this.invert = invert;
+ this.text = null;
+ this.ready = false;
+ }
+
+ /**
+ * Check if the colours are inverted.
+ *
+ * @return TRUE if the colours are inverted
+ */
+ public boolean isColorInvert() {
+ return invert;
+ }
+
+ /**
+ * Return the textual representation of the included {@link Image}.
+ *
+ * @return the {@link String} representation
+ */
+ public String getText() {
+ if (text == null) {
+ if (image == null || size == null || size.width == 0
+ || size.height == 0) {
+ return "";
+ }
+
+ int mult = 1;
+ if (mode == Mode.DOUBLE_RESOLUTION || mode == Mode.DOUBLE_DITHERING) {
+ mult = 2;
+ }
+
+ Dimension srcSize = getSize(image);
+ srcSize = new Dimension(srcSize.width * 2, srcSize.height);
+ int x = 0;
+ int y = 0;
+
+ int w = size.width * mult;
+ int h = size.height * mult;
+
+ // Default = original ratio or original size if none
+ if (w < 0 || h < 0) {
+ if (w < 0 && h < 0) {
+ w = srcSize.width * mult;
+ h = srcSize.height * mult;
+ } else {
+ double ratioSrc = (double) srcSize.width
+ / (double) srcSize.height;
+ if (w < 0) {
+ w = (int) Math.round(h * ratioSrc);
+ } else {
+ h = (int) Math.round(w / ratioSrc);
+ }
+ }
+ }
+
+ // Fail safe: we consider this to be too much
+ if (w > 1000 || h > 1000) {
+ return "[IMAGE TOO BIG]";
+ }
+
+ BufferedImage buff = new BufferedImage(w, h,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics gfx = buff.getGraphics();
+
+ double ratioAsked = (double) (w) / (double) (h);
+ double ratioSrc = (double) srcSize.height / (double) srcSize.width;
+ double ratio = ratioAsked * ratioSrc;
+ if (srcSize.width < srcSize.height) {
+ h = (int) Math.round(ratio * h);
+ y = (buff.getHeight() - h) / 2;
+ } else {
+ w = (int) Math.round(w / ratio);
+ x = (buff.getWidth() - w) / 2;
+ }
+
+ if (gfx.drawImage(image, x, y, w, h, new ImageObserver() {
+ @Override
+ public boolean imageUpdate(Image img, int infoflags, int x,
+ int y, int width, int height) {
+ ImageTextAwt.this.ready = true;
+ return true;
+ }
+ })) {
+ ready = true;
+ }
+
+ while (!ready) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ gfx.dispose();
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int row = 0; row + (mult - 1) < buff.getHeight(); row += mult) {
+ if (row > 0) {
+ builder.append('\n');
+ }
+
+ for (int col = 0; col + (mult - 1) < buff.getWidth(); col += mult) {
+ if (mult == 1) {
+ char car = ' ';
+ float brightness = getBrightness(buff.getRGB(col, row));
+ if (mode == Mode.DITHERING)
+ car = getDitheringChar(brightness, " ░▒▓█");
+ if (mode == Mode.ASCII)
+ car = getDitheringChar(brightness, " .-+=o8#");
+
+ builder.append(car);
+ } else if (mult == 2) {
+ builder.append(getBlockChar( //
+ buff.getRGB(col, row),//
+ buff.getRGB(col + 1, row),//
+ buff.getRGB(col, row + 1),//
+ buff.getRGB(col + 1, row + 1),//
+ mode == Mode.DOUBLE_DITHERING//
+ ));
+ }
+ }
+ }
+
+ text = builder.toString();
+ }
+
+ return text;
+ }
+
+ @Override
+ public String toString() {
+ return getText();
+ }
+
+ /**
+ * Return the size of the given {@link Image}.
+ *
+ * @param img
+ * the image to measure
+ *
+ * @return the size
+ */
+ static private Dimension getSize(Image img) {
+ Dimension size = null;
+ while (size == null) {
+ int w = img.getWidth(null);
+ int h = img.getHeight(null);
+ if (w > -1 && h > -1) {
+ size = new Dimension(w, h);
+ } else {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ return size;
+ }
+
+ /**
+ * Return the {@link Character} corresponding to the given brightness level
+ * from the evenly-separated given {@link Character}s.
+ *
+ * @param brightness
+ * the brightness level
+ * @param cars
+ * the {@link Character}s to choose from, from less bright to
+ * most bright; <b>MUST</b> contain at least one
+ * {@link Character}
+ *
+ * @return the {@link Character} to use
+ */
+ private char getDitheringChar(float brightness, String cars) {
+ int index = Math.round(brightness * (cars.length() - 1));
+ return cars.charAt(index);
+ }
+
+ /**
+ * Return the {@link Character} corresponding to the 4 given colours in
+ * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode.
+ *
+ * @param upperleft
+ * the upper left colour
+ * @param upperright
+ * the upper right colour
+ * @param lowerleft
+ * the lower left colour
+ * @param lowerright
+ * the lower right colour
+ * @param dithering
+ * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for
+ * {@link Mode#DOUBLE_RESOLUTION}
+ *
+ * @return the {@link Character} to use
+ */
+ private char getBlockChar(int upperleft, int upperright, int lowerleft,
+ int lowerright, boolean dithering) {
+ int choice = 0;
+
+ if (getBrightness(upperleft) > 0.5f) {
+ choice += 1;
+ }
+ if (getBrightness(upperright) > 0.5f) {
+ choice += 2;
+ }
+ if (getBrightness(lowerleft) > 0.5f) {
+ choice += 4;
+ }
+ if (getBrightness(lowerright) > 0.5f) {
+ choice += 8;
+ }
+
+ switch (choice) {
+ case 0:
+ return ' ';
+ case 1:
+ return '▘';
+ case 2:
+ return '▝';
+ case 3:
+ return '▀';
+ case 4:
+ return '▖';
+ case 5:
+ return '▌';
+ case 6:
+ return '▞';
+ case 7:
+ return '▛';
+ case 8:
+ return '▗';
+ case 9:
+ return '▚';
+ case 10:
+ return '▐';
+ case 11:
+ return '▜';
+ case 12:
+ return '▄';
+ case 13:
+ return '▙';
+ case 14:
+ return '▟';
+ case 15:
+ if (dithering) {
+ float avg = 0;
+ avg += getBrightness(upperleft);
+ avg += getBrightness(upperright);
+ avg += getBrightness(lowerleft);
+ avg += getBrightness(lowerright);
+ avg /= 4;
+
+ // Since all the quarters are > 0.5, avg is between 0.5 and 1.0
+ // So, expand the range of the value
+ avg = (avg - 0.5f) * 2;
+
+ // Do not use the " " char, as it would make a
+ // "all quarters > 0.5" pixel go black
+ return getDitheringChar(avg, "░▒▓█");
+ }
+
+ return '█';
+ }
+
+ return ' ';
+ }
+
+ /**
+ * Temporary array used so not to create a lot of new ones.
+ */
+ private float[] tmp = new float[4];
+
+ /**
+ * Return the brightness value to use from the given ARGB colour.
+ *
+ * @param argb
+ * the argb colour
+ *
+ * @return the brightness to sue for computations
+ */
+ private float getBrightness(int argb) {
+ if (invert) {
+ return 1 - rgb2hsb(argb, tmp)[2];
+ }
+
+ return rgb2hsb(argb, tmp)[2];
+ }
+
+ /**
+ * Convert the given ARGB colour in HSL/HSB, either into the supplied array
+ * or into a new one if array is NULL.
+ *
+ * <p>
+ * ARGB pixels are given in 0xAARRGGBB format, while the returned array will
+ * contain Hue, Saturation, Lightness/Brightness, Alpha, in this order. H,
+ * S, L and A are all ranging from 0 to 1 (indeed, H is in 1/360th).
+ * </p>
+ * pixel
+ *
+ * @param argb
+ * the ARGB colour pixel to convert
+ * @param array
+ * the array to convert into or NULL to create a new one
+ *
+ * @return the array containing the HSL/HSB converted colour
+ */
+ static float[] rgb2hsb(int argb, float[] array) {
+ int a, r, g, b;
+ a = ((argb & 0xff000000) >> 24);
+ r = ((argb & 0x00ff0000) >> 16);
+ g = ((argb & 0x0000ff00) >> 8);
+ b = ((argb & 0x000000ff));
+
+ if (array == null) {
+ array = new float[4];
+ }
+
+ Color.RGBtoHSB(r, g, b, array);
+
+ array[3] = a;
+
+ return array;
+
+ // // other implementation:
+ //
+ // float a, r, g, b;
+ // a = ((argb & 0xff000000) >> 24) / 255.0f;
+ // r = ((argb & 0x00ff0000) >> 16) / 255.0f;
+ // g = ((argb & 0x0000ff00) >> 8) / 255.0f;
+ // b = ((argb & 0x000000ff)) / 255.0f;
+ //
+ // float rgbMin, rgbMax;
+ // rgbMin = Math.min(r, Math.min(g, b));
+ // rgbMax = Math.max(r, Math.max(g, b));
+ //
+ // float l;
+ // l = (rgbMin + rgbMax) / 2;
+ //
+ // float s;
+ // if (rgbMin == rgbMax) {
+ // s = 0;
+ // } else {
+ // if (l <= 0.5) {
+ // s = (rgbMax - rgbMin) / (rgbMax + rgbMin);
+ // } else {
+ // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin);
+ // }
+ // }
+ //
+ // float h;
+ // if (r > g && r > b) {
+ // h = (g - b) / (rgbMax - rgbMin);
+ // } else if (g > b) {
+ // h = 2.0f + (b - r) / (rgbMax - rgbMin);
+ // } else {
+ // h = 4.0f + (r - g) / (rgbMax - rgbMin);
+ // }
+ // h /= 6; // from 0 to 1
+ //
+ // return new float[] { h, s, l, a };
+ //
+ // // // natural mode:
+ // //
+ // // int aa = (int) Math.round(100 * a);
+ // // int hh = (int) (360 * h);
+ // // if (hh < 0)
+ // // hh += 360;
+ // // int ss = (int) Math.round(100 * s);
+ // // int ll = (int) Math.round(100 * l);
+ // //
+ // // return new int[] { hh, ss, ll, aa };
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.imageio.ImageIO;
+
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.ImageUtils;
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * This class offer some utilities based around images and uses java.awt.
+ *
+ * @author niki
+ */
+public class ImageUtilsAwt extends ImageUtils {
+ @Override
+ protected boolean check() {
+ // Will not work if ImageIO is not available
+ ImageIO.getCacheDirectory();
+ return true;
+ }
+
+ @Override
+ public void saveAsImage(Image img, File target, String format)
+ throws IOException {
+ try {
+ BufferedImage image = fromImage(img);
+
+ boolean ok = false;
+ try {
+
+ ok = ImageIO.write(image, format, target);
+ } catch (IOException e) {
+ ok = false;
+ }
+
+ // Some formats are not reliable
+ // Second chance: PNG
+ if (!ok && !format.equals("png")) {
+ try {
+ ok = ImageIO.write(image, "png", target);
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IOException("Undocumented exception occured, "
+ + "converting to IOException", e);
+ }
+ }
+
+ if (!ok) {
+ throw new IOException(
+ "Cannot find a writer for this image and format: "
+ + format);
+ }
+ } catch (IOException e) {
+ throw new IOException("Cannot write image to " + target, e);
+ }
+ }
+
+ /**
+ * Convert the given {@link Image} into a {@link BufferedImage} object,
+ * respecting the EXIF transformations if any.
+ *
+ * @param img
+ * the {@link Image}
+ *
+ * @return the {@link Image} object
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ public static BufferedImage fromImage(Image img) throws IOException {
+ InputStream in = img.newInputStream();
+ BufferedImage image;
+ try {
+ int orientation;
+ try {
+ orientation = getExifTransorm(in);
+ } catch (Exception e) {
+ // no EXIF transform, ok
+ orientation = -1;
+ }
+
+ in.reset();
+
+ try {
+ image = ImageIO.read(in);
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IOException("Undocumented exception occured, "
+ + "converting to IOException", e);
+ }
+
+ if (image == null) {
+ String extra = "";
+ if (img.getSize() <= 2048) {
+ try {
+ extra = ", content: "
+ + new String(img.getData(), "UTF-8");
+ } catch (Exception e) {
+ extra = ", content unavailable";
+ }
+ }
+ String ssize = StringUtils.formatNumber(img.getSize());
+ throw new IOException(
+ "Failed to convert input to image, size was: " + ssize
+ + extra);
+ }
+
+ // Note: this code has been found on Internet;
+ // thank you anonymous coder.
+ int width = image.getWidth();
+ int height = image.getHeight();
+ AffineTransform affineTransform = new AffineTransform();
+
+ switch (orientation) {
+ case 1:
+ affineTransform = null;
+ break;
+ case 2: // Flip X
+ affineTransform.scale(-1.0, 1.0);
+ affineTransform.translate(-width, 0);
+ break;
+ case 3: // PI rotation
+ affineTransform.translate(width, height);
+ affineTransform.rotate(Math.PI);
+ break;
+ case 4: // Flip Y
+ affineTransform.scale(1.0, -1.0);
+ affineTransform.translate(0, -height);
+ break;
+ case 5: // - PI/2 and Flip X
+ affineTransform.rotate(-Math.PI / 2);
+ affineTransform.scale(-1.0, 1.0);
+ break;
+ case 6: // -PI/2 and -width
+ affineTransform.translate(height, 0);
+ affineTransform.rotate(Math.PI / 2);
+ break;
+ case 7: // PI/2 and Flip
+ affineTransform.scale(-1.0, 1.0);
+ affineTransform.translate(-height, 0);
+ affineTransform.translate(0, width);
+ affineTransform.rotate(3 * Math.PI / 2);
+ break;
+ case 8: // PI / 2
+ affineTransform.translate(0, width);
+ affineTransform.rotate(3 * Math.PI / 2);
+ break;
+ default:
+ affineTransform = null;
+ break;
+ }
+
+ if (affineTransform != null) {
+ AffineTransformOp affineTransformOp = new AffineTransformOp(
+ affineTransform, AffineTransformOp.TYPE_BILINEAR);
+
+ BufferedImage transformedImage = new BufferedImage(width,
+ height, image.getType());
+ transformedImage = affineTransformOp.filter(image,
+ transformedImage);
+
+ image = transformedImage;
+ }
+ //
+ } finally {
+ in.close();
+ }
+
+ return image;
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.SwingUtilities;
+
+import be.nikiroo.utils.Progress;
+
+/**
+ * A graphical control to show the progress of a {@link Progress}.
+ * <p>
+ * This control is <b>NOT</b> thread-safe.
+ *
+ * @author niki
+ */
+public class ProgressBar extends JPanel {
+ private static final long serialVersionUID = 1L;
+
+ private Map<Progress, JProgressBar> bars;
+ private List<ActionListener> actionListeners;
+ private List<ActionListener> updateListeners;
+ private Progress pg;
+ private Object lock = new Object();
+
+ public ProgressBar() {
+ bars = new HashMap<Progress, JProgressBar>();
+ actionListeners = new ArrayList<ActionListener>();
+ updateListeners = new ArrayList<ActionListener>();
+ }
+
+ public void setProgress(final Progress pg) {
+ this.pg = pg;
+
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ if (pg != null) {
+ final JProgressBar bar = new JProgressBar();
+ bar.setStringPainted(true);
+
+ bars.clear();
+ bars.put(pg, bar);
+
+ bar.setMinimum(pg.getMin());
+ bar.setMaximum(pg.getMax());
+ bar.setValue(pg.getProgress());
+ bar.setString(pg.getName());
+
+ pg.addProgressListener(new Progress.ProgressListener() {
+ @Override
+ public void progress(Progress progress, String name) {
+ final Progress.ProgressListener l = this;
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ Map<Progress, JProgressBar> newBars = new HashMap<Progress, JProgressBar>();
+ newBars.put(pg, bar);
+
+ bar.setMinimum(pg.getMin());
+ bar.setMaximum(pg.getMax());
+ bar.setValue(pg.getProgress());
+ bar.setString(pg.getName());
+
+ synchronized (lock) {
+ for (Progress pgChild : getChildrenAsOrderedList(pg)) {
+ JProgressBar barChild = bars
+ .get(pgChild);
+ if (barChild == null) {
+ barChild = new JProgressBar();
+ barChild.setStringPainted(true);
+ }
+
+ newBars.put(pgChild, barChild);
+
+ barChild.setMinimum(pgChild.getMin());
+ barChild.setMaximum(pgChild.getMax());
+ barChild.setValue(pgChild.getProgress());
+ barChild.setString(pgChild.getName());
+ }
+
+ if (ProgressBar.this.pg == null) {
+ bars.clear();
+ } else {
+ bars = newBars;
+ }
+ }
+
+ if (ProgressBar.this.pg != null) {
+ if (pg.isDone()) {
+ pg.removeProgressListener(l);
+ for (ActionListener listener : actionListeners) {
+ listener.actionPerformed(new ActionEvent(
+ ProgressBar.this, 0,
+ "done"));
+ }
+ }
+
+ update();
+ }
+ }
+ });
+ }
+ });
+ }
+
+ update();
+ }
+ });
+ }
+
+ public void addActionListener(ActionListener l) {
+ actionListeners.add(l);
+ }
+
+ public void clearActionListeners() {
+ actionListeners.clear();
+ }
+
+ public void addUpdateListener(ActionListener l) {
+ updateListeners.add(l);
+ }
+
+ public void clearUpdateListeners() {
+ updateListeners.clear();
+ }
+
+ public int getProgress() {
+ if (pg == null) {
+ return 0;
+ }
+
+ return pg.getProgress();
+ }
+
+ // only named ones
+ private List<Progress> getChildrenAsOrderedList(Progress pg) {
+ List<Progress> children = new ArrayList<Progress>();
+
+ synchronized (lock) {
+ for (Progress child : pg.getChildren()) {
+ if (child.getName() != null && !child.getName().isEmpty()) {
+ children.add(child);
+ }
+ children.addAll(getChildrenAsOrderedList(child));
+ }
+ }
+
+ return children;
+ }
+
+ private void update() {
+ synchronized (lock) {
+ invalidate();
+ removeAll();
+
+ if (pg != null) {
+ setLayout(new GridLayout(bars.size(), 1));
+ add(bars.get(pg), 0);
+ for (Progress child : getChildrenAsOrderedList(pg)) {
+ JProgressBar jbar = bars.get(child);
+ if (jbar != null) {
+ add(jbar);
+ }
+ }
+ }
+
+ validate();
+ repaint();
+ }
+
+ for (ActionListener listener : updateListeners) {
+ listener.actionPerformed(new ActionEvent(this, 0, "update"));
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.Color;
+import java.awt.GradientPaint;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.RadialGradientPaint;
+import java.awt.RenderingHints;
+
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
+
+/**
+ * Some Java Swing utilities.
+ *
+ * @author niki
+ */
+public class UIUtils {
+ /**
+ * Set a fake "native look & feel" for the application if possible
+ * (check for the one currently in use, then try GTK).
+ * <p>
+ * <b>Must</b> be called prior to any GUI work.
+ */
+ static public void setLookAndFeel() {
+ // native look & feel
+ try {
+ String noLF = "javax.swing.plaf.metal.MetalLookAndFeel";
+ String lf = UIManager.getSystemLookAndFeelClassName();
+ if (lf.equals(noLF))
+ lf = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
+ UIManager.setLookAndFeel(lf);
+ } catch (InstantiationException e) {
+ } catch (ClassNotFoundException e) {
+ } catch (UnsupportedLookAndFeelException e) {
+ } catch (IllegalAccessException e) {
+ }
+ }
+
+ /**
+ * Draw a 3D-looking ellipse at the given location, if the given
+ * {@link Graphics} object is compatible (with {@link Graphics2D}); draw a
+ * simple ellipse if not.
+ *
+ * @param g
+ * the {@link Graphics} to draw on
+ * @param color
+ * the base colour
+ * @param x
+ * the X coordinate
+ * @param y
+ * the Y coordinate
+ * @param width
+ * the width radius
+ * @param height
+ * the height radius
+ */
+ static public void drawEllipse3D(Graphics g, Color color, int x, int y, int width, int height) {
+ drawEllipse3D(g, color, x, y, width, height, true);
+ }
+
+ /**
+ * Draw a 3D-looking ellipse at the given location, if the given
+ * {@link Graphics} object is compatible (with {@link Graphics2D}); draw a
+ * simple ellipse if not.
+ *
+ * @param g
+ * the {@link Graphics} to draw on
+ * @param color
+ * the base colour
+ * @param x
+ * the X coordinate
+ * @param y
+ * the Y coordinate
+ * @param width
+ * the width radius
+ * @param height
+ * the height radius
+ * @param fill
+ * fill the content of the ellipse
+ */
+ static public void drawEllipse3D(Graphics g, Color color, int x, int y,
+ int width, int height, boolean fill) {
+ if (g instanceof Graphics2D) {
+ Graphics2D g2 = (Graphics2D) g;
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+
+ // Retains the previous state
+ Paint oldPaint = g2.getPaint();
+
+ // Base shape
+ g2.setColor(color);
+ if (fill) {
+ g2.fillOval(x, y, width, height);
+ } else {
+ g2.drawOval(x, y, width, height);
+ }
+
+ // Compute dark/bright colours
+ Paint p = null;
+ Color dark = color.darker().darker();
+ Color bright = color.brighter().brighter();
+ Color darkEnd = new Color(dark.getRed(), dark.getGreen(),
+ dark.getBlue(), 0);
+ Color darkPartial = new Color(dark.getRed(), dark.getGreen(),
+ dark.getBlue(), 64);
+ Color brightEnd = new Color(bright.getRed(), bright.getGreen(),
+ bright.getBlue(), 0);
+
+ // Adds shadows at the bottom left
+ p = new GradientPaint(0, height, dark, width, 0, darkEnd);
+ g2.setPaint(p);
+ if (fill) {
+ g2.fillOval(x, y, width, height);
+ } else {
+ g2.drawOval(x, y, width, height);
+ }
+ // Adds highlights at the top right
+ p = new GradientPaint(width, 0, bright, 0, height, brightEnd);
+ g2.setPaint(p);
+ if (fill) {
+ g2.fillOval(x, y, width, height);
+ } else {
+ g2.drawOval(x, y, width, height);
+ }
+
+ // Darken the edges
+ p = new RadialGradientPaint(x + width / 2f, y + height / 2f,
+ Math.min(width / 2f, height / 2f), new float[] { 0f, 1f },
+ new Color[] { darkEnd, darkPartial },
+ RadialGradientPaint.CycleMethod.NO_CYCLE);
+ g2.setPaint(p);
+ if (fill) {
+ g2.fillOval(x, y, width, height);
+ } else {
+ g2.drawOval(x, y, width, height);
+ }
+
+ // Adds inner highlight at the top right
+ p = new RadialGradientPaint(x + 3f * width / 4f, y + height / 4f,
+ Math.min(width / 4f, height / 4f),
+ new float[] { 0.0f, 0.8f },
+ new Color[] { bright, brightEnd },
+ RadialGradientPaint.CycleMethod.NO_CYCLE);
+ g2.setPaint(p);
+ if (fill) {
+ g2.fillOval(x * 2, y, width, height);
+ } else {
+ g2.drawOval(x * 2, y, width, height);
+ }
+
+ // Reset original paint
+ g2.setPaint(oldPaint);
+ } else {
+ g.setColor(color);
+ if (fill) {
+ g.fillOval(x, y, width, height);
+ } else {
+ g.drawOval(x, y, width, height);
+ }
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+/**
+ * FlowLayout subclass that fully supports wrapping of components.
+ *
+ * @author https://tips4java.wordpress.com/2008/11/06/wrap-layout/
+ */
+public class WrapLayout extends FlowLayout {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new <code>WrapLayout</code> with a left alignment and a
+ * default 5-unit horizontal and vertical gap.
+ */
+ public WrapLayout() {
+ super();
+ }
+
+ /**
+ * Constructs a new <code>FlowLayout</code> with the specified alignment and
+ * a default 5-unit horizontal and vertical gap. The value of the alignment
+ * argument must be one of <code>WrapLayout</code>, <code>WrapLayout</code>,
+ * or <code>WrapLayout</code>.
+ *
+ * @param align
+ * the alignment value
+ */
+ public WrapLayout(int align) {
+ super(align);
+ }
+
+ /**
+ * Creates a new flow layout manager with the indicated alignment and the
+ * indicated horizontal and vertical gaps.
+ * <p>
+ * The value of the alignment argument must be one of
+ * <code>WrapLayout</code>, <code>WrapLayout</code>, or
+ * <code>WrapLayout</code>.
+ *
+ * @param align
+ * the alignment value
+ * @param hgap
+ * the horizontal gap between components
+ * @param vgap
+ * the vertical gap between components
+ */
+ public WrapLayout(int align, int hgap, int vgap) {
+ super(align, hgap, vgap);
+ }
+
+ /**
+ * Returns the preferred dimensions for this layout given the <i>visible</i>
+ * components in the specified target container.
+ *
+ * @param target
+ * the component which needs to be laid out
+ * @return the preferred dimensions to lay out the subcomponents of the
+ * specified container
+ */
+ @Override
+ public Dimension preferredLayoutSize(Container target) {
+ return layoutSize(target, true);
+ }
+
+ /**
+ * Returns the minimum dimensions needed to layout the <i>visible</i>
+ * components contained in the specified target container.
+ *
+ * @param target
+ * the component which needs to be laid out
+ * @return the minimum dimensions to lay out the subcomponents of the
+ * specified container
+ */
+ @Override
+ public Dimension minimumLayoutSize(Container target) {
+ Dimension minimum = layoutSize(target, false);
+ minimum.width -= (getHgap() + 1);
+ return minimum;
+ }
+
+ /**
+ * Returns the minimum or preferred dimension needed to layout the target
+ * container.
+ *
+ * @param target
+ * target to get layout size for
+ * @param preferred
+ * should preferred size be calculated
+ * @return the dimension to layout the target container
+ */
+ private Dimension layoutSize(Container target, boolean preferred) {
+ synchronized (target.getTreeLock()) {
+ // Each row must fit with the width allocated to the containter.
+ // When the container width = 0, the preferred width of the
+ // container
+ // has not yet been calculated so lets ask for the maximum.
+
+ int targetWidth = target.getSize().width;
+ Container container = target;
+
+ while (container.getSize().width == 0
+ && container.getParent() != null) {
+ container = container.getParent();
+ }
+
+ targetWidth = container.getSize().width;
+
+ if (targetWidth == 0)
+ targetWidth = Integer.MAX_VALUE;
+
+ int hgap = getHgap();
+ int vgap = getVgap();
+ Insets insets = target.getInsets();
+ int horizontalInsetsAndGap = insets.left + insets.right
+ + (hgap * 2);
+ int maxWidth = targetWidth - horizontalInsetsAndGap;
+
+ // Fit components into the allowed width
+
+ Dimension dim = new Dimension(0, 0);
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ int nmembers = target.getComponentCount();
+
+ for (int i = 0; i < nmembers; i++) {
+ Component m = target.getComponent(i);
+
+ if (m.isVisible()) {
+ Dimension d = preferred ? m.getPreferredSize() : m
+ .getMinimumSize();
+
+ // Can't add the component to current row. Start a new
+ // row.
+
+ if (rowWidth + d.width > maxWidth) {
+ addRow(dim, rowWidth, rowHeight);
+ rowWidth = 0;
+ rowHeight = 0;
+ }
+
+ // Add a horizontal gap for all components after the
+ // first
+
+ if (rowWidth != 0) {
+ rowWidth += hgap;
+ }
+
+ rowWidth += d.width;
+ rowHeight = Math.max(rowHeight, d.height);
+ }
+ }
+
+ addRow(dim, rowWidth, rowHeight);
+
+ dim.width += horizontalInsetsAndGap;
+ dim.height += insets.top + insets.bottom + vgap * 2;
+
+ // When using a scroll pane or the DecoratedLookAndFeel we need
+ // to
+ // make sure the preferred size is less than the size of the
+ // target containter so shrinking the container size works
+ // correctly. Removing the horizontal gap is an easy way to do
+ // this.
+
+ Container scrollPane = SwingUtilities.getAncestorOfClass(
+ JScrollPane.class, target);
+
+ if (scrollPane != null && target.isValid()) {
+ dim.width -= (hgap + 1);
+ }
+
+ return dim;
+ }
+ }
+
+ /*
+ * A new row has been completed. Use the dimensions of this row to update
+ * the preferred size for the container.
+ *
+ * @param dim update the width and height when appropriate
+ *
+ * @param rowWidth the width of the row to add
+ *
+ * @param rowHeight the height of the row to add
+ */
+ private void addRow(Dimension dim, int rowWidth, int rowHeight) {
+ dim.width = Math.max(dim.width, rowWidth);
+
+ if (dim.height > 0) {
+ dim.height += getVgap();
+ }
+
+ dim.height += rowHeight;
+ }
+}
\ No newline at end of file
--- /dev/null
+package be.nikiroo.utils.ui.test;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.ui.ProgressBar;
+
+public class ProgressBarManualTest extends JFrame {
+ private static final long serialVersionUID = 1L;
+ private int i = 0;
+
+ public ProgressBarManualTest() {
+ final ProgressBar bar = new ProgressBar();
+ final Progress pg = new Progress("name");
+ final Progress pg2 = new Progress("second level", 0, 2);
+ final Progress pg3 = new Progress("third level");
+
+ setLayout(new BorderLayout());
+ this.add(bar, BorderLayout.SOUTH);
+
+ final JButton b = new JButton("Set pg to 10%");
+ b.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ switch (i) {
+ case 0:
+ pg.setProgress(10);
+ pg2.setProgress(0);
+ b.setText("Set pg to 20%");
+ break;
+ case 1:
+ pg.setProgress(20);
+ b.setText("Add pg2 (0-2)");
+ break;
+ case 2:
+ pg.addProgress(pg2, 80);
+ pg2.setProgress(0);
+ b.setText("Add pg3 (0-100)");
+ break;
+ case 3:
+ pg2.addProgress(pg3, 2);
+ pg3.setProgress(0);
+ b.setText("Set pg3 to 10%");
+ break;
+ case 4:
+ pg3.setProgress(10);
+ b.setText("Set pg3 to 20%");
+ break;
+ case 5:
+ pg3.setProgress(20);
+ b.setText("Set pg3 to 60%");
+ break;
+ case 6:
+ pg3.setProgress(60);
+ b.setText("Set pg3 to 100%");
+ break;
+ case 7:
+ pg3.setProgress(100);
+ b.setText("[done]");
+ break;
+ }
+
+ i++;
+ }
+ });
+ this.add(b, BorderLayout.CENTER);
+
+ setSize(800, 600);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ bar.setProgress(pg);
+ }
+
+ public static void main(String[] args) {
+ new ProgressBarManualTest().setVisible(true);
+ }
+}
--- /dev/null
+package be.nikiroo.utils.ui.test;
+
+public class TestUI {
+ // TODO: make a GUI tester
+ public TestUI() {
+ ProgressBarManualTest a = new ProgressBarManualTest();
+ }
+}