001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018: package org.apache.tools.ant.taskdefs.optional.i18n;
019:
020: import java.io.BufferedReader;
021: import java.io.BufferedWriter;
022: import java.io.File;
023: import java.io.FileInputStream;
024: import java.io.FileOutputStream;
025: import java.io.IOException;
026: import java.io.InputStreamReader;
027: import java.io.OutputStreamWriter;
028: import java.util.Hashtable;
029: import java.util.Locale;
030: import java.util.Vector;
031: import org.apache.tools.ant.BuildException;
032: import org.apache.tools.ant.DirectoryScanner;
033: import org.apache.tools.ant.Project;
034: import org.apache.tools.ant.taskdefs.MatchingTask;
035: import org.apache.tools.ant.types.FileSet;
036: import org.apache.tools.ant.util.FileUtils;
037: import org.apache.tools.ant.util.LineTokenizer;
038:
039: /**
040: * Translates text embedded in files using Resource Bundle files.
041: * Since ant 1.6 preserves line endings
042: *
043: */
044: public class Translate extends MatchingTask {
045: /**
046: * search a bundle matching the specified language, the country and the variant
047: */
048: private static final int BUNDLE_SPECIFIED_LANGUAGE_COUNTRY_VARIANT = 0;
049: /**
050: * search a bundle matching the specified language, and the country
051: */
052: private static final int BUNDLE_SPECIFIED_LANGUAGE_COUNTRY = 1;
053: /**
054: * search a bundle matching the specified language only
055: */
056: private static final int BUNDLE_SPECIFIED_LANGUAGE = 2;
057: /**
058: * search a bundle matching nothing special
059: */
060: private static final int BUNDLE_NOMATCH = 3;
061: /**
062: * search a bundle matching the language, the country and the variant
063: * of the current locale of the computer
064: */
065: private static final int BUNDLE_DEFAULT_LANGUAGE_COUNTRY_VARIANT = 4;
066: /**
067: * search a bundle matching the language, and the country
068: * of the current locale of the computer
069: */
070: private static final int BUNDLE_DEFAULT_LANGUAGE_COUNTRY = 5;
071: /**
072: * search a bundle matching the language only
073: * of the current locale of the computer
074: */
075: private static final int BUNDLE_DEFAULT_LANGUAGE = 6;
076: /**
077: * number of possibilities for the search
078: */
079: private static final int BUNDLE_MAX_ALTERNATIVES = BUNDLE_DEFAULT_LANGUAGE + 1;
080: /**
081: * Family name of resource bundle
082: */
083: private String bundle;
084:
085: /**
086: * Locale specific language of the resource bundle
087: */
088: private String bundleLanguage;
089:
090: /**
091: * Locale specific country of the resource bundle
092: */
093: private String bundleCountry;
094:
095: /**
096: * Locale specific variant of the resource bundle
097: */
098: private String bundleVariant;
099:
100: /**
101: * Destination directory
102: */
103: private File toDir;
104:
105: /**
106: * Source file encoding scheme
107: */
108: private String srcEncoding;
109:
110: /**
111: * Destination file encoding scheme
112: */
113: private String destEncoding;
114:
115: /**
116: * Resource Bundle file encoding scheme, defaults to srcEncoding
117: */
118: private String bundleEncoding;
119:
120: /**
121: * Starting token to identify keys
122: */
123: private String startToken;
124:
125: /**
126: * Ending token to identify keys
127: */
128: private String endToken;
129:
130: /**
131: * Whether or not to create a new destination file.
132: * Defaults to <code>false</code>.
133: */
134: private boolean forceOverwrite;
135:
136: /**
137: * Vector to hold source file sets.
138: */
139: private Vector filesets = new Vector();
140:
141: /**
142: * Holds key value pairs loaded from resource bundle file
143: */
144: private Hashtable resourceMap = new Hashtable();
145: /**
146:
147: * Used to resolve file names.
148: */
149: private static final FileUtils FILE_UTILS = FileUtils
150: .getFileUtils();
151:
152: /**
153: * Last Modified Timestamp of resource bundle file being used.
154: */
155: private long[] bundleLastModified = new long[BUNDLE_MAX_ALTERNATIVES];
156:
157: /**
158: * Last Modified Timestamp of source file being used.
159: */
160: private long srcLastModified;
161:
162: /**
163: * Last Modified Timestamp of destination file being used.
164: */
165: private long destLastModified;
166:
167: /**
168: * Has at least one file from the bundle been loaded?
169: */
170: private boolean loaded = false;
171:
172: /**
173: * Sets Family name of resource bundle; required.
174: * @param bundle family name of resource bundle
175: */
176: public void setBundle(String bundle) {
177: this .bundle = bundle;
178: }
179:
180: /**
181: * Sets locale specific language of resource bundle; optional.
182: * @param bundleLanguage langage of the bundle
183: */
184: public void setBundleLanguage(String bundleLanguage) {
185: this .bundleLanguage = bundleLanguage;
186: }
187:
188: /**
189: * Sets locale specific country of resource bundle; optional.
190: * @param bundleCountry country of the bundle
191: */
192: public void setBundleCountry(String bundleCountry) {
193: this .bundleCountry = bundleCountry;
194: }
195:
196: /**
197: * Sets locale specific variant of resource bundle; optional.
198: * @param bundleVariant locale variant of resource bundle
199: */
200: public void setBundleVariant(String bundleVariant) {
201: this .bundleVariant = bundleVariant;
202: }
203:
204: /**
205: * Sets Destination directory; required.
206: * @param toDir destination directory
207: */
208: public void setToDir(File toDir) {
209: this .toDir = toDir;
210: }
211:
212: /**
213: * Sets starting token to identify keys; required.
214: * @param startToken starting token to identify keys
215: */
216: public void setStartToken(String startToken) {
217: this .startToken = startToken;
218: }
219:
220: /**
221: * Sets ending token to identify keys; required.
222: * @param endToken ending token to identify keys
223: */
224: public void setEndToken(String endToken) {
225: this .endToken = endToken;
226: }
227:
228: /**
229: * Sets source file encoding scheme; optional,
230: * defaults to encoding of local system.
231: * @param srcEncoding source file encoding
232: */
233: public void setSrcEncoding(String srcEncoding) {
234: this .srcEncoding = srcEncoding;
235: }
236:
237: /**
238: * Sets destination file encoding scheme; optional. Defaults to source file
239: * encoding
240: * @param destEncoding destination file encoding scheme
241: */
242: public void setDestEncoding(String destEncoding) {
243: this .destEncoding = destEncoding;
244: }
245:
246: /**
247: * Sets Resource Bundle file encoding scheme; optional. Defaults to source file
248: * encoding
249: * @param bundleEncoding bundle file encoding scheme
250: */
251: public void setBundleEncoding(String bundleEncoding) {
252: this .bundleEncoding = bundleEncoding;
253: }
254:
255: /**
256: * Whether or not to overwrite existing file irrespective of
257: * whether it is newer than the source file as well as the
258: * resource bundle file.
259: * Defaults to false.
260: * @param forceOverwrite whether or not to overwrite existing files
261: */
262: public void setForceOverwrite(boolean forceOverwrite) {
263: this .forceOverwrite = forceOverwrite;
264: }
265:
266: /**
267: * Adds a set of files to translate as a nested fileset element.
268: * @param set the fileset to be added
269: */
270: public void addFileset(FileSet set) {
271: filesets.addElement(set);
272: }
273:
274: /**
275: * Check attributes values, load resource map and translate
276: * @throws BuildException if the required attributes are not set
277: * Required : <ul>
278: * <li>bundle</li>
279: * <li>starttoken</li>
280: * <li>endtoken</li>
281: * </ul>
282: */
283: public void execute() throws BuildException {
284: if (bundle == null) {
285: throw new BuildException(
286: "The bundle attribute must be set.", getLocation());
287: }
288:
289: if (startToken == null) {
290: throw new BuildException(
291: "The starttoken attribute must be set.",
292: getLocation());
293: }
294:
295: if (endToken == null) {
296: throw new BuildException(
297: "The endtoken attribute must be set.",
298: getLocation());
299: }
300:
301: if (bundleLanguage == null) {
302: Locale l = Locale.getDefault();
303: bundleLanguage = l.getLanguage();
304: }
305:
306: if (bundleCountry == null) {
307: bundleCountry = Locale.getDefault().getCountry();
308: }
309:
310: if (bundleVariant == null) {
311: Locale l = new Locale(bundleLanguage, bundleCountry);
312: bundleVariant = l.getVariant();
313: }
314:
315: if (toDir == null) {
316: throw new BuildException(
317: "The todir attribute must be set.", getLocation());
318: }
319:
320: if (!toDir.exists()) {
321: toDir.mkdirs();
322: } else if (toDir.isFile()) {
323: throw new BuildException(toDir + " is not a directory");
324: }
325:
326: if (srcEncoding == null) {
327: srcEncoding = System.getProperty("file.encoding");
328: }
329:
330: if (destEncoding == null) {
331: destEncoding = srcEncoding;
332: }
333:
334: if (bundleEncoding == null) {
335: bundleEncoding = srcEncoding;
336: }
337:
338: loadResourceMaps();
339:
340: translate();
341: }
342:
343: /**
344: * Load resource maps based on resource bundle encoding scheme.
345: * The resource bundle lookup searches for resource files with various
346: * suffixes on the basis of (1) the desired locale and (2) the default
347: * locale (basebundlename), in the following order from lower-level
348: * (more specific) to parent-level (less specific):
349: *
350: * basebundlename + "_" + language1 + "_" + country1 + "_" + variant1
351: * basebundlename + "_" + language1 + "_" + country1
352: * basebundlename + "_" + language1
353: * basebundlename
354: * basebundlename + "_" + language2 + "_" + country2 + "_" + variant2
355: * basebundlename + "_" + language2 + "_" + country2
356: * basebundlename + "_" + language2
357: *
358: * To the generated name, a ".properties" string is appeneded and
359: * once this file is located, it is treated just like a properties file
360: * but with bundle encoding also considered while loading.
361: */
362: private void loadResourceMaps() throws BuildException {
363: Locale locale = new Locale(bundleLanguage, bundleCountry,
364: bundleVariant);
365: String language = locale.getLanguage().length() > 0 ? "_"
366: + locale.getLanguage() : "";
367: String country = locale.getCountry().length() > 0 ? "_"
368: + locale.getCountry() : "";
369: String variant = locale.getVariant().length() > 0 ? "_"
370: + locale.getVariant() : "";
371: String bundleFile = bundle + language + country + variant;
372: processBundle(bundleFile,
373: BUNDLE_SPECIFIED_LANGUAGE_COUNTRY_VARIANT, false);
374:
375: bundleFile = bundle + language + country;
376: processBundle(bundleFile, BUNDLE_SPECIFIED_LANGUAGE_COUNTRY,
377: false);
378:
379: bundleFile = bundle + language;
380: processBundle(bundleFile, BUNDLE_SPECIFIED_LANGUAGE, false);
381:
382: bundleFile = bundle;
383: processBundle(bundleFile, BUNDLE_NOMATCH, false);
384:
385: //Load default locale bundle files
386: //using default file encoding scheme.
387: locale = Locale.getDefault();
388:
389: language = locale.getLanguage().length() > 0 ? "_"
390: + locale.getLanguage() : "";
391: country = locale.getCountry().length() > 0 ? "_"
392: + locale.getCountry() : "";
393: variant = locale.getVariant().length() > 0 ? "_"
394: + locale.getVariant() : "";
395: bundleEncoding = System.getProperty("file.encoding");
396:
397: bundleFile = bundle + language + country + variant;
398: processBundle(bundleFile,
399: BUNDLE_DEFAULT_LANGUAGE_COUNTRY_VARIANT, false);
400:
401: bundleFile = bundle + language + country;
402: processBundle(bundleFile, BUNDLE_DEFAULT_LANGUAGE_COUNTRY,
403: false);
404:
405: bundleFile = bundle + language;
406: processBundle(bundleFile, BUNDLE_DEFAULT_LANGUAGE, true);
407: }
408:
409: /**
410: * Process each file that makes up this bundle.
411: */
412: private void processBundle(final String bundleFile, final int i,
413: final boolean checkLoaded) throws BuildException {
414: final File propsFile = getProject().resolveFile(
415: bundleFile + ".properties");
416: FileInputStream ins = null;
417: try {
418: ins = new FileInputStream(propsFile);
419: loaded = true;
420: bundleLastModified[i] = propsFile.lastModified();
421: log("Using " + propsFile, Project.MSG_DEBUG);
422: loadResourceMap(ins);
423: } catch (IOException ioe) {
424: log(propsFile + " not found.", Project.MSG_DEBUG);
425: //if all resource files associated with this bundle
426: //have been scanned for and still not able to
427: //find a single resrouce file, throw exception
428: if (!loaded && checkLoaded) {
429: throw new BuildException(ioe.getMessage(),
430: getLocation());
431: }
432: }
433: }
434:
435: /**
436: * Load resourceMap with key value pairs. Values of existing keys
437: * are not overwritten. Bundle's encoding scheme is used.
438: */
439: private void loadResourceMap(FileInputStream ins)
440: throws BuildException {
441: try {
442: BufferedReader in = null;
443: InputStreamReader isr = new InputStreamReader(ins,
444: bundleEncoding);
445: in = new BufferedReader(isr);
446: String line = null;
447: while ((line = in.readLine()) != null) {
448: //So long as the line isn't empty and isn't a comment...
449: if (line.trim().length() > 1 && '#' != line.charAt(0)
450: && '!' != line.charAt(0)) {
451: //Legal Key-Value separators are :, = and white space.
452: int sepIndex = line.indexOf('=');
453: if (-1 == sepIndex) {
454: sepIndex = line.indexOf(':');
455: }
456: if (-1 == sepIndex) {
457: for (int k = 0; k < line.length(); k++) {
458: if (Character.isSpaceChar(line.charAt(k))) {
459: sepIndex = k;
460: break;
461: }
462: }
463: }
464: //Only if we do have a key is there going to be a value
465: if (-1 != sepIndex) {
466: String key = line.substring(0, sepIndex).trim();
467: String value = line.substring(sepIndex + 1)
468: .trim();
469: //Handle line continuations, if any
470: while (value.endsWith("\\")) {
471: value = value.substring(0,
472: value.length() - 1);
473: if ((line = in.readLine()) != null) {
474: value = value + line.trim();
475: } else {
476: break;
477: }
478: }
479: if (key.length() > 0) {
480: //Has key already been loaded into resourceMap?
481: if (resourceMap.get(key) == null) {
482: resourceMap.put(key, value);
483: }
484: }
485: }
486: }
487: }
488: if (in != null) {
489: in.close();
490: }
491: } catch (IOException ioe) {
492: throw new BuildException(ioe.getMessage(), getLocation());
493: }
494: }
495:
496: /**
497: * Reads source file line by line using the source encoding and
498: * searches for keys that are sandwiched between the startToken
499: * and endToken. The values for these keys are looked up from
500: * the hashtable and substituted. If the hashtable doesn't
501: * contain the key, they key itself is used as the value.
502: * Detination files and directories are created as needed.
503: * The destination file is overwritten only if
504: * the forceoverwritten attribute is set to true if
505: * the source file or any associated bundle resource file is
506: * newer than the destination file.
507: */
508: private void translate() throws BuildException {
509: int filesProcessed = 0;
510: for (int i = 0; i < filesets.size(); i++) {
511: FileSet fs = (FileSet) filesets.elementAt(i);
512: DirectoryScanner ds = fs.getDirectoryScanner(getProject());
513: String[] srcFiles = ds.getIncludedFiles();
514: for (int j = 0; j < srcFiles.length; j++) {
515: try {
516: File dest = FILE_UTILS.resolveFile(toDir,
517: srcFiles[j]);
518: //Make sure parent dirs exist, else, create them.
519: try {
520: File destDir = new File(dest.getParent());
521: if (!destDir.exists()) {
522: destDir.mkdirs();
523: }
524: } catch (Exception e) {
525: log(
526: "Exception occurred while trying to check/create "
527: + " parent directory. "
528: + e.getMessage(),
529: Project.MSG_DEBUG);
530: }
531: destLastModified = dest.lastModified();
532: File src = FILE_UTILS.resolveFile(ds.getBasedir(),
533: srcFiles[j]);
534: srcLastModified = src.lastModified();
535: //Check to see if dest file has to be recreated
536: boolean needsWork = forceOverwrite
537: || destLastModified < srcLastModified;
538: if (!needsWork) {
539: for (int icounter = 0; icounter < BUNDLE_MAX_ALTERNATIVES; icounter++) {
540: needsWork = (destLastModified < bundleLastModified[icounter]);
541: if (needsWork) {
542: break;
543: }
544: }
545: }
546: if (needsWork) {
547: log("Processing " + srcFiles[j],
548: Project.MSG_DEBUG);
549: FileOutputStream fos = new FileOutputStream(
550: dest);
551: BufferedWriter out = new BufferedWriter(
552: new OutputStreamWriter(fos,
553: destEncoding));
554: FileInputStream fis = new FileInputStream(src);
555: BufferedReader in = new BufferedReader(
556: new InputStreamReader(fis, srcEncoding));
557: String line;
558: LineTokenizer lineTokenizer = new LineTokenizer();
559: lineTokenizer.setIncludeDelims(true);
560: line = lineTokenizer.getToken(in);
561: while ((line) != null) {
562: // 2003-02-21 new replace algorithm by tbee (tbee@tbee.org)
563: // because it wasn't able to replace something like "@aaa;@bbb;"
564:
565: // is there a startToken
566: // and there is still stuff following the startToken
567: int startIndex = line.indexOf(startToken);
568: while (startIndex >= 0
569: && (startIndex + startToken
570: .length()) <= line.length()) {
571: // the new value, this needs to be here
572: // because it is required to calculate the next position to
573: // search from at the end of the loop
574: String replace = null;
575:
576: // we found a starttoken, is there an endtoken following?
577: // start at token+tokenlength because start and end
578: // token may be indentical
579: int endIndex = line.indexOf(endToken,
580: startIndex
581: + startToken.length());
582: if (endIndex < 0) {
583: startIndex += 1;
584: } else {
585: // grab the token
586: String token = line.substring(
587: startIndex
588: + startToken
589: .length(),
590: endIndex);
591:
592: // If there is a white space or = or :, then
593: // it isn't to be treated as a valid key.
594: boolean validToken = true;
595: for (int k = 0; k < token.length()
596: && validToken; k++) {
597: char c = token.charAt(k);
598: if (c == ':'
599: || c == '='
600: || Character
601: .isSpaceChar(c)) {
602: validToken = false;
603: }
604: }
605: if (!validToken) {
606: startIndex += 1;
607: } else {
608: // find the replace string
609: if (resourceMap
610: .containsKey(token)) {
611: replace = (String) resourceMap
612: .get(token);
613: } else {
614: log(
615: "Replacement string missing for: "
616: + token,
617: Project.MSG_VERBOSE);
618: replace = startToken
619: + token + endToken;
620: }
621:
622: // generate the new line
623: line = line.substring(0,
624: startIndex)
625: + replace
626: + line
627: .substring(endIndex
628: + endToken
629: .length());
630:
631: // set start position for next search
632: startIndex += replace.length();
633: }
634: }
635:
636: // find next starttoken
637: startIndex = line.indexOf(startToken,
638: startIndex);
639: }
640: out.write(line);
641: line = lineTokenizer.getToken(in);
642: }
643: if (in != null) {
644: in.close();
645: }
646: if (out != null) {
647: out.close();
648: }
649: ++filesProcessed;
650: } else {
651: log("Skipping " + srcFiles[j]
652: + " as destination file is up to date",
653: Project.MSG_VERBOSE);
654: }
655: } catch (IOException ioe) {
656: throw new BuildException(ioe.getMessage(),
657: getLocation());
658: }
659: }
660: }
661: log("Translation performed on " + filesProcessed + " file(s).",
662: Project.MSG_DEBUG);
663: }
664: }
|