--- /dev/null
+# Required parameters (the commented out ones are supposed to change per project):
+
+#MAIN = path to main java source to compile
+#MORE = path to supplementary needed resources not linked from MAIN
+#NAME = name of project (used for jar output file)
+#PREFIX = usually /usr/local (where to install the program)
+#TEST = path to main test source to compile
+#JAR_FLAGS += a list of things to pack, each usually prefixed with "-C bin/"
+#SJAR_FLAGS += a list of things to pack, each usually prefixed with "-C src/", for *-sources.jar files
+
+JAVAC = javac
+JAVAC_FLAGS += -encoding UTF-8 -d ./bin/ -cp ./src/ -Xdiags:verbose
+JAVA = java
+JAVA_FLAGS += -cp ./bin/
+JAR = jar
+RJAR = java
+RJAR_FLAGS += -jar
+
+# Usual options:
+# make : to build the jar file
+# make libs : to update the libraries into src/
+# make build : to update the binaries (not the jar)
+# make test : to update the test binaries
+# make build jar : to update the binaries and jar file
+# make clean : to clean the directory of intermediate files
+# make mrpropre : to clean the directory of all outputs
+# make run : to run the program from the binaries
+# make run-test : to run the test program from the binaries
+# make jrun : to run the program from the jar file
+# make install : to install the application into $PREFIX
+
+# Note: build is actually slower than rebuild in most cases except when
+# small changes only are detected ; so we use rebuild by default
+
+all: build jar
+
+.PHONY: all clean mrproper mrpropre build run jrun jar resources install libs love
+
+bin:
+ @mkdir -p bin
+
+jar: $(NAME).jar
+
+build: resources
+ @echo Compiling program...
+ @echo " src/$(MAIN)"
+ @$(JAVAC) $(JAVAC_FLAGS) "src/$(MAIN).java"
+ @[ "$(MORE)" = "" ] || for sup in $(MORE); do \
+ echo " src/$$sup" ;\
+ $(JAVAC) $(JAVAC_FLAGS) "src/$$sup.java" ; \
+ done
+
+test:
+ @[ -e bin/$(MAIN).class ] || echo You need to build the sources
+ @[ -e bin/$(MAIN).class ]
+ @echo Compiling test program...
+ @[ "$(TEST)" != "" ] || echo No test sources defined.
+ @[ "$(TEST)" = "" ] || for sup in $(TEST); do \
+ echo " src/$$sup" ;\
+ $(JAVAC) $(JAVAC_FLAGS) "src/$$sup.java" ; \
+ done
+
+clean:
+ rm -rf bin/
+ @echo Removing sources taken from libs...
+ @for lib in libs/*-sources.jar; do \
+ basename "$$lib"; \
+ jar tf "$$lib" | while read -r ln; do \
+ [ -f "src/$$ln" ] && rm "src/$$ln"; \
+ done; \
+ jar tf "$$lib" | tac | while read -r ln; do \
+ [ -d "src/$$ln" ] && rmdir "src/$$ln" 2>/dev/null || true; \
+ done; \
+ done
+
+mrproper: mrpropre
+
+mrpropre: clean
+ rm -f $(NAME).jar
+
+love:
+ @echo " ...not war."
+
+resources: libs
+ @echo Copying resources into bin/...
+ @cd src && find . | grep -v '\.java$$' | while read -r ln; do \
+ if [ -f "$$ln" ]; then \
+ dir="`dirname "$$ln"`"; \
+ mkdir -p "../bin/$$dir" ; \
+ cp "$$ln" "../bin/$$ln" ; \
+ fi ; \
+ done
+
+libs: bin
+ @[ -e bin/libs -o ! -d libs ] || echo Extracting sources from libs...
+ @[ -e bin/libs -o ! -d libs ] || (cd src && for lib in ../libs/*-sources.jar; do \
+ basename "$$lib"; \
+ jar xf "$$lib"; \
+ done )
+ @[ ! -d libs ] || touch bin/libs
+
+$(NAME).jar: resources
+ @[ -e bin/$(MAIN).class ] || echo You need to build the sources
+ @[ -e bin/$(MAIN).class ]
+ @echo Making JAR file...
+ @echo > bin/manifest
+ @[ "$(SJAR_FLAGS)" = "" ] || echo Creating $(NAME)-sources.jar...
+ @[ "$(SJAR_FLAGS)" = "" ] || $(JAR) cfm $(NAME)-sources.jar bin/manifest $(SJAR_FLAGS)
+ @[ "$(SJAR_FLAGS)" = "" ] || [ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`-sources.jar"...
+ @[ "$(SJAR_FLAGS)" = "" ] || [ ! -e VERSION ] || cp $(NAME)-sources.jar "$(NAME)-`cat VERSION`-sources.jar"
+ @echo "Main-Class: `echo "$(MAIN)" | sed 's:/:.:g'`" > bin/manifest
+ @echo >> bin/manifest
+ $(JAR) cfm $(NAME).jar bin/manifest $(JAR_FLAGS)
+ @[ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`.jar"...
+ @[ ! -e VERSION ] || cp $(NAME).jar "$(NAME)-`cat VERSION`.jar"
+
+run:
+ @[ -e bin/$(MAIN).class ] || echo You need to build the sources
+ @[ -e bin/$(MAIN).class ]
+ @echo Running "$(NAME)"...
+ $(JAVA) $(JAVA_FLAGS) $(MAIN)
+
+jrun:
+ @[ -e $(NAME).jar ] || echo You need to build the jar
+ @[ -e $(NAME).jar ]
+ @echo Running "$(NAME).jar"...
+ $(RJAR) $(RJAR_FLAGS) $(NAME).jar
+
+run-test:
+ @[ "$(TEST)" = "" -o -e "bin/$(TEST).class" ] || echo You need to build the test sources
+ @[ "$(TEST)" = "" -o -e "bin/$(TEST).class" ]
+ @echo Running tests for "$(NAME)"...
+ @[ "$(TEST)" != "" ] || echo No test sources defined.
+ [ "$(TEST)" = "" ] || $(JAVA) $(JAVA_FLAGS) $(TEST)
+
+install:
+ @[ -e $(NAME).jar ] || echo You need to build the jar
+ @[ -e $(NAME).jar ]
+ mkdir -p "$(PREFIX)/lib" "$(PREFIX)/bin"
+ cp $(NAME).jar "$(PREFIX)/lib/"
+ echo "#!/bin/sh" > "$(PREFIX)/bin/$(NAME)"
+ echo "$(RJAR) $(RJAR_FLAGS) \"$(PREFIX)/lib/$(NAME).jar\" \"$$@\"" >> "$(PREFIX)/bin/$(NAME)"
+ chmod a+rx "$(PREFIX)/bin/$(NAME)"
+
--- /dev/null
+#!/bin/sh
+
+# default:
+PREFIX=/usr/local
+PROGS="java javac jar"
+
+valid=true
+while [ "$*" != "" ]; do
+ key=`echo "$1" | cut -c1-9`
+ val=`echo "$1" | cut -c10-`
+ case "$key" in
+ --prefix=)
+ PREFIX="$val"
+ ;;
+ *)
+ echo "Unsupported parameter: '$1'" >&2
+ valid=false
+ ;;
+ esac
+ shift
+done
+
+[ $valid = false ] && exit 1
+
+MESS="A required program cannot be found:"
+for prog in $PROGS; do
+ out="`whereis -b "$prog" 2>/dev/null`"
+ if [ "$out" = "$prog:" ]; then
+ echo "$MESS $prog" >&2
+ valid=false
+ fi
+done
+
+[ $valid = false ] && exit 2
+
+echo "MAIN = be/nikiroo/utils/resources/TransBundle" > Makefile
+echo "MORE = be/nikiroo/utils/StringUtils be/nikiroo/utils/IOUtils be/nikiroo/utils/MarkableFileInputStream" >> Makefile
+echo "NAME = nikiroo-utils" >> Makefile
+echo "PREFIX = $PREFIX" >> Makefile
+echo "JAR_FLAGS += -C bin/ be" >> Makefile
+echo "SJAR_FLAGS += -C src/ be" >> Makefile
+
+cat Makefile.base >> Makefile
+
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * This class offer some utilities based around {@link Stream}s.
+ *
+ * @author niki
+ */
+public class IOUtils {
+ /**
+ * Write the data to the given {@link File}.
+ *
+ * @param in
+ * the data source
+ * @param target
+ * the target {@link File}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static void write(InputStream in, File target) throws IOException {
+ OutputStream out = new FileOutputStream(target);
+ try {
+ write(in, out);
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Write the data to the given {@link OutputStream}.
+ *
+ * @param in
+ * the data source
+ * @param target
+ * the target {@link OutputStream}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static void write(InputStream in, OutputStream out)
+ throws IOException {
+ byte buffer[] = new byte[4069];
+ for (int len = 0; (len = in.read(buffer)) > 0;) {
+ out.write(buffer, 0, len);
+ }
+ }
+
+ /**
+ * 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 + "/"));
+ }
+ for (File file : target.listFiles()) {
+ 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 srctIsRoot
+ * 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();
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ FileWriter writerVersion = new FileWriter(new File(dir, filename));
+ try {
+ writerVersion.write(content);
+ } finally {
+ writerVersion.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 {
+ BufferedReader reader = new BufferedReader(new FileReader(file));
+ try {
+ StringBuilder builder = new StringBuilder();
+ for (String line = reader.readLine(); line != null; line = reader
+ .readLine()) {
+ builder.append(line);
+ }
+ return builder.toString();
+ } finally {
+ reader.close();
+ }
+ }
+
+ /**
+ * 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
+ */
+ public static void deltree(File target) {
+ for (File file : target.listFiles()) {
+ if (file.isDirectory()) {
+ deltree(file);
+ } else {
+ if (!file.delete()) {
+ System.err.println("Cannot delete file: "
+ + file.getAbsolutePath());
+ }
+ }
+ }
+
+ if (!target.delete()) {
+ System.err.println("Cannot delete file: "
+ + target.getAbsolutePath());
+ }
+ }
+}
--- /dev/null
+package be.nikiroo.utils;
+
+import java.io.FileInputStream;
+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 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;
+
+import java.awt.Image;
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+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.Base64;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+import javax.imageio.ImageIO;
+
+import org.unbescape.html.HtmlEscape;
+import org.unbescape.html.HtmlEscapeLevel;
+import org.unbescape.html.HtmlEscapeType;
+
+/**
+ * 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
+ * is applied or if there is enough horizontal space for it to be aligned.
+ */
+ public enum Alignment {
+ /** Aligned at left. */
+ Beginning,
+ /** Centered. */
+ Center,
+ /** Aligned at right. */
+ End
+ }
+
+ static private Pattern marks = Pattern
+ .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
+
+ /**
+ * 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, Alignment.Beginning);
+ }
+
+ /**
+ * 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
+ *
+ * @return the resulting {@link String} of size <i>size</i> minimum
+ */
+ static public String padString(String text, int width, boolean cut,
+ Alignment align) {
+
+ 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.End)
+ align = Alignment.Beginning;
+
+ switch (align) {
+ case Beginning:
+ text = text + new String(new char[diff]).replace('\0', ' ');
+ break;
+ case End:
+ text = new String(new char[diff]).replace('\0', ' ') + text;
+ break;
+ case Center:
+ default:
+ 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;
+ }
+ }
+ }
+
+ return text;
+ }
+
+ /**
+ * 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);
+ 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 time in milliseconds to {@link String} in a "static" way
+ * (to exchange data over the wire, for instance).
+ *
+ * @param time
+ * the time in milliseconds
+ *
+ * @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 time as a {@link String} to milliseconds in a "static"
+ * way (to exchange data over the wire, for instance).
+ *
+ * @param time
+ * the time as a {@link String}
+ *
+ * @return the time in milliseconds
+ */
+ static public long toTime(String display) {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ try {
+ return sdf.parse(display).getTime();
+ } catch (ParseException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Convert the given {@link Image} object into a Base64 representation of
+ * the same {@link Image}. object.
+ *
+ * @param image
+ * the {@link Image} object to convert
+ *
+ * @return the Base64 representation
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ static public String fromImage(BufferedImage image) throws IOException {
+ String imageString = null;
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ ImageIO.write(image, "jpeg", out);
+ byte[] imageBytes = out.toByteArray();
+
+ imageString = new String(Base64.getEncoder().encode(imageBytes));
+
+ out.close();
+
+ return imageString;
+ }
+
+ /**
+ * Convert the given {@link File} image into a Base64 representation of the
+ * same {@link File}.
+ *
+ * @param file
+ * the {@link File} image to convert
+ *
+ * @return the Base64 representation
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ static public String fromStream(InputStream in) throws IOException {
+ String fileString = null;
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ byte[] buf = new byte[8192];
+
+ int c = 0;
+ while ((c = in.read(buf, 0, buf.length)) > 0) {
+ out.write(buf, 0, c);
+ }
+ out.flush();
+ in.close();
+
+ fileString = new String(Base64.getEncoder().encode(out.toByteArray()));
+ out.close();
+
+ return fileString;
+ }
+
+ /**
+ * Convert the given Base64 representation of an image into an {@link Image}
+ * object.
+ *
+ * @param b64data
+ * the {@link Image} in Base64 format
+ *
+ * @return the {@link Image} object
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ static public BufferedImage toImage(String b64data) throws IOException {
+ ByteArrayInputStream in = new ByteArrayInputStream(Base64.getDecoder()
+ .decode(b64data));
+ return toImage(in);
+ }
+
+ /**
+ * Convert the given {@link InputStream} (which must allow calls to
+ * {@link InputStream#reset()}) into an {@link Image} object.
+ *
+ * @param in
+ * the 'resetable' {@link InputStream}
+ *
+ * @return the {@link Image} object
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ static public BufferedImage toImage(InputStream in) throws IOException {
+ int orientation;
+ try {
+ orientation = getExifTransorm(in);
+ } catch (Exception e) {
+ // no EXIF transform, ok
+ orientation = -1;
+ }
+
+ in.reset();
+ BufferedImage image = ImageIO.read(in);
+
+ if (image == null) {
+ throw new IOException("Failed to convert input to image");
+ }
+
+ // 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:
+ 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(height, width,
+ image.getType());
+ transformedImage = affineTransformOp
+ .filter(image, transformedImage);
+
+ image = transformedImage;
+ }
+ //
+
+ return image;
+ }
+
+ /**
+ * Return a hash of the given {@link String}.
+ *
+ * @param input
+ * the input data
+ *
+ * @return the hash
+ */
+ static public String getHash(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.update(input.getBytes());
+ 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;
+ }
+ }
+
+ /**
+ * 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
+ */
+ static private 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;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ return HtmlEscape.unescapeHtml(builder.toString());
+ }
+
+ /**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * Some small utilities used through the pogram.
+ *
+ * @author niki
+ */
+package be.nikiroo.utils;
\ No newline at end of file
--- /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.UnsupportedEncodingException;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
+
+import be.nikiroo.utils.resources.Bundles;
+import be.nikiroo.utils.resources.Meta;
+
+/**
+ * This class encapsulate a {@link ResourceBundle} in UTF-8. It only allows to
+ * retrieve values associated to an enumeration, and allows some additional
+ * methods.
+ *
+ * @author niki
+ *
+ * @param <E>
+ * the enum to use to get values out of this class
+ */
+public class Bundle<E extends Enum<E>> {
+ protected Class<E> type;
+ protected Enum<?> name;
+ private ResourceBundle map;
+
+ /**
+ * 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}
+ */
+ protected Bundle(Class<E> type, Enum<?> name) {
+ this.type = type;
+ this.name = name;
+ setBundle(name, Locale.getDefault());
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String}.
+ *
+ * @param mame
+ * 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 getStringX(id, "");
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link String} suffixed
+ * with the runtime value "_suffix" (that is, "_" and suffix).
+ *
+ * @param mame
+ * 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) {
+ String key = id.name()
+ + ((suffix == null || suffix.isEmpty()) ? "" : "_"
+ + suffix.toUpperCase());
+
+ if (containsKey(key)) {
+ return getString(key).trim();
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Boolean}.
+ *
+ * @param mame
+ * the id of the value to get
+ *
+ * @return the associated value
+ */
+ public Boolean getBoolean(E id) {
+ String str = getString(id);
+ if (str != null && str.length() > 0) {
+ 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 the value associated to the given id as a {@link boolean}.
+ *
+ * @param mame
+ * 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 b = getBoolean(id);
+ if (b != null)
+ return b;
+
+ return def;
+ }
+
+ /**
+ * Return the value associated to the given id as an {@link Integer}.
+ *
+ * @param mame
+ * the id of the value to get
+ *
+ * @return the associated value
+ */
+ public Integer getInteger(E id) {
+ try {
+ return Integer.parseInt(getString(id));
+ } catch (Exception e) {
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link int}.
+ *
+ * @param mame
+ * 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 i = getInteger(id);
+ if (i != null)
+ return i;
+
+ return def;
+ }
+
+ /**
+ * Return the value associated to the given id as a {@link Character}.
+ *
+ * @param mame
+ * the id of the value to get
+ *
+ * @return the associated value
+ */
+ public char getChar(E id) {
+ String s = getString(id).trim();
+ if (s.length() > 0) {
+ return s.charAt(0);
+ }
+
+ return ' ';
+ }
+
+ /**
+ * Create/update the .properties file. 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
+ *
+ * @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 = E.valueOf(type, field.getName());
+ String info = getMetaInfo(meta);
+
+ if (info != null) {
+ writer.write(info);
+ writer.write("\n");
+ }
+
+ writeValue(writer, id);
+ }
+ }
+
+ writer.close();
+ }
+
+ /**
+ * 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) {
+ try {
+ map.getObject(key);
+ return true;
+ } catch (MissingResourceException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Get the value for the given key if it exists in the internal map.
+ *
+ * @param key
+ * the key to check for
+ *
+ * @return true if it does
+ */
+ protected String getString(String key) {
+ if (containsKey(key)) {
+ try {
+ // Note: it is also possible to fix the borked charset issue
+ // with a custom ResourceBundle#Control class, but this one,
+ // while a workaround, depend less upon the JRE classes, which
+ // may change
+ return new String(map.getString(key).getBytes("ISO-8859-1"),
+ "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // Those 2 encodings are always supported
+ e.printStackTrace();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 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 what = meta.what();
+ String where = meta.where();
+ String format = meta.format();
+ String info = meta.info();
+
+ int opt = what.length() + where.length() + format.length();
+ if (opt + info.length() == 0)
+ return null;
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("# ");
+
+ if (opt > 0) {
+ builder.append("(");
+ if (what.length() > 0) {
+ builder.append("WHAT: " + what);
+ if (where.length() + format.length() > 0)
+ builder.append(", ");
+ }
+
+ if (where.length() > 0) {
+ builder.append("WHERE: " + where);
+ if (format.length() > 0)
+ builder.append(", ");
+ }
+
+ if (format.length() > 0) {
+ builder.append("FORMAT: " + format);
+ }
+
+ builder.append(")");
+ if (info.length() > 0) {
+ builder.append("\n# ");
+ }
+ }
+
+ builder.append(info);
+
+ return builder.toString();
+ }
+
+ /**
+ * The display name used in the <tt>.properties file</tt>.
+ *
+ * @return the name
+ */
+ protected String getBundleDisplayName() {
+ return name.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 id to the config file, i.e., "MY_ID = my_curent_value"
+ * followed by a new line
+ *
+ * @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 {
+ writeValue(writer, id.name(), getString(id));
+ }
+
+ /**
+ * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
+ * followed by a new line
+ *
+ * @param writer
+ * the {@link Writer} to write into
+ * @param id
+ * the id to write
+ * @param value
+ * the id's value
+ *
+ * @throws IOException
+ * in case of IO error
+ */
+ protected void writeValue(Writer writer, String id, String value)
+ throws IOException {
+ 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}
+ *
+ * @throws IOException
+ * in case of IO errors
+ */
+ protected File getUpdateFile(String path) {
+ return new File(path, name.name() + ".properties");
+ }
+
+ /**
+ * Change the currently used bundle.
+ *
+ * @param name
+ * the name of the bundle to load
+ * @param locale
+ * the {@link Locale} to use
+ */
+ protected void setBundle(Enum<?> name, Locale locale) {
+ map = null;
+ String dir = Bundles.getDirectory();
+
+ if (dir != null) {
+ try {
+ File file = getPropertyFile(dir, name.name(), locale);
+ if (file != null) {
+ Reader reader = new InputStreamReader(new FileInputStream(
+ file), "UTF8");
+ map = new PropertyResourceBundle(reader);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (map == null) {
+ map = ResourceBundle.getBundle(type.getPackage().getName() + "."
+ + name.name(), locale);
+ }
+ }
+
+ /**
+ * Return the resource file that is closer to the {@link Locale}.
+ *
+ * @param dir
+ * the dirctory to look into
+ * @param name
+ * the file basename (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;
+ } else {
+ file = null;
+ }
+ }
+
+ return file;
+ }
+}
--- /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.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 {
+ /**
+ * What kind of item this key represent (a Key, a Label text, a format to
+ * use for something else...).
+ *
+ * @return what it is
+ */
+ String what();
+
+ /**
+ * Where in the application will this key appear (in the action keys, in a
+ * menu, in a message...).
+ *
+ * @return where it is
+ */
+ String where();
+
+ /**
+ * What format should/must this key be in.
+ *
+ * @return the format it is in
+ */
+ String format();
+
+ /**
+ * Free info text to help translate.
+ *
+ * @return some info
+ */
+ String info();
+}
--- /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;
+
+import be.nikiroo.utils.resources.Bundles;
+
+/**
+ * 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>
+ *
+ * @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) {
+ super(type, name);
+ setLanguage(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
+ */
+ public TransBundle(Class<E> type, Enum<?> name, String language) {
+ super(type, name);
+ setLanguage(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);
+ } else {
+ result = null;
+ }
+
+ if (values != null && values.length > 0 && result != null)
+ return String.format(locale, result, values);
+ else
+ 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.
+ *
+ *
+ * @return the known language codes
+ */
+ public List<String> getKnownLanguages() {
+ return getKnownLanguages(name);
+ }
+
+ /**
+ * 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 setLanguage(String language) {
+ defaultLocale = (language == null || language.length() == 0);
+ locale = getLocaleFor(language);
+ setBundle(name, locale);
+ }
+
+ @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();
+
+ setLanguage(null); // default locale
+ super.updateFile(path);
+
+ for (String lang : getKnownLanguages()) {
+ setLanguage(lang);
+ super.updateFile(path);
+ }
+
+ setLanguage(prev);
+ }
+
+ @Override
+ protected File getUpdateFile(String path) {
+ String code = locale.toString();
+ File file = null;
+ if (!defaultLocale && code.length() > 0) {
+ file = new File(path, name.name() + "_" + code + ".properties");
+ } else {
+ // Default properties file:
+ file = new File(path, name.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);
+ writeValue(writer, name, value);
+ }
+ }
+
+ /**
+ * 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 the default {@link Locale} if
+ * it is not known
+ */
+ static private Locale getLocaleFor(String language) {
+ Locale locale;
+
+ if (language == null) {
+ locale = Locale.getDefault();
+ } else {
+ 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.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 = (ZipEntry) 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) {
+ final ArrayList<String> retval = new ArrayList<String>();
+ final File[] fileList = directory.listFiles();
+ for (final File file : fileList) {
+ if (file.isDirectory()) {
+ retval.addAll(getResourcesFromDirectory(file, pattern));
+ } else {
+ try {
+ final String fileName = file.getCanonicalPath();
+ final boolean accept = pattern.matcher(fileName).matches();
+ if (accept) {
+ retval.add(fileName);
+ }
+ } catch (final IOException e) {
+ throw new Error(e);
+ }
+ }
+ }
+
+ return retval;
+ }
+}
--- /dev/null
+/**
+ * This package encloses the classes needed to use
+ * {@link be.nikiroo.utils.resources.bundles.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