1. /*
  2. * Copyright 2001-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs.optional.i18n;
  18. import java.io.BufferedReader;
  19. import java.io.BufferedWriter;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.io.FileOutputStream;
  23. import java.io.IOException;
  24. import java.io.InputStreamReader;
  25. import java.io.OutputStreamWriter;
  26. import java.util.Hashtable;
  27. import java.util.Locale;
  28. import java.util.Vector;
  29. import org.apache.tools.ant.BuildException;
  30. import org.apache.tools.ant.DirectoryScanner;
  31. import org.apache.tools.ant.Project;
  32. import org.apache.tools.ant.taskdefs.MatchingTask;
  33. import org.apache.tools.ant.types.FileSet;
  34. import org.apache.tools.ant.util.FileUtils;
  35. import org.apache.tools.ant.util.LineTokenizer;
  36. /**
  37. * Translates text embedded in files using Resource Bundle files.
  38. * Since ant 1.6 preserves line endings
  39. *
  40. */
  41. public class Translate extends MatchingTask {
  42. /**
  43. * search a bundle matching the specified language, the country and the variant
  44. */
  45. private static final int BUNDLE_SPECIFIED_LANGUAGE_COUNTRY_VARIANT = 0;
  46. /**
  47. * search a bundle matching the specified language, and the country
  48. */
  49. private static final int BUNDLE_SPECIFIED_LANGUAGE_COUNTRY = 1;
  50. /**
  51. * search a bundle matching the specified language only
  52. */
  53. private static final int BUNDLE_SPECIFIED_LANGUAGE = 2;
  54. /**
  55. * search a bundle matching nothing special
  56. */
  57. private static final int BUNDLE_NOMATCH = 3;
  58. /**
  59. * search a bundle matching the language, the country and the variant
  60. * of the current locale of the computer
  61. */
  62. private static final int BUNDLE_DEFAULT_LANGUAGE_COUNTRY_VARIANT = 4;
  63. /**
  64. * search a bundle matching the language, and the country
  65. * of the current locale of the computer
  66. */
  67. private static final int BUNDLE_DEFAULT_LANGUAGE_COUNTRY = 5;
  68. /**
  69. * search a bundle matching the language only
  70. * of the current locale of the computer
  71. */
  72. private static final int BUNDLE_DEFAULT_LANGUAGE = 6;
  73. /**
  74. * number of possibilities for the search
  75. */
  76. private static final int BUNDLE_MAX_ALTERNATIVES = BUNDLE_DEFAULT_LANGUAGE + 1;
  77. /**
  78. * Family name of resource bundle
  79. */
  80. private String bundle;
  81. /**
  82. * Locale specific language of the resource bundle
  83. */
  84. private String bundleLanguage;
  85. /**
  86. * Locale specific country of the resource bundle
  87. */
  88. private String bundleCountry;
  89. /**
  90. * Locale specific variant of the resource bundle
  91. */
  92. private String bundleVariant;
  93. /**
  94. * Destination directory
  95. */
  96. private File toDir;
  97. /**
  98. * Source file encoding scheme
  99. */
  100. private String srcEncoding;
  101. /**
  102. * Destination file encoding scheme
  103. */
  104. private String destEncoding;
  105. /**
  106. * Resource Bundle file encoding scheme, defaults to srcEncoding
  107. */
  108. private String bundleEncoding;
  109. /**
  110. * Starting token to identify keys
  111. */
  112. private String startToken;
  113. /**
  114. * Ending token to identify keys
  115. */
  116. private String endToken;
  117. /**
  118. * Whether or not to create a new destination file.
  119. * Defaults to <code>false</code>.
  120. */
  121. private boolean forceOverwrite;
  122. /**
  123. * Vector to hold source file sets.
  124. */
  125. private Vector filesets = new Vector();
  126. /**
  127. * Holds key value pairs loaded from resource bundle file
  128. */
  129. private Hashtable resourceMap = new Hashtable();
  130. /**
  131. * Used to resolve file names.
  132. */
  133. private FileUtils fileUtils = FileUtils.newFileUtils();
  134. /**
  135. * Last Modified Timestamp of resource bundle file being used.
  136. */
  137. private long[] bundleLastModified = new long[BUNDLE_MAX_ALTERNATIVES];
  138. /**
  139. * Last Modified Timestamp of source file being used.
  140. */
  141. private long srcLastModified;
  142. /**
  143. * Last Modified Timestamp of destination file being used.
  144. */
  145. private long destLastModified;
  146. /**
  147. * Has at least one file from the bundle been loaded?
  148. */
  149. private boolean loaded = false;
  150. /**
  151. * Sets Family name of resource bundle; required.
  152. * @param bundle family name of resource bundle
  153. */
  154. public void setBundle(String bundle) {
  155. this.bundle = bundle;
  156. }
  157. /**
  158. * Sets locale specific language of resource bundle; optional.
  159. * @param bundleLanguage langage of the bundle
  160. */
  161. public void setBundleLanguage(String bundleLanguage) {
  162. this.bundleLanguage = bundleLanguage;
  163. }
  164. /**
  165. * Sets locale specific country of resource bundle; optional.
  166. * @param bundleCountry country of the bundle
  167. */
  168. public void setBundleCountry(String bundleCountry) {
  169. this.bundleCountry = bundleCountry;
  170. }
  171. /**
  172. * Sets locale specific variant of resource bundle; optional.
  173. * @param bundleVariant locale variant of resource bundle
  174. */
  175. public void setBundleVariant(String bundleVariant) {
  176. this.bundleVariant = bundleVariant;
  177. }
  178. /**
  179. * Sets Destination directory; required.
  180. * @param toDir destination directory
  181. */
  182. public void setToDir(File toDir) {
  183. this.toDir = toDir;
  184. }
  185. /**
  186. * Sets starting token to identify keys; required.
  187. * @param startToken starting token to identify keys
  188. */
  189. public void setStartToken(String startToken) {
  190. this.startToken = startToken;
  191. }
  192. /**
  193. * Sets ending token to identify keys; required.
  194. * @param endToken ending token to identify keys
  195. */
  196. public void setEndToken(String endToken) {
  197. this.endToken = endToken;
  198. }
  199. /**
  200. * Sets source file encoding scheme; optional,
  201. * defaults to encoding of local system.
  202. * @param srcEncoding source file encoding
  203. */
  204. public void setSrcEncoding(String srcEncoding) {
  205. this.srcEncoding = srcEncoding;
  206. }
  207. /**
  208. * Sets destination file encoding scheme; optional. Defaults to source file
  209. * encoding
  210. * @param destEncoding destination file encoding scheme
  211. */
  212. public void setDestEncoding(String destEncoding) {
  213. this.destEncoding = destEncoding;
  214. }
  215. /**
  216. * Sets Resource Bundle file encoding scheme; optional. Defaults to source file
  217. * encoding
  218. * @param bundleEncoding bundle file encoding scheme
  219. */
  220. public void setBundleEncoding(String bundleEncoding) {
  221. this.bundleEncoding = bundleEncoding;
  222. }
  223. /**
  224. * Whether or not to overwrite existing file irrespective of
  225. * whether it is newer than the source file as well as the
  226. * resource bundle file.
  227. * Defaults to false.
  228. * @param forceOverwrite whether or not to overwrite existing files
  229. */
  230. public void setForceOverwrite(boolean forceOverwrite) {
  231. this.forceOverwrite = forceOverwrite;
  232. }
  233. /**
  234. * Adds a set of files to translate as a nested fileset element.
  235. * @param set the fileset to be added
  236. */
  237. public void addFileset(FileSet set) {
  238. filesets.addElement(set);
  239. }
  240. /**
  241. * Check attributes values, load resource map and translate
  242. * @throws BuildException if the required attributes are not set
  243. * Required : <ul>
  244. * <li>bundle</li>
  245. * <li>starttoken</li>
  246. * <li>endtoken</li>
  247. * </ul>
  248. */
  249. public void execute() throws BuildException {
  250. if (bundle == null) {
  251. throw new BuildException("The bundle attribute must be set.",
  252. getLocation());
  253. }
  254. if (startToken == null) {
  255. throw new BuildException("The starttoken attribute must be set.",
  256. getLocation());
  257. }
  258. if (endToken == null) {
  259. throw new BuildException("The endtoken attribute must be set.",
  260. getLocation());
  261. }
  262. if (bundleLanguage == null) {
  263. Locale l = Locale.getDefault();
  264. bundleLanguage = l.getLanguage();
  265. }
  266. if (bundleCountry == null) {
  267. bundleCountry = Locale.getDefault().getCountry();
  268. }
  269. if (bundleVariant == null) {
  270. Locale l = new Locale(bundleLanguage, bundleCountry);
  271. bundleVariant = l.getVariant();
  272. }
  273. if (toDir == null) {
  274. throw new BuildException("The todir attribute must be set.",
  275. getLocation());
  276. }
  277. if (!toDir.exists()) {
  278. toDir.mkdirs();
  279. } else if (toDir.isFile()) {
  280. throw new BuildException(toDir + " is not a directory");
  281. }
  282. if (srcEncoding == null) {
  283. srcEncoding = System.getProperty("file.encoding");
  284. }
  285. if (destEncoding == null) {
  286. destEncoding = srcEncoding;
  287. }
  288. if (bundleEncoding == null) {
  289. bundleEncoding = srcEncoding;
  290. }
  291. loadResourceMaps();
  292. translate();
  293. }
  294. /**
  295. * Load resource maps based on resource bundle encoding scheme.
  296. * The resource bundle lookup searches for resource files with various
  297. * suffixes on the basis of (1) the desired locale and (2) the default
  298. * locale (basebundlename), in the following order from lower-level
  299. * (more specific) to parent-level (less specific):
  300. *
  301. * basebundlename + "_" + language1 + "_" + country1 + "_" + variant1
  302. * basebundlename + "_" + language1 + "_" + country1
  303. * basebundlename + "_" + language1
  304. * basebundlename
  305. * basebundlename + "_" + language2 + "_" + country2 + "_" + variant2
  306. * basebundlename + "_" + language2 + "_" + country2
  307. * basebundlename + "_" + language2
  308. *
  309. * To the generated name, a ".properties" string is appeneded and
  310. * once this file is located, it is treated just like a properties file
  311. * but with bundle encoding also considered while loading.
  312. */
  313. private void loadResourceMaps() throws BuildException {
  314. Locale locale = new Locale(bundleLanguage,
  315. bundleCountry,
  316. bundleVariant);
  317. String language = locale.getLanguage().length() > 0
  318. ? "_" + locale.getLanguage() : "";
  319. String country = locale.getCountry().length() > 0
  320. ? "_" + locale.getCountry() : "";
  321. String variant = locale.getVariant().length() > 0
  322. ? "_" + locale.getVariant() : "";
  323. String bundleFile = bundle + language + country + variant;
  324. processBundle(bundleFile, BUNDLE_SPECIFIED_LANGUAGE_COUNTRY_VARIANT, false);
  325. bundleFile = bundle + language + country;
  326. processBundle(bundleFile, BUNDLE_SPECIFIED_LANGUAGE_COUNTRY, false);
  327. bundleFile = bundle + language;
  328. processBundle(bundleFile, BUNDLE_SPECIFIED_LANGUAGE, false);
  329. bundleFile = bundle;
  330. processBundle(bundleFile, BUNDLE_NOMATCH, false);
  331. //Load default locale bundle files
  332. //using default file encoding scheme.
  333. locale = Locale.getDefault();
  334. language = locale.getLanguage().length() > 0
  335. ? "_" + locale.getLanguage() : "";
  336. country = locale.getCountry().length() > 0
  337. ? "_" + locale.getCountry() : "";
  338. variant = locale.getVariant().length() > 0
  339. ? "_" + locale.getVariant() : "";
  340. bundleEncoding = System.getProperty("file.encoding");
  341. bundleFile = bundle + language + country + variant;
  342. processBundle(bundleFile, BUNDLE_DEFAULT_LANGUAGE_COUNTRY_VARIANT, false);
  343. bundleFile = bundle + language + country;
  344. processBundle(bundleFile, BUNDLE_DEFAULT_LANGUAGE_COUNTRY, false);
  345. bundleFile = bundle + language;
  346. processBundle(bundleFile, BUNDLE_DEFAULT_LANGUAGE, true);
  347. }
  348. /**
  349. * Process each file that makes up this bundle.
  350. */
  351. private void processBundle(final String bundleFile, final int i,
  352. final boolean checkLoaded) throws BuildException {
  353. final File propsFile = getProject().resolveFile(bundleFile + ".properties");
  354. FileInputStream ins = null;
  355. try {
  356. ins = new FileInputStream(propsFile);
  357. loaded = true;
  358. bundleLastModified[i] = propsFile.lastModified();
  359. log("Using " + propsFile, Project.MSG_DEBUG);
  360. loadResourceMap(ins);
  361. } catch (IOException ioe) {
  362. log(propsFile + " not found.", Project.MSG_DEBUG);
  363. //if all resource files associated with this bundle
  364. //have been scanned for and still not able to
  365. //find a single resrouce file, throw exception
  366. if (!loaded && checkLoaded) {
  367. throw new BuildException(ioe.getMessage(), getLocation());
  368. }
  369. }
  370. }
  371. /**
  372. * Load resourceMap with key value pairs. Values of existing keys
  373. * are not overwritten. Bundle's encoding scheme is used.
  374. */
  375. private void loadResourceMap(FileInputStream ins) throws BuildException {
  376. try {
  377. BufferedReader in = null;
  378. InputStreamReader isr = new InputStreamReader(ins, bundleEncoding);
  379. in = new BufferedReader(isr);
  380. String line = null;
  381. while ((line = in.readLine()) != null) {
  382. //So long as the line isn't empty and isn't a comment...
  383. if (line.trim().length() > 1 && '#' != line.charAt(0) && '!' != line.charAt(0)) {
  384. //Legal Key-Value separators are :, = and white space.
  385. int sepIndex = line.indexOf('=');
  386. if (-1 == sepIndex) {
  387. sepIndex = line.indexOf(':');
  388. }
  389. if (-1 == sepIndex) {
  390. for (int k = 0; k < line.length(); k++) {
  391. if (Character.isSpaceChar(line.charAt(k))) {
  392. sepIndex = k;
  393. break;
  394. }
  395. }
  396. }
  397. //Only if we do have a key is there going to be a value
  398. if (-1 != sepIndex) {
  399. String key = line.substring(0, sepIndex).trim();
  400. String value = line.substring(sepIndex + 1).trim();
  401. //Handle line continuations, if any
  402. while (value.endsWith("\\")) {
  403. value = value.substring(0, value.length() - 1);
  404. if ((line = in.readLine()) != null) {
  405. value = value + line.trim();
  406. } else {
  407. break;
  408. }
  409. }
  410. if (key.length() > 0) {
  411. //Has key already been loaded into resourceMap?
  412. if (resourceMap.get(key) == null) {
  413. resourceMap.put(key, value);
  414. }
  415. }
  416. }
  417. }
  418. }
  419. if (in != null) {
  420. in.close();
  421. }
  422. } catch (IOException ioe) {
  423. throw new BuildException(ioe.getMessage(), getLocation());
  424. }
  425. }
  426. /**
  427. * Reads source file line by line using the source encoding and
  428. * searches for keys that are sandwiched between the startToken
  429. * and endToken. The values for these keys are looked up from
  430. * the hashtable and substituted. If the hashtable doesn't
  431. * contain the key, they key itself is used as the value.
  432. * Detination files and directories are created as needed.
  433. * The destination file is overwritten only if
  434. * the forceoverwritten attribute is set to true if
  435. * the source file or any associated bundle resource file is
  436. * newer than the destination file.
  437. */
  438. private void translate() throws BuildException {
  439. for (int i = 0; i < filesets.size(); i++) {
  440. FileSet fs = (FileSet) filesets.elementAt(i);
  441. DirectoryScanner ds = fs.getDirectoryScanner(getProject());
  442. String[] srcFiles = ds.getIncludedFiles();
  443. for (int j = 0; j < srcFiles.length; j++) {
  444. try {
  445. File dest = fileUtils.resolveFile(toDir, srcFiles[j]);
  446. //Make sure parent dirs exist, else, create them.
  447. try {
  448. File destDir = new File(dest.getParent());
  449. if (!destDir.exists()) {
  450. destDir.mkdirs();
  451. }
  452. } catch (Exception e) {
  453. log("Exception occurred while trying to check/create "
  454. + " parent directory. " + e.getMessage(),
  455. Project.MSG_DEBUG);
  456. }
  457. destLastModified = dest.lastModified();
  458. File src = fileUtils.resolveFile(ds.getBasedir(), srcFiles[j]);
  459. srcLastModified = src.lastModified();
  460. //Check to see if dest file has to be recreated
  461. boolean needsWork = forceOverwrite
  462. || destLastModified < srcLastModified;
  463. if (!needsWork) {
  464. for (int icounter = 0; icounter < BUNDLE_MAX_ALTERNATIVES; icounter++) {
  465. needsWork = (destLastModified < bundleLastModified[icounter]);
  466. if (needsWork) {
  467. break;
  468. }
  469. }
  470. }
  471. if (needsWork) {
  472. log("Processing " + srcFiles[j],
  473. Project.MSG_DEBUG);
  474. FileOutputStream fos = new FileOutputStream(dest);
  475. BufferedWriter out
  476. = new BufferedWriter(new OutputStreamWriter(fos, destEncoding));
  477. FileInputStream fis = new FileInputStream(src);
  478. BufferedReader in
  479. = new BufferedReader(new InputStreamReader(fis, srcEncoding));
  480. String line;
  481. LineTokenizer lineTokenizer = new LineTokenizer();
  482. lineTokenizer.setIncludeDelims(true);
  483. line = lineTokenizer.getToken(in);
  484. while ((line) != null) {
  485. // 2003-02-21 new replace algorithm by tbee (tbee@tbee.org)
  486. // because it wasn't able to replace something like "@aaa;@bbb;"
  487. // is there a startToken
  488. // and there is still stuff following the startToken
  489. int startIndex = line.indexOf(startToken);
  490. while (startIndex >= 0
  491. && (startIndex + startToken.length()) <= line.length()) {
  492. // the new value, this needs to be here
  493. // because it is required to calculate the next position to search from
  494. // at the end of the loop
  495. String replace = null;
  496. // we found a starttoken, is there an endtoken following?
  497. // start at token+tokenlength because start and end
  498. // token may be indentical
  499. int endIndex = line.indexOf(endToken, startIndex + startToken.length());
  500. if (endIndex < 0) {
  501. startIndex += 1;
  502. } else {
  503. // grab the token
  504. String token
  505. = line.substring(startIndex + startToken.length(), endIndex);
  506. // If there is a white space or = or :, then
  507. // it isn't to be treated as a valid key.
  508. boolean validToken = true;
  509. for (int k = 0; k < token.length() && validToken; k++) {
  510. char c = token.charAt(k);
  511. if (c == ':' || c == '='
  512. || Character.isSpaceChar(c)) {
  513. validToken = false;
  514. }
  515. }
  516. if (!validToken) {
  517. startIndex += 1;
  518. } else {
  519. // find the replace string
  520. if (resourceMap.containsKey(token)) {
  521. replace = (String) resourceMap.get(token);
  522. } else {
  523. replace = token;
  524. }
  525. // generate the new line
  526. line = line.substring(0, startIndex)
  527. + replace
  528. + line.substring(endIndex + endToken.length());
  529. // set start position for next search
  530. startIndex += replace.length();
  531. }
  532. }
  533. // find next starttoken
  534. startIndex = line.indexOf(startToken, startIndex);
  535. }
  536. out.write(line);
  537. line = lineTokenizer.getToken(in);
  538. }
  539. if (in != null) {
  540. in.close();
  541. }
  542. if (out != null) {
  543. out.close();
  544. }
  545. } else {
  546. log("Skipping " + srcFiles[j]
  547. + " as destination file is up to date",
  548. Project.MSG_VERBOSE);
  549. }
  550. } catch (IOException ioe) {
  551. throw new BuildException(ioe.getMessage(), getLocation());
  552. }
  553. }
  554. }
  555. }
  556. }