001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.resources;
018:
019: import java.io.*;
020: import java.util.*;
021: import java.text.MessageFormat;
022: import java.lang.reflect.Field;
023:
024: /**
025: * Resource compiler. This class is run from the command line at compile time only.
026: * {@code IndexedResourceCompiler} scans for {@code .properties} files and copies their content
027: * to {@code .utf} files using UTF8 encoding. It also checks for key validity and checks values
028: * for {@link MessageFormat} compatibility. Finally, it creates a {@code FooKeys.java} source
029: * file declaring resource keys as integer constants.
030: * <p>
031: * This class <strong>must</strong> be run from the maven root of Geotools project.
032: * <p>
033: * {@code IndexedResourceCompiler} and all {@code FooKeys} classes don't need to be included in the
034: * final JAR file. They are used at compile time only and no other classes should keep reference to
035: * them.
036: *
037: * @since 2.4
038: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/resources/IndexedResourceCompiler.java $
039: * @version $Id: IndexedResourceCompiler.java 26165 2007-07-06 17:02:26Z desruisseaux $
040: * @author Martin Desruisseaux
041: */
042: public final class IndexedResourceCompiler implements Comparator {
043: /**
044: * The base directory for {@code "java"} {@code "resources"} sub-directories.
045: * The directory structure must be consistent with Maven conventions.
046: *
047: * @see #sourceDirectory
048: */
049: private static final File SOURCE_DIRECTORY = new File(
050: "modules/library/metadata/src/main");
051:
052: /**
053: * The resources to process.
054: */
055: private static final Class[] RESOURCES_TO_PROCESS = {
056: org.geotools.resources.i18n.Descriptions.class,
057: org.geotools.resources.i18n.Vocabulary.class,
058: org.geotools.resources.i18n.Logging.class,
059: org.geotools.resources.i18n.Errors.class };
060:
061: /**
062: * Extension for properties source files.
063: * Must be in the {@code ${sourceDirectory}/java} directory.
064: */
065: private static final String PROPERTIES_EXT = ".properties";
066:
067: /**
068: * Extension for resource target files.
069: * Will be be in the {@code ${sourceDirectory}/resources} directory.
070: */
071: private static final String RESOURCES_EXT = ".utf";
072:
073: /**
074: * Prefix for argument count in resource key names. For example, a resource
075: * expecting one argument may have a key name like "HELLO_$1".
076: */
077: private static final String ARGUMENT_COUNT_PREFIX = "_$";
078:
079: /**
080: * The maximal length of comment lines.
081: */
082: private static final int COMMENT_LENGTH = 92;
083:
084: /**
085: * The base directory for {@code "java"} {@code "resources"} sub-directories.
086: * The directory structure must be consistent with Maven conventions.
087: */
088: private final File sourceDirectory;
089:
090: /**
091: * Integer IDs allocated to resource keys. This map will be shared for all languages
092: * of a given resource bundle.
093: */
094: private final Map/*<Integer,String>*/allocatedIDs = new HashMap();
095:
096: /**
097: * Resource keys and their localized values. This map will be cleared for each language
098: * in a resource bundle.
099: */
100: private final Map/*<String,String>*/resources = new HashMap();
101:
102: /**
103: * The output stream for printing message.
104: */
105: private final PrintWriter out;
106:
107: /**
108: * Constructs a new {@code IndexedResourceCompiler}. This method will immediately look for
109: * a {@code FooKeys.class} file. If one is found, integer keys are loaded in order to reuse
110: * the same values.
111: *
112: * @param sourceDirectory The base directory for {@code "java"} {@code "resources"}
113: * sub-directories. The directory structure must be consistent with Maven conventions.
114: * @param bundleClass The resource bundle base class
115: * (e.g. <code>{@linkplain org.geotools.resources.i18n.Vocabulary}.class}</code>).
116: * @param renumber {@code true} for renumbering all key values.
117: * @param out The output stream for printing message.
118: * @throws IOException if an input/output operation failed.
119: */
120: private IndexedResourceCompiler(final File sourceDirectory,
121: final Class bundleClass, final boolean renumber,
122: final PrintWriter out) throws IOException {
123: this .sourceDirectory = sourceDirectory;
124: this .out = out;
125: if (!renumber)
126: try {
127: final String classname = toKeyClass(bundleClass
128: .getName());
129: final Field[] fields = Class.forName(classname)
130: .getFields();
131: out.print("Loading ");
132: out.println(classname);
133: /*
134: * Copies all fields into {@link #allocatedIDs} map.
135: */
136: Field.setAccessible(fields, true);
137: for (int i = fields.length; --i >= 0;) {
138: final Field field = fields[i];
139: final String key = field.getName();
140: try {
141: final Object ID = field.get(null);
142: if (ID instanceof Integer) {
143: allocatedIDs.put((Integer) ID, key);
144: }
145: } catch (IllegalAccessException exception) {
146: final File source = new File(classname.replace(
147: '.', '/')
148: + ".class");
149: warning(source, key, "Access denied", exception);
150: }
151: }
152: } catch (ClassNotFoundException exception) {
153: /*
154: * 'VocabularyKeys.class' doesn't exist. This is okay (probably normal).
155: * We will create 'VocabularyKeys.java' later using automatic key values.
156: */
157: }
158: }
159:
160: /**
161: * Returns the class name for the keys. For example if {@code bundleClass} is
162: * {@code "org.geotools.resources.i18n.Vocabulary"}, then this method returns
163: * {@code "org.geotools.resources.i18n.VocabularyKeys"}.
164: */
165: private static String toKeyClass(String bundleClass) {
166: if (bundleClass.endsWith("s")) {
167: bundleClass = bundleClass.substring(0,
168: bundleClass.length() - 1);
169: }
170: return bundleClass + "Keys";
171: }
172:
173: /**
174: * Load the specified property file.
175: */
176: private static Properties loadPropertyFile(final File file)
177: throws IOException {
178: final InputStream input = new FileInputStream(file);
179: final Properties properties = new Properties();
180: properties.load(input);
181: input.close();
182: return properties;
183: }
184:
185: /**
186: * Loads all properties from a {@code .properties} file. Resource keys are checked for naming
187: * conventions (i.e. resources expecting some arguments must have a key name ending with
188: * {@code "_$n"} where {@code "n"} is the number of arguments). This method transforms resource
189: * values into legal {@link MessageFormat} patterns when necessary.
190: *
191: * @param file The properties file to read.
192: * @throws IOException if an input/output operation failed.
193: */
194: private void processPropertyFile(final File file)
195: throws IOException {
196: final Properties properties = loadPropertyFile(file);
197: resources.clear();
198: for (final Iterator it = properties.entrySet().iterator(); it
199: .hasNext();) {
200: final Map.Entry entry = (Map.Entry) it.next();
201: final String key = (String) entry.getKey();
202: final String value = (String) entry.getValue();
203: /*
204: * Checks key and value validity.
205: */
206: if (key.trim().length() == 0) {
207: warning(file, key, "Empty key.", null);
208: continue;
209: }
210: if (value.trim().length() == 0) {
211: warning(file, key, "Empty value.", null);
212: continue;
213: }
214: /*
215: * Checks if the resource value is a legal MessageFormat pattern.
216: */
217: final MessageFormat message;
218: try {
219: message = new MessageFormat(
220: toMessageFormatString(value));
221: } catch (IllegalArgumentException exception) {
222: warning(file, key, "Bad resource value", exception);
223: continue;
224: }
225: /*
226: * Checks if the expected arguments count (according to naming conventions)
227: * matches the arguments count found in the MessageFormat pattern.
228: */
229: final int argumentCount;
230: final int index = key.lastIndexOf(ARGUMENT_COUNT_PREFIX);
231: if (index < 0) {
232: argumentCount = 0;
233: resources.put(key, value); // Text will not be formatted using MessageFormat.
234: } else
235: try {
236: String suffix = key.substring(index
237: + ARGUMENT_COUNT_PREFIX.length());
238: argumentCount = Integer.parseInt(suffix);
239: resources.put(key, message.toPattern());
240: } catch (NumberFormatException exception) {
241: warning(file, key, "Bad number in resource key",
242: exception);
243: continue;
244: }
245: final int expected = message.getFormats().length;
246: if (argumentCount != expected) {
247: final String suffix = ARGUMENT_COUNT_PREFIX + expected;
248: warning(file, key, "Key name should ends with \""
249: + suffix + "\".", null);
250: continue;
251: }
252: }
253: /*
254: * Allocates an ID for each new key.
255: */
256: final String[] keys = (String[]) resources.keySet().toArray(
257: new String[resources.size()]);
258: Arrays.sort(keys, this );
259: int freeID = 0;
260: for (int i = 0; i < keys.length; i++) {
261: final String key = keys[i];
262: if (!allocatedIDs.containsValue(key)) {
263: Integer ID;
264: do {
265: ID = new Integer(freeID++);
266: } while (allocatedIDs.containsKey(ID));
267: allocatedIDs.put(ID, key);
268: }
269: }
270: }
271:
272: /**
273: * Write UTF file. Method {@link #processPropertyFile} should be invoked beforehand to
274: * {@code writeUTFFile}.
275: *
276: * @param file The destination file.
277: * @throws IOException if an input/output operation failed.
278: */
279: private void writeUTFFile(final File file) throws IOException {
280: final int count = allocatedIDs.isEmpty() ? 0
281: : ((Integer) Collections.max(allocatedIDs.keySet()))
282: .intValue() + 1;
283: final DataOutputStream out = new DataOutputStream(
284: new BufferedOutputStream(new FileOutputStream(file)));
285: out.writeInt(count);
286: for (int i = 0; i < count; i++) {
287: final String value = (String) resources.get(allocatedIDs
288: .get(new Integer(i)));
289: out.writeUTF((value != null) ? value : "");
290: }
291: out.close();
292: }
293:
294: /**
295: * Changes a "normal" text string into a pattern compatible with {@link MessageFormat}.
296: * The main operation consists of changing ' for '', except for '{' and '}' strings.
297: */
298: private static String toMessageFormatString(final String text) {
299: int level = 0;
300: int last = -1;
301: final StringBuffer buffer = new StringBuffer(text);
302: search: for (int i = 0; i < buffer.length(); i++) { // Length of 'buffer' will vary.
303: switch (buffer.charAt(i)) {
304: /*
305: * Left and right braces take us up or down a level. Quotes will only be doubled
306: * if we are at level 0. If the brace is between quotes it will not be taken into
307: * account as it will have been skipped over during the previous pass through the
308: * loop.
309: */
310: case '{':
311: level++;
312: last = i;
313: break;
314: case '}':
315: level--;
316: last = i;
317: break;
318: case '\'': {
319: /*
320: * If a brace ('{' or '}') is found between quotes, the entire block is
321: * ignored and we continue with the character following the closing quote.
322: */
323: if (i + 2 < buffer.length()
324: && buffer.charAt(i + 2) == '\'') {
325: switch (buffer.charAt(i + 1)) {
326: case '{':
327: i += 2;
328: continue search;
329: case '}':
330: i += 2;
331: continue search;
332: }
333: }
334: if (level <= 0) {
335: /*
336: * If we weren't between braces, we must double the quotes.
337: */
338: buffer.insert(i++, '\'');
339: continue search;
340: }
341: /*
342: * If we find ourselves between braces, we don't normally need to double
343: * our quotes. However, the format {0,choice,...} is an exception.
344: */
345: if (last >= 0 && buffer.charAt(last) == '{') {
346: int scan = last;
347: do
348: if (scan >= i)
349: continue search;
350: while (Character.isDigit(buffer.charAt(++scan)));
351: final String choice = ",choice,";
352: final int end = scan + choice.length();
353: if (end < buffer.length()
354: && buffer.substring(scan, end)
355: .equalsIgnoreCase(choice)) {
356: buffer.insert(i++, '\'');
357: continue search;
358: }
359: }
360: }
361: }
362: }
363: return buffer.toString();
364: }
365:
366: /**
367: * Prints a message to the output stream.
368: *
369: * @param file File that produced the error, or {@code null} if none.
370: * @param key Resource key that produced the error, or {@code null} if none.
371: * @param message The message string.
372: * @param exception An optional exception that is the cause of this warning.
373: */
374: private void warning(final File file, final String key,
375: final String message, final Exception exception) {
376: out.print("ERROR ");
377: if (file != null) {
378: String filename = file.getPath();
379: if (filename.endsWith(PROPERTIES_EXT)) {
380: filename = filename.substring(0, filename.length()
381: - PROPERTIES_EXT.length());
382: }
383: out.print('(');
384: out.print(filename);
385: out.print(')');
386: }
387: out.print(": ");
388: if (key != null) {
389: out.print('"');
390: out.print(key);
391: out.print('"');
392: }
393: out.println();
394: out.print(message);
395: if (exception != null) {
396: out.print(": ");
397: out.print(exception.getLocalizedMessage());
398: }
399: out.println();
400: out.println();
401: out.flush();
402: }
403:
404: /**
405: * Creates a source file for resource keys.
406: *
407: * @param bundleClass The resource bundle base class
408: * (e.g. <code>{@linkplain org.geotools.resources.i18n.Vocabulary}.class}</code>).
409: * @throws IOException if an input/output operation failed.
410: */
411: private void writeJavaSource(final Class bundleClass)
412: throws IOException {
413: final String fullname = toKeyClass(bundleClass.getName());
414: final int packageEnd = fullname.lastIndexOf('.');
415: final String packageName = fullname.substring(0, packageEnd);
416: final String classname = fullname.substring(packageEnd + 1);
417: final File file = new File(sourceDirectory, "java/"
418: + fullname.replace('.', '/') + ".java");
419: if (!file.getParentFile().isDirectory()) {
420: warning(file, null, "Parent directory not found.", null);
421: return;
422: }
423: final BufferedWriter out = new BufferedWriter(
424: new OutputStreamWriter(new FileOutputStream(file),
425: "UTF-8"));
426: out
427: .write("/*\n"
428: + " * GeoTools - OpenSource mapping toolkit\n"
429: + " * http://geotools.org\n"
430: + " * (C) 2003-2007, Geotools Project Managment Committee (PMC)\n"
431: + " * \n"
432: + " * This library is free software; you can redistribute it and/or\n"
433: + " * modify it under the terms of the GNU Lesser General Public\n"
434: + " * License as published by the Free Software Foundation; either\n"
435: + " * version 2.1 of the License, or (at your option) any later version.\n"
436: + " * \n"
437: + " * This library is distributed in the hope that it will be useful,\n"
438: + " * but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
439: + " * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n"
440: + " * Lesser General Public License for more details.\n"
441: + " * \n"
442: + " * THIS IS AN AUTOMATICALLY GENERATED FILE. DO NOT EDIT!\n"
443: + " * Generated with: org.geotools.resources.IndexedResourceCompiler\n"
444: + " */\n");
445: out.write("package ");
446: out.write(packageName);
447: out.write(";\n\n\n");
448: out
449: .write("/**\n"
450: + " * Resource keys. This class is used when compiling sources, but\n"
451: + " * no dependencies to {@code ResourceKeys} should appear in any\n"
452: + " * resulting class files. Since Java compiler inlines final integer\n"
453: + " * values, using long identifiers will not bloat constant pools of\n"
454: + " * classes compiled against the interface, provided that no class\n"
455: + " * implements this interface.\n"
456: + " *\n"
457: + " * @see org.geotools.resources.IndexedResourceBundle\n"
458: + " * @see org.geotools.resources.IndexedResourceCompiler\n"
459: + " * @source \u0024URL\u0024\n" + " */\n");
460: out.write("public final class ");
461: out.write(classname);
462: out.write(" {\n");
463: out.write(" private ");
464: out.write(classname);
465: out.write("() {\n");
466: out.write(" }\n");
467: final Map.Entry[] entries = (Map.Entry[]) allocatedIDs
468: .entrySet().toArray(new Map.Entry[allocatedIDs.size()]);
469: Arrays.sort(entries, this );
470: for (int i = 0; i < entries.length; i++) {
471: out.write('\n');
472: final String key = (String) entries[i].getValue();
473: final String ID = entries[i].getKey().toString();
474: String message = (String) resources.get(key);
475: if (message != null) {
476: out.write(" /**\n");
477: while (((message = message.trim()).length()) != 0) {
478: out.write(" * ");
479: int stop = message.length();
480: if (stop > COMMENT_LENGTH) {
481: stop = COMMENT_LENGTH;
482: while (stop > 20
483: && !Character.isWhitespace(message
484: .charAt(stop))) {
485: stop--;
486: }
487: }
488: out.write(message.substring(0, stop).trim());
489: out.write('\n');
490: message = message.substring(stop);
491: }
492: out.write(" */\n");
493: }
494: out.write(" public static final int ");
495: out.write(key);
496: out.write(" = ");
497: out.write(ID);
498: out.write(";\n");
499: }
500: out.write("}\n");
501: out.close();
502: }
503:
504: /**
505: * Compares two resource keys. Object {@code o1} and {@code o2} are usually {@link String}
506: * objects representing resource keys (for example, "{@code MISMATCHED_DIMENSION}"), but
507: * may also be {@link java.util.Map.Entry}.
508: */
509: public int compare(Object o1, Object o2) {
510: if (o1 instanceof Map.Entry)
511: o1 = ((Map.Entry) o1).getValue();
512: if (o2 instanceof Map.Entry)
513: o2 = ((Map.Entry) o2).getValue();
514: final String key1 = (String) o1;
515: final String key2 = (String) o2;
516: return key1.compareTo(key2);
517: }
518:
519: /**
520: * Scans the package for resources.
521: *
522: * @param sourceDirectory The base directory for {@code "java"} {@code "resources"}
523: * sub-directories. The directory structure must be consistent with Maven conventions.
524: * @param bundleClass The resource bundle base class
525: * (e.g. <code>{@linkplain org.geotools.resources.i18n.Vocabulary}.class}</code>).
526: * @param renumber {@code true} for renumbering all key values.
527: * @param out The output stream for printing message.
528: * @throws IOException if an input/output operation failed.
529: */
530: private static void scanForResources(final File sourceDirectory,
531: final Class bundleClass, final boolean renumber,
532: final PrintWriter out) throws IOException {
533: final String fullname = bundleClass.getName();
534: final int packageEnd = fullname.lastIndexOf('.');
535: final String packageName = fullname.substring(0, packageEnd);
536: final String classname = fullname.substring(packageEnd + 1);
537: final String packageDir = packageName.replace('.', '/');
538: final File srcDir = new File(sourceDirectory, "java/"
539: + packageDir);
540: final File utfDir = new File(sourceDirectory, "resources/"
541: + packageDir);
542: if (!srcDir.isDirectory()) {
543: out.print('"');
544: out.print(srcDir.getPath());
545: out.println("\" is not a directory.");
546: return;
547: }
548: if (!utfDir.isDirectory()) {
549: out.print('"');
550: out.print(utfDir.getPath());
551: out.println("\" is not a directory.");
552: return;
553: }
554: IndexedResourceCompiler compiler = null;
555: final File[] content = srcDir.listFiles();
556: File defaultLanguage = null;
557: for (int i = 0; i < content.length; i++) {
558: final File file = content[i];
559: final String filename = file.getName();
560: if (filename.startsWith(classname)
561: && filename.endsWith(PROPERTIES_EXT)) {
562: if (compiler == null) {
563: compiler = new IndexedResourceCompiler(
564: sourceDirectory, bundleClass, renumber, out);
565: }
566: compiler.processPropertyFile(file);
567: final String noExt = filename.substring(0, filename
568: .length()
569: - PROPERTIES_EXT.length());
570: final File utfFile = new File(utfDir, noExt
571: + RESOURCES_EXT);
572: compiler.writeUTFFile(utfFile);
573: if (noExt.equals(classname)) {
574: defaultLanguage = file;
575: }
576: }
577: }
578: if (compiler != null) {
579: if (defaultLanguage != null) {
580: compiler.resources.clear();
581: compiler.resources
582: .putAll(loadPropertyFile(defaultLanguage));
583: }
584: compiler.writeJavaSource(bundleClass);
585: }
586: }
587:
588: /**
589: * Run the resource compiler.
590: *
591: * @param args The command-line arguments.
592: * @param sourceDirectory The base directory for {@code "java"} {@code "resources"}
593: * sub-directories. The directory structure must be consistent with Maven conventions.
594: * @param resourcesToProcess The resource bundle base classes
595: * (e.g. <code>{@linkplain org.geotools.resources.i18n.Vocabulary}.class}</code>).
596: */
597: public static void main(String[] args, final File sourceDirectory,
598: final Class[] resourcesToProcess) {
599: final Arguments arguments = new Arguments(args);
600: final boolean renumber = arguments.getFlag("-renumber");
601: final PrintWriter out = arguments.out;
602: args = arguments.getRemainingArguments(0);
603: if (!sourceDirectory.isDirectory()) {
604: out.print(sourceDirectory);
605: out.println(" not found or is not a directory.");
606: return;
607: }
608: for (int i = 0; i < resourcesToProcess.length; i++) {
609: try {
610: scanForResources(sourceDirectory,
611: resourcesToProcess[i], renumber, out);
612: } catch (IOException exception) {
613: out.println(exception.getLocalizedMessage());
614: }
615: }
616: out.flush();
617: }
618:
619: /**
620: * Run the compiler for GeoTools resources.
621: */
622: public static void main(final String[] args) {
623: main(args, SOURCE_DIRECTORY, RESOURCES_TO_PROCESS);
624: }
625: }
|