/** * eAdventure (formerly <e-Adventure> and <e-Game>) is a research project of the * <e-UCM> research group. * * Copyright 2005-2010 <e-UCM> research group. * * You can access a list of all the contributors to eAdventure at: * http://e-adventure.e-ucm.es/contributors * * <e-UCM> is a research group of the Department of Software Engineering * and Artificial Intelligence at the Complutense University of Madrid * (School of Computer Science). * * C Profesor Jose Garcia Santesmases sn, * 28040 Madrid (Madrid), Spain. * * For more info please visit: <http://e-adventure.e-ucm.es> or * <http://www.e-ucm.es> * * **************************************************************************** * * This file is part of eAdventure, version 2.0 * * eAdventure is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * eAdventure is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with eAdventure. If not, see <http://www.gnu.org/licenses/>. */ package es.eucm.ead.editor.util.i18n; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; /** * Automatically creates and updates R.java (for resources) and Messages.java * (for internationalized strings) files used for internationalization (I18N). * The R classes contain generated maps of all available resources in the * classpath, and should be regenerated as part of the release process. This * method is much quicker than scanning jar-files at execution time. * Messages.java files have a similar structure and workflow. * * R.java files imitate a similar mechanism developed for Android applications; * you can read more about it at * http://developer.android.com/guide/topics/resources/accessing-resources.html */ public class ResourceCreator { private static final Logger log = Logger.getLogger(ResourceCreator.class .getName()); private static final String eol = System.getProperty("line.separator"); private static final String utf8 = "UTF-8"; /** * Generate the R.java file with the 'R' class for the given project and * package * * @param args * project URL: the location of the project for which the R file * must be generated * packageName: the name of the main package in * the project */ public static void main(String[] args) throws IOException { String regenName = ResourceCreator.class.getCanonicalName(); if (args.length < 3 || args.length > 4 || (args.length > 0 && args[0].equals("-h"))) { log .log( Level.SEVERE, "Syntax: java -cp <classpath> {0}" + " <project-location> <package-name> <license-file> [<source-location>]\n" + "Where \n" + " classpath - " + "the classpath you are using now\n" + " project-location - " + "location of the project whose resources you want to index\n" + " license-file - " + "name of the file with the license you want to pre-pend\n" + " package-name - " + "name of the package where R.java / Messages.java files should be generated\n" + " source-location - " + "location for resulting R.java / Messages.java files; if absent, stdout is used\n", regenName); System.exit(-1); } String projectURL = args[0]; // .../eadventure.editor-engine String packageName = args[1]; // ead.editor String licenseFileName = args[2]; // etc/LICENSE.txt PrintStream out = (args.length == 3) ? System.out : new PrintStream( args[3], utf8); String importName = ResourceCreator.class.getCanonicalName().replace( ResourceCreator.class.getSimpleName(), "I18N"); // Expect a project-location/src/main/resources folder File resources = new File(projectURL + File.separator + "src" + File.separator + "main" + File.separator + "resources"); // build a paramString to include in class comment StringBuilder sb = new StringBuilder(); for (String s : args) { sb.append(" \"").append(s).append("\""); } String parameterString = sb.toString(); // write single R file log.log(Level.FINE, "\tProcessing resources (from {0})", resources); printLicense(licenseFileName, out); out.println(resourceFileContents(packageName, importName, regenName, parameterString, resources)); if (out != System.out) { out.close(); } // find each Messages.properties file, and generate a mirror // Messages.java file log.fine("\tProcessing messages..."); for (File propsFile : new FileFinder(resources, "Messages.properties")) { String p = propsFile.getPath(); File outputFile = new File(p.replace( "main" + File.separator + "resources", "main" + File.separator + "java").replace(".properties", ".java")); String startOfPackage = "main" + File.separator + "resources"; String truePackage = p.substring( p.indexOf(startOfPackage) + startOfPackage.length() + 1, p.indexOf(propsFile.getName()) - 1).replace(File.separator, "."); log.log(Level.FINE, "\t{0} (from {1})", new Object[] { truePackage, propsFile }); out = (args.length == 3) ? System.out : new PrintStream(outputFile, utf8); // write this Messages file printLicense(licenseFileName, out); out.println(messageFileContents(truePackage, importName, regenName, parameterString, propsFile)); if (out != System.out) { out.close(); } } } private static class FileFinder implements Iterable<File> { private final ArrayList<File> found = new ArrayList<File>(); public FileFinder(File dir, String name) { find(dir, name); } private void find(File dir, String name) { for (File f : dir.listFiles()) { if (f.isFile() && f.getName().equals(name)) { found.add(f); } else if (f.isDirectory()) { find(f, name); } } } @Override public Iterator<File> iterator() { return found.iterator(); } } private static String resourceFileContents(String packageName, String importName, String regenName, String parameterString, File resources) { return "package " + packageName + ";" + eol + eol + "import java.util.Set;" + eol + "import java.util.TreeSet;" + eol + eol + "import " + importName + ";" + eol + eol + "/**" + eol + " * Resource index for this package (statically compiled)." + eol + " *" + eol + " * This is an AUTOMATICALLY-GENERATED file - " + eol + " * Run class " + regenName + " with suitable parameters" + eol + " * to re-create or update this class" + eol + " */" + eol + "@edu.umd.cs.findbugs.annotations.SuppressFBWarnings" + eol + "public class R {" + eol + eol + createResourceContents(resources, "Drawable") + "}" + eol; } private static String messageFileContents(String packageName, String importName, String regenName, String parameterString, File resource) { return "package " + packageName + ";" + eol + eol + "import " + importName + ";" + eol + eol + "/**" + eol + " * Message index for this class (bound at run-time according to user preferences)" + eol + " *" + eol + " * This is an AUTOMATICALLY-GENERATED file - " + eol + " * Run class " + regenName + " with suitable parameters" + eol + " * to re-create or update this class" + eol + " */" + eol + "@edu.umd.cs.findbugs.annotations.SuppressFBWarnings" + eol + "public class Messages {" + eol + eol + createMessageContents(resource) + "}" + eol; } /** * Writes license-file contents into a PrintStream */ private static void printLicense(String fileName, PrintStream out) { File f = new File(fileName); BufferedReader br = null; try { out.println("/**"); br = new BufferedReader(new InputStreamReader( new FileInputStream(f), "UTF-8")); while (br.ready()) { String read = trimRight(br.readLine()); out.println((read.isEmpty() ? " *" : " * ") + read); } out.println(" */"); out.println(); } catch (IOException e) { log.log(Level.SEVERE, "Error adding license from '{0}'", f .getAbsolutePath()); log.log(Level.SEVERE, "Exception is", e); } finally { try { if (br != null) { br.close(); } } catch (IOException e) { log.log(Level.SEVERE, "Close exception is", e); } } } private static String trimRight(String s) { boolean onlySpacesFound = true; int lastChar = 0; int pos = 0; for (char c : s.toCharArray()) { if (!Character.isSpaceChar(c)) { onlySpacesFound = false; lastChar = pos; } pos++; } return onlySpacesFound ? "" : s.substring(0, lastChar + 1); } /** * Write resource-list into R class. Notice that "qualifiers" (-something * extensions in the leading directories) are ignored, to allow for * internationalization. Examples: * <ul> * <li>drawable/EditorIcon16x16_bw.png becomes * EditorIcon16x16_bw_png </li> * <li>drawable/SplashScreenLogo.png becomes * SplashScreenLogo.png </li> * <li>drawable-es_ES/SplashScreenLogo.png becomes * es_ES/SplashScreenLogo_png </li> * <li>drawable/conditions/vars.png becomes * conditions__vars_png </li> * <li>drawable-es_ES/SplashScreenLogo.png becomes * es_ES/conditions__vars_png * </li> * </ul> * * @param location * the location of the resources * @param className * the name of the new class * @return A string with the full definition of the sub-class */ private static String createResourceContents(File location, final String className) { StringBuilder classContent = new StringBuilder("\tpublic static class " + className + " {" + eol); FileFilter ff = new FileFilter() { @Override public boolean accept(File file) { return file.getName().startsWith(className.toLowerCase()); } }; Set<String> files = new TreeSet<String>(); for (File resources : location.getAbsoluteFile().listFiles(ff)) { String localeString = resources.getName().contains("-") ? resources .getName().replaceAll(".*[-]", "") + "//" : ""; for (File file : resources.listFiles()) { if (file.getName().startsWith(".")) { // ignore . and .. } else if (file.isDirectory()) recursive(files, file.getName() + File.separator, file); else { files.add(localeString + file.getName()); } } } Set<String> res = new TreeSet<String>(); for (String resource : files) { // removes locales resource = resource.replaceAll(".*[/][/]", ""); if (!resource.matches("^[a-zA-Z0-9_/]+[.][a-zA-Z0-9_]+$")) { log .log( Level.WARNING, "Sorry, '{0}'' has an invalid name. \n" + "\tPlease avoid spaces and any non-alphanumeric characters, " + "such as ''-+'' or '':''; ''_'' is ok, though", resource); } else if (resource.matches(".*[_][_].*")) { log.log(Level.WARNING, "Sorry, '{0}' has an invalid name. \n" + "\tPlease avoid two ''__'' in a row; " + "we use it for '/'-substitution", resource); } else { resource = resource.replaceAll("/", "__").replace(".", "_"); if (!res.contains(resource)) { classContent.append("\t\tpublic static String ").append( resource).append(";").append(eol); res.add(resource); } } } classContent.append(eol).append("\t\tstatic {").append(eol).append( "\t\t\tSet<String> files = new TreeSet<String>();").append(eol) .append(eol); for (String file : files) { classContent.append("\t\t\tfiles.add(\"").append( file.replaceAll("[/][/]", "/")).append("\");").append(eol); } classContent.append(eol).append( "\t\t\tI18N.initializeResources(Drawable.class.getName()," + " Drawable.class, files);").append(eol).append( "\t\t}").append(eol).append("\t}").append(eol); return classContent.toString(); } /** * Write message-list into Messages class. Only messages that exist in the * default language are included. * * @param location * the location of the source .properties file * @param className * the name of the new class * @return A string with the full definition of the sub-class */ private static String createMessageContents(File location) { Properties properties = new Properties(); try { properties.load(new InputStreamReader( new FileInputStream(location), utf8)); } catch (IOException e) { log.log(Level.SEVERE, "Sorry, '{0}' is not a valid properties file:" + "\n\t{1}\n", new Object[] { location, e.getMessage() }); log.log(Level.SEVERE, "Exception is ", e); return "ERROR GENERATING FROM " + location; } ArrayList<String> keys = new ArrayList<String>(); for (Object o : properties.keySet()) { keys.add(o.toString()); } Collections.sort(keys); StringBuilder classContent = new StringBuilder(); for (String key : keys) { classContent.append("\tpublic static String ").append(key).append( ";").append(eol); } classContent.append(eol).append("\tstatic {").append(eol).append( "\t\tI18N.initializeMessages(Messages.class.getName()," + " Messages.class);").append(eol).append("\t}") .append(eol); return classContent.toString(); } /** * Recursive method to visit all the sub-folders in the resource structure. * * @param files * @param currentPath * @param currentDir */ private static void recursive(Set<String> files, String currentPath, File currentDir) { for (File file : currentDir.listFiles()) { if (file.isDirectory()) recursive(files, currentPath + File.separator, file); else { files.add(currentPath + file.getName()); } } } }