net.java.hulp.i18n.buildtools.I18NTask Maven / Gradle / Ivy
The newest version!
//Copyright (c) 2007 Sun Microsystems
//
//Permission is hereby granted, free of charge, to any person
//obtaining a copy of this software and associated documentation
//files (the "Software"), to deal in the Software without
//restriction, including without limitation the rights to use,
//copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the
//Software is furnished to do so, subject to the following
//conditions:
//
//The above copyright notice and this permission notice shall be
//included in all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//OTHER DEALINGS IN THE SOFTWARE.
package net.java.hulp.i18n.buildtools;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.DirSet;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Extracts internationalizable text from class files and puts these strings in a
* resource bundle.
*
* The directory of classfiles needs to be specified as well as the location of the
* resource bundle to be updated.
*
* @author fkieviet
*/
public class I18NTask extends Task {
private File classesDir;
private File targetBundle;
private boolean strict = true;
private String prefix;
private String prefixU;
private String pattern = "([A-Z]\\d\\d\\d)(: )(.*)";
private Pattern splitter;
private Set dirSets;
/**
* Constructor
*/
public I18NTask() {
dirSets = new HashSet();
}
/**
* @see org.apache.tools.ant.Task#execute()
*/
public void execute() {
if (classesDir == null && dirSets.size() == 0) {
throw new BuildException("Directory must be specified using the " +
"\"dir\" attribute or nested \"dirset\" " +
"type");
}
if (targetBundle == null) {
throw new BuildException("File must be specified");
}
splitter = Pattern.compile(pattern, Pattern.DOTALL);
extract(targetBundle.getAbsolutePath());
}
/*
* Converts unicodes to encoded \uxxxx and escapes
* special characters with a preceding slash
* From JDK Properties
*/
private String saveConvert(String theString, boolean escapeSpace) {
int len = theString.length();
int bufLen = len * 2;
if (bufLen < 0) {
bufLen = Integer.MAX_VALUE;
}
StringBuffer outBuffer = new StringBuffer(bufLen);
for(int x=0; x 61) && (aChar < 127)) {
if (aChar == '\\') {
outBuffer.append('\\'); outBuffer.append('\\');
continue;
}
outBuffer.append(aChar);
continue;
}
switch(aChar) {
case ' ':
if (x == 0 || escapeSpace)
outBuffer.append('\\');
outBuffer.append(' ');
break;
case '\t':outBuffer.append('\\'); outBuffer.append('t');
break;
case '\n':outBuffer.append('\\'); outBuffer.append('n');
break;
case '\r':outBuffer.append('\\'); outBuffer.append('r');
break;
case '\f':outBuffer.append('\\'); outBuffer.append('f');
break;
case '=': // Fall through
case ':': // Fall through
case '#': // Fall through
case '!':
outBuffer.append('\\'); outBuffer.append(aChar);
break;
default:
if ((aChar < 0x0020) || (aChar > 0x007e)) {
outBuffer.append('\\');
outBuffer.append('u');
outBuffer.append(toHex((aChar >> 12) & 0xF));
outBuffer.append(toHex((aChar >> 8) & 0xF));
outBuffer.append(toHex((aChar >> 4) & 0xF));
outBuffer.append(toHex( aChar & 0xF));
} else {
outBuffer.append(aChar);
}
}
}
return outBuffer.toString();
}
/**
* Convert a nibble to a hex character
* @param nibble the nibble to convert.
*/
private static char toHex(int nibble) {
return hexDigit[(nibble & 0xF)];
}
/** A table of hex digits */
private static final char[] hexDigit = {
'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
};
/**
* Extracts internationalizable text out of a jar and puts it in a properties file
*
* @param dir classes dir
* @param propertiesPath properties to update
*/
public void extract(String propertiesPath) {
try {
boolean duplicateIDsError = false;
boolean couldBeImproved = false;
String effectiveprefix = this.prefixU != null ? this.prefixU.toUpperCase() : this.prefix;
if (effectiveprefix == null) {
effectiveprefix = "";
}
// Iterate over the directory/directories and gather bundle information
List list = new ArrayList();
if (dirSets.size() <= 0) {
readDir(list, classesDir.getAbsolutePath(), classesDir.getAbsolutePath(), true);
} else {
Iterator it = dirSets.iterator();
while (it.hasNext()) {
DirSet dirSet = it.next();
DirectoryScanner ds = null;
ds = dirSet.getDirectoryScanner(getProject());
File rootDir = dirSet.getDir(getProject());
String[] srcDirs = ds.getIncludedDirectories();
for (int jj = 0; jj < srcDirs.length; jj++) {
readDir(list, rootDir.getAbsolutePath(),
rootDir.getAbsolutePath() + File.separator + srcDirs[jj],
false);
}
}
}
// Prepare bundle in memory buffer
StringWriter outbuf = new StringWriter();
PrintWriter out = new PrintWriter(outbuf);
out.println("# DO NOT EDIT");
out.println("# THIS FILE IS GENERATED AUTOMATICALLY FROM JAVA SOURCES/CLASSES");
out.println();
// Sort by text
TextEntry[] entries = list.toArray(new TextEntry[list.size()]);
Set sortedByText = new TreeSet(new Comparator() {
public int compare(TextEntry lhs, TextEntry rhs) {
return lhs.getText().compareTo(rhs.getText());
}
});
sortedByText.addAll(Arrays.asList(entries));
// Build BY-ID and BY-CONTENT maps
Map> byId = new HashMap>();
Map> byContent = new HashMap>();
for (int i = 0; i < entries.length; i++) {
TextEntry c = entries[i];
if (byId.get(c.getID()) == null) {
byId.put(c.getID(), new ArrayList());
}
byId.get(c.getID()).add(c);
if (byContent.get(c.getContent()) == null) {
byContent.put(c.getContent(), new ArrayList());
}
byContent.get(c.getContent()).add(c);
}
// Print all messages to the bundle
for (Iterator iter = sortedByText.iterator(); iter.hasNext();) {
TextEntry e = iter.next();
List sources = byId.get(e.getID());
// Collect in which classes this message was used and check if there
// are duplicate IDs
List classnames = new ArrayList();
for (Iterator iterator = sources.iterator(); iterator.hasNext();) {
TextEntry e2 = iterator.next();
// Sanitize classname: slashes -> dots, no .class at the end
String clname = e2.getClassname();
clname = clname.replace(File.separatorChar, '.');
clname = clname.replace('$', '.') + '\t';
clname = clname.replace(".class\t", "");
classnames.add(clname);
// Check for duplicates
if (!e2.getContent().equals(e.getContent())) {
duplicateIDsError = true;
System.err.println();
System.err.println("DIFFERENT TEXTS, SAME IDS: ");
System.err.println(e.getClassname());
System.err.println(e.getText());
System.err.println(e2.getClassname());
System.err.println(e2.getText());
}
}
// Write out classnames in which this message was used
Collections.sort(classnames);
for (String classname: classnames) {
out.println("# " + classname);
}
// Write the actual message
out.println(effectiveprefix + e.getID() + " = " + saveConvert(e.getContent(), false));
out.println();
}
// Check for duplicate texts with different IDs
for (Iterator>> iter = byContent.entrySet().iterator(); iter.hasNext();) {
Map.Entry> e = (Map.Entry>) iter.next();
List dups = (List) e.getValue();
Set ids = new TreeSet(new Comparator() {
public int compare(TextEntry lhs, TextEntry rhs) {
return lhs.getID().compareTo(rhs.getID());
}
});
ids.addAll(dups);
if (ids.size() > 1) {
System.err.println();
System.err.println("SAME TEXTS, DIFFERENT IDS");
for (Iterator iterator = dups.iterator(); iterator.hasNext();) {
couldBeImproved = true;
TextEntry dup = iterator.next();
System.err.println(dup.getClassname());
System.err.println(dup.getText());
}
}
}
// Close memory buffer (there will be no resource leak if this is not done)
out.close();
out = null;
// Update bundle if contents has changed
String old = read(propertiesPath);
String newContents = outbuf.getBuffer().toString();
//if we are on cygwin using unix Eol conventions...
if (isUnixEol(old) && !isUnixEol(newContents)) {
//convert new to same eol conventions as old:
newContents = toUnixEols(newContents);
//NOTE: this assumes we DO NOT generate the initial file on cygwin.
}
if (!newContents.equals(old)) {
System.out.println("Updating " + propertiesPath);
write(propertiesPath, newContents);
} else {
System.out.println("Up to date: " + propertiesPath);
}
// Check results
if (duplicateIDsError) {
throw new BuildException("Duplicate ids but different texts; "
+ "see console output for details");
}
if (couldBeImproved && strict) {
throw new BuildException("Duplicate texts but different ids; "
+ "see console output for details");
}
} catch (Exception e) {
throw new BuildException("Failed to extract bundle information or create the bundle. Bundle="
+ propertiesPath + ". Error was: " + e, e);
}
}
/**
* @param s Stream to close
*/
public static void safeClose(InputStream s) {
if (s != null) {
try {
s.close();
} catch (Exception ignore) {
// ignore
}
}
}
/**
* @param s Stream to close
*/
public static void safeClose(OutputStream s) {
if (s != null) {
try {
s.close();
} catch (Exception ignore) {
// ignore
}
}
}
/**
* @param s Stream to close
*/
public static void safeClose(Writer s) {
if (s != null) {
try {
s.close();
} catch (Exception ignore) {
// ignore
}
}
}
private static String read(String path) throws Exception {
File f = new File(path);
if (!f.exists()) {
return null;
}
int len = (int) f.length();
byte[] buf = new byte[len];
FileInputStream inp = null;
try {
inp = new FileInputStream(path);
int nbread = inp.read(buf);
if (nbread != len) {
throw new IOException(nbread + " read, " + len + " length");
}
return new String(buf);
} finally {
safeClose(inp);
}
}
private static void write(String path, String contents) throws Exception {
FileOutputStream out = null;
try {
out = new FileOutputStream(path);
out.write(contents.getBytes("8859_1"));
} finally {
safeClose(out);
}
}
/**
* TAG_NOTHING means that the ConstantPool Entry is invalid.
**/
public static final int TAG_NOTHING = -1;
/**
* TAG_UTF8 = CONSTANT_UTF8
**/
public static final int TAG_UTF8 = 1;
/**
* TAG_INTEGER = CONSTANT_INTEGER
**/
public static final int TAG_INTEGER = 3;
/**
* TAG_FLOAT = CONSTANT_FLOAT
**/
public static final int TAG_FLOAT = 4;
/**
* TAG_LONG = CONSTANT_LONG
**/
public static final int TAG_LONG = 5;
/**
* TAG_DOUBLE = CONSTANT_DOUBLE
**/
public static final int TAG_DOUBLE = 6;
/**
* TAG_CLASS = CONSTANT_CLASS
**/
public static final int TAG_CLASS = 7;
/**
* TAG_STRING = CONSTANT_STRING
**/
public static final int TAG_STRING = 8;
/**
* TAG_FIELDREF = CONSTANT_FIELDREF
**/
public static final int TAG_FIELDREF = 9;
/**
* TAG_METHODREF = CONSTANT_METHODREF
**/
public static final int TAG_METHODREF = 10;
/**
* TAG_INTERFACEREF = CONSTANT_INTERFACEREF
**/
public static final int TAG_INTERFACEREF = 11;
/**
* TAG_NAMETYPE = CONSTANT_NAMETYPE
**/
public static final int TAG_NAMETYPE = 12;
/**
* A collection of string entries associated with a class in a URL
*
* @author fkieviet
*/
private class TextEntry {
private String jarURL;
private String classname;
private String text;
private String id;
private String content;
/**
* @param jarurl jar
* @param classname classname
* @param text String
*/
public TextEntry(String jarurl, String classname, String text) {
jarURL = jarurl;
this.classname = classname;
this.text = text;
}
/**
* Getter for classname
*
* @return String
*/
public String getClassname() {
return classname;
}
/**
* Setter for classname
*
* @param classname StringThe classname to set.
*/
public void setClassname(String classname) {
this.classname = classname;
}
/**
* Getter for jarURL
*
* @return String
*/
public String getJarURL() {
return jarURL;
}
/**
* Getter for strings
*
* @return List
*/
public String getText() {
return text;
}
/**
* @return ID
*/
public String getID() {
if (id == null) {
Matcher m = splitter.matcher(text);
if (!m.matches()) {
throw new RuntimeException("String not match pattern: " + text);
}
id = m.group(1);
content = m.group(3);
}
return id;
}
/**
* @return content
*/
public String getContent() {
if (content == null) {
getID();
}
return content;
}
/**
* @param s to test
* @return true if matches
*/
public boolean matches(String s) {
Matcher m = splitter.matcher(s);
return m.matches();
}
}
private static boolean isArchive(ZipEntry entry) {
for (int i = 0; i < mArchiveExtensions.length; i++) {
if (entry.getName().endsWith(mArchiveExtensions[i])) {
return true;
}
}
return false;
}
private static String toUnixEols(String xx)
//delete CR chars
{
if (xx != null) {
return xx.replaceAll("\r", "");
}
return xx;
}
private static boolean isUnixEol(String xx)
//true if string has no CR chars
{
if (xx == null) return false;
return !(xx.contains("\r"));
}
private static String[] mArchiveExtensions = new String[] {".jar", ".zip", ".nbm", ".war", ".ear", ".rar", ".sar"};
private void readDir(List list, String root, String currentPath, boolean recurse) throws Exception {
File[] entries = new File(currentPath).listFiles();
for (int i = 0; i < entries.length; i++) {
if (entries[i].isDirectory() && recurse) {
readDir(list, root, entries[i].getAbsolutePath(), recurse);
} else {
if (entries[i].getName().endsWith(".class")) {
InputStream inp = null;
try {
inp = new FileInputStream(entries[i]);
List strings = readStrings(inp);
for (Iterator iter = strings.iterator(); iter.hasNext();) {
String s = iter.next();
Matcher m = splitter.matcher(s);
if (m.matches()) {
list.add(new TextEntry(currentPath,
entries[i].getAbsolutePath().substring(root.length() + 1), s));
}
}
} catch (Exception ex) {
throw new Exception("Inspection failed of " + entries[i].getName() + ": " + ex, ex);
} finally {
safeClose(inp);
}
}
}
}
}
/**
* @param list List
* @param jar jar to explore
* @param currentPath Path prefix
* @throws Exception on fault
*/
public void readJar(List list, InputStream jar, String currentPath) throws Exception {
ZipInputStream inp = new ZipInputStream(jar);
for (;;) {
ZipEntry entry = inp.getNextEntry();
if (entry == null) {
break;
} else if (entry.isDirectory()) {
// Ignore
} else if (isArchive(entry)) {
String path = currentPath + entry.getName() + "#/";
readJar(list, inp, path);
} else {
if (entry.getName().endsWith(".class")) {
try {
List strings = readStrings(inp);
for (Iterator iter = strings.iterator(); iter.hasNext();) {
String s = iter.next();
Matcher m = splitter.matcher(s);
if (m.matches()) {
list.add(new TextEntry(currentPath, entry.getName(), s));
}
}
} catch (Exception ex) {
throw new Exception("Inspection failed of " + entry.getName() + ": " + ex, ex);
}
}
}
}
}
/**
* Reads all strings from the constant pool of a class file
*
* @param s class file
* @return list of strings
* @throws Exception on failure
*/
public static List readStrings(InputStream s) throws Exception {
DataInputStream inp = new DataInputStream(s);
// Sentinel
int magic = inp.readInt();
if (magic != 0xCAFEBABE) {
throw new Exception("Invalid Magic Number");
}
// Version
// short minorVersion =
inp.readShort();
// short majorVersion =
inp.readShort();
// Constant pool
int nEntries = inp.readShort();
List strings = new ArrayList();
for (int i = 1; i < nEntries; i++) {
byte tagByte = inp.readByte();
switch(tagByte) {
case TAG_UTF8 :
String utfString = inp.readUTF();
strings.add(utfString);
break;
case TAG_INTEGER:
// int intValue =
inp.readInt();
break;
case TAG_FLOAT:
// float floatValue =
inp.readFloat();
break;
case TAG_LONG:
// long longValue =
inp.readLong();
// Long takes two ConstantPool Entries.
i++;
break;
case TAG_DOUBLE:
// double doubleValue =
inp.readDouble();
// Double takes two ConstantPool Entries.
i++;
break;
case TAG_CLASS: {
// int classIndex =
inp.readShort();
break;
}
case TAG_STRING:
// int stringIndex =
inp.readShort();
// TODO
break;
case TAG_FIELDREF: {
// int classIndex =
inp.readShort();
// int nameType =
inp.readShort();
break;
}
case TAG_METHODREF: {
// int classIndex =
inp.readShort();
// int nameType =
inp.readShort();
break;
}
case TAG_INTERFACEREF: {
// int classIndex =
inp.readShort();
// int nameType =
inp.readShort();
break;
}
case TAG_NAMETYPE: {
// int nameIndex =
inp.readShort();
// int descIndex =
inp.readShort();
break;
}
default:
throw new Exception("Unknown tagbyte " + tagByte
+ " in constant pool (entry " + i + " out of " + nEntries + ")");
}
}
return strings;
}
/**
* Getter for dir
*
* @return String
*/
public File getDir() {
return classesDir;
}
/**
* Setter for dir
*
* @param dir StringThe dir to set.
*/
public void setDir(File dir) {
this.classesDir = dir;
}
/**
* Getter for file
*
* @return String
*/
public File getFile() {
return targetBundle;
}
/**
* Setter for file
*
* @param file StringThe file to set.
*/
public void setFile(File file) {
this.targetBundle = file;
}
/**
* Getter for prefix
*
* @return String
*/
public String getPrefix() {
return prefix;
}
/**
* Setter for prefix
*
* @param prefix StringThe prefix to set.
*/
public void setPrefix(String prefix) {
this.prefix = prefix;
}
/**
* Getter for strict
*
* @return boolean
*/
public boolean getStrict() {
return strict;
}
/**
* Setter for strict
*
* @param strict booleanThe strict to set.
*/
public void setStrict(boolean strict) {
this.strict = strict;
}
/**
* Getter for prefixU
*
* @return String
*/
public String getPrefixU() {
return prefixU;
}
/**
* Setter for prefixU
*
* @param prefixU StringThe prefixU to set.
*/
public void setPrefixU(String prefixU) {
this.prefixU = prefixU;
}
/**
* Getter for pattern
*
* @return String
*/
public String getPattern() {
return pattern;
}
/**
* Setter for pattern
*
* @param pattern StringThe pattern to set.
*/
public void setPattern(String pattern) {
this.pattern = pattern;
}
/**
*
*
* @param
* @return
* @exception
* @see
*/
public void addDirset(DirSet set) {
dirSets.add(set);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy