KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > cocoon > transformation > I18nTransformer


1 /*
2  * Copyright 1999-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 package org.apache.cocoon.transformation;
17
18 import org.apache.avalon.framework.activity.Disposable;
19 import org.apache.avalon.framework.configuration.Configurable;
20 import org.apache.avalon.framework.configuration.Configuration;
21 import org.apache.avalon.framework.configuration.ConfigurationException;
22 import org.apache.avalon.framework.parameters.Parameters;
23 import org.apache.avalon.framework.service.ServiceException;
24 import org.apache.avalon.framework.service.ServiceManager;
25 import org.apache.avalon.framework.service.Serviceable;
26
27 import org.apache.cocoon.ProcessingException;
28 import org.apache.cocoon.caching.CacheableProcessingComponent;
29 import org.apache.cocoon.components.treeprocessor.variables.VariableExpressionTokenizer;
30 import org.apache.cocoon.components.treeprocessor.variables.VariableResolver;
31 import org.apache.cocoon.components.treeprocessor.variables.VariableResolverFactory;
32 import org.apache.cocoon.environment.SourceResolver;
33 import org.apache.cocoon.i18n.Bundle;
34 import org.apache.cocoon.i18n.BundleFactory;
35 import org.apache.cocoon.i18n.I18nUtils;
36 import org.apache.cocoon.sitemap.PatternException;
37 import org.apache.cocoon.xml.ParamSaxBuffer;
38 import org.apache.cocoon.xml.SaxBuffer;
39
40 import org.apache.excalibur.source.SourceValidity;
41 import org.xml.sax.Attributes JavaDoc;
42 import org.xml.sax.SAXException JavaDoc;
43 import org.xml.sax.helpers.AttributesImpl JavaDoc;
44
45 import java.io.IOException JavaDoc;
46 import java.text.DateFormat JavaDoc;
47 import java.text.DecimalFormat JavaDoc;
48 import java.text.DecimalFormatSymbols JavaDoc;
49 import java.text.NumberFormat JavaDoc;
50 import java.text.ParseException JavaDoc;
51 import java.text.SimpleDateFormat JavaDoc;
52 import java.util.Collections JavaDoc;
53 import java.util.Date JavaDoc;
54 import java.util.HashMap JavaDoc;
55 import java.util.HashSet JavaDoc;
56 import java.util.Iterator JavaDoc;
57 import java.util.Locale JavaDoc;
58 import java.util.Map JavaDoc;
59 import java.util.MissingResourceException JavaDoc;
60 import java.util.Set JavaDoc;
61 import java.util.StringTokenizer JavaDoc;
62
63 /**
64  * @cocoon.sitemap.component.documentation
65  * Internationalization transformer is used to transform i18n markup into text
66  * based on a particular locale.
67  *
68  * @cocoon.sitemap.component.name i18n
69  * @cocoon.sitemap.component.documentation.caching TBD
70  * @cocoon.sitemap.component.logger sitemap.transformer.i18n
71  *
72  * <h3>i18n transformer</h3>
73  * <p>The i18n transformer works by finding a translation for the user's locale
74  * in the configured catalogues. Locale is determined based on the request,
75  * session, or a cookie data. See {@link org.apache.cocoon.acting.LocaleAction}
76  * for details.</p>
77  *
78  * <p>For the passed local it then attempts to find a message catalogue that
79  * satisifies the locale, and uses it for for processing text replacement
80  * directed by i18n markup.</p>
81  *
82  * <p>Message catalogues are maintained in separate files, with a naming
83  * convention similar to that of {@link java.util.ResourceBundle}. I.e.
84  * <code>basename_locale</code>, where <i>basename</i> can be any name,
85  * and <i>locale</i> can be any locale specified using ISO 639/3166
86  * characters (eg. <code>en_AU</code>, <code>de_AT</code>, <code>es</code>).</p>
87  *
88  * <p><strong>NOTE:</strong> ISO 639 is not a stable standard; some of the
89  * language codes it defines (specifically, iw, ji, and in) have changed
90  * (see {@link java.util.Locale} for details).
91  *
92  * <h3>Message Catalogues</h3>
93  * <p>Catalogues are of the following format:
94  * <pre>
95  * &lt;?xml version="1.0"?&gt;
96  * &lt;!-- message catalogue file for locale ... --&gt;
97  * &lt;catalogue xml:lang=&quot;locale&quot;&gt;
98  * &lt;message key="key"&gt;text &lt;i&gt;or&lt;/i&gt; markup&lt;/message&gt;
99  * ....
100  * &lt;/catalogue&gt;
101  * </pre>
102  * Where <code>key</code> specifies a particular message for that
103  * language.
104  *
105  * <h3>Usage</h3>
106  * <p>Files to be translated contain the following markup:
107  * <pre>
108  * &lt;?xml version="1.0"?&gt;
109  * ... some text, translate &lt;i18n:text&gt;key&lt;/i18n:text&gt;
110  * </pre>
111  * At runtime, the i18n transformer will find a message catalogue for the
112  * user's locale, and will appropriately replace the text between the
113  * <code>&lt;i18n:text&gt;</code> markup, using the value between the tags as
114  * the lookup key.</p>
115  *
116  * <p>If the i18n transformer cannot find an appropriate message catalogue for
117  * the user's given locale, it will recursively try to locate a <i>parent</i>
118  * message catalogue, until a valid catalogue can be found.
119  * ie:
120  * <ul>
121  * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>_<i>variant</i>.xml
122  * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>.xml
123  * <li><strong>catalogue</strong>_<i>language</i>.xml
124  * <li><strong>catalogue</strong>.xml
125  * </ul>
126  * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i>
127  * (no variant), the following search will occur:
128  * <ul>
129  * <li><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
130  * <li><strong>messages</strong>_<i>en</i>.xml
131  * <li><strong>messages</strong>.xml
132  * </ul>
133  * This allows the developer to write a hierarchy of message catalogues,
134  * at each defining messages with increasing depth of variation.</p>
135  *
136  * <p>In addition, catalogues can be split across multiple locations. For example,
137  * there can be a default catalogue in one directory with a user or client specific
138  * catalogue in another directory. The catalogues will be searched in the order of
139  * the locations specified still following the locale ordering specified above.
140  * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i>
141  * (no variant) and locations of <i>translations/client</i> and <i>translations</i>,
142  * the following search will occur:
143  * <ul>
144  * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
145  * <li><i>translations/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
146  * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>.xml
147  * <li><i>translations/</i><strong>messages</strong>_<i>en</i.xml
148  * <li><i>translations/client/</i><strong>messages</strong>.xml
149  * <li><i>translations/</i><strong>messages</strong>.xml
150  * </ul>
151  * </p>
152  *
153  * <p>The <code>i18n:text</code> element can optionally take an attribute
154  * <code>i18n:catalogue</code> to indicate which specific catalogue to use.
155  * The value of this attribute should be the id of the catalogue to use
156  * (see sitemap configuration).
157  *
158  * <h3>Sitemap configuration</h3>
159  * <pre>
160  * &lt;map:transformer name="i18n"
161  * SRC="org.apache.cocoon.transformation.I18nTransformer"&gt;
162  *
163  * &lt;catalogues default="someId"&gt;
164  * &lt;catalogue id="someId" name="messages" [location="translations"]&gt;
165  * [&lt;location&gt;translations/client&lt;/location&gt;]
166  * [&lt;location&gt;translations&lt;/location&gt;]
167  * &lt;/catalogue&gt;
168  * ...
169  * &lt;/catalogues&gt;
170  * &lt;untranslated-text&gt;untranslated&lt;/untranslated-text&gt;
171  * &lt;cache-at-startup&gt;true&lt;/cache-at-startup&gt;
172  * &lt;/map:transformer&gt;
173  * </pre>
174  * Where:
175  * <ul>
176  * <li><strong>catalogues</strong>: container element in which the catalogues
177  * are defined. It must have an attribute 'default' whose value is one
178  * of the id's of the catalogue elements. (<i>mandatory</i>).
179  * <li><strong>catalogue</strong>: specifies a catalogue. It takes 2 required
180  * attributes: id (can be wathever you like) and name (base name of the catalogue).
181  * The location (location of the message catalogue) is also required, but can be
182  * specified either as an attribute or as one or more subelements, but not both.
183  * If more than one location is specified the catalogues will be searched in the
184  * order they appear in the configuration. The name and location can contain
185  * references to inputmodules (same syntax as in other places in the
186  * sitemap). They are resolved on each usage of the transformer, so they can
187  * refer to e.g. request parameters. (<i>at least 1 catalogue
188  * element required</i>). After input module references are resolved the location
189  * string can be the root of a URI. For example, specifying a location of
190  * cocoon:/test with a name of messages and a locale of en_GB will cause the
191  * sitemap to try to process cocoon:/test/messages_en_GB.xml,
192  * cocoon:/test/messages_en.xml and cocoon:/test/messages.xml.
193  * <li><strong>untranslated-text</strong>: text used for
194  * untranslated keys (default is to output the key name).
195  * <li><strong>cache-at-startup</strong>: flag whether to cache
196  * messages at startup (false by default).
197  * </ul>
198  *
199  * <h3>Pipeline Usage</h3>
200  * <p>To use the transformer in a pipeline, simply specify it in a particular
201  * transform, and pass locale parameter:
202  * <pre>
203  * &lt;map:match pattern="file"&gt;
204  * &lt;map:generate SRC="file.xml"/&gt;
205  * &lt;map:transform type="i18n"&gt;
206  * &lt;map:parameter name="locale" value="..."/&gt;
207  * &lt;/map:transform&gt;
208  * &lt;map:serialize/&gt;
209  * &lt;/map:match&gt;
210  * </pre>
211  * You can use {@link org.apache.cocoon.acting.LocaleAction} or any other
212  * way to provide transformer with a locale.</p>
213  *
214  * <p>If in certain pipeline, you want to use a different catalogue as the
215  * default catalogue, you can do so by specifying a parameter called
216  * <strong>default-catalogue-id</strong>.
217  *
218  * <p>The <strong>untranslated-text</strong> can also be overridden at the
219  * pipeline level by specifying it as a parameter.</p>
220  *
221  *
222  * <h3>i18n markup</h3>
223  *
224  * <p>For date, time and number formatting use the following tags:
225  * <ul>
226  * <li><strong>&lt;i18n:date/&gt;</strong> gives localized date.</li>
227  * <li><strong>&lt;i18n:date-time/&gt;</strong> gives localized date and time.</li>
228  * <li><strong>&lt;i18n:time/&gt;</strong> gives localized time.</li>
229  * <li><strong>&lt;i18n:number/&gt;</strong> gives localized number.</li>
230  * <li><strong>&lt;i18n:currency/&gt;</strong> gives localized currency.</li>
231  * <li><strong>&lt;i18n:percent/&gt;</strong> gives localized percent.</li>
232  * </ul>
233  * Elements <code>date</code>, <code>date-time</code> and <code>time</code>
234  * accept <code>pattern</code> and <code>src-pattern</code> attribute, with
235  * values of:
236  * <ul>
237  * <li><code>short</code>
238  * <li><code>medium</code>
239  * <li><code>long</code>
240  * <li><code>full</code>
241  * </ul>
242  * See {@link java.text.DateFormat} for more info on these values.</p>
243  *
244  * <p>Elements <code>date</code>, <code>date-time</code>, <code>time</code> and
245  * <code>number</code>, a different <code>locale</code> and
246  * <code>source-locale</code> can be specified:
247  * <pre>
248  * &lt;i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
249  * 12/24/01
250  * &lt;/i18n:date&gt;
251  * </pre>
252  * Will result in 24.12.2001.</p>
253  *
254  * <p>A given real <code>pattern</code> and <code>src-pattern</code> (not
255  * keywords <code>short, medium, long, full</code>) overrides any value
256  * specified by <code>locale</code> and <code>src-locale</code> attributes.</p>
257  *
258  * <p>Future work coming:
259  * <ul>
260  * <li>Introduce new &lt;get-locale/&gt; element
261  * <li>Move all formatting routines to I18nUtils
262  * </ul>
263  *
264  * @author <a HREF="mailto:kpiroumian@apache.org">Konstantin Piroumian</a>
265  * @author <a HREF="mailto:mattam@netcourrier.com">Matthieu Sozeau</a>
266  * @author <a HREF="mailto:crafterm@apache.org">Marcus Crafter</a>
267  * @author <a HREF="mailto:Michael.Enke@wincor-nixdorf.com">Michael Enke</a>
268  * @version CVS $Id: I18nTransformer.java 312968 2005-10-11 22:28:46Z vgritsenko $
269  */

270 public class I18nTransformer extends AbstractTransformer
271                              implements CacheableProcessingComponent,
272                                         Serviceable, Configurable, Disposable {
273
274     /**
275      * The namespace for i18n is "http://apache.org/cocoon/i18n/2.1".
276      */

277     public static final String JavaDoc I18N_NAMESPACE_URI =
278             "http://apache.org/cocoon/i18n/2.1";
279
280     /**
281      * The old namespace for i18n is "http://apache.org/cocoon/i18n/2.0".
282      */

283     public static final String JavaDoc I18N_OLD_NAMESPACE_URI =
284             "http://apache.org/cocoon/i18n/2.0";
285
286     /**
287      * Did we already encountered an old namespace? This is static to ensure
288      * that the associated message will be logged only once.
289      */

290     private static boolean deprecationFound = false;
291
292     //
293
// i18n elements
294
//
295

296     /**
297      * <code>i18n:text</code> element is used to translate any text, with
298      * or without markup. Example:
299      * <pre>
300      * &lt;i18n:text&gt;
301      * This is &lt;strong&gt;translated&lt;/strong&gt; string.
302      * &lt;/i18n:text&gt;
303      * </pre>
304      */

305     public static final String JavaDoc I18N_TEXT_ELEMENT = "text";
306
307     /**
308      * <code>i18n:translate</code> element is used to translate text with
309      * parameter substitution. Example:
310      * <pre>
311      * &lt;i18n:translate&gt;
312      * &lt;i18n:text&gt;This is translated string with {0} param&lt;/i18n:text&gt;
313      * &lt;i18n:param&gt;1&lt;/i18n:param&gt;
314      * &lt;/i18n:translate&gt;
315      * </pre>
316      * The <code>i18n:text</code> fragment can include markup and parameters
317      * at any place. Also do parameters, which can include <code>i18n:text</code>,
318      * <code>i18n:date</code>, etc. elements (without keys only).
319      *
320      * @see #I18N_TEXT_ELEMENT
321      * @see #I18N_PARAM_ELEMENT
322      */

323     public static final String JavaDoc I18N_TRANSLATE_ELEMENT = "translate";
324
325     /**
326      * <code>i18n:choose</code> element is used to translate elements in-place.
327      * The first <code>i18n:when</code> element matching the current locale
328      * is selected and the others are discarded.
329      *
330      * <p>To specify what to do if no locale matched, simply add a node with
331      * <code>locale="*"</code>. <em>Note that this element must be the last
332      * child of &lt;i18n:choose&gt;.</em></p>
333      * <pre>
334      * &lt;i18n:choose&gt;
335      * &lt;i18n:when locale="en"&gt;
336      * Good Morning
337      * &lt;/en&gt;
338      * &lt;i18n:when locale="fr"&gt;
339      * Bonjour
340      * &lt;/jp&gt;
341      * &lt;i18n:when locale="jp"&gt;
342      * Aligato?
343      * &lt;/jp&gt;
344      * &lt;i18n:otherwise&gt;
345      * Sorry, i don't know how to say hello in your language
346      * &lt;/jp&gt;
347      * &lt;i18n:translate&gt;
348      * </pre>
349      * <p>You can include any markup within <code>i18n:when</code> elements,
350      * with the exception of other <code>i18n:*</code> elements.</p>
351      *
352      * @see #I18N_IF_ELEMENT
353      * @see #I18N_LOCALE_ATTRIBUTE
354      * @since 2.1
355      */

356     public static final String JavaDoc I18N_CHOOSE_ELEMENT = "choose";
357
358     /**
359      * <code>i18n:when</code> is used to test a locale.
360      * It can be used within <code>i18:choose</code> elements or alone.
361      * <em>Note: Using <code>locale="*"</code> here has no sense.</em>
362      * Example:
363      * <pre>
364      * &lt;greeting&gt;
365      * &lt;i18n:when locale="en"&gt;Hello&lt;/i18n:when&gt;
366      * &lt;i18n:when locale="fr"&gt;Bonjour&lt;/i18n:when&gt;
367      * &lt;/greeting&gt;
368      * </pre>
369      *
370      * @see #I18N_LOCALE_ATTRIBUTE
371      * @see #I18N_CHOOSE_ELEMENT
372      * @since 2.1
373      */

374     public static final String JavaDoc I18N_WHEN_ELEMENT = "when";
375
376     /**
377      * <code>i18n:if</code> is used to test a locale. Example:
378      * <pre>
379      * &lt;greeting&gt;
380      * &lt;i18n:if locale="en"&gt;Hello&lt;/i18n:when&gt;
381      * &lt;i18n:if locale="fr"&gt;Bonjour&lt;/i18n:when&gt;
382      * &lt;/greeting&gt;
383      * </pre>
384      *
385      * @see #I18N_LOCALE_ATTRIBUTE
386      * @see #I18N_CHOOSE_ELEMENT
387      * @see #I18N_WHEN_ELEMENT
388      * @since 2.1
389      */

390     public static final String JavaDoc I18N_IF_ELEMENT = "if";
391
392     /**
393      * <code>i18n:otherwise</code> is used to match any locale when
394      * no matching locale has been found inside an <code>i18n:choose</code>
395      * block.
396      *
397      * @see #I18N_CHOOSE_ELEMENT
398      * @see #I18N_WHEN_ELEMENT
399      * @since 2.1
400      */

401     public static final String JavaDoc I18N_OTHERWISE_ELEMENT = "otherwise";
402
403     /**
404      * <code>i18n:param</code> is used with i18n:translate to provide
405      * substitution params. The param can have <code>i18n:text</code> as
406      * its value to provide multilungual value. Parameters can have
407      * additional attributes to be used for formatting:
408      * <ul>
409      * <li><code>type</code>: can be <code>date, date-time, time,
410      * number, currency, currency-no-unit or percent</code>.
411      * Used to format params before substitution.</li>
412      * <li><code>value</code>: the value of the param. If no value is
413      * specified then the text inside of the param element will be used.</li>
414      * <li><code>locale</code>: used only with <code>number, date, time,
415      * date-time</code> types and used to override the current locale to
416      * format the given value.</li>
417      * <li><code>src-locale</code>: used with <code>number, date, time,
418      * date-time</code> types and specify the locale that should be used to
419      * parse the given value.</li>
420      * <li><code>pattern</code>: used with <code>number, date, time,
421      * date-time</code> types and specify the pattern that should be used
422      * to format the given value.</li>
423      * <li><code>src-pattern</code>: used with <code>number, date, time,
424      * date-time</code> types and specify the pattern that should be used
425      * to parse the given value.</li>
426      * </ul>
427      *
428      * @see #I18N_TRANSLATE_ELEMENT
429      * @see #I18N_DATE_ELEMENT
430      * @see #I18N_TIME_ELEMENT
431      * @see #I18N_DATE_TIME_ELEMENT
432      * @see #I18N_NUMBER_ELEMENT
433      */

434     public static final String JavaDoc I18N_PARAM_ELEMENT = "param";
435
436     /**
437      * This attribute affects a name to the param that could be used
438      * for substitution.
439      *
440      * @since 2.1
441      */

442     public static final String JavaDoc I18N_PARAM_NAME_ATTRIBUTE = "name";
443
444     /**
445      * <code>i18n:date</code> is used to provide a localized date string.
446      * Allowed attributes are: <code>pattern, src-pattern, locale,
447      * src-locale</code>. Usage examples:
448      * <pre>
449      * &lt;i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
450      * 12/24/01
451      * &lt;/i18n:date&gt;
452      *
453      * &lt;i18n:date pattern="dd/MM/yyyy" /&gt;
454      * </pre>
455      *
456      * If no value is specified then the current date will be used. E.g.:
457      * <pre>
458      * &lt;i18n:date /&gt;
459      * </pre>
460      * Displays the current date formatted with default pattern for
461      * the current locale.
462      *
463      * @see #I18N_PARAM_ELEMENT
464      * @see #I18N_DATE_TIME_ELEMENT
465      * @see #I18N_TIME_ELEMENT
466      * @see #I18N_NUMBER_ELEMENT
467      */

468     public static final String JavaDoc I18N_DATE_ELEMENT = "date";
469
470     /**
471      * <code>i18n:date-time</code> is used to provide a localized date and
472      * time string. Allowed attributes are: <code>pattern, src-pattern,
473      * locale, src-locale</code>. Usage examples:
474      * <pre>
475      * &lt;i18n:date-time src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
476      * 12/24/01 1:00 AM
477      * &lt;/i18n:date&gt;
478      *
479      * &lt;i18n:date-time pattern="dd/MM/yyyy hh:mm" /&gt;
480      * </pre>
481      *
482      * If no value is specified then the current date and time will be used.
483      * E.g.:
484      * <pre>
485      * &lt;i18n:date-time /&gt;
486      * </pre>
487      * Displays the current date formatted with default pattern for
488      * the current locale.
489      *
490      * @see #I18N_PARAM_ELEMENT
491      * @see #I18N_DATE_ELEMENT
492      * @see #I18N_TIME_ELEMENT
493      * @see #I18N_NUMBER_ELEMENT
494      */

495     public static final String JavaDoc I18N_DATE_TIME_ELEMENT = "date-time";
496
497     /**
498      * <code>i18n:time</code> is used to provide a localized time string.
499      * Allowed attributes are: <code>pattern, src-pattern, locale,
500      * src-locale</code>. Usage examples:
501      * <pre>
502      * &lt;i18n:time src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
503      * 1:00 AM
504      * &lt;/i18n:time&gt;
505      *
506      * &lt;i18n:time pattern="hh:mm:ss" /&gt;
507      * </pre>
508      *
509      * If no value is specified then the current time will be used. E.g.:
510      * <pre>
511      * &lt;i18n:time /&gt;
512      * </pre>
513      * Displays the current time formatted with default pattern for
514      * the current locale.
515      *
516      * @see #I18N_PARAM_ELEMENT
517      * @see #I18N_DATE_TIME_ELEMENT
518      * @see #I18N_DATE_ELEMENT
519      * @see #I18N_NUMBER_ELEMENT
520      */

521     public static final String JavaDoc I18N_TIME_ELEMENT = "time";
522
523     /**
524      * <code>i18n:number</code> is used to provide a localized number string.
525      * Allowed attributes are: <code>pattern, src-pattern, locale, src-locale,
526      * type</code>. Usage examples:
527      * <pre>
528      * &lt;i18n:number src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
529      * 1000.0
530      * &lt;/i18n:number&gt;
531      *
532      * &lt;i18n:number type="currency" /&gt;
533      * </pre>
534      *
535      * If no value is specifies then 0 will be used.
536      *
537      * @see #I18N_PARAM_ELEMENT
538      * @see #I18N_DATE_TIME_ELEMENT
539      * @see #I18N_TIME_ELEMENT
540      * @see #I18N_DATE_ELEMENT
541      */

542     public static final String JavaDoc I18N_NUMBER_ELEMENT = "number";
543
544     /**
545      * Currency element name
546      */

547     public static final String JavaDoc I18N_CURRENCY_ELEMENT = "currency";
548
549     /**
550      * Percent element name
551      */

552     public static final String JavaDoc I18N_PERCENT_ELEMENT = "percent";
553
554     /**
555      * Integer currency element name
556      */

557     public static final String JavaDoc I18N_INT_CURRENCY_ELEMENT = "int-currency";
558
559     /**
560      * Currency without unit element name
561      */

562     public static final String JavaDoc I18N_CURRENCY_NO_UNIT_ELEMENT = "currency-no-unit";
563
564     /**
565      * Integer currency without unit element name
566      */

567     public static final String JavaDoc I18N_INT_CURRENCY_NO_UNIT_ELEMENT = "int-currency-no-unit";
568
569     //
570
// i18n general attributes
571
//
572

573     /**
574      * This attribute is used with i18n:text element to indicate the key of
575      * the according message. The character data of the element will be used
576      * if no message is found by this key. E.g.:
577      * <pre>
578      * &lt;i18n:text i18n:key="a_key"&gt;article_text1&lt;/i18n:text&gt;
579      * </pre>
580      */

581     public static final String JavaDoc I18N_KEY_ATTRIBUTE = "key";
582
583     /**
584      * This attribute is used with <strong>any</strong> element (even not i18n)
585      * to translate attribute values. Should contain whitespace separated
586      * attribute names that should be translated:
587      * <pre>
588      * &lt;para title="first" name="article" i18n:attr="title name"/&gt;
589      * </pre>
590      * Attribute value considered as key in message catalogue.
591      */

592     public static final String JavaDoc I18N_ATTR_ATTRIBUTE = "attr";
593
594     /**
595      * This attribute is used with <strong>any</strong> element (even not i18n)
596      * to evaluate attribute values. Should contain whitespace separated
597      * attribute names that should be evaluated:
598      * <pre>
599      * &lt;para title="first" name="{one} {two}" i18n:attr="name"/&gt;
600      * </pre>
601      * Attribute value considered as expression containing text and catalogue
602      * keys in curly braces.
603      */

604     public static final String JavaDoc I18N_EXPR_ATTRIBUTE = "expr";
605
606     //
607
// i18n number and date formatting attributes
608
//
609

610     /**
611      * This attribute is used with date and number formatting elements to
612      * indicate the pattern that should be used to parse the element value.
613      *
614      * @see #I18N_PARAM_ELEMENT
615      * @see #I18N_DATE_TIME_ELEMENT
616      * @see #I18N_DATE_ELEMENT
617      * @see #I18N_TIME_ELEMENT
618      * @see #I18N_NUMBER_ELEMENT
619      */

620     public static final String JavaDoc I18N_SRC_PATTERN_ATTRIBUTE = "src-pattern";
621
622     /**
623      * This attribute is used with date and number formatting elements to
624      * indicate the pattern that should be used to format the element value.
625      *
626      * @see #I18N_PARAM_ELEMENT
627      * @see #I18N_DATE_TIME_ELEMENT
628      * @see #I18N_DATE_ELEMENT
629      * @see #I18N_TIME_ELEMENT
630      * @see #I18N_NUMBER_ELEMENT
631      */

632     public static final String JavaDoc I18N_PATTERN_ATTRIBUTE = "pattern";
633
634     /**
635      * This attribute is used with date and number formatting elements to
636      * indicate the locale that should be used to format the element value.
637      * Also used for in-place translations.
638      *
639      * @see #I18N_PARAM_ELEMENT
640      * @see #I18N_DATE_TIME_ELEMENT
641      * @see #I18N_DATE_ELEMENT
642      * @see #I18N_TIME_ELEMENT
643      * @see #I18N_NUMBER_ELEMENT
644      * @see #I18N_WHEN_ELEMENT
645      */

646     public static final String JavaDoc I18N_LOCALE_ATTRIBUTE = "locale";
647
648     /**
649      * This attribute is used with date and number formatting elements to
650      * indicate the locale that should be used to parse the element value.
651      *
652      * @see #I18N_PARAM_ELEMENT
653      * @see #I18N_DATE_TIME_ELEMENT
654      * @see #I18N_DATE_ELEMENT
655      * @see #I18N_TIME_ELEMENT
656      * @see #I18N_NUMBER_ELEMENT
657      */

658     public static final String JavaDoc I18N_SRC_LOCALE_ATTRIBUTE = "src-locale";
659
660     /**
661      * This attribute is used with date and number formatting elements to
662      * indicate the value that should be parsed and formatted. If value
663      * attribute is not used then the character data of the element will be used.
664      *
665      * @see #I18N_PARAM_ELEMENT
666      * @see #I18N_DATE_TIME_ELEMENT
667      * @see #I18N_DATE_ELEMENT
668      * @see #I18N_TIME_ELEMENT
669      * @see #I18N_NUMBER_ELEMENT
670      */

671     public static final String JavaDoc I18N_VALUE_ATTRIBUTE = "value";
672
673     /**
674      * This attribute is used with <code>i18:param</code> to
675      * indicate the parameter type: <code>date, time, date-time</code> or
676      * <code>number, currency, percent, int-currency, currency-no-unit,
677      * int-currency-no-unit</code>.
678      * Also used with <code>i18:translate</code> to indicate inplace
679      * translations: <code>inplace</code>
680      * @deprecated since 2.1. Use nested tags instead, e.g.:
681      * &lt;i18n:param&gt;&lt;i18n:date/&gt;&lt;/i18n:param&gt;
682      */

683     public static final String JavaDoc I18N_TYPE_ATTRIBUTE = "type";
684
685     /**
686      * This attribute is used to specify a different locale for the
687      * currency. When specified, this locale will be combined with
688      * the "normal" locale: e.g. the seperator symbols are taken from
689      * the normal locale but the currency symbol and possition will
690      * be taken from the currency locale.
691      * This enables to see a currency formatted for Euro but with US
692      * grouping and decimal char.
693      */

694     public static final String JavaDoc CURRENCY_LOCALE_ATTRIBUTE = "currency";
695
696     /**
697      * This attribute can be used on <code>i18n:text</code> to indicate the catalogue
698      * from which the key should be retrieved. This attribute is optional,
699      * if it is not mentioned the default catalogue is used.
700      */

701     public static final String JavaDoc I18N_CATALOGUE_ATTRIBUTE = "catalogue";
702
703     //
704
// Configuration parameters
705
//
706

707     /**
708      * This configuration parameter specifies the default locale to be used.
709      */

710     public static final String JavaDoc I18N_LOCALE = "locale";
711
712     /**
713      * This configuration parameter specifies the id of the catalogue to be used as
714      * default catalogue, allowing to redefine the default catalogue on the pipeline
715      * level.
716      */

717     public static final String JavaDoc I18N_DEFAULT_CATALOGUE_ID = "default-catalogue-id";
718
719     /**
720      * This configuration parameter specifies the message that should be
721      * displayed in case of a not translated text (message not found).
722      */

723     public static final String JavaDoc I18N_UNTRANSLATED = "untranslated-text";
724
725     /**
726      * This configuration parameter specifies if the message catalog should be
727      * cached at startup.
728      */

729     public static final String JavaDoc I18N_CACHE_STARTUP = "cache-at-startup";
730
731     /**
732      * <code>fraction-digits</code> attribute is used with
733      * <code>i18:number</code> to
734      * indicate the number of digits behind the fraction
735      */

736     public static final String JavaDoc I18N_FRACTION_DIGITS_ATTRIBUTE = "fraction-digits";
737
738     //
739
// States of the transformer
740
//
741

742     private static final int STATE_OUTSIDE = 0;
743     private static final int STATE_INSIDE_TEXT = 10;
744     private static final int STATE_INSIDE_PARAM = 20;
745     private static final int STATE_INSIDE_TRANSLATE = 30;
746     private static final int STATE_INSIDE_CHOOSE = 50;
747     private static final int STATE_INSIDE_WHEN = 51;
748     private static final int STATE_INSIDE_OTHERWISE = 52;
749     private static final int STATE_INSIDE_DATE = 60;
750     private static final int STATE_INSIDE_DATE_TIME = 61;
751     private static final int STATE_INSIDE_TIME = 62;
752     private static final int STATE_INSIDE_NUMBER = 63;
753
754     // All date-time related parameter types and element names
755
private static final Set JavaDoc dateTypes;
756
757     // All number related parameter types and element names
758
private static final Set JavaDoc numberTypes;
759
760     // Date pattern types map: short, medium, long, full
761
private static final Map JavaDoc datePatterns;
762
763     static {
764         // initialize date types set
765
HashSet JavaDoc set = new HashSet JavaDoc(5);
766         set.add(I18N_DATE_ELEMENT);
767         set.add(I18N_TIME_ELEMENT);
768         set.add(I18N_DATE_TIME_ELEMENT);
769         dateTypes = Collections.unmodifiableSet(set);
770
771         // initialize number types set
772
set = new HashSet JavaDoc(9);
773         set.add(I18N_NUMBER_ELEMENT);
774         set.add(I18N_PERCENT_ELEMENT);
775         set.add(I18N_CURRENCY_ELEMENT);
776         set.add(I18N_INT_CURRENCY_ELEMENT);
777         set.add(I18N_CURRENCY_NO_UNIT_ELEMENT);
778         set.add(I18N_INT_CURRENCY_NO_UNIT_ELEMENT);
779         numberTypes = Collections.unmodifiableSet(set);
780
781         // Initialize date patterns map
782
Map JavaDoc map = new HashMap JavaDoc(7);
783         map.put("SHORT", new Integer JavaDoc(DateFormat.SHORT));
784         map.put("MEDIUM", new Integer JavaDoc(DateFormat.MEDIUM));
785         map.put("LONG", new Integer JavaDoc(DateFormat.LONG));
786         map.put("FULL", new Integer JavaDoc(DateFormat.FULL));
787         datePatterns = Collections.unmodifiableMap(map);
788     }
789
790
791     //
792
// Global configuration variables
793
//
794

795     /**
796      * Component (service) manager
797      */

798     protected ServiceManager manager;
799
800     /**
801      * Message bundle loader factory component (service)
802      */

803     protected BundleFactory factory;
804
805     /**
806      * All catalogues (keyed by catalogue id). The values are instances
807      * of {@link CatalogueInfo}.
808      */

809     private Map JavaDoc catalogues;
810
811     /**
812      * Default (global) catalogue
813      */

814     private CatalogueInfo defaultCatalogue;
815
816     /**
817      * Default (global) untranslated message value
818      */

819     private String JavaDoc defaultUntranslated;
820
821     //
822
// Local configuration variables
823
//
824

825     protected Map JavaDoc objectModel;
826
827     /**
828      * Locale
829      */

830     protected Locale JavaDoc locale;
831
832     /**
833      * Catalogue (local)
834      */

835     private CatalogueInfo catalogue;
836
837     /**
838      * Current (local) untranslated message value
839      */

840     private String JavaDoc untranslated;
841
842     /**
843      * {@link SaxBuffer} containing the contents of {@link #untranslated}.
844      */

845     private ParamSaxBuffer untranslatedRecorder;
846
847     //
848
// Current state of the transformer
849
//
850

851     /**
852      * Current state of the transformer. Default value is STATE_OUTSIDE.
853      */

854     private int current_state;
855
856     /**
857      * Previous state of the transformer.
858      * Used in text translation inside params and translate elements.
859      */

860     private int prev_state;
861
862     /**
863      * The i18n:key attribute is stored for the current element.
864      * If no translation found for the key then the character data of element is
865      * used as default value.
866      */

867     private String JavaDoc currentKey;
868
869     /**
870      * Contains the id of the current catalogue if it was explicitely mentioned
871      * on an i18n:text element, otherwise it is null.
872      */

873     private String JavaDoc currentCatalogueId;
874
875     /**
876      * Character data buffer. used to concat chunked character data
877      */

878     private StringBuffer JavaDoc strBuffer;
879
880     /**
881      * A flag for copying the node when doing in-place translation
882      */

883     private boolean translate_copy;
884
885     // A flag for copying the _GOOD_ node and not others
886
// when doing in-place translation within i18n:choose
887
private boolean translate_end;
888
889     // Translated text. Inside i18n:translate, collects character events.
890
private ParamSaxBuffer tr_text_recorder;
891
892     // Current "i18n:text" events
893
private ParamSaxBuffer text_recorder;
894
895     // Current parameter events
896
private SaxBuffer param_recorder;
897
898     // Param count when not using i18n:param name="..."
899
private int param_count;
900
901     // Param name attribute for substitution.
902
private String JavaDoc param_name;
903
904     // i18n:param's hashmap for substitution
905
private HashMap JavaDoc indexedParams;
906
907     // Current parameter value (translated or not)
908
private String JavaDoc param_value;
909
910     // Date and number elements and params formatting attributes with values.
911
private HashMap JavaDoc formattingParams;
912
913
914     /**
915      * Returns the current locale setting of this transformer instance.
916      * @return current Locale object
917      */

918     public Locale JavaDoc getLocale() {
919         return this.locale;
920     }
921
922     /**
923      * Implemenation of CacheableProcessingComponents.
924      * Generates unique key for the current locale.
925      */

926     public java.io.Serializable JavaDoc getKey() {
927         // TODO: Key should be composed out of used catalogues locations, and locale.
928
// Right now it is hardcoded only to default catalogue location.
929
StringBuffer JavaDoc key = new StringBuffer JavaDoc();
930         if (catalogue != null) {
931             key.append(catalogue.getLocation()[0]);
932         }
933         key.append("?");
934         if (locale != null) {
935             key.append(locale.getLanguage());
936             key.append("_");
937             key.append(locale.getCountry());
938             key.append("_");
939             key.append(locale.getVariant());
940         }
941         return key.toString();
942     }
943
944     /**
945      * Implementation of CacheableProcessingComponent.
946      * Generates validity object for this transformer or <code>null</code>
947      * if this instance is not cacheable.
948      */

949     public SourceValidity getValidity() {
950         // FIXME (KP): Cache validity should be generated by
951
// Bundle implementations.
952
return org.apache.excalibur.source.impl.validity.NOPValidity.SHARED_INSTANCE;
953     }
954
955     /**
956      * Look up the {@link BundleFactory} to be used.
957      */

958     public void service(ServiceManager manager) throws ServiceException {
959         this.manager = manager;
960         try {
961             this.factory = (BundleFactory) manager.lookup(BundleFactory.ROLE);
962         } catch (ServiceException e) {
963             getLogger().debug("Failed to lookup <" + BundleFactory.ROLE + ">", e);
964             throw e;
965         }
966     }
967
968     /**
969      * Implementation of Configurable interface.
970      * Configure this transformer.
971      */

972     public void configure(Configuration conf) throws ConfigurationException {
973         // Read in the config options from the transformer definition
974
Configuration cataloguesConf = conf.getChild("catalogues", false);
975         if (cataloguesConf == null) {
976             throw new ConfigurationException("I18nTransformer requires <catalogues> configuration at " +
977                                              conf.getLocation());
978         }
979
980         // new configuration style
981
Configuration[] catalogueConfs = cataloguesConf.getChildren("catalogue");
982         catalogues = new HashMap JavaDoc(catalogueConfs.length + 3);
983         for (int i = 0; i < catalogueConfs.length; i++) {
984             String JavaDoc id = catalogueConfs[i].getAttribute("id");
985             String JavaDoc name = catalogueConfs[i].getAttribute("name");
986
987             String JavaDoc[] locations;
988             String JavaDoc location = catalogueConfs[i].getAttribute("location", null);
989             Configuration[] locationConf =
990                 catalogueConfs[i].getChildren("location");
991             if (location != null) {
992                 if (locationConf.length > 0) {
993                     String JavaDoc msg = "I18nTransformer: Location attribute cannot be " +
994                                  "specified with location elements";
995                     getLogger().error(msg);
996                     throw new ConfigurationException(msg);
997                 }
998
999                 if (getLogger().isDebugEnabled()) {
1000                    getLogger().debug("name=" + name + ", location=" +
1001                                      location);
1002                }
1003                locations = new String JavaDoc[1];
1004                locations[0] = location;
1005            } else {
1006                if (locationConf.length == 0) {
1007                    String JavaDoc msg = "I18nTransformer: A location attribute or location " +
1008                                 "elements must be specified";
1009                    getLogger().error(msg);
1010                    throw new ConfigurationException(msg);
1011                }
1012
1013                locations = new String JavaDoc[locationConf.length];
1014                for (int j=0; j < locationConf.length; ++j) {
1015                    locations[j] = locationConf[j].getValue();
1016                    if (getLogger().isDebugEnabled()) {
1017                        getLogger().debug("name=" + name + ", location=" +
1018                                          locations[j]);
1019                    }
1020                }
1021            }
1022
1023            CatalogueInfo catalogueInfo;
1024            try {
1025                catalogueInfo = new CatalogueInfo(name, locations);
1026            } catch (PatternException e) {
1027                throw new ConfigurationException("I18nTransformer: Error in name or location " +
1028                                                 "attribute on catalogue element with id " + id, e);
1029            }
1030            catalogues.put(id, catalogueInfo);
1031        }
1032
1033        String JavaDoc defaultCatalogueId = cataloguesConf.getAttribute("default");
1034        defaultCatalogue = (CatalogueInfo) catalogues.get(defaultCatalogueId);
1035        if (defaultCatalogue == null) {
1036            throw new ConfigurationException("I18nTransformer: Default catalogue id '" +
1037                                             defaultCatalogueId + "' denotes a nonexisting catalogue");
1038        }
1039
1040        // Obtain default text to use for untranslated messages
1041
defaultUntranslated = conf.getChild(I18N_UNTRANSLATED).getValue(null);
1042        if (getLogger().isDebugEnabled()) {
1043            getLogger().debug("Default untranslated text is '" + defaultUntranslated + "'");
1044        }
1045    }
1046
1047    /**
1048     * Setup current instance of transformer.
1049     */

1050    public void setup(SourceResolver resolver, Map JavaDoc objectModel, String JavaDoc source,
1051                      Parameters parameters)
1052    throws ProcessingException, SAXException JavaDoc, IOException JavaDoc {
1053
1054        this.objectModel = objectModel;
1055
1056        untranslated = parameters.getParameter(I18N_UNTRANSLATED, defaultUntranslated);
1057        if (untranslated != null) {
1058            untranslatedRecorder = new ParamSaxBuffer();
1059            untranslatedRecorder.characters(untranslated.toCharArray(), 0, untranslated.length());
1060        }
1061
1062        // Get current locale
1063
String JavaDoc lc = parameters.getParameter(I18N_LOCALE, null);
1064        Locale JavaDoc locale = I18nUtils.parseLocale(lc);
1065        if (getLogger().isDebugEnabled()) {
1066            getLogger().debug("Using locale '" + locale + "'");
1067        }
1068
1069        // Initialize instance state variables
1070
this.locale = locale;
1071        this.current_state = STATE_OUTSIDE;
1072        this.prev_state = STATE_OUTSIDE;
1073        this.currentKey = null;
1074        this.currentCatalogueId = null;
1075        this.translate_copy = false;
1076        this.tr_text_recorder = null;
1077        this.text_recorder = new ParamSaxBuffer();
1078        this.param_count = 0;
1079        this.param_name = null;
1080        this.param_value = null;
1081        this.param_recorder = null;
1082        this.indexedParams = new HashMap JavaDoc(3);
1083        this.formattingParams = null;
1084        this.strBuffer = null;
1085
1086        // give the catalogue variable its value -- first look if it's locally overridden
1087
// and otherwise use the component-wide defaults.
1088
String JavaDoc catalogueId = parameters.getParameter(I18N_DEFAULT_CATALOGUE_ID, null);
1089        if (catalogueId != null) {
1090            CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues.get(catalogueId);
1091            if (catalogueInfo == null) {
1092                throw new ProcessingException("I18nTransformer: '" +
1093                                              catalogueId +
1094                                              "' is not an existing catalogue id.");
1095            }
1096            catalogue = catalogueInfo;
1097        } else {
1098            catalogue = defaultCatalogue;
1099        }
1100
1101        if (getLogger().isDebugEnabled()) {
1102            getLogger().debug("Default catalogue is " + catalogue.getName());
1103        }
1104    }
1105
1106
1107    //
1108
// Standard SAX event handlers
1109
//
1110

1111    public void startElement(String JavaDoc uri, String JavaDoc name, String JavaDoc raw,
1112                             Attributes JavaDoc attr)
1113    throws SAXException JavaDoc {
1114
1115        // Handle previously buffered characters
1116
if (current_state != STATE_OUTSIDE && strBuffer != null) {
1117            i18nCharacters(strBuffer.toString());
1118            strBuffer = null;
1119        }
1120
1121        // Process start element event
1122
if (I18N_NAMESPACE_URI.equals(uri)) {
1123            if (getLogger().isDebugEnabled()) {
1124                getLogger().debug("Starting i18n element: " + name);
1125            }
1126            startI18NElement(name, attr);
1127
1128        } else if (I18N_OLD_NAMESPACE_URI.equals(uri)) {
1129            if (!deprecationFound) {
1130                deprecationFound = true;
1131                getLogger().warn("The namespace <" + I18N_OLD_NAMESPACE_URI +
1132                                 "> is deprecated, use: <" + I18N_NAMESPACE_URI + ">");
1133            }
1134            if (getLogger().isDebugEnabled()) {
1135                getLogger().debug("Starting deprecated i18n element: " + name);
1136            }
1137            startI18NElement(name, attr);
1138
1139        } else {
1140            // We have a non i18n element event
1141
if (current_state == STATE_OUTSIDE) {
1142                super.startElement(uri, name, raw,
1143                                   translateAttributes(name, attr));
1144            } else if (current_state == STATE_INSIDE_PARAM) {
1145                param_recorder.startElement(uri, name, raw, attr);
1146            } else if (current_state == STATE_INSIDE_TEXT) {
1147                text_recorder.startElement(uri, name, raw, attr);
1148            } else if ((current_state == STATE_INSIDE_WHEN ||
1149                    current_state == STATE_INSIDE_OTHERWISE)
1150                    && translate_copy) {
1151
1152                super.startElement(uri, name, raw, attr);
1153            }
1154        }
1155    }
1156
1157    public void endElement(String JavaDoc uri, String JavaDoc name, String JavaDoc raw)
1158    throws SAXException JavaDoc {
1159
1160        // Handle previously buffered characters
1161
if (current_state != STATE_OUTSIDE && strBuffer != null) {
1162            i18nCharacters(strBuffer.toString());
1163            strBuffer = null;
1164        }
1165
1166        if (I18N_NAMESPACE_URI.equals(uri) || I18N_OLD_NAMESPACE_URI.equals(uri)) {
1167            endI18NElement(name);
1168        } else if (current_state == STATE_INSIDE_PARAM) {
1169            param_recorder.endElement(uri, name, raw);
1170        } else if (current_state == STATE_INSIDE_TEXT) {
1171            text_recorder.endElement(uri, name, raw);
1172        } else if (current_state == STATE_INSIDE_CHOOSE ||
1173                (current_state == STATE_INSIDE_WHEN ||
1174                current_state == STATE_INSIDE_OTHERWISE)
1175                && !translate_copy) {
1176
1177            // Output nothing
1178
} else {
1179            super.endElement(uri, name, raw);
1180        }
1181    }
1182
1183    public void characters(char[] ch, int start, int len)
1184    throws SAXException JavaDoc {
1185
1186        if (current_state == STATE_OUTSIDE ||
1187                ((current_state == STATE_INSIDE_WHEN ||
1188                current_state == STATE_INSIDE_OTHERWISE) && translate_copy)) {
1189
1190            super.characters(ch, start, len);
1191        } else {
1192            // Perform buffering to prevent chunked character data
1193
if (strBuffer == null) {
1194                strBuffer = new StringBuffer JavaDoc();
1195            }
1196            strBuffer.append(ch, start, len);
1197        }
1198    }
1199
1200    //
1201
// i18n specific event handlers
1202
//
1203

1204    private void startI18NElement(String JavaDoc name, Attributes JavaDoc attr)
1205    throws SAXException JavaDoc {
1206
1207        if (getLogger().isDebugEnabled()) {
1208            getLogger().debug("Start i18n element: " + name);
1209        }
1210
1211        if (I18N_TEXT_ELEMENT.equals(name)) {
1212            if (current_state != STATE_OUTSIDE
1213                    && current_state != STATE_INSIDE_PARAM
1214                    && current_state != STATE_INSIDE_TRANSLATE) {
1215
1216                throw new SAXException JavaDoc(
1217                        getClass().getName()
1218                        + ": nested i18n:text elements are not allowed."
1219                        + " Current state: " + current_state);
1220            }
1221
1222            prev_state = current_state;
1223            current_state = STATE_INSIDE_TEXT;
1224
1225            currentKey = attr.getValue("", I18N_KEY_ATTRIBUTE);
1226            if (currentKey == null) {
1227                // Try the namespaced attribute
1228
currentKey = attr.getValue(I18N_NAMESPACE_URI, I18N_KEY_ATTRIBUTE);
1229                if (currentKey == null) {
1230                    // Try the old namespace
1231
currentKey = attr.getValue(I18N_OLD_NAMESPACE_URI, I18N_KEY_ATTRIBUTE);
1232                }
1233            }
1234
1235            currentCatalogueId = attr.getValue("", I18N_CATALOGUE_ATTRIBUTE);
1236            if (currentCatalogueId == null) {
1237                // Try the namespaced attribute
1238
currentCatalogueId = attr.getValue(I18N_NAMESPACE_URI, I18N_CATALOGUE_ATTRIBUTE);
1239            }
1240
1241            if (prev_state != STATE_INSIDE_PARAM) {
1242                tr_text_recorder = null;
1243            }
1244
1245            if (currentKey != null) {
1246                tr_text_recorder = getMessage(currentKey, (ParamSaxBuffer)null);
1247            }
1248
1249        } else if (I18N_TRANSLATE_ELEMENT.equals(name)) {
1250            if (current_state != STATE_OUTSIDE) {
1251                throw new SAXException JavaDoc(
1252                        getClass().getName()
1253                        + ": i18n:translate element must be used "
1254                        + "outside of other i18n elements. Current state: "
1255                        + current_state);
1256            }
1257
1258            prev_state = current_state;
1259            current_state = STATE_INSIDE_TRANSLATE;
1260        } else if (I18N_PARAM_ELEMENT.equals(name)) {
1261            if (current_state != STATE_INSIDE_TRANSLATE) {
1262                throw new SAXException JavaDoc(
1263                        getClass().getName()
1264                        + ": i18n:param element can be used only inside "
1265                        + "i18n:translate element. Current state: "
1266                        + current_state);
1267            }
1268
1269            param_name = attr.getValue(I18N_PARAM_NAME_ATTRIBUTE);
1270            if (param_name == null) {
1271                param_name = String.valueOf(param_count++);
1272            }
1273
1274            param_recorder = new SaxBuffer();
1275            setFormattingParams(attr);
1276            current_state = STATE_INSIDE_PARAM;
1277        } else if (I18N_CHOOSE_ELEMENT.equals(name)) {
1278            if (current_state != STATE_OUTSIDE) {
1279                throw new SAXException JavaDoc(
1280                        getClass().getName()
1281                        + ": i18n:choose elements cannot be used"
1282                        + "inside of other i18n elements.");
1283            }
1284
1285            translate_copy = false;
1286            translate_end = false;
1287            prev_state = current_state;
1288            current_state = STATE_INSIDE_CHOOSE;
1289        } else if (I18N_WHEN_ELEMENT.equals(name) ||
1290                I18N_IF_ELEMENT.equals(name)) {
1291
1292            if (I18N_WHEN_ELEMENT.equals(name) &&
1293                    current_state != STATE_INSIDE_CHOOSE) {
1294                throw new SAXException JavaDoc(
1295                        getClass().getName()
1296                        + ": i18n:when elements are can be used only"
1297                        + "inside of i18n:choose elements.");
1298            }
1299
1300            if (I18N_IF_ELEMENT.equals(name) &&
1301                    current_state != STATE_OUTSIDE) {
1302                throw new SAXException JavaDoc(
1303                        getClass().getName()
1304                        + ": i18n:if elements cannot be nested.");
1305            }
1306
1307            String JavaDoc locale = attr.getValue(I18N_LOCALE_ATTRIBUTE);
1308            if (locale == null)
1309                throw new SAXException JavaDoc(
1310                        getClass().getName()
1311                        + ": i18n:" + name
1312                        + " element cannot be used without 'locale' attribute.");
1313
1314            if ((!translate_end && current_state == STATE_INSIDE_CHOOSE)
1315                    || current_state == STATE_OUTSIDE) {
1316
1317                // Perform soft locale matching
1318
if (this.locale.toString().startsWith(locale)) {
1319                    if (getLogger().isDebugEnabled()) {
1320                        getLogger().debug("Locale matching: " + locale);
1321                    }
1322                    translate_copy = true;
1323                }
1324            }
1325
1326            prev_state = current_state;
1327            current_state = STATE_INSIDE_WHEN;
1328
1329        } else if (I18N_OTHERWISE_ELEMENT.equals(name)) {
1330            if (current_state != STATE_INSIDE_CHOOSE) {
1331                throw new SAXException JavaDoc(
1332                        getClass().getName()
1333                        + ": i18n:otherwise elements are not allowed "
1334                        + "only inside i18n:choose.");
1335            }
1336
1337            getLogger().debug("Matching any locale");
1338            if (!translate_end) {
1339                translate_copy = true;
1340            }
1341
1342            prev_state = current_state;
1343            current_state = STATE_INSIDE_OTHERWISE;
1344
1345        } else if (I18N_DATE_ELEMENT.equals(name)) {
1346            if (current_state != STATE_OUTSIDE
1347                    && current_state != STATE_INSIDE_TEXT
1348                    && current_state != STATE_INSIDE_PARAM) {
1349                throw new SAXException JavaDoc(
1350                        getClass().getName()
1351                        + ": i18n:date elements are not allowed "
1352                        + "inside of other i18n elements.");
1353            }
1354
1355            setFormattingParams(attr);
1356            prev_state = current_state;
1357            current_state = STATE_INSIDE_DATE;
1358        } else if (I18N_DATE_TIME_ELEMENT.equals(name)) {
1359            if (current_state != STATE_OUTSIDE
1360                    && current_state != STATE_INSIDE_TEXT
1361                    && current_state != STATE_INSIDE_PARAM) {
1362                throw new SAXException JavaDoc(
1363                        getClass().getName()
1364                        + ": i18n:date-time elements are not allowed "
1365                        + "inside of other i18n elements.");
1366            }
1367
1368            setFormattingParams(attr);
1369            prev_state = current_state;
1370            current_state = STATE_INSIDE_DATE_TIME;
1371        } else if (I18N_TIME_ELEMENT.equals(name)) {
1372            if (current_state != STATE_OUTSIDE
1373                    && current_state != STATE_INSIDE_TEXT
1374                    && current_state != STATE_INSIDE_PARAM) {
1375                throw new SAXException JavaDoc(
1376                        getClass().getName()
1377                        + ": i18n:date elements are not allowed "
1378                        + "inside of other i18n elements.");
1379            }
1380
1381            setFormattingParams(attr);
1382            prev_state = current_state;
1383            current_state = STATE_INSIDE_TIME;
1384        } else if (I18N_NUMBER_ELEMENT.equals(name)) {
1385            if (current_state != STATE_OUTSIDE
1386                    && current_state != STATE_INSIDE_TEXT
1387                    && current_state != STATE_INSIDE_PARAM) {
1388                throw new SAXException JavaDoc(
1389                        getClass().getName()
1390                        + ": i18n:number elements are not allowed "
1391                        + "inside of other i18n elements.");
1392            }
1393
1394            setFormattingParams(attr);
1395            prev_state = current_state;
1396            current_state = STATE_INSIDE_NUMBER;
1397        }
1398    }
1399
1400    // Get all possible i18n formatting attribute values and store in a Map
1401
private void setFormattingParams(Attributes JavaDoc attr) {
1402        // average number of attributes is 3
1403
formattingParams = new HashMap JavaDoc(3);
1404
1405        String JavaDoc attr_value = attr.getValue(I18N_SRC_PATTERN_ATTRIBUTE);
1406        if (attr_value != null) {
1407            formattingParams.put(I18N_SRC_PATTERN_ATTRIBUTE, attr_value);
1408        }
1409
1410        attr_value = attr.getValue(I18N_PATTERN_ATTRIBUTE);
1411        if (attr_value != null) {
1412            formattingParams.put(I18N_PATTERN_ATTRIBUTE, attr_value);
1413        }
1414
1415        attr_value = attr.getValue(I18N_VALUE_ATTRIBUTE);
1416        if (attr_value != null) {
1417            formattingParams.put(I18N_VALUE_ATTRIBUTE, attr_value);
1418        }
1419
1420        attr_value = attr.getValue(I18N_LOCALE_ATTRIBUTE);
1421        if (attr_value != null) {
1422            formattingParams.put(I18N_LOCALE_ATTRIBUTE, attr_value);
1423        }
1424
1425        attr_value = attr.getValue(CURRENCY_LOCALE_ATTRIBUTE);
1426        if (attr_value != null) {
1427            formattingParams.put(CURRENCY_LOCALE_ATTRIBUTE, attr_value);
1428        }
1429
1430        attr_value = attr.getValue(I18N_SRC_LOCALE_ATTRIBUTE);
1431        if (attr_value != null) {
1432            formattingParams.put(I18N_SRC_LOCALE_ATTRIBUTE, attr_value);
1433        }
1434
1435        attr_value = attr.getValue(I18N_TYPE_ATTRIBUTE);
1436        if (attr_value != null) {
1437            formattingParams.put(I18N_TYPE_ATTRIBUTE, attr_value);
1438        }
1439
1440        attr_value = attr.getValue(I18N_FRACTION_DIGITS_ATTRIBUTE);
1441        if (attr_value != null) {
1442            formattingParams.put(I18N_FRACTION_DIGITS_ATTRIBUTE, attr_value);
1443        }
1444    }
1445
1446    private void endI18NElement(String JavaDoc name) throws SAXException JavaDoc {
1447        if (getLogger().isDebugEnabled()) {
1448            getLogger().debug("End i18n element: " + name);
1449        }
1450
1451        switch (current_state) {
1452            case STATE_INSIDE_TEXT:
1453                endTextElement();
1454                break;
1455
1456            case STATE_INSIDE_TRANSLATE:
1457                endTranslateElement();
1458                break;
1459
1460            case STATE_INSIDE_CHOOSE:
1461                endChooseElement();
1462                break;
1463
1464            case STATE_INSIDE_WHEN:
1465            case STATE_INSIDE_OTHERWISE:
1466                endWhenElement();
1467                break;
1468
1469            case STATE_INSIDE_PARAM:
1470                endParamElement();
1471                break;
1472
1473            case STATE_INSIDE_DATE:
1474            case STATE_INSIDE_DATE_TIME:
1475            case STATE_INSIDE_TIME:
1476                endDate_TimeElement();
1477                break;
1478
1479            case STATE_INSIDE_NUMBER:
1480                endNumberElement();
1481                break;
1482        }
1483    }
1484
1485    private void i18nCharacters(String JavaDoc textValue) throws SAXException JavaDoc {
1486        // Trim text values to avoid parsing errors.
1487
textValue = textValue.trim();
1488        if (textValue.length() == 0) {
1489            return;
1490        }
1491
1492        if (getLogger().isDebugEnabled()) {
1493            getLogger().debug( "i18n message text = '" + textValue + "'" );
1494        }
1495
1496        char[] ch = textValue.toCharArray();
1497        switch (current_state) {
1498            case STATE_INSIDE_TEXT:
1499                text_recorder.characters(ch, 0, ch.length);
1500                break;
1501
1502            case STATE_INSIDE_PARAM:
1503                param_recorder.characters(ch, 0, ch.length);
1504                break;
1505
1506            case STATE_INSIDE_WHEN:
1507            case STATE_INSIDE_OTHERWISE:
1508                // Previously handeld to avoid the String() conversion.
1509
break;
1510
1511            case STATE_INSIDE_TRANSLATE:
1512                if(tr_text_recorder == null) {
1513                    tr_text_recorder = new ParamSaxBuffer();
1514                }
1515                tr_text_recorder.characters(ch, 0, ch.length);
1516                break;
1517
1518            case STATE_INSIDE_CHOOSE:
1519                // No characters allowed. Send an exception ?
1520
getLogger().debug("No characters allowed inside <i18n:choose> tags");
1521                break;
1522
1523            case STATE_INSIDE_DATE:
1524            case STATE_INSIDE_DATE_TIME:
1525            case STATE_INSIDE_TIME:
1526            case STATE_INSIDE_NUMBER:
1527                if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null) {
1528                    formattingParams.put(I18N_VALUE_ATTRIBUTE, textValue);
1529                } else {
1530                    // ignore the text inside of date element
1531
}
1532                break;
1533
1534            default:
1535                throw new IllegalStateException JavaDoc(getClass().getName() +
1536                                                " developer's fault: characters not handled. " +
1537                                                "Current state: " + current_state);
1538        }
1539    }
1540
1541    // Translate all attributes that are listed in i18n:attr attribute
1542
private Attributes JavaDoc translateAttributes(final String JavaDoc element, Attributes JavaDoc attr)
1543    throws SAXException JavaDoc {
1544        if (attr == null) {
1545            return null;
1546        }
1547
1548        AttributesImpl JavaDoc tempAttr = null;
1549
1550        // Translate all attributes from i18n:attr="name1 name2 ..."
1551
// using their values as keys.
1552
int attrIndex = attr.getIndex(I18N_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE);
1553        if (attrIndex == -1) {
1554            // Try the old namespace
1555
attrIndex = attr.getIndex(I18N_OLD_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE);
1556        }
1557
1558        if (attrIndex != -1) {
1559            StringTokenizer JavaDoc st = new StringTokenizer JavaDoc(attr.getValue(attrIndex));
1560
1561            // Make a copy which we are going to modify
1562
tempAttr = new AttributesImpl JavaDoc(attr);
1563            // Remove the i18n:attr attribute - we don't need it anymore
1564
tempAttr.removeAttribute(attrIndex);
1565
1566            // Iterate through listed attributes and translate them
1567
while (st.hasMoreElements()) {
1568                final String JavaDoc name = st.nextToken();
1569
1570                int index = tempAttr.getIndex(name);
1571                if (index == -1) {
1572                    getLogger().warn("Attribute " +
1573                                     name + " not found in element <" + element + ">");
1574                    continue;
1575                }
1576
1577                String JavaDoc value = translateAttribute(element, name, tempAttr.getValue(index));
1578                if (value != null) {
1579                    // Set the translated value. If null, do nothing.
1580
tempAttr.setValue(index, value);
1581                }
1582            }
1583
1584            attr = tempAttr;
1585        }
1586
1587        // Translate all attributes from i18n:expr="name1 name2 ..."
1588
// using their values as keys.
1589
attrIndex = attr.getIndex(I18N_NAMESPACE_URI, I18N_EXPR_ATTRIBUTE);
1590        if (attrIndex != -1) {
1591            StringTokenizer JavaDoc st = new StringTokenizer JavaDoc(attr.getValue(attrIndex));
1592
1593            if (tempAttr == null) {
1594                tempAttr = new AttributesImpl JavaDoc(attr);
1595            }
1596            tempAttr.removeAttribute(attrIndex);
1597
1598            // Iterate through listed attributes and evaluate them
1599
while (st.hasMoreElements()) {
1600                final String JavaDoc name = st.nextToken();
1601
1602                int index = tempAttr.getIndex(name);
1603                if (index == -1) {
1604                    getLogger().warn("Attribute " +
1605                                     name + " not found in element <" + element + ">");
1606                    continue;
1607                }
1608
1609                final StringBuffer JavaDoc translated = new StringBuffer JavaDoc();
1610
1611                // Evaluate {..} expression
1612
VariableExpressionTokenizer.TokenReciever tr = new VariableExpressionTokenizer.TokenReciever () {
1613                    private String JavaDoc catalogueName;
1614
1615                    public void addToken(int type, String JavaDoc value) {
1616                        if (type == MODULE) {
1617                            this.catalogueName = value;
1618                        } else if (type == VARIABLE) {
1619                            translated.append(translateAttribute(element, name, value));
1620                        } else if (type == TEXT) {
1621                            if (this.catalogueName != null) {
1622                                translated.append(translateAttribute(element,
1623                                                                     name,
1624                                                                     this.catalogueName + ":" + value));
1625                                this.catalogueName = null;
1626                            } else if (value != null) {
1627                                translated.append(value);
1628                            }
1629                        }
1630                    }
1631                };
1632
1633                try {
1634                    VariableExpressionTokenizer.tokenize(tempAttr.getValue(index), tr);
1635                } catch (PatternException e) {
1636                    throw new SAXException JavaDoc(e);
1637                }
1638
1639                // Set the translated value.
1640
tempAttr.setValue(index, translated.toString());
1641            }
1642
1643            attr = tempAttr;
1644        }
1645
1646        // nothing to translate, just return
1647
return attr;
1648    }
1649
1650    /**
1651     * Translate attribute value.
1652     * Value can be prefixed with catalogue ID and semicolon.
1653     * @return Translated text, untranslated text, or null.
1654     */

1655    private String JavaDoc translateAttribute(String JavaDoc element, String JavaDoc name, String JavaDoc key) {
1656        // Check if the key contains a colon, if so the text before
1657
// the colon denotes a catalogue ID.
1658
int colonPos = key.indexOf(":");
1659        String JavaDoc catalogueID = null;
1660        if (colonPos != -1) {
1661            catalogueID = key.substring(0, colonPos);
1662            key = key.substring(colonPos + 1, key.length());
1663        }
1664
1665        final SaxBuffer text = getMessage(catalogueID, key);
1666        if (text == null) {
1667            getLogger().warn("Translation not found for attribute " +
1668                             name + " in element <" + element + ">");
1669            return untranslated;
1670        }
1671        return text.toString();
1672    }
1673
1674    private void endTextElement() throws SAXException JavaDoc {
1675        switch (prev_state) {
1676            case STATE_OUTSIDE:
1677                if (tr_text_recorder == null) {
1678                    if (currentKey == null) {
1679                        // Use the text as key. Not recommended for large strings,
1680
// especially if they include markup.
1681
tr_text_recorder = getMessage(text_recorder.toString(), text_recorder);
1682                    } else {
1683                        // We have the key, but couldn't find a translation
1684
if (getLogger().isDebugEnabled()) {
1685                            getLogger().debug("Translation not found for key '" + currentKey + "'");
1686                        }
1687
1688                        // Use the untranslated-text only when the content of the i18n:text
1689
// element was empty
1690
if (text_recorder.isEmpty() && untranslatedRecorder != null) {
1691                            tr_text_recorder = untranslatedRecorder;
1692                        } else {
1693                            tr_text_recorder = text_recorder;
1694                        }
1695                    }
1696                }
1697
1698                if (tr_text_recorder != null) {
1699                    tr_text_recorder.toSAX(this.contentHandler);
1700                }
1701
1702                text_recorder.recycle();
1703                tr_text_recorder = null;
1704                currentKey = null;
1705                currentCatalogueId = null;
1706                break;
1707
1708            case STATE_INSIDE_TRANSLATE:
1709                if (tr_text_recorder == null) {
1710                    if (!text_recorder.isEmpty()) {
1711                        tr_text_recorder = getMessage(text_recorder.toString(), text_recorder);
1712                        if (tr_text_recorder == text_recorder) {
1713                            // If the default value was returned, make a copy
1714
tr_text_recorder = new ParamSaxBuffer(text_recorder);
1715                        }
1716                    }
1717                }
1718
1719                text_recorder.recycle();
1720                break;
1721
1722            case STATE_INSIDE_PARAM:
1723                // We send the translated text to the param recorder, after trying to translate it.
1724
// Remember you can't give a key when inside a param, that'll be nonsense!
1725
// No need to clone. We just send the events.
1726
if (!text_recorder.isEmpty()) {
1727                    getMessage(text_recorder.toString(), text_recorder).toSAX(param_recorder);
1728                    text_recorder.recycle();
1729                }
1730                break;
1731        }
1732
1733        current_state = prev_state;
1734        prev_state = STATE_OUTSIDE;
1735    }
1736
1737    // Process substitution parameter
1738
private void endParamElement() throws SAXException JavaDoc {
1739        String JavaDoc paramType = (String JavaDoc)formattingParams.get(I18N_TYPE_ATTRIBUTE);
1740        if (paramType != null) {
1741            // We have a typed parameter
1742

1743            if (getLogger().isDebugEnabled()) {
1744                getLogger().debug("Param type: " + paramType);
1745            }
1746            if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null && param_value != null) {
1747                if (getLogger().isDebugEnabled()) {
1748                    getLogger().debug("Put param value: " + param_value);
1749                }
1750                formattingParams.put(I18N_VALUE_ATTRIBUTE, param_value);
1751            }
1752
1753            // Check if we have a date or a number parameter
1754
if (dateTypes.contains(paramType)) {
1755                if (getLogger().isDebugEnabled()) {
1756                    getLogger().debug("Formatting date_time param: " + formattingParams);
1757                }
1758                param_value = formatDate_Time(formattingParams);
1759            } else if (numberTypes.contains(paramType)) {
1760                if (getLogger().isDebugEnabled()) {
1761                    getLogger().debug("Formatting number param: " + formattingParams);
1762                }
1763                param_value = formatNumber(formattingParams);
1764            }
1765            if (getLogger().isDebugEnabled()) {
1766                getLogger().debug("Added substitution param: " + param_value);
1767            }
1768        }
1769
1770        param_value = null;
1771        current_state = STATE_INSIDE_TRANSLATE;
1772
1773        if(param_recorder == null) {
1774            return;
1775        }
1776
1777        indexedParams.put(param_name, param_recorder);
1778        param_recorder = null;
1779    }
1780
1781    private void endTranslateElement() throws SAXException JavaDoc {
1782        if (tr_text_recorder != null) {
1783            if (getLogger().isDebugEnabled()) {
1784                getLogger().debug("End of translate with params. " +
1785                                  "Fragment for substitution : " + tr_text_recorder);
1786            }
1787            tr_text_recorder.toSAX(super.contentHandler, indexedParams);
1788            tr_text_recorder = null;
1789            text_recorder.recycle();
1790        }
1791
1792        indexedParams.clear();
1793        param_count = 0;
1794        current_state = STATE_OUTSIDE;
1795    }
1796
1797    private void endChooseElement() {
1798        current_state = STATE_OUTSIDE;
1799    }
1800
1801    private void endWhenElement() {
1802        current_state = prev_state;
1803        if (translate_copy) {
1804            translate_copy = false;
1805            translate_end = true;
1806        }
1807    }
1808
1809    private void endDate_TimeElement() throws SAXException JavaDoc {
1810        String JavaDoc result = formatDate_Time(formattingParams);
1811        switch(prev_state) {
1812            case STATE_OUTSIDE:
1813                super.contentHandler.characters(result.toCharArray(), 0,
1814                                                result.length());
1815                break;
1816            case STATE_INSIDE_PARAM:
1817                param_recorder.characters(result.toCharArray(), 0, result.length());
1818                break;
1819            case STATE_INSIDE_TEXT:
1820                text_recorder.characters(result.toCharArray(), 0, result.length());
1821                break;
1822        }
1823        current_state = prev_state;
1824    }
1825
1826    // Helper method: creates Locale object from a string value in a map
1827
private Locale JavaDoc getLocale(Map JavaDoc params, String JavaDoc attribute) {
1828        // the specific locale value
1829
String JavaDoc lc = (String JavaDoc)params.get(attribute);
1830        return I18nUtils.parseLocale(lc, this.locale);
1831    }
1832
1833    private String JavaDoc formatDate_Time(Map JavaDoc params) throws SAXException JavaDoc {
1834        // Check that we have not null params
1835
if (params == null) {
1836            throw new IllegalArgumentException JavaDoc("Nothing to format");
1837        }
1838
1839        // Formatters
1840
SimpleDateFormat JavaDoc to_fmt;
1841        SimpleDateFormat JavaDoc from_fmt;
1842
1843        // Date formatting styles
1844
int srcStyle = DateFormat.DEFAULT;
1845        int style = DateFormat.DEFAULT;
1846
1847        // Date formatting patterns
1848
boolean realPattern = false;
1849        boolean realSrcPattern = false;
1850
1851        // From locale
1852
Locale JavaDoc srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE);
1853        // To locale
1854
Locale JavaDoc loc = getLocale(params, I18N_LOCALE_ATTRIBUTE);
1855
1856        // From pattern
1857
String JavaDoc srcPattern = (String JavaDoc)params.get(I18N_SRC_PATTERN_ATTRIBUTE);
1858        // To pattern
1859
String JavaDoc pattern = (String JavaDoc)params.get(I18N_PATTERN_ATTRIBUTE);
1860        // The date value
1861
String JavaDoc value = (String JavaDoc)params.get(I18N_VALUE_ATTRIBUTE);
1862
1863        // A src-pattern attribute is present
1864
if (srcPattern != null) {
1865            // Check if we have a real pattern
1866
Integer JavaDoc patternValue = (Integer JavaDoc)datePatterns.get(srcPattern.toUpperCase());
1867            if (patternValue != null) {
1868                srcStyle = patternValue.intValue();
1869            } else {
1870                realSrcPattern = true;
1871            }
1872        }
1873
1874        // A pattern attribute is present
1875
if (pattern != null) {
1876            Integer JavaDoc patternValue = (Integer JavaDoc)datePatterns.get(pattern.toUpperCase());
1877            if (patternValue != null) {
1878                style = patternValue.intValue();
1879            } else {
1880                realPattern = true;
1881            }
1882        }
1883
1884        // If we are inside of a typed param
1885
String JavaDoc paramType = (String JavaDoc)formattingParams.get(I18N_TYPE_ATTRIBUTE);
1886
1887        // Initializing date formatters
1888
if (current_state == STATE_INSIDE_DATE ||
1889                I18N_DATE_ELEMENT.equals(paramType)) {
1890
1891            to_fmt = (SimpleDateFormat JavaDoc)DateFormat.getDateInstance(style, loc);
1892            from_fmt = (SimpleDateFormat JavaDoc)DateFormat.getDateInstance(
1893                    srcStyle,
1894                    srcLoc
1895            );
1896        } else if (current_state == STATE_INSIDE_DATE_TIME ||
1897                I18N_DATE_TIME_ELEMENT.equals(paramType)) {
1898            to_fmt = (SimpleDateFormat JavaDoc)DateFormat.getDateTimeInstance(
1899                    style,
1900                    style,
1901                    loc
1902            );
1903            from_fmt = (SimpleDateFormat JavaDoc)DateFormat.getDateTimeInstance(
1904                    srcStyle,
1905                    srcStyle,
1906                    srcLoc
1907            );
1908        } else {
1909            // STATE_INSIDE_TIME or param type='time'
1910
to_fmt = (SimpleDateFormat JavaDoc)DateFormat.getTimeInstance(style, loc);
1911            from_fmt = (SimpleDateFormat JavaDoc)DateFormat.getTimeInstance(
1912                    srcStyle,
1913                    srcLoc
1914            );
1915        }
1916
1917        // parsed date object
1918
Date JavaDoc dateValue;
1919
1920        // pattern overwrites locale format
1921
if (realSrcPattern) {
1922            from_fmt.applyPattern(srcPattern);
1923        }
1924
1925        if (realPattern) {
1926            to_fmt.applyPattern(pattern);
1927        }
1928
1929        // get current date and time by default
1930
if (value == null) {
1931            dateValue = new Date JavaDoc();
1932        } else {
1933            try {
1934                dateValue = from_fmt.parse(value);
1935            } catch (ParseException JavaDoc pe) {
1936                throw new SAXException JavaDoc(
1937                        this.getClass().getName()
1938                        + "i18n:date - parsing error.", pe
1939                );
1940            }
1941        }
1942
1943        // we have all necessary data here: do formatting.
1944
if (getLogger().isDebugEnabled()) {
1945            getLogger().debug("### Formatting date: " + dateValue + " with localized pattern " +
1946                              to_fmt.toLocalizedPattern() + " for locale: " + locale);
1947        }
1948        return to_fmt.format(dateValue);
1949    }
1950
1951    private void endNumberElement() throws SAXException JavaDoc {
1952        String JavaDoc result = formatNumber(formattingParams);
1953        switch(prev_state) {
1954            case STATE_OUTSIDE:
1955                super.contentHandler.characters(result.toCharArray(), 0, result.length());
1956                break;
1957            case STATE_INSIDE_PARAM:
1958                param_recorder.characters(result.toCharArray(), 0, result.length());
1959                break;
1960            case STATE_INSIDE_TEXT:
1961                text_recorder.characters(result.toCharArray(), 0, result.length());
1962                break;
1963        }
1964        current_state = prev_state;
1965    }
1966
1967    private String JavaDoc formatNumber(Map JavaDoc params) throws SAXException JavaDoc {
1968        if (params == null) {
1969            throw new SAXException JavaDoc(
1970                    this.getClass().getName()
1971                    + ": i18n:number - error in element attributes."
1972            );
1973        }
1974
1975        // from pattern
1976
String JavaDoc srcPattern = (String JavaDoc)params.get(I18N_SRC_PATTERN_ATTRIBUTE);
1977        // to pattern
1978
String JavaDoc pattern = (String JavaDoc)params.get(I18N_PATTERN_ATTRIBUTE);
1979        // the number value
1980
String JavaDoc value = (String JavaDoc)params.get(I18N_VALUE_ATTRIBUTE);
1981
1982        if (value == null) return "";
1983        // type
1984
String JavaDoc type = (String JavaDoc)params.get(I18N_TYPE_ATTRIBUTE);
1985
1986        // fraction-digits
1987
int fractionDigits = -1;
1988        try {
1989            String JavaDoc fd = (String JavaDoc)params.get(I18N_FRACTION_DIGITS_ATTRIBUTE);
1990            if (fd != null)
1991                fractionDigits = Integer.parseInt(fd);
1992        } catch (NumberFormatException JavaDoc nfe) {
1993            getLogger().warn("Error in number format with fraction-digits", nfe);
1994        }
1995
1996        // parsed number
1997
Number JavaDoc numberValue;
1998
1999        // locale, may be switched locale
2000
Locale JavaDoc loc = getLocale(params, I18N_LOCALE_ATTRIBUTE);
2001        Locale JavaDoc srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE);
2002        // currency locale
2003
Locale JavaDoc currencyLoc = getLocale(params, CURRENCY_LOCALE_ATTRIBUTE);
2004        // decimal and grouping locale
2005
Locale JavaDoc dgLoc = null;
2006        if (currencyLoc != null) {
2007            // the reasoning here is: if there is a currency locale, then start from that
2008
// one but take certain properties (like decimal and grouping seperation symbols)
2009
// from the default locale (this happens further on).
2010
dgLoc = loc;
2011            loc = currencyLoc;
2012        }
2013
2014        // src format
2015
DecimalFormat JavaDoc from_fmt = (DecimalFormat JavaDoc)NumberFormat.getInstance(srcLoc);
2016        int int_currency = 0;
2017
2018        // src-pattern overwrites locale format
2019
if (srcPattern != null) {
2020            from_fmt.applyPattern(srcPattern);
2021        }
2022
2023        // to format
2024
DecimalFormat JavaDoc to_fmt;
2025        char dec = from_fmt.getDecimalFormatSymbols().getDecimalSeparator();
2026        int decAt = 0;
2027        boolean appendDec = false;
2028
2029        if (type == null || type.equals( I18N_NUMBER_ELEMENT )) {
2030            to_fmt = (DecimalFormat JavaDoc)NumberFormat.getInstance(loc);
2031            to_fmt.setMaximumFractionDigits(309);
2032            for (int i = value.length() - 1;
2033                 i >= 0 && value.charAt(i) != dec; i--, decAt++) {
2034            }
2035
2036            if (decAt < value.length())to_fmt.setMinimumFractionDigits(decAt);
2037            decAt = 0;
2038            for (int i = 0; i < value.length() && value.charAt(i) != dec; i++) {
2039                if (Character.isDigit(value.charAt(i))) {
2040                    decAt++;
2041                }
2042            }
2043
2044            to_fmt.setMinimumIntegerDigits(decAt);
2045            if (value.charAt(value.length() - 1) == dec) {
2046                appendDec = true;
2047            }
2048        } else if (type.equals( I18N_CURRENCY_ELEMENT )) {
2049            to_fmt = (DecimalFormat JavaDoc)NumberFormat.getCurrencyInstance(loc);
2050        } else if (type.equals( I18N_INT_CURRENCY_ELEMENT )) {
2051            to_fmt = (DecimalFormat JavaDoc)NumberFormat.getCurrencyInstance(loc);
2052            int_currency = 1;
2053            for (int i = 0; i < to_fmt.getMaximumFractionDigits(); i++) {
2054                int_currency *= 10;
2055            }
2056        } else if ( type.equals( I18N_CURRENCY_NO_UNIT_ELEMENT ) ) {
2057            DecimalFormat JavaDoc tmp = (DecimalFormat JavaDoc) NumberFormat.getCurrencyInstance( loc );
2058            to_fmt = (DecimalFormat JavaDoc) NumberFormat.getInstance( loc );
2059            to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits());
2060            to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits());
2061        } else if ( type.equals( I18N_INT_CURRENCY_NO_UNIT_ELEMENT ) ) {
2062            DecimalFormat JavaDoc tmp = (DecimalFormat JavaDoc) NumberFormat.getCurrencyInstance( loc );
2063            int_currency = 1;
2064            for ( int i = 0; i < tmp.getMaximumFractionDigits(); i++ )
2065                int_currency *= 10;
2066            to_fmt = (DecimalFormat JavaDoc) NumberFormat.getInstance( loc );
2067            to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits());
2068            to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits());
2069        } else if (type.equals( I18N_PERCENT_ELEMENT )) {
2070            to_fmt = (DecimalFormat JavaDoc)NumberFormat.getPercentInstance(loc);
2071        } else {
2072            throw new SAXException JavaDoc("&lt;i18n:number>: unknown type: " + type);
2073        }
2074
2075        if(fractionDigits > -1) {
2076            to_fmt.setMinimumFractionDigits(fractionDigits);
2077            to_fmt.setMaximumFractionDigits(fractionDigits);
2078        }
2079
2080        if(dgLoc != null) {
2081            DecimalFormat JavaDoc df = (DecimalFormat JavaDoc)NumberFormat.getCurrencyInstance(dgLoc);
2082            DecimalFormatSymbols JavaDoc dfsNew = df.getDecimalFormatSymbols();
2083            DecimalFormatSymbols JavaDoc dfsOrig = to_fmt.getDecimalFormatSymbols();
2084            dfsOrig.setDecimalSeparator(dfsNew.getDecimalSeparator());
2085            dfsOrig.setMonetaryDecimalSeparator(dfsNew.getMonetaryDecimalSeparator());
2086            dfsOrig.setGroupingSeparator(dfsNew.getGroupingSeparator());
2087            to_fmt.setDecimalFormatSymbols(dfsOrig);
2088        }
2089
2090        // pattern overwrites locale format
2091
if (pattern != null) {
2092            to_fmt.applyPattern(pattern);
2093        }
2094
2095        if (value == null) {
2096            numberValue = new Long JavaDoc(0);
2097        } else {
2098            try {
2099                numberValue = from_fmt.parse(value);
2100                if (int_currency > 0) {
2101                    numberValue = new Double JavaDoc(numberValue.doubleValue() /
2102                                             int_currency);
2103                } else {
2104                    // what?
2105
}
2106            } catch (ParseException JavaDoc pe) {
2107                throw new SAXException JavaDoc(
2108                        this.getClass().getName()
2109                        + "i18n:number - parsing error.", pe
2110                );
2111            }
2112        }
2113
2114        // we have all necessary data here: do formatting.
2115
String JavaDoc result = to_fmt.format(numberValue);
2116        if (appendDec) result = result + dec;
2117        if (getLogger().isDebugEnabled()) {
2118            getLogger().debug("i18n:number result: " + result);
2119        }
2120        return result;
2121    }
2122
2123    //-- Dictionary handling routines
2124

2125    /**
2126     * Helper method to retrieve a message from the dictionary.
2127     *
2128     * @param catalogueID if not null, this catalogue will be used instead of the default one.
2129     * @return SaxBuffer containing message, or null if not found.
2130     */

2131    private ParamSaxBuffer getMessage(String JavaDoc catalogueID, String JavaDoc key) {
2132        if (getLogger().isDebugEnabled()) {
2133            getLogger().debug("Getting key " + key + " from catalogue " + catalogueID);
2134        }
2135
2136        CatalogueInfo catalogue = this.catalogue;
2137        if (catalogueID != null) {
2138            catalogue = (CatalogueInfo)catalogues.get(catalogueID);
2139            if (catalogue == null) {
2140                if (getLogger().isWarnEnabled()) {
2141                    getLogger().warn("Catalogue not found: " + catalogueID +
2142                                     ", will not translate key " + key);
2143                }
2144                return null;
2145            }
2146        }
2147
2148        Bundle bundle = catalogue.getCatalogue();
2149        if (bundle == null) {
2150            // Can't translate
2151
getLogger().debug("Untranslated key: '" + key + "'");
2152            return null;
2153        }
2154
2155        try {
2156            return (ParamSaxBuffer) bundle.getObject(key);
2157        } catch (MissingResourceException JavaDoc e) {
2158            getLogger().debug("Untranslated key: '" + key + "'");
2159        }
2160
2161        return null;
2162    }
2163
2164    /**
2165     * Helper method to retrieve a message from the current dictionary.
2166     * A default value is returned if message is not found.
2167     *
2168     * @return SaxBuffer containing message, or defaultValue if not found.
2169     */

2170    private ParamSaxBuffer getMessage(String JavaDoc key, ParamSaxBuffer defaultValue) {
2171        SaxBuffer value = getMessage(currentCatalogueId, key);
2172        if (value == null) {
2173            return defaultValue;
2174        }
2175
2176        return new ParamSaxBuffer(value);
2177    }
2178
2179    public void recycle() {
2180        this.untranslatedRecorder = null;
2181        this.catalogue = null;
2182        this.objectModel = null;
2183
2184        // Release catalogues which were selected for current locale
2185
Iterator JavaDoc i = catalogues.values().iterator();
2186        while (i.hasNext()) {
2187            CatalogueInfo catalogueInfo = (CatalogueInfo) i.next();
2188            catalogueInfo.releaseCatalog();
2189        }
2190
2191        super.recycle();
2192    }
2193
2194    public void dispose() {
2195        if (manager != null) {
2196            manager.release(factory);
2197        }
2198        factory = null;
2199        manager = null;
2200        catalogues = null;
2201    }
2202
2203
2204    /**
2205     * Holds information about one catalogue. The location and name of the catalogue
2206     * can contain references to input modules, and are resolved upon each transformer
2207     * usage. It is important that releaseCatalog is called when the transformer is recycled.
2208     */

2209    private final class CatalogueInfo {
2210        VariableResolver name;
2211        VariableResolver[] locations;
2212        String JavaDoc resolvedName;
2213        String JavaDoc[] resolvedLocations;
2214        Bundle catalogue;
2215
2216        public CatalogueInfo(String JavaDoc name, String JavaDoc[] locations) throws PatternException {
2217            this.name = VariableResolverFactory.getResolver(name, manager);
2218            this.locations = new VariableResolver[locations.length];
2219            for (int i=0; i < locations.length; ++i) {
2220                this.locations[i] = VariableResolverFactory.getResolver(locations[i], manager);
2221            }
2222        }
2223
2224        public String JavaDoc getName() {
2225            try {
2226                if (resolvedName == null) {
2227                    resolve();
2228                }
2229            } catch (Exception JavaDoc e) {
2230                // Ignore the error for now
2231
}
2232            return resolvedName;
2233        }
2234
2235        public String JavaDoc[] getLocation() {
2236            try {
2237                if (resolvedName == null) {
2238                    resolve();
2239                }
2240            } catch (Exception JavaDoc e) {
2241                // Ignore the error for now
2242
}
2243            return resolvedLocations;
2244        }
2245
2246        private void resolve() throws Exception JavaDoc {
2247            if (resolvedLocations == null) {
2248                resolvedLocations = new String JavaDoc[locations.length];
2249                for (int i=0; i < resolvedLocations.length; ++i) {
2250                    resolvedLocations[i] = locations[i].resolve(null, objectModel);
2251                }
2252            }
2253            if (resolvedName == null) {
2254                resolvedName = name.resolve(null, objectModel);
2255            }
2256        }
2257
2258        public Bundle getCatalogue() {
2259            if (catalogue == null) {
2260                try {
2261                    resolve();
2262                    catalogue = factory.select(resolvedLocations, resolvedName, locale);
2263                } catch (Exception JavaDoc e) {
2264                    getLogger().error("Error obtaining catalogue '" + getName() +
2265                                      "' from <" + getLocation() + "> for locale " +
2266                                      locale, e);
2267                }
2268            }
2269
2270            return catalogue;
2271        }
2272
2273        public void releaseCatalog() {
2274            if (catalogue != null) {
2275                factory.release(catalogue);
2276            }
2277            catalogue = null;
2278            resolvedName = null;
2279            resolvedLocations = null;
2280        }
2281    }
2282}
2283
Popular Tags