/*---------------- FILE HEADER ------------------------------------------
This file is part of deegree.
Copyright (C) 2001 by:
EXSE, Department of Geography, University of Bonn
http://www.giub.uni-bonn.de/exse/
lat/lon GmbH
http://www.lat-lon.de
It has been implemented within SEAGIS - An OpenSource implementation of OpenGIS specification
(C) 2001, Institut de Recherche pour le D�veloppement (http://sourceforge.net/projects/seagis/)
SEAGIS Contacts: Surveillance de l'Environnement Assist�e par Satellite
Institut de Recherche pour le D�veloppement / US-Espace
mailto:seasnet@teledetection.fr
This library 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 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Contact:
Andreas Poth
lat/lon GmbH
Aennchenstr. 19
53115 Bonn
Germany
E-Mail: poth@lat-lon.de
Klaus Greve
Department of Geography
University of Bonn
Meckenheimer Allee 166
53115 Bonn
Germany
E-Mail: klaus.greve@uni-bonn.de
---------------------------------------------------------------------------*/
package org.deegree.model.csct.resources;
// Collections
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.text.MessageFormat;
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.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
/**
* Resources compiler. This class is run from the command-line at compile-time only.
* <code>ResourceCompiler</code> scan for <code>.properties</code> files and copy their
* content to <code>.utf</code> files using UTF8 encoding. It also check for key validity
* (making sure that the same set of keys is defined in every language) and change values
* for {@link MessageFormat} compatibility. Lastly, it create a <code>ResourceKeys.java</code>
* source file declaring resource keys as integer constants.
* <br><br>
* <code>ResourceCompiler</code> and all <code>ResourceKeys</code> classes don't need to be
* included in the final JAR file. They are used at compile-time only and no other classes
* should keep reference to them.
*
* @version 1.0
* @author Martin Desruisseaux
*/
final class ResourceCompiler implements Comparator
{
/**
* Special order for resource keys starting
* with the specified prefix.
*/
private static final String[] ORDER=
{
"ERROR_"
};
/**
* The class name for the interfaces to be generated.
*/
private static final String CLASS_NAME = "ResourceKeys";
/**
* Extension for properties files.
*/
private static final String PROPERTIES_EXT = ".properties";
/**
* Extension for resources files.
*/
private static final String RESOURCES_EXT = ".utf";
/**
* Prefix for argument count in resource key names.
* For example a resource expecting one argument may
* has a key name like "HELLO_$1".
*/
private static final String ARGUMENT_COUNT_PREFIX = "_$";
/**
* Integer IDs allocated to resource keys. This map
* use <code><Integer,String></code> entries.
*/
private final Map allocatedIDs = new HashMap();
/**
* Resources keys and their localized values. This map
* use <code><String,String></code> entries.
*/
private final Map resources = new HashMap();
/**
* Construct a new <code>ResourceCompiler</code>. This method will
* immediately looks for a <code>ResourceKeys.class</code> file. If
* one is found, integer keys are loaded in order to reuse same values.
*
* @param directory The resource directory. This directory should or
* will contains the following input and output files:
* <ul>
* <li><code>resources*.properties</code> (mandatory input)</li>
* <li><code>ResourceKeys.class</code> (optional input)</li>
* <li><code>resources*.utf</code> (output)</li>
* <li><code>ResourceKeys.class</code> (output)</li>
* </ul>
*
* @throws IOException if an input/output operation failed.
*/
private ResourceCompiler(final File directory) throws IOException
{
try
{
String classname;
classname = toRelative(new File(directory, CLASS_NAME));
classname = classname.replace(File.separatorChar, '.');
final Field[] fields = Class.forName(classname).getFields();
/*
* Copy all fields into {@link #allocatedIDs} map.
*/
Field.setAccessible(fields, true);
for (int i=fields.length; --i>=0;)
{
final Field field = fields[i];
final String key = field.getName();
try
{
final Object ID = field.get(null);
if (ID instanceof Integer)
{
allocatedIDs.put( ID, key);
}
}
catch (IllegalAccessException exception)
{
warning(toSourceFile(classname), key, "Access denied", exception);
}
}
}
catch (ClassNotFoundException exception)
{
// 'ResourceKeys.class' doesn't exists. This is okay (probably normal).
// We will create 'ResourceKeys.java' later using automatic key values.
}
}
/**
* Scan the specified directory and all subdirectory for resources.
*
* @param directory The root directory.
* @throws IOException if an input/output operation failed.
*/
private static void scanForResources(final File directory) throws IOException
{
ResourceCompiler compiler = null;
final File[] content = directory.listFiles();
for (int i=0; i<content.length; i++)
{
final File file = content[i];
if (file.isDirectory())
{
scanForResources(file);
continue;
}
if (file.getName().endsWith(PROPERTIES_EXT))
{
if (compiler==null)
{
compiler = new ResourceCompiler(directory);
}
compiler.loadPropertyFile(file);
String path = file.getPath();
path = path.substring(0, path.length()-PROPERTIES_EXT.length())+RESOURCES_EXT;
compiler.writeUTFFile(new File(path));
}
}
if (compiler!=null)
{
compiler.writeJavaSource(new File(directory, CLASS_NAME+".java"));
}
}
/**
* Load all properties from a <code>.properties</code> file. Resource keys are
* checked for naming convention (i.e. resources expecting some arguments must
* have a key ending with "_$n" where "n" is the number of arguments). This
* method transform resource values in legal {@link MessageFormat} patterns
* when necessary.
*
* @param file Resource file to read.
* @throws IOException if an input/output operation failed.
*/
private void loadPropertyFile(final File file) throws IOException
{
final InputStream input=new FileInputStream(file);
final Properties properties=new Properties();
properties.load(input);
input.close();
final boolean hasBug = System.getProperty("java.version", "").startsWith("1.3");
resources.clear();
for (final Iterator it=properties.entrySet().iterator(); it.hasNext();)
{
final Map.Entry entry = (Map.Entry) it.next();
final String key = (String) entry.getKey();
final String value = (String) entry.getValue();
/*
* Check key and value validity.
*/
if (key.trim().length()==0)
{
warning(file, key, "Empty key.", null);
continue;
}
if (value.trim().length()==0)
{
warning(file, key, "Empty value.", null);
continue;
}
/*
* Check if the resource value is a legal MessageFormat pattern.
*/
final MessageFormat message;
try
{
message = new MessageFormat(toMessageFormatString(value));
}
catch (IllegalArgumentException exception)
{
warning(file, key, "Bad resource value", exception);
continue;
}
/*
* Check if the expected arguments count (according naming convention)
* matches the arguments count found in the MessageFormat's pattern.
*/
final int argumentCount;
final int index = key.lastIndexOf(ARGUMENT_COUNT_PREFIX);
if (index<0)
{
argumentCount = 0;
resources.put(key, value);
}
else try
{
argumentCount = Integer.parseInt(key.substring(index+ARGUMENT_COUNT_PREFIX.length()));
resources.put(key, message.toPattern());
}
catch (NumberFormatException exception)
{
warning(file, key, "Bad number in resource key", exception);
continue;
}
final int expected=message.getFormats().length;
if (argumentCount!=expected)
{
if (!hasBug) // Work around for bug in JDK 1.3 (fixed in JDK 1.4).
{
warning(file, key, "Key name should ends with \""+ARGUMENT_COUNT_PREFIX+expected+"\".", null);
continue;
}
}
}
/*
* Finished loading properties. Now, check if some keys are missing.
*/
if (!allocatedIDs.isEmpty())
{
final Set missing = new HashSet(allocatedIDs.values());
missing.removeAll(resources.keySet());
for (final Iterator it=missing.iterator(); it.hasNext();)
{
final String key = (String) it.next();
warning(file, key, "Key defined in previous languages is missing in current one.", null);
}
// Second check
missing.clear();
missing.addAll(resources.keySet());
missing.removeAll(allocatedIDs.values());
for (final Iterator it=missing.iterator(); it.hasNext();)
{
final String key = (String) it.next();
warning(file, key, "Key was not defined in previous languages.", null);
}
}
/*
* Allocate an ID for each new keys.
*/
final String[] keys = (String[]) resources.keySet().toArray(new String[resources.size()]);
Arrays.sort(keys, this);
int freeID = 0;
for (int i=0; i<keys.length; i++)
{
final String key = keys[i];
if (!allocatedIDs.containsValue(key))
{
Integer ID;
while (allocatedIDs.containsKey(ID=new Integer(freeID++)));
allocatedIDs.put(ID, key);
}
}
}
/**
* Write UTF file. Method {@link #loadPropertyFile} should
* be invoked before to <code>writeUTFFile</code>.
*
* @param file The destination file.
* @throws IOException if an input/output operation failed.
*/
private void writeUTFFile(final File file) throws IOException
{
final int count = allocatedIDs.isEmpty() ? 0 : ((Integer) Collections.max(allocatedIDs.keySet())).intValue()+1;
final DataOutputStream out=new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
out.writeInt(count);
for (int i=0; i<count; i++)
{
final String value = (String) resources.get(allocatedIDs.get(new Integer(i)));
out.writeUTF((value!=null) ? value : "");
}
out.close();
}
/**
* Returns the source file for the specified class.
*/
private static File toSourceFile(final String classname)
{return new File(classname.replace('.','/')+".java");}
/**
* Returns the class name for the specified source file.
* The returned class name to not include package name.
*/
private static String toClassName(final File file)
{
String name = file.getName();
final int index = name.lastIndexOf('.');
if (index>=0) name = name.substring(0, index);
return name;
}
/**
* Make a file path relative to the classpath. The file path may be
* relative (to current <code>chdir</code>) or absolute. This method
* find the canonical form of <code>path</code> and compare it with
* canonical forms of every paths in the class path. If a classpath
* matchs the begining of <code>path</code>, then the corresponding
* part of <code>path</code> is removed. If there is more than one
* matches, the one resulting in the shortest relative path is choosen.
*/
private static String toRelative(final File path) throws IOException
{
String bestpath = null;
final String absolutePath = path.getCanonicalPath();
final String fileSeparator = System.getProperty("file.separator", "/");
final StringTokenizer tokr = new StringTokenizer(System.getProperty("java.class.path", "."),
System.getProperty("path.separator", ":"));
while (tokr.hasMoreTokens())
{
String classpath = new File(tokr.nextToken()).getCanonicalPath();
if (!classpath.endsWith(fileSeparator)) classpath+=fileSeparator;
if (absolutePath.startsWith(classpath))
{
final String candidate = absolutePath.substring(classpath.length());
if (bestpath==null || bestpath.length()>candidate.length())
{
// Choose the shortest path.
bestpath = candidate;
}
}
}
return (bestpath!=null) ? bestpath : path.getPath();
}
/**
* Change a "normal" text string into a pattern compatible with {@link MessageFormat}.
* The main operation consist in changing ' for '', except for '{' and '}' strings.
*/
private static String toMessageFormatString(final String text)
{
int level = 0;
int last = -1;
final StringBuffer buffer = new StringBuffer(text);
search: for (int i=0; i<buffer.length(); i++) // Length of 'buffer' will vary.
{
switch (buffer.charAt(i))
{
/*
* Les accolades ouvrantes et fermantes nous font monter et descendre
* d'un niveau. Les guillemets ne seront doubl�s que si on se trouve
* au niveau 0. Si l'accolade �tait entre des guillemets, il ne sera
* pas pris en compte car il aura �t� saut� lors du passage pr�c�dent
* de la boucle.
*/
case '{' : level++; last=i; break;
case '}' : level--; last=i; break;
case '\'':
{
/*
* Si on d�tecte une accolade entre guillemets ('{' ou '}'),
* on ignore tout ce bloc et on continue au caract�re qui
* suit le guillemet fermant.
*/
if (i+2<buffer.length() && buffer.charAt(i+2)=='\'')
{
switch (buffer.charAt(i+1))
{
case '{': i+=2; continue search;
case '}': i+=2; continue search;
}
}
if (level<=0)
{
/*
* Si nous n'�tions pas entre des accolades,
* alors il faut doubler les guillemets.
*/
buffer.insert(i++, '\'');
continue search;
}
/*
* Si on se trouve entre des accolades, on ne doit normalement pas
* doubler les guillemets. Toutefois, le format {0,choice,...} est
* une exception.
*/
if (last>=0 && buffer.charAt(last)=='{')
{
int scan=last;
do if (scan>=i) continue search;
while (Character.isDigit(buffer.charAt(++scan)));
final String choice=",choice,";
final int end=scan+choice.length();
if (end<buffer.length() && buffer.substring(scan, end).equalsIgnoreCase(choice))
{
buffer.insert(i++, '\'');
continue search;
}
}
}
}
}
return buffer.toString();
}
/**
* Print a message to the error output stream {@link System#err}.
*
* @param file File that produced the error, or <code>null</code> if none.
* @param key Resource key that produced the error, or <code>null</code> if none.
* @param message The message string.
* @param exception An optional exception that is the cause of this warning.
*/
private static void warning(final File file, final String key, final String message, final Exception exception)
{
System.out.flush();
System.err.print("ERROR ");
if (file!=null)
{
String filename = file.getPath();
if (filename.endsWith(PROPERTIES_EXT))
{
filename = filename.substring(0, filename.length()-PROPERTIES_EXT.length());
}
System.err.print('(');
System.err.print(filename);
System.err.print(')');
}
System.err.print(": ");
if (key!=null)
{
System.err.print('"');
System.err.print(key);
System.err.print('"');
}
System.err.println();
System.err.print(message);
if (exception!=null)
{
System.err.print(": ");
System.err.print(exception.getLocalizedMessage());
}
System.err.println();
System.err.println();
System.err.flush();
}
/**
* Write <code>count</code> spaces to the <code>out</code> stream.
* @throws IOException if an input/output operation failed.
*/
private static void writeWhiteSpaces(final Writer out, int count) throws IOException
{while (--count>=0) out.write(' ');}
/**
* Write a multi-lines text to the specified output stream. All
* occurences of '\r' will be replaced by the line separator for
* the underlying operating system.
*
* @param out The output stream.
* @param text The text to write.
* @throws IOException if an input/output operation failed.
*/
private static void writeMultiLines(final BufferedWriter out, final String text) throws IOException
{
final StringTokenizer tokr = new StringTokenizer(text, "\n");
while (tokr.hasMoreTokens())
{
out.write(tokr.nextToken());
out.newLine();
}
}
/**
* Create a source file for resources keys.
*
* @param file The destination file.
* @throws IOException if an input/output operation failed.
*/
private void writeJavaSource(final File file) throws IOException
{
final String packageName = toRelative(file.getParentFile()).replace(File.separatorChar, '.');
final BufferedWriter out = new BufferedWriter(new FileWriter(file));
writeMultiLines(out,
"/*\n" +
" * SEAGIS - An OpenSource implementation of OpenGIS specification\n" +
" * (C) 2001, Institut de Recherche pour le D�veloppement\n" +
" *\n" +
" * THIS IS AN AUTOMATICALLY GENERATED FILE. DO NOT EDIT!\n" +
" * Generated with: org.deegree.model.csct.resources.ResourceCompiler\n" +
" */\n");
out.write("package ");
out.write(packageName);
out.write(";");
out.newLine();
out.newLine();
out.newLine();
writeMultiLines(out,
"/**\n" +
" * Resource keys. This interface is used when compiling sources, but\n" +
" * no dependencies to <code>ResourceKeys</code> should appear in any\n" +
" * resulting class files. Since Java compiler inline final integers\n" +
" * values, using long identifiers will not bloat constant pools of\n" +
" * classes compiled against the interface, providing that no class\n" +
" * implements this interface.\n" +
" *\n" +
" * @see org.deegree.model.csct.resources.ResourceBundle\n" +
" * @see org.deegree.model.csct.resources.ResourceCompiler\n" +
" */\n");
out.write("public interface ");
out.write(toClassName(file));
out.newLine();
out.write('{');
out.newLine();
final Map.Entry[] entries = (Map.Entry[]) allocatedIDs.entrySet().toArray(new Map.Entry[allocatedIDs.size()]);
Arrays.sort(entries, this);
int maxLength=0;
for (int i=entries.length; --i>=0;)
{
final int length = ((String) entries[i].getValue()).length();
if (length>maxLength) maxLength=length;
}
for (int i=0; i<entries.length; i++)
{
final String key = (String) entries[i].getValue();
final String ID = entries[i].getKey().toString();
if (i!=0 && compare(entries[i-1], key)<-1)
{
out.newLine();
}
writeWhiteSpaces(out, 4);
out.write("public static final int ");
out.write(key);
writeWhiteSpaces(out, maxLength-key.length());
out.write(" = ");
writeWhiteSpaces(out, 5-ID.length());
out.write(ID);
out.write(";");
out.newLine();
}
out.write('}');
out.newLine();
out.close();
}
/**
* Compare two resource keys. Object <code>o1</code> and <code>o2</code>
* are usually {@link String} objects representing resource keys (for
* example "<code>MISMATCHED_DIMENSION</code>"). This method compares
* strings as of {@link String#compareTo}, except that string starting
* with one of the prefix enumetated in {@link #ORDER} will appear last
* in the sorted array.
*/
public int compare(Object o1, Object o2)
{
if (o1 instanceof Map.Entry) o1 = ((Map.Entry) o1).getValue();
if (o2 instanceof Map.Entry) o2 = ((Map.Entry) o2).getValue();
final String key1 = (String) o1;
final String key2 = (String) o2;
int i1=ORDER.length; while (--i1>=0) if (key1.startsWith(ORDER[i1])) break;
int i2=ORDER.length; while (--i2>=0) if (key2.startsWith(ORDER[i2])) break;
if (i1 < i2) return -2;
if (i1 > i2) return +2;
return XMath.sgn(key1.compareTo(key2));
}
/**
* Run the resources compilator.
*
* @param args Command-line arguments.
* @throws IOException if an input/output operation failed.
*/
public static void main(final String[] args) throws IOException
{
for (int i=0; i<args.length; i++)
{
scanForResources(new File(args[i]));
}
}
}