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