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
024package org.eigenbase.resgen;
025
026import org.apache.tools.ant.BuildException;
027
028import java.io.*;
029import java.net.URL;
030import 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 */
040class 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