/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package org.apache.tools.ant.taskdefs.optional.i18n;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.LineTokenizer;

Translates text embedded in files using Resource Bundle files. Since ant 1.6 preserves line endings
/** * Translates text embedded in files using Resource Bundle files. * Since ant 1.6 preserves line endings * */
public class Translate extends MatchingTask {
search a bundle matching the specified language, the country and the variant
/** * search a bundle matching the specified language, the country and the variant */
private static final int BUNDLE_SPECIFIED_LANGUAGE_COUNTRY_VARIANT = 0;
search a bundle matching the specified language, and the country
/** * search a bundle matching the specified language, and the country */
private static final int BUNDLE_SPECIFIED_LANGUAGE_COUNTRY = 1;
search a bundle matching the specified language only
/** * search a bundle matching the specified language only */
private static final int BUNDLE_SPECIFIED_LANGUAGE = 2;
search a bundle matching nothing special
/** * search a bundle matching nothing special */
private static final int BUNDLE_NOMATCH = 3;
search a bundle matching the language, the country and the variant of the current locale of the computer
/** * search a bundle matching the language, the country and the variant * of the current locale of the computer */
private static final int BUNDLE_DEFAULT_LANGUAGE_COUNTRY_VARIANT = 4;
search a bundle matching the language, and the country of the current locale of the computer
/** * search a bundle matching the language, and the country * of the current locale of the computer */
private static final int BUNDLE_DEFAULT_LANGUAGE_COUNTRY = 5;
search a bundle matching the language only of the current locale of the computer
/** * search a bundle matching the language only * of the current locale of the computer */
private static final int BUNDLE_DEFAULT_LANGUAGE = 6;
number of possibilities for the search
/** * number of possibilities for the search */
private static final int BUNDLE_MAX_ALTERNATIVES = BUNDLE_DEFAULT_LANGUAGE + 1;
Family name of resource bundle
/** * Family name of resource bundle */
private String bundle;
Locale specific language of the resource bundle
/** * Locale specific language of the resource bundle */
private String bundleLanguage;
Locale specific country of the resource bundle
/** * Locale specific country of the resource bundle */
private String bundleCountry;
Locale specific variant of the resource bundle
/** * Locale specific variant of the resource bundle */
private String bundleVariant;
Destination directory
/** * Destination directory */
private File toDir;
Source file encoding scheme
/** * Source file encoding scheme */
private String srcEncoding;
Destination file encoding scheme
/** * Destination file encoding scheme */
private String destEncoding;
Resource Bundle file encoding scheme, defaults to srcEncoding
/** * Resource Bundle file encoding scheme, defaults to srcEncoding */
private String bundleEncoding;
Starting token to identify keys
/** * Starting token to identify keys */
private String startToken;
Ending token to identify keys
/** * Ending token to identify keys */
private String endToken;
Whether or not to create a new destination file. Defaults to false.
/** * Whether or not to create a new destination file. * Defaults to <code>false</code>. */
private boolean forceOverwrite;
Vector to hold source file sets.
/** * Vector to hold source file sets. */
private List<FileSet> filesets = new Vector<>();
Holds key value pairs loaded from resource bundle file
/** * Holds key value pairs loaded from resource bundle file */
private Map<String, String> resourceMap = new Hashtable<>();
Used to resolve file names.
/** * Used to resolve file names. */
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
Last Modified Timestamp of resource bundle file being used.
/** * Last Modified Timestamp of resource bundle file being used. */
private long[] bundleLastModified = new long[BUNDLE_MAX_ALTERNATIVES];
Last Modified Timestamp of source file being used.
/** * Last Modified Timestamp of source file being used. */
private long srcLastModified;
Last Modified Timestamp of destination file being used.
/** * Last Modified Timestamp of destination file being used. */
private long destLastModified;
Has at least one file from the bundle been loaded?
/** * Has at least one file from the bundle been loaded? */
private boolean loaded = false;
Sets Family name of resource bundle; required.
Params:
  • bundle – family name of resource bundle
/** * Sets Family name of resource bundle; required. * @param bundle family name of resource bundle */
public void setBundle(String bundle) { this.bundle = bundle; }
Sets locale specific language of resource bundle; optional.
Params:
  • bundleLanguage – language of the bundle
/** * Sets locale specific language of resource bundle; optional. * @param bundleLanguage language of the bundle */
public void setBundleLanguage(String bundleLanguage) { this.bundleLanguage = bundleLanguage; }
Sets locale specific country of resource bundle; optional.
Params:
  • bundleCountry – country of the bundle
/** * Sets locale specific country of resource bundle; optional. * @param bundleCountry country of the bundle */
public void setBundleCountry(String bundleCountry) { this.bundleCountry = bundleCountry; }
Sets locale specific variant of resource bundle; optional.
Params:
  • bundleVariant – locale variant of resource bundle
/** * Sets locale specific variant of resource bundle; optional. * @param bundleVariant locale variant of resource bundle */
public void setBundleVariant(String bundleVariant) { this.bundleVariant = bundleVariant; }
Sets Destination directory; required.
Params:
  • toDir – destination directory
/** * Sets Destination directory; required. * @param toDir destination directory */
public void setToDir(File toDir) { this.toDir = toDir; }
Sets starting token to identify keys; required.
Params:
  • startToken – starting token to identify keys
/** * Sets starting token to identify keys; required. * @param startToken starting token to identify keys */
public void setStartToken(String startToken) { this.startToken = startToken; }
Sets ending token to identify keys; required.
Params:
  • endToken – ending token to identify keys
/** * Sets ending token to identify keys; required. * @param endToken ending token to identify keys */
public void setEndToken(String endToken) { this.endToken = endToken; }
Sets source file encoding scheme; optional, defaults to encoding of local system.
Params:
  • srcEncoding – source file encoding
/** * Sets source file encoding scheme; optional, * defaults to encoding of local system. * @param srcEncoding source file encoding */
public void setSrcEncoding(String srcEncoding) { this.srcEncoding = srcEncoding; }
Sets destination file encoding scheme; optional. Defaults to source file encoding
Params:
  • destEncoding – destination file encoding scheme
/** * Sets destination file encoding scheme; optional. Defaults to source file * encoding * @param destEncoding destination file encoding scheme */
public void setDestEncoding(String destEncoding) { this.destEncoding = destEncoding; }
Sets Resource Bundle file encoding scheme; optional. Defaults to source file encoding
Params:
  • bundleEncoding – bundle file encoding scheme
/** * Sets Resource Bundle file encoding scheme; optional. Defaults to source file * encoding * @param bundleEncoding bundle file encoding scheme */
public void setBundleEncoding(String bundleEncoding) { this.bundleEncoding = bundleEncoding; }
Whether or not to overwrite existing file irrespective of whether it is newer than the source file as well as the resource bundle file. Defaults to false.
Params:
  • forceOverwrite – whether or not to overwrite existing files
/** * Whether or not to overwrite existing file irrespective of * whether it is newer than the source file as well as the * resource bundle file. * Defaults to false. * @param forceOverwrite whether or not to overwrite existing files */
public void setForceOverwrite(boolean forceOverwrite) { this.forceOverwrite = forceOverwrite; }
Adds a set of files to translate as a nested fileset element.
Params:
  • set – the fileset to be added
/** * Adds a set of files to translate as a nested fileset element. * @param set the fileset to be added */
public void addFileset(FileSet set) { filesets.add(set); }
Check attributes values, load resource map and translate
Throws:
  • BuildException – if the required attributes are not set Required :
    • bundle
    • starttoken
    • endtoken
/** * Check attributes values, load resource map and translate * @throws BuildException if the required attributes are not set * Required : <ul> * <li>bundle</li> * <li>starttoken</li> * <li>endtoken</li> * </ul> */
@Override public void execute() throws BuildException { if (bundle == null) { throw new BuildException("The bundle attribute must be set.", getLocation()); } if (startToken == null) { throw new BuildException("The starttoken attribute must be set.", getLocation()); } if (endToken == null) { throw new BuildException("The endtoken attribute must be set.", getLocation()); } if (bundleLanguage == null) { Locale l = Locale.getDefault(); bundleLanguage = l.getLanguage(); } if (bundleCountry == null) { bundleCountry = Locale.getDefault().getCountry(); } if (bundleVariant == null) { Locale l = new Locale(bundleLanguage, bundleCountry); bundleVariant = l.getVariant(); } if (toDir == null) { throw new BuildException("The todir attribute must be set.", getLocation()); } if (!toDir.exists()) { toDir.mkdirs(); } else if (toDir.isFile()) { throw new BuildException("%s is not a directory", toDir); } if (srcEncoding == null) { srcEncoding = System.getProperty("file.encoding"); } if (destEncoding == null) { destEncoding = srcEncoding; } if (bundleEncoding == null) { bundleEncoding = srcEncoding; } loadResourceMaps(); translate(); }
Load resource maps based on resource bundle encoding scheme. The resource bundle lookup searches for resource files with various suffixes on the basis of (1) the desired locale and (2) the default locale (basebundlename), in the following order from lower-level (more specific) to parent-level (less specific): basebundlename + "_" + language1 + "_" + country1 + "_" + variant1 basebundlename + "_" + language1 + "_" + country1 basebundlename + "_" + language1 basebundlename basebundlename + "_" + language2 + "_" + country2 + "_" + variant2 basebundlename + "_" + language2 + "_" + country2 basebundlename + "_" + language2 To the generated name, a ".properties" string is appended and once this file is located, it is treated just like a properties file but with bundle encoding also considered while loading.
/** * Load resource maps based on resource bundle encoding scheme. * The resource bundle lookup searches for resource files with various * suffixes on the basis of (1) the desired locale and (2) the default * locale (basebundlename), in the following order from lower-level * (more specific) to parent-level (less specific): * * basebundlename + "_" + language1 + "_" + country1 + "_" + variant1 * basebundlename + "_" + language1 + "_" + country1 * basebundlename + "_" + language1 * basebundlename * basebundlename + "_" + language2 + "_" + country2 + "_" + variant2 * basebundlename + "_" + language2 + "_" + country2 * basebundlename + "_" + language2 * * To the generated name, a ".properties" string is appended and * once this file is located, it is treated just like a properties file * but with bundle encoding also considered while loading. */
private void loadResourceMaps() throws BuildException { Locale locale = new Locale(bundleLanguage, bundleCountry, bundleVariant); String language = locale.getLanguage().isEmpty() ? "" : "_" + locale.getLanguage(); String country = locale.getCountry().isEmpty() ? "" : "_" + locale.getCountry(); String variant = locale.getVariant().isEmpty() ? "" : "_" + locale.getVariant(); processBundle(bundle + language + country + variant, BUNDLE_SPECIFIED_LANGUAGE_COUNTRY_VARIANT, false); processBundle(bundle + language + country, BUNDLE_SPECIFIED_LANGUAGE_COUNTRY, false); processBundle(bundle + language, BUNDLE_SPECIFIED_LANGUAGE, false); processBundle(bundle, BUNDLE_NOMATCH, false); //Load default locale bundle files //using default file encoding scheme. locale = Locale.getDefault(); language = locale.getLanguage().isEmpty() ? "" : "_" + locale.getLanguage(); country = locale.getCountry().isEmpty() ? "" : "_" + locale.getCountry(); variant = locale.getVariant().isEmpty() ? "" : "_" + locale.getVariant(); bundleEncoding = System.getProperty("file.encoding"); processBundle(bundle + language + country + variant, BUNDLE_DEFAULT_LANGUAGE_COUNTRY_VARIANT, false); processBundle(bundle + language + country, BUNDLE_DEFAULT_LANGUAGE_COUNTRY, false); processBundle(bundle + language, BUNDLE_DEFAULT_LANGUAGE, true); }
Process each file that makes up this bundle.
/** * Process each file that makes up this bundle. */
private void processBundle(final String bundleFile, final int i, final boolean checkLoaded) throws BuildException { final File propsFile = getProject().resolveFile(bundleFile + ".properties"); InputStream ins = null; try { ins = Files.newInputStream(propsFile.toPath()); loaded = true; bundleLastModified[i] = propsFile.lastModified(); log("Using " + propsFile, Project.MSG_DEBUG); loadResourceMap(ins); } catch (IOException ioe) { log(propsFile + " not found.", Project.MSG_DEBUG); //if all resource files associated with this bundle //have been scanned for and still not able to //find a single resource file, throw exception if (!loaded && checkLoaded) { throw new BuildException(ioe.getMessage(), getLocation()); } } }
Load resourceMap with key value pairs. Values of existing keys are not overwritten. Bundle's encoding scheme is used.
/** * Load resourceMap with key value pairs. Values of existing keys * are not overwritten. Bundle's encoding scheme is used. */
private void loadResourceMap(InputStream ins) throws BuildException { try (BufferedReader in = new BufferedReader(new InputStreamReader(ins, bundleEncoding))) { String line; while ((line = in.readLine()) != null) { //So long as the line isn't empty and isn't a comment... if (line.trim().length() > 1 && '#' != line.charAt(0) && '!' != line.charAt(0)) { //Legal Key-Value separators are :, = and white space. int sepIndex = line.indexOf('='); if (-1 == sepIndex) { sepIndex = line.indexOf(':'); } if (-1 == sepIndex) { for (int k = 0; k < line.length(); k++) { if (Character.isSpaceChar(line.charAt(k))) { sepIndex = k; break; } } } //Only if we do have a key is there going to be a value if (-1 != sepIndex) { String key = line.substring(0, sepIndex).trim(); String value = line.substring(sepIndex + 1).trim(); //Handle line continuations, if any while (value.endsWith("\\")) { value = value.substring(0, value.length() - 1); line = in.readLine(); if (line != null) { value += line.trim(); } else { break; } } if (!key.isEmpty()) { //Has key already been loaded into resourceMap? resourceMap.putIfAbsent(key, value); } } } } } catch (IOException ioe) { throw new BuildException(ioe.getMessage(), getLocation()); } }
Reads source file line by line using the source encoding and searches for keys that are sandwiched between the startToken and endToken. The values for these keys are looked up from the hashtable and substituted. If the hashtable doesn't contain the key, they key itself is used as the value. Destination files and directories are created as needed. The destination file is overwritten only if the forceoverwritten attribute is set to true if the source file or any associated bundle resource file is newer than the destination file.
/** * Reads source file line by line using the source encoding and * searches for keys that are sandwiched between the startToken * and endToken. The values for these keys are looked up from * the hashtable and substituted. If the hashtable doesn't * contain the key, they key itself is used as the value. * Destination files and directories are created as needed. * The destination file is overwritten only if * the forceoverwritten attribute is set to true if * the source file or any associated bundle resource file is * newer than the destination file. */
private void translate() throws BuildException { int filesProcessed = 0; for (FileSet fs : filesets) { DirectoryScanner ds = fs.getDirectoryScanner(getProject()); for (String srcFile : ds.getIncludedFiles()) { try { File dest = FILE_UTILS.resolveFile(toDir, srcFile); //Make sure parent dirs exist, else, create them. try { File destDir = new File(dest.getParent()); if (!destDir.exists()) { destDir.mkdirs(); } } catch (Exception e) { log("Exception occurred while trying to check/create " + " parent directory. " + e.getMessage(), Project.MSG_DEBUG); } destLastModified = dest.lastModified(); File src = FILE_UTILS.resolveFile(ds.getBasedir(), srcFile); srcLastModified = src.lastModified(); //Check to see if dest file has to be recreated boolean needsWork = forceOverwrite || destLastModified < srcLastModified; if (!needsWork) { for (int icounter = 0; icounter < BUNDLE_MAX_ALTERNATIVES; icounter++) { needsWork = (destLastModified < bundleLastModified[icounter]); if (needsWork) { break; } } } if (needsWork) { log("Processing " + srcFile, Project.MSG_DEBUG); translateOneFile(src, dest); ++filesProcessed; } else { log("Skipping " + srcFile + " as destination file is up to date", Project.MSG_VERBOSE); } } catch (IOException ioe) { throw new BuildException(ioe.getMessage(), getLocation()); } } } log("Translation performed on " + filesProcessed + " file(s).", Project.MSG_DEBUG); } private void translateOneFile(File src, File dest) throws IOException { try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter( Files.newOutputStream(dest.toPath()), destEncoding)); BufferedReader in = new BufferedReader(new InputStreamReader( Files.newInputStream(src.toPath()), srcEncoding))) { LineTokenizer lineTokenizer = new LineTokenizer(); lineTokenizer.setIncludeDelims(true); String line = lineTokenizer.getToken(in); while (line != null) { // 2003-02-21 new replace algorithm by tbee (tbee@tbee.org) // because it wasn't able to replace something like "@aaa;@bbb;" // is there a startToken // and there is still stuff following the startToken int startIndex = line.indexOf(startToken); while (startIndex >= 0 && (startIndex + startToken.length()) <= line.length()) { // the new value, this needs to be here // because it is required to calculate the next position to // search from at the end of the loop String replace = null; // we found a starttoken, is there an endtoken following? // start at token+tokenlength because start and end // token may be identical int endIndex = line.indexOf(endToken, startIndex + startToken.length()); if (endIndex < 0) { startIndex += 1; } else { // grab the token String token = line.substring(startIndex + startToken.length(), endIndex); // If there is a white space or = or :, then // it isn't to be treated as a valid key. boolean validToken = true; for (int k = 0; k < token.length() && validToken; k++) { char c = token.charAt(k); if (c == ':' || c == '=' || Character.isSpaceChar(c)) { validToken = false; } } if (!validToken) { startIndex += 1; } else { // find the replace string if (resourceMap.containsKey(token)) { replace = resourceMap.get(token); } else { log("Replacement string missing for: " + token, Project.MSG_VERBOSE); replace = startToken + token + endToken; } // generate the new line line = line.substring(0, startIndex) + replace + line.substring(endIndex + endToken.length()); // set start position for next search startIndex += replace.length(); } } // find next starttoken startIndex = line.indexOf(startToken, startIndex); } out.write(line); line = lineTokenizer.getToken(in); } } } }