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