From: Niki Roo Date: Sun, 12 Feb 2017 15:06:27 +0000 (+0100) Subject: Initial commit, version 0.9.2 X-Git-Tag: nikiroo-utils-0.9.6~5 X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=ec1f3444e9f238ce1559d5fff32eb5a7ab8aba53;p=nikiroo-utils.git Initial commit, version 0.9.2 --- ec1f3444e9f238ce1559d5fff32eb5a7ab8aba53 diff --git a/Makefile.base b/Makefile.base new file mode 100644 index 0000000..9ae14f8 --- /dev/null +++ b/Makefile.base @@ -0,0 +1,144 @@ +# 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)" + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..2003b63 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.9.2 diff --git a/configure.sh b/configure.sh new file mode 100755 index 0000000..ed494b7 --- /dev/null +++ b/configure.sh @@ -0,0 +1,44 @@ +#!/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 + diff --git a/libs/unbescape-1.1.4-sources.jar b/libs/unbescape-1.1.4-sources.jar new file mode 100644 index 0000000..01ddb56 Binary files /dev/null and b/libs/unbescape-1.1.4-sources.jar differ diff --git a/src/be/nikiroo/utils/IOUtils.java b/src/be/nikiroo/utils/IOUtils.java new file mode 100644 index 0000000..3516fd2 --- /dev/null +++ b/src/be/nikiroo/utils/IOUtils.java @@ -0,0 +1,216 @@ +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 .zip 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. + *

+ * 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()); + } + } +} diff --git a/src/be/nikiroo/utils/MarkableFileInputStream.java b/src/be/nikiroo/utils/MarkableFileInputStream.java new file mode 100644 index 0000000..f4d95d5 --- /dev/null +++ b/src/be/nikiroo/utils/MarkableFileInputStream.java @@ -0,0 +1,51 @@ +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 diff --git a/src/be/nikiroo/utils/StringUtils.java b/src/be/nikiroo/utils/StringUtils.java new file mode 100644 index 0000000..993f62b --- /dev/null +++ b/src/be/nikiroo/utils/StringUtils.java @@ -0,0 +1,607 @@ +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 size + */ + 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 size 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. + * + *

+ * Note: this code has been found on internet; thank you anonymous coder. + *

+ * + * @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); + } +} diff --git a/src/be/nikiroo/utils/package-info.java b/src/be/nikiroo/utils/package-info.java new file mode 100644 index 0000000..4951378 --- /dev/null +++ b/src/be/nikiroo/utils/package-info.java @@ -0,0 +1,6 @@ +/** + * Some small utilities used through the pogram. + * + * @author niki + */ +package be.nikiroo.utils; \ No newline at end of file diff --git a/src/be/nikiroo/utils/resources/Bundle.java b/src/be/nikiroo/utils/resources/Bundle.java new file mode 100644 index 0000000..1c63d69 --- /dev/null +++ b/src/be/nikiroo/utils/resources/Bundle.java @@ -0,0 +1,480 @@ +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 + * the enum to use to get values out of this class + */ +public class Bundle> { + protected Class 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 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 .properties file. + * + * @return the name + */ + protected String getBundleDisplayName() { + return name.toString(); + } + + /** + * Write the header found in the configuration .properties 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 .properties) + * @param locale + * the {@link Locale} + * + * @return the closest match or NULL if none + */ + private File getPropertyFile(String dir, String name, Locale locale) { + List locales = new ArrayList(); + 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; + } +} diff --git a/src/be/nikiroo/utils/resources/Bundles.java b/src/be/nikiroo/utils/resources/Bundles.java new file mode 100644 index 0000000..ad7b99d --- /dev/null +++ b/src/be/nikiroo/utils/resources/Bundles.java @@ -0,0 +1,40 @@ +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 .properties + * 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 .properties + * 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 .properties + * files in. + * + * @return the directory + */ + static public String getDirectory() { + return Bundles.confDir; + } +} diff --git a/src/be/nikiroo/utils/resources/Meta.java b/src/be/nikiroo/utils/resources/Meta.java new file mode 100644 index 0000000..3e14557 --- /dev/null +++ b/src/be/nikiroo/utils/resources/Meta.java @@ -0,0 +1,46 @@ +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(); +} diff --git a/src/be/nikiroo/utils/resources/TransBundle.java b/src/be/nikiroo/utils/resources/TransBundle.java new file mode 100644 index 0000000..61bc922 --- /dev/null +++ b/src/be/nikiroo/utils/resources/TransBundle.java @@ -0,0 +1,327 @@ +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. + *

+ * Two special cases are handled for the used enum: + *

    + *
  • NULL will always will return an empty {@link String}
  • + *
  • DUMMY will return "[DUMMY]" (maybe with a suffix and/or "NOUTF")
  • + *
+ * + * @author niki + */ +public class TransBundle> extends Bundle { + 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 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 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 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. + *

+ * Note: this method is NOT 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 getKnownLanguages(Enum name) { + List resources = new LinkedList(); + + 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; + } +} diff --git a/src/be/nikiroo/utils/resources/TransBundle_ResourceList.java b/src/be/nikiroo/utils/resources/TransBundle_ResourceList.java new file mode 100644 index 0000000..519b33a --- /dev/null +++ b/src/be/nikiroo/utils/resources/TransBundle_ResourceList.java @@ -0,0 +1,107 @@ +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 getResources(final Pattern pattern) { + final ArrayList retval = new ArrayList(); + 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 getResources(final String element, + final Pattern pattern) { + final ArrayList retval = new ArrayList(); + 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 getResourcesFromJarFile(final File file, + final Pattern pattern) { + final ArrayList retval = new ArrayList(); + 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 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 getResourcesFromDirectory( + final File directory, final Pattern pattern) { + final ArrayList retval = new ArrayList(); + 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; + } +} diff --git a/src/be/nikiroo/utils/resources/package-info.java b/src/be/nikiroo/utils/resources/package-info.java new file mode 100644 index 0000000..783cd03 --- /dev/null +++ b/src/be/nikiroo/utils/resources/package-info.java @@ -0,0 +1,14 @@ +/** + * This package encloses the classes needed to use + * {@link be.nikiroo.utils.resources.bundles.Bundle}s + *

+ * Those are basically a .properties resource linked to an enumeration + * listing all the fields you can use. The classes can also be used to update + * the linked .properties files (or export them, which is useful when + * you work from a JAR file). + *

+ * All those classes expect UTF-8 content only. + * + * @author niki + */ +package be.nikiroo.utils.resources; \ No newline at end of file