001 /*
002 // $Id: //open/util/resgen/src/org/eigenbase/resgen/XmlFileTask.java#12 $
003 // Package org.eigenbase.resgen is an i18n resource generator.
004 // Copyright (C) 2005-2008 The Eigenbase Project
005 // Copyright (C) 2005-2008 Disruptive Tech
006 // Copyright (C) 2005-2008 LucidEra, Inc.
007 // Portions Copyright (C) 2001-2005 Kana Software, Inc. and others.
008 //
009 // This library is free software; you can redistribute it and/or modify it
010 // under the terms of the GNU Lesser General Public License as published by the
011 // Free Software Foundation; either version 2 of the License, or (at your
012 // option) any later version approved by The Eigenbase Project.
013 //
014 // This library is distributed in the hope that it will be useful,
015 // but WITHOUT ANY WARRANTY; without even the implied warranty of
016 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
017 // GNU Lesser General Public License for more details.
018 //
019 // You should have received a copy of the GNU Lesser General Public License
020 // along with this library; if not, write to the Free Software
021 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
022 */
023
024 package org.eigenbase.resgen;
025
026 import org.apache.tools.ant.BuildException;
027
028 import java.io.*;
029 import java.net.URL;
030 import java.util.*;
031
032 /**
033 * Ant task which processes an XML file and generates a C++ or Java class from
034 * the resources in it.
035 *
036 * @author jhyde
037 * @since 19 September, 2005
038 * @version $Id: //open/util/resgen/src/org/eigenbase/resgen/XmlFileTask.java#12 $
039 */
040 class XmlFileTask extends FileTask
041 {
042 final String baseClassName;
043 final String cppBaseClassName;
044
045 XmlFileTask(ResourceGenTask.Include include, String fileName,
046 String className, String baseClassName, boolean outputJava,
047 String cppClassName, String cppBaseClassName, boolean outputCpp)
048 {
049 this.include = include;
050 this.fileName = fileName;
051 this.outputJava = outputJava;
052 if (className == null) {
053 className = Util.fileNameToClassName(fileName, ".xml");
054 }
055 this.className = className;
056 if (baseClassName == null) {
057 baseClassName = "org.eigenbase.resgen.ShadowResourceBundle";
058 }
059 this.baseClassName = baseClassName;
060
061 this.outputCpp = outputCpp;
062 if (cppClassName == null) {
063 cppClassName = Util.fileNameToCppClassName(fileName, ".xml");
064 }
065 this.cppClassName = cppClassName;
066 if (cppBaseClassName == null) {
067 cppBaseClassName = "ResourceBundle";
068 }
069 this.cppBaseClassName = cppBaseClassName;
070 }
071
072 void process(ResourceGen generator) throws IOException {
073 URL url = Util.convertPathToURL(getFile());
074 ResourceDef.ResourceBundle resourceList = Util.load(url);
075 if (resourceList.locale == null) {
076 throw new BuildException(
077 "Resource file " + url + " must have locale");
078 }
079
080 ArrayList localeNames = new ArrayList();
081 if (include.root.locales == null) {
082 localeNames.add(resourceList.locale);
083 } else {
084 StringTokenizer tokenizer = new StringTokenizer(include.root.locales,",");
085 while (tokenizer.hasMoreTokens()) {
086 String token = tokenizer.nextToken();
087 localeNames.add(token);
088 }
089 }
090
091 if (!localeNames.contains(resourceList.locale)) {
092 throw new BuildException(
093 "Resource file " + url + " has locale '" +
094 resourceList.locale +
095 "' which is not in the 'locales' list");
096 }
097
098 Locale[] locales = new Locale[localeNames.size()];
099 for (int i = 0; i < locales.length; i++) {
100 String localeName = (String) localeNames.get(i);
101 locales[i] = Util.parseLocale(localeName);
102 if (locales[i] == null) {
103 throw new BuildException(
104 "Invalid locale " + localeName);
105 }
106 }
107
108
109 if (outputJava) {
110 generateJava(generator, resourceList, null);
111 }
112
113 generateProperties(generator, resourceList, null);
114
115 for (int i = 0; i < locales.length; i++) {
116 Locale locale = locales[i];
117 if (outputJava) {
118 generateJava(generator, resourceList, locale);
119 }
120 generateProperties(generator, resourceList, locale);
121 }
122
123 if (outputCpp) {
124 generateCpp(generator, resourceList);
125 }
126 }
127
128 private void generateProperties(
129 ResourceGen generator,
130 ResourceDef.ResourceBundle resourceList,
131 Locale locale) {
132 String fileName = Util.getClassNameSansPackage(className, locale) + ".properties";
133 File file = new File(getResourceDirectory(), fileName);
134 File srcFile = locale == null ?
135 getFile() :
136 new File(getSrcDirectory(), fileName);
137 if (file.exists()) {
138 if (locale != null) {
139 if (file.equals(srcFile)) {
140 // The locale.properties file already exists, and the
141 // source and target locale.properties files are the
142 // same. No need to create it, or even to issue a warning.
143 // We were only going to create an empty file, anyway.
144 return;
145 }
146 }
147 if (file.lastModified() >= srcFile.lastModified()) {
148 generator.comment(file + " is up to date");
149 return;
150 }
151 if (!file.canWrite()) {
152 generator.comment(file + " is read-only");
153 return;
154 }
155 }
156 generator.comment("Generating " + file);
157 final FileOutputStream out;
158 try {
159 if (file.getParentFile() != null) {
160 file.getParentFile().mkdirs();
161 }
162 out = new FileOutputStream(file);
163 } catch (FileNotFoundException e) {
164 throw new BuildException("Error while writing " + file, e);
165 }
166 PrintWriter pw = new PrintWriter(out);
167 try {
168 if (locale == null) {
169 generateBaseProperties(resourceList, pw);
170 } else {
171 generateProperties(pw, file, srcFile, locale);
172 }
173 } finally {
174 pw.close();
175 }
176 }
177
178
179 /**
180 * Generates a properties file containing a line for each resource.
181 */
182 private void generateBaseProperties(
183 ResourceDef.ResourceBundle resourceList,
184 PrintWriter pw)
185 {
186 String fullClassName = getClassName(null);
187 pw.println("# This file contains the resources for");
188 pw.println("# class '" + fullClassName + "'; the base locale is '" +
189 resourceList.locale + "'.");
190 pw.println("# It was generated by " + ResourceGen.class);
191
192 pw.println("# from " + getFileForComments());
193 if (include.root.commentStyle !=
194 ResourceGenTask.COMMENT_STYLE_SCM_SAFE)
195 {
196 pw.println("# on " + new Date().toString() + ".");
197 }
198 pw.println();
199 for (int i = 0; i < resourceList.resources.length; i++) {
200 ResourceDef.Resource resource = resourceList.resources[i];
201 final String name = resource.name;
202 if (resource.text == null) {
203 throw new BuildException(
204 "Resource '" + name + "' has no message");
205 }
206 final String message = resource.text.cdata;
207 if (message == null) {
208 continue;
209 }
210 if (count(resource.text.cdata, '\'') % 2 != 0) {
211 System.out.println(
212 "WARNING: The message for resource '" + resource.name
213 + "' has an odd number of single-quotes. These should"
214 + " probably be doubled (to include an single-quote in"
215 + " a message) or closed (to include a literal string"
216 + " in a message).");
217 }
218 pw.println(name + "=" + Util.quoteForProperties(message));
219 }
220 pw.println("# End " + fullClassName + ".properties");
221 }
222
223 /**
224 * Returns the number of occurrences of a given character in a string.
225 *
226 * <p>For example, {@code count("foobar", 'o')} returns 2.
227 *
228 * @param s String
229 * @param c Character
230 * @return Number of occurrences
231 */
232 private int count(String s, char c) {
233 int count = 0;
234 for (int i = 0; i < s.length(); i++) {
235 if (s.charAt(i) == c) {
236 ++count;
237 }
238 }
239 return count;
240 }
241
242 /**
243 * Generates a properties file for a given locale. If there is a source
244 * file for the locale, it is copied. Otherwise generates a file with
245 * headers but no resources.
246 *
247 * @param pw Output file writer
248 * @param targetFile the locale-specific output file
249 * @param srcFile The locale-specific properties file, e.g.
250 * "source/happy/BirthdayResource_fr-FR.properties". It may not exist,
251 * but if it does, we copy it.
252 * @param locale Locale, never null
253 * @pre locale != null
254 */
255 private void generateProperties(
256 PrintWriter pw,
257 File targetFile,
258 File srcFile,
259 Locale locale)
260 {
261 if (srcFile.exists() && srcFile.canRead() && !targetFile.equals(srcFile)) {
262 try {
263 final FileReader reader = new FileReader(srcFile);
264
265 final char[] cbuf = new char[1000];
266 int charsRead;
267 while ((charsRead = reader.read(cbuf)) > 0) {
268 pw.write(cbuf, 0, charsRead);
269 }
270 return;
271 } catch (IOException e) {
272 throw new BuildException("Error while copying from '" +
273 srcFile + "'");
274 }
275 }
276
277 // Generate an empty file.
278 String fullClassName = getClassName(locale);
279 pw.println("# This file contains the resources for");
280 pw.println("# class '" + fullClassName + "' and locale '" + locale + "'.");
281 pw.println("# It was generated by " + ResourceGen.class);
282 pw.println("# from " + getFileForComments());
283 if (include.root.commentStyle !=
284 ResourceGenTask.COMMENT_STYLE_SCM_SAFE)
285 {
286 pw.println("# on " + new Date().toString() + ".");
287 }
288 pw.println();
289 pw.println("# This file is intentionally blank. Add property values");
290 pw.println("# to this file to override the translations in the base");
291 String basePropertiesFileName = Util.getClassNameSansPackage(className, locale) + ".properties";
292 pw.println("# properties file, " + basePropertiesFileName);
293 pw.println();
294 pw.println("# End " + fullClassName + ".properties");
295 }
296
297 private String getClassName(Locale locale) {
298 String s = className;
299 if (locale != null) {
300 s += '_' + locale.toString();
301 }
302 return s;
303 }
304
305 protected void generateCpp(
306 ResourceGen generator,
307 ResourceDef.ResourceBundle resourceList)
308 {
309 String defaultExceptionClass = resourceList.cppExceptionClassName;
310 String defaultExceptionLocation = resourceList.cppExceptionClassLocation;
311 if (defaultExceptionClass != null &&
312 defaultExceptionLocation == null) {
313 throw new BuildException(
314 "C++ exception class is defined without a header file location in "
315 + getFile());
316 }
317
318 for (int i = 0; i < resourceList.resources.length; i++) {
319 ResourceDef.Resource resource = resourceList.resources[i];
320
321 if (resource.text == null) {
322 throw new BuildException(
323 "Resource '" + resource.name + "' has no message");
324 }
325
326 if (resource instanceof ResourceDef.Exception) {
327 ResourceDef.Exception exception =
328 (ResourceDef.Exception)resource;
329
330 if (exception.cppClassName != null &&
331 (exception.cppClassLocation == null &&
332 defaultExceptionLocation == null)) {
333 throw new BuildException(
334 "C++ exception class specified for "
335 + exception.name
336 + " without specifiying a header location in "
337 + getFile());
338 }
339
340 if (defaultExceptionClass == null &&
341 exception.cppClassName == null) {
342 throw new BuildException(
343 "No exception class specified for "
344 + exception.name
345 + " in "
346 + getFile());
347 }
348 }
349 }
350
351
352 String hFilename = cppClassName + ".h";
353 String cppFileName = cppClassName + ".cpp";
354
355 File hFile = new File(include.root.dest, hFilename);
356 File cppFile = new File(include.root.dest, cppFileName);
357
358 boolean allUpToDate = true;
359
360 if (!checkUpToDate(generator, hFile)) {
361 allUpToDate = false;
362 }
363
364 if (!checkUpToDate(generator, cppFile)) {
365 allUpToDate = false;
366 }
367
368 if (allUpToDate && !include.root.force) {
369 return;
370 }
371
372 generator.comment("Generating " + hFile);
373
374 final FileOutputStream hOut;
375 try {
376 makeParentDirs(hFile);
377
378 hOut = new FileOutputStream(hFile);
379 } catch (FileNotFoundException e) {
380 throw new BuildException("Error while writing " + hFile, e);
381 }
382
383 String className = Util.removePackage(this.className);
384 String baseClassName = Util.removePackage(this.cppBaseClassName);
385
386 PrintWriter pw = new PrintWriter(hOut);
387 try {
388 final CppHeaderGenerator gen =
389 new CppHeaderGenerator(getFile(), hFile,
390 className, baseClassName, defaultExceptionClass);
391 configureCommentStyle(gen);
392 gen.generateModule(generator, resourceList, pw);
393 } finally {
394 pw.close();
395 }
396
397 generator.comment("Generating " + cppFile);
398
399 final FileOutputStream cppOut;
400 try {
401 makeParentDirs(cppFile);
402
403 cppOut = new FileOutputStream(cppFile);
404 } catch (FileNotFoundException e) {
405 throw new BuildException("Error while writing " + cppFile, e);
406 }
407
408 pw = new PrintWriter(cppOut);
409 try {
410 final CppGenerator gen =
411 new CppGenerator(getFile(), cppFile, className, baseClassName,
412 defaultExceptionClass, hFilename);
413 configureCommentStyle(gen);
414 gen.generateModule(generator, resourceList, pw);
415 } finally {
416 pw.close();
417 }
418 }
419 }
420
421 // End XmlFileTask.java