1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.cocoon.transformation;
18
19 import org.apache.avalon.framework.activity.Disposable;
20 import org.apache.avalon.framework.configuration.Configurable;
21 import org.apache.avalon.framework.configuration.Configuration;
22 import org.apache.avalon.framework.configuration.ConfigurationException;
23 import org.apache.avalon.framework.parameters.Parameters;
24 import org.apache.avalon.framework.service.ServiceException;
25 import org.apache.avalon.framework.service.ServiceManager;
26 import org.apache.avalon.framework.service.Serviceable;
27
28 import org.apache.cocoon.ProcessingException;
29 import org.apache.cocoon.caching.CacheableProcessingComponent;
30 import org.apache.cocoon.components.treeprocessor.variables.VariableExpressionTokenizer;
31 import org.apache.cocoon.components.treeprocessor.variables.VariableResolver;
32 import org.apache.cocoon.components.treeprocessor.variables.VariableResolverFactory;
33 import org.apache.cocoon.environment.SourceResolver;
34 import org.apache.cocoon.i18n.Bundle;
35 import org.apache.cocoon.i18n.BundleFactory;
36 import org.apache.cocoon.i18n.I18nUtils;
37 import org.apache.cocoon.sitemap.PatternException;
38 import org.apache.cocoon.xml.ParamSaxBuffer;
39 import org.apache.cocoon.xml.SaxBuffer;
40
41 import org.apache.excalibur.source.SourceValidity;
42 import org.xml.sax.Attributes;
43 import org.xml.sax.SAXException;
44 import org.xml.sax.helpers.AttributesImpl;
45
46 import java.io.IOException;
47 import java.text.DateFormat;
48 import java.text.DecimalFormat;
49 import java.text.DecimalFormatSymbols;
50 import java.text.NumberFormat;
51 import java.text.ParseException;
52 import java.text.SimpleDateFormat;
53 import java.util.Collections;
54 import java.util.Date;
55 import java.util.HashMap;
56 import java.util.HashSet;
57 import java.util.Iterator;
58 import java.util.Locale;
59 import java.util.Map;
60 import java.util.MissingResourceException;
61 import java.util.Set;
62 import java.util.StringTokenizer;
63
64 /**
65 * @cocoon.sitemap.component.documentation
66 * Internationalization transformer is used to transform i18n markup into text
67 * based on a particular locale.
68 *
69 * @cocoon.sitemap.component.name i18n
70 * @cocoon.sitemap.component.documentation.caching TBD
71 * @cocoon.sitemap.component.logger sitemap.transformer.i18n
72 *
73 * <h3>I18n Transformer</h3>
74 * <p>The i18n transformer works by finding a translation for the user's locale
75 * in the configured catalogues. Locale is passed as parameter to the transformer,
76 * and it can be determined based on the request, session, or a cookie data by
77 * the {@link org.apache.cocoon.acting.LocaleAction}.</p>
78 *
79 * <p>For the passed local it then attempts to find a message catalogue that
80 * satisifies the locale, and uses it for for processing text replacement
81 * directed by i18n markup.</p>
82 *
83 * <p>Message catalogues are maintained in separate files, with a naming
84 * convention similar to that of {@link java.util.ResourceBundle}. I.e.
85 * <code>basename_locale</code>, where <i>basename</i> can be any name,
86 * and <i>locale</i> can be any locale specified using ISO 639/3166
87 * characters (eg. <code>en_AU</code>, <code>de_AT</code>, <code>es</code>).</p>
88 *
89 * <p><strong>NOTE:</strong> ISO 639 is not a stable standard; some of the
90 * language codes it defines (specifically, iw, ji, and in) have changed
91 * (see {@link java.util.Locale} for details).
92 *
93 * <h3>Message Catalogues</h3>
94 * <p>Catalogues are of the following format:
95 * <pre>
96 * <?xml version="1.0"?>
97 * <!-- message catalogue file for locale ... -->
98 * <catalogue xml:lang="locale">
99 * <message key="key">text <i>or</i> markup</message>
100 * ....
101 * </catalogue>
102 * </pre>
103 * Where <code>key</code> specifies a particular message for that
104 * language.
105 *
106 * <h3>Usage</h3>
107 * <p>Files to be translated contain the following markup:
108 * <pre>
109 * <?xml version="1.0"?>
110 * ... some text, translate <i18n:text>key</i18n:text>
111 * </pre>
112 * At runtime, the i18n transformer will find a message catalogue for the
113 * user's locale, and will appropriately replace the text between the
114 * <code><i18n:text></code> markup, using the value between the tags as
115 * the lookup key.</p>
116 *
117 * <p>If the i18n transformer cannot find an appropriate message catalogue for
118 * the user's given locale, it will recursively try to locate a <i>parent</i>
119 * message catalogue, until a valid catalogue can be found.
120 * ie:
121 * <ul>
122 * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>_<i>variant</i>.xml
123 * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>.xml
124 * <li><strong>catalogue</strong>_<i>language</i>.xml
125 * <li><strong>catalogue</strong>.xml
126 * </ul>
127 * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i>
128 * (no variant), the following search will occur:
129 * <ul>
130 * <li><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
131 * <li><strong>messages</strong>_<i>en</i>.xml
132 * <li><strong>messages</strong>.xml
133 * </ul>
134 * This allows the developer to write a hierarchy of message catalogues,
135 * at each defining messages with increasing depth of variation.</p>
136 *
137 * <p>In addition, catalogues can be split across multiple locations. For example,
138 * there can be a default catalogue in one directory with a user or client specific
139 * catalogue in another directory. The catalogues will be searched in the order of
140 * the locations specified still following the locale ordering specified above.
141 * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i>
142 * (no variant) and locations of <i>translations/client</i> and <i>translations</i>,
143 * the following search will occur:
144 * <ul>
145 * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
146 * <li><i>translations/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
147 * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>.xml
148 * <li><i>translations/</i><strong>messages</strong>_<i>en</i.xml
149 * <li><i>translations/client/</i><strong>messages</strong>.xml
150 * <li><i>translations/</i><strong>messages</strong>.xml
151 * </ul>
152 * </p>
153 *
154 * <p>The <code>i18n:text</code> element can optionally take an attribute
155 * <code>i18n:catalogue</code> to indicate which specific catalogue to use.
156 * The value of this attribute should be the id of the catalogue to use
157 * (see sitemap configuration).
158 *
159 * <h3>Sitemap Configuration</h3>
160 * <pre>
161 * <map:transformer name="i18n"
162 * src="org.apache.cocoon.transformation.I18nTransformer">
163 *
164 * <catalogues default="someId">
165 * <catalogue id="someId" name="messages" [location="translations"]>
166 * [<location>translations/client</location>]
167 * [<location>translations</location>]
168 * </catalogue>
169 * ...
170 * </catalogues>
171 * <untranslated-text>untranslated</untranslated-text>
172 * <preload>en_US</preload>
173 * <preload catalogue="someId">fr_CA</preload>
174 * </map:transformer>
175 * </pre>
176 * Where:
177 * <ul>
178 * <li><strong>catalogues</strong>: container element in which the catalogues
179 * are defined. It must have an attribute 'default' whose value is one
180 * of the id's of the catalogue elements. (<i>mandatory</i>).
181 * <li><strong>catalogue</strong>: specifies a catalogue. It takes 2 required
182 * attributes: id (can be wathever you like) and name (base name of the catalogue).
183 * The location (location of the message catalogue) is also required, but can be
184 * specified either as an attribute or as one or more subelements, but not both.
185 * If more than one location is specified the catalogues will be searched in the
186 * order they appear in the configuration. The name and location can contain
187 * references to inputmodules (same syntax as in other places in the
188 * sitemap). They are resolved on each usage of the transformer, so they can
189 * refer to e.g. request parameters. (<i>at least 1 catalogue
190 * element required</i>). After input module references are resolved the location
191 * string can be the root of a URI. For example, specifying a location of
192 * cocoon:/test with a name of messages and a locale of en_GB will cause the
193 * sitemap to try to process cocoon:/test/messages_en_GB.xml,
194 * cocoon:/test/messages_en.xml and cocoon:/test/messages.xml.
195 * <li><strong>untranslated-text</strong>: text used for
196 * untranslated keys (default is to output the key name).
197 * <li><strong>preload</strong>: locale of the catalogue to preload. Will attempt
198 * to resolve all configured catalogues for specified locale. If optional
199 * <code>catalogue</code> attribute is present, will preload only specified
200 * catalogue. Multiple <code>preload</code> elements can be specified.
201 * </ul>
202 *
203 * <h3>Pipeline Usage</h3>
204 * <p>To use the transformer in a pipeline, simply specify it in a particular
205 * transform, and pass locale parameter:
206 * <pre>
207 * <map:match pattern="file">
208 * <map:generate src="file.xml"/>
209 * <map:transform type="i18n">
210 * <map:parameter name="locale" value="..."/>
211 * </map:transform>
212 * <map:serialize/>
213 * </map:match>
214 * </pre>
215 * You can use {@link org.apache.cocoon.acting.LocaleAction} or any other
216 * way to provide transformer with a locale.</p>
217 *
218 * <p>If in certain pipeline, you want to use a different catalogue as the
219 * default catalogue, you can do so by specifying a parameter called
220 * <strong>default-catalogue-id</strong>.
221 *
222 * <p>The <strong>untranslated-text</strong> can also be overridden at the
223 * pipeline level by specifying it as a parameter.</p>
224 *
225 *
226 * <h3>i18n markup</h3>
227 *
228 * <p>For date, time and number formatting use the following tags:
229 * <ul>
230 * <li><strong><i18n:date/></strong> gives localized date.</li>
231 * <li><strong><i18n:date-time/></strong> gives localized date and time.</li>
232 * <li><strong><i18n:time/></strong> gives localized time.</li>
233 * <li><strong><i18n:number/></strong> gives localized number.</li>
234 * <li><strong><i18n:currency/></strong> gives localized currency.</li>
235 * <li><strong><i18n:percent/></strong> gives localized percent.</li>
236 * </ul>
237 * Elements <code>date</code>, <code>date-time</code> and <code>time</code>
238 * accept <code>pattern</code> and <code>src-pattern</code> attribute, with
239 * values of:
240 * <ul>
241 * <li><code>short</code>
242 * <li><code>medium</code>
243 * <li><code>long</code>
244 * <li><code>full</code>
245 * </ul>
246 * See {@link java.text.DateFormat} for more info on these values.</p>
247 *
248 * <p>Elements <code>date</code>, <code>date-time</code>, <code>time</code> and
249 * <code>number</code>, a different <code>locale</code> and
250 * <code>source-locale</code> can be specified:
251 * <pre>
252 * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE">
253 * 12/24/01
254 * </i18n:date>
255 * </pre>
256 * Will result in 24.12.2001.</p>
257 *
258 * <p>A given real <code>pattern</code> and <code>src-pattern</code> (not
259 * keywords <code>short, medium, long, full</code>) overrides any value
260 * specified by <code>locale</code> and <code>src-locale</code> attributes.</p>
261 *
262 * <p>Future work coming:
263 * <ul>
264 * <li>Introduce new <get-locale/> element
265 * <li>Move all formatting routines to I18nUtils
266 * </ul>
267 *
268 * @author <a href="mailto:kpiroumian@apache.org">Konstantin Piroumian</a>
269 * @author <a href="mailto:mattam@netcourrier.com">Matthieu Sozeau</a>
270 * @author <a href="mailto:crafterm@apache.org">Marcus Crafter</a>
271 * @author <a href="mailto:Michael.Enke@wincor-nixdorf.com">Michael Enke</a>
272 * @version $Id: I18nTransformer.java 474832 2006-11-14 15:56:43Z vgritsenko $
273 */
274 public class I18nTransformer extends AbstractTransformer
275 implements CacheableProcessingComponent,
276 Serviceable, Configurable, Disposable {
277
278 /**
279 * The namespace for i18n is "http://apache.org/cocoon/i18n/2.1".
280 */
281 public static final String I18N_NAMESPACE_URI = I18nUtils.NAMESPACE_URI;
282
283 /**
284 * The old namespace for i18n is "http://apache.org/cocoon/i18n/2.0".
285 */
286 public static final String I18N_OLD_NAMESPACE_URI = I18nUtils.OLD_NAMESPACE_URI;
287
288 //
289 // i18n elements
290 //
291
292 /**
293 * <code>i18n:text</code> element is used to translate any text, with
294 * or without markup. Example:
295 * <pre>
296 * <i18n:text>
297 * This is <strong>translated</strong> string.
298 * </i18n:text>
299 * </pre>
300 */
301 public static final String I18N_TEXT_ELEMENT = "text";
302
303 /**
304 * <code>i18n:translate</code> element is used to translate text with
305 * parameter substitution. Example:
306 * <pre>
307 * <i18n:translate>
308 * <i18n:text>This is translated string with {0} param</i18n:text>
309 * <i18n:param>1</i18n:param>
310 * </i18n:translate>
311 * </pre>
312 * The <code>i18n:text</code> fragment can include markup and parameters
313 * at any place. Also do parameters, which can include <code>i18n:text</code>,
314 * <code>i18n:date</code>, etc. elements (without keys only).
315 *
316 * @see #I18N_TEXT_ELEMENT
317 * @see #I18N_PARAM_ELEMENT
318 */
319 public static final String I18N_TRANSLATE_ELEMENT = "translate";
320
321 /**
322 * <code>i18n:choose</code> element is used to translate elements in-place.
323 * The first <code>i18n:when</code> element matching the current locale
324 * is selected and the others are discarded.
325 *
326 * <p>To specify what to do if no locale matched, simply add a node with
327 * <code>locale="*"</code>. <em>Note that this element must be the last
328 * child of <i18n:choose>.</em></p>
329 * <pre>
330 * <i18n:choose>
331 * <i18n:when locale="en">
332 * Good Morning
333 * </en>
334 * <i18n:when locale="fr">
335 * Bonjour
336 * </jp>
337 * <i18n:when locale="jp">
338 * Aligato?
339 * </jp>
340 * <i18n:otherwise>
341 * Sorry, i don't know how to say hello in your language
342 * </jp>
343 * <i18n:translate>
344 * </pre>
345 * <p>You can include any markup within <code>i18n:when</code> elements,
346 * with the exception of other <code>i18n:*</code> elements.</p>
347 *
348 * @see #I18N_IF_ELEMENT
349 * @see #I18N_LOCALE_ATTRIBUTE
350 * @since 2.1
351 */
352 public static final String I18N_CHOOSE_ELEMENT = "choose";
353
354 /**
355 * <code>i18n:when</code> is used to test a locale.
356 * It can be used within <code>i18:choose</code> elements or alone.
357 * <em>Note: Using <code>locale="*"</code> here has no sense.</em>
358 * Example:
359 * <pre>
360 * <greeting>
361 * <i18n:when locale="en">Hello</i18n:when>
362 * <i18n:when locale="fr">Bonjour</i18n:when>
363 * </greeting>
364 * </pre>
365 *
366 * @see #I18N_LOCALE_ATTRIBUTE
367 * @see #I18N_CHOOSE_ELEMENT
368 * @since 2.1
369 */
370 public static final String I18N_WHEN_ELEMENT = "when";
371
372 /**
373 * <code>i18n:if</code> is used to test a locale. Example:
374 * <pre>
375 * <greeting>
376 * <i18n:if locale="en">Hello</i18n:when>
377 * <i18n:if locale="fr">Bonjour</i18n:when>
378 * </greeting>
379 * </pre>
380 *
381 * @see #I18N_LOCALE_ATTRIBUTE
382 * @see #I18N_CHOOSE_ELEMENT
383 * @see #I18N_WHEN_ELEMENT
384 * @since 2.1
385 */
386 public static final String I18N_IF_ELEMENT = "if";
387
388 /**
389 * <code>i18n:otherwise</code> is used to match any locale when
390 * no matching locale has been found inside an <code>i18n:choose</code>
391 * block.
392 *
393 * @see #I18N_CHOOSE_ELEMENT
394 * @see #I18N_WHEN_ELEMENT
395 * @since 2.1
396 */
397 public static final String I18N_OTHERWISE_ELEMENT = "otherwise";
398
399 /**
400 * <code>i18n:param</code> is used with i18n:translate to provide
401 * substitution params. The param can have <code>i18n:text</code> as
402 * its value to provide multilungual value. Parameters can have
403 * additional attributes to be used for formatting:
404 * <ul>
405 * <li><code>type</code>: can be <code>date, date-time, time,
406 * number, currency, currency-no-unit or percent</code>.
407 * Used to format params before substitution.</li>
408 * <li><code>value</code>: the value of the param. If no value is
409 * specified then the text inside of the param element will be used.</li>
410 * <li><code>locale</code>: used only with <code>number, date, time,
411 * date-time</code> types and used to override the current locale to
412 * format the given value.</li>
413 * <li><code>src-locale</code>: used with <code>number, date, time,
414 * date-time</code> types and specify the locale that should be used to
415 * parse the given value.</li>
416 * <li><code>pattern</code>: used with <code>number, date, time,
417 * date-time</code> types and specify the pattern that should be used
418 * to format the given value.</li>
419 * <li><code>src-pattern</code>: used with <code>number, date, time,
420 * date-time</code> types and specify the pattern that should be used
421 * to parse the given value.</li>
422 * </ul>
423 *
424 * @see #I18N_TRANSLATE_ELEMENT
425 * @see #I18N_DATE_ELEMENT
426 * @see #I18N_TIME_ELEMENT
427 * @see #I18N_DATE_TIME_ELEMENT
428 * @see #I18N_NUMBER_ELEMENT
429 */
430 public static final String I18N_PARAM_ELEMENT = "param";
431
432 /**
433 * This attribute affects a name to the param that could be used
434 * for substitution.
435 *
436 * @since 2.1
437 */
438 public static final String I18N_PARAM_NAME_ATTRIBUTE = "name";
439
440 /**
441 * <code>i18n:date</code> is used to provide a localized date string.
442 * Allowed attributes are: <code>pattern, src-pattern, locale,
443 * src-locale</code>. Usage examples:
444 * <pre>
445 * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE">
446 * 12/24/01
447 * </i18n:date>
448 *
449 * <i18n:date pattern="dd/MM/yyyy" />
450 * </pre>
451 *
452 * If no value is specified then the current date will be used. E.g.:
453 * <pre>
454 * <i18n:date />
455 * </pre>
456 * Displays the current date formatted with default pattern for
457 * the current locale.
458 *
459 * @see #I18N_PARAM_ELEMENT
460 * @see #I18N_DATE_TIME_ELEMENT
461 * @see #I18N_TIME_ELEMENT
462 * @see #I18N_NUMBER_ELEMENT
463 */
464 public static final String I18N_DATE_ELEMENT = "date";
465
466 /**
467 * <code>i18n:date-time</code> is used to provide a localized date and
468 * time string. Allowed attributes are: <code>pattern, src-pattern,
469 * locale, src-locale</code>. Usage examples:
470 * <pre>
471 * <i18n:date-time src-pattern="short" src-locale="en_US" locale="de_DE">
472 * 12/24/01 1:00 AM
473 * </i18n:date>
474 *
475 * <i18n:date-time pattern="dd/MM/yyyy hh:mm" />
476 * </pre>
477 *
478 * If no value is specified then the current date and time will be used.
479 * E.g.:
480 * <pre>
481 * <i18n:date-time />
482 * </pre>
483 * Displays the current date formatted with default pattern for
484 * the current locale.
485 *
486 * @see #I18N_PARAM_ELEMENT
487 * @see #I18N_DATE_ELEMENT
488 * @see #I18N_TIME_ELEMENT
489 * @see #I18N_NUMBER_ELEMENT
490 */
491 public static final String I18N_DATE_TIME_ELEMENT = "date-time";
492
493 /**
494 * <code>i18n:time</code> is used to provide a localized time string.
495 * Allowed attributes are: <code>pattern, src-pattern, locale,
496 * src-locale</code>. Usage examples:
497 * <pre>
498 * <i18n:time src-pattern="short" src-locale="en_US" locale="de_DE">
499 * 1:00 AM
500 * </i18n:time>
501 *
502 * <i18n:time pattern="hh:mm:ss" />
503 * </pre>
504 *
505 * If no value is specified then the current time will be used. E.g.:
506 * <pre>
507 * <i18n:time />
508 * </pre>
509 * Displays the current time formatted with default pattern for
510 * the current locale.
511 *
512 * @see #I18N_PARAM_ELEMENT
513 * @see #I18N_DATE_TIME_ELEMENT
514 * @see #I18N_DATE_ELEMENT
515 * @see #I18N_NUMBER_ELEMENT
516 */
517 public static final String I18N_TIME_ELEMENT = "time";
518
519 /**
520 * <code>i18n:number</code> is used to provide a localized number string.
521 * Allowed attributes are: <code>pattern, src-pattern, locale, src-locale,
522 * type</code>. Usage examples:
523 * <pre>
524 * <i18n:number src-pattern="short" src-locale="en_US" locale="de_DE">
525 * 1000.0
526 * </i18n:number>
527 *
528 * <i18n:number type="currency" />
529 * </pre>
530 *
531 * If no value is specifies then 0 will be used.
532 *
533 * @see #I18N_PARAM_ELEMENT
534 * @see #I18N_DATE_TIME_ELEMENT
535 * @see #I18N_TIME_ELEMENT
536 * @see #I18N_DATE_ELEMENT
537 */
538 public static final String I18N_NUMBER_ELEMENT = "number";
539
540 /**
541 * Currency element name
542 */
543 public static final String I18N_CURRENCY_ELEMENT = "currency";
544
545 /**
546 * Percent element name
547 */
548 public static final String I18N_PERCENT_ELEMENT = "percent";
549
550 /**
551 * Integer currency element name
552 */
553 public static final String I18N_INT_CURRENCY_ELEMENT = "int-currency";
554
555 /**
556 * Currency without unit element name
557 */
558 public static final String I18N_CURRENCY_NO_UNIT_ELEMENT = "currency-no-unit";
559
560 /**
561 * Integer currency without unit element name
562 */
563 public static final String I18N_INT_CURRENCY_NO_UNIT_ELEMENT = "int-currency-no-unit";
564
565 //
566 // i18n general attributes
567 //
568
569 /**
570 * This attribute is used with i18n:text element to indicate the key of
571 * the according message. The character data of the element will be used
572 * if no message is found by this key. E.g.:
573 * <pre>
574 * <i18n:text i18n:key="a_key">article_text1</i18n:text>
575 * </pre>
576 */
577 public static final String I18N_KEY_ATTRIBUTE = "key";
578
579 /**
580 * This attribute is used with <strong>any</strong> element (even not i18n)
581 * to translate attribute values. Should contain whitespace separated
582 * attribute names that should be translated:
583 * <pre>
584 * <para title="first" name="article" i18n:attr="title name"/>
585 * </pre>
586 * Attribute value considered as key in message catalogue.
587 */
588 public static final String I18N_ATTR_ATTRIBUTE = "attr";
589
590 /**
591 * This attribute is used with <strong>any</strong> element (even not i18n)
592 * to evaluate attribute values. Should contain whitespace separated
593 * attribute names that should be evaluated:
594 * <pre>
595 * <para title="first" name="{one} {two}" i18n:attr="name"/>
596 * </pre>
597 * Attribute value considered as expression containing text and catalogue
598 * keys in curly braces.
599 */
600 public static final String I18N_EXPR_ATTRIBUTE = "expr";
601
602 //
603 // i18n number and date formatting attributes
604 //
605
606 /**
607 * This attribute is used with date and number formatting elements to
608 * indicate the pattern that should be used to parse the element value.
609 *
610 * @see #I18N_PARAM_ELEMENT
611 * @see #I18N_DATE_TIME_ELEMENT
612 * @see #I18N_DATE_ELEMENT
613 * @see #I18N_TIME_ELEMENT
614 * @see #I18N_NUMBER_ELEMENT
615 */
616 public static final String I18N_SRC_PATTERN_ATTRIBUTE = "src-pattern";
617
618 /**
619 * This attribute is used with date and number formatting elements to
620 * indicate the pattern that should be used to format the element value.
621 *
622 * @see #I18N_PARAM_ELEMENT
623 * @see #I18N_DATE_TIME_ELEMENT
624 * @see #I18N_DATE_ELEMENT
625 * @see #I18N_TIME_ELEMENT
626 * @see #I18N_NUMBER_ELEMENT
627 */
628 public static final String I18N_PATTERN_ATTRIBUTE = "pattern";
629
630 /**
631 * This attribute is used with date and number formatting elements to
632 * indicate the locale that should be used to format the element value.
633 * Also used for in-place translations.
634 *
635 * @see #I18N_PARAM_ELEMENT
636 * @see #I18N_DATE_TIME_ELEMENT
637 * @see #I18N_DATE_ELEMENT
638 * @see #I18N_TIME_ELEMENT
639 * @see #I18N_NUMBER_ELEMENT
640 * @see #I18N_WHEN_ELEMENT
641 */
642 public static final String I18N_LOCALE_ATTRIBUTE = "locale";
643
644 /**
645 * This attribute is used with date and number formatting elements to
646 * indicate the locale that should be used to parse the element value.
647 *
648 * @see #I18N_PARAM_ELEMENT
649 * @see #I18N_DATE_TIME_ELEMENT
650 * @see #I18N_DATE_ELEMENT
651 * @see #I18N_TIME_ELEMENT
652 * @see #I18N_NUMBER_ELEMENT
653 */
654 public static final String I18N_SRC_LOCALE_ATTRIBUTE = "src-locale";
655
656 /**
657 * This attribute is used with date and number formatting elements to
658 * indicate the value that should be parsed and formatted. If value
659 * attribute is not used then the character data of the element will be used.
660 *
661 * @see #I18N_PARAM_ELEMENT
662 * @see #I18N_DATE_TIME_ELEMENT
663 * @see #I18N_DATE_ELEMENT
664 * @see #I18N_TIME_ELEMENT
665 * @see #I18N_NUMBER_ELEMENT
666 */
667 public static final String I18N_VALUE_ATTRIBUTE = "value";
668
669 /**
670 * This attribute is used with <code>i18:param</code> to
671 * indicate the parameter type: <code>date, time, date-time</code> or
672 * <code>number, currency, percent, int-currency, currency-no-unit,
673 * int-currency-no-unit</code>.
674 * Also used with <code>i18:translate</code> to indicate inplace
675 * translations: <code>inplace</code>
676 * @deprecated since 2.1. Use nested tags instead, e.g.:
677 * <i18n:param><i18n:date/></i18n:param>
678 */
679 public static final String I18N_TYPE_ATTRIBUTE = "type";
680
681 /**
682 * This attribute is used to specify a different locale for the
683 * currency. When specified, this locale will be combined with
684 * the "normal" locale: e.g. the seperator symbols are taken from
685 * the normal locale but the currency symbol and possition will
686 * be taken from the currency locale.
687 * This enables to see a currency formatted for Euro but with US
688 * grouping and decimal char.
689 */
690 public static final String CURRENCY_LOCALE_ATTRIBUTE = "currency";
691
692 /**
693 * This attribute can be used on <code>i18n:text</code> to indicate the catalogue
694 * from which the key should be retrieved. This attribute is optional,
695 * if it is not mentioned the default catalogue is used.
696 */
697 public static final String I18N_CATALOGUE_ATTRIBUTE = "catalogue";
698
699 //
700 // Configuration parameters
701 //
702
703 /**
704 * This configuration parameter specifies the default locale to be used.
705 */
706 public static final String I18N_LOCALE = "locale";
707
708 /**
709 * This configuration parameter specifies the id of the catalogue to be used as
710 * default catalogue, allowing to redefine the default catalogue on the pipeline
711 * level.
712 */
713 public static final String I18N_DEFAULT_CATALOGUE_ID = "default-catalogue-id";
714
715 /**
716 * This configuration parameter specifies the message that should be
717 * displayed in case of a not translated text (message not found).
718 */
719 public static final String I18N_UNTRANSLATED = "untranslated-text";
720
721 /**
722 * This configuration parameter specifies locale for which catalogues should
723 * be preloaded.
724 */
725 public static final String I18N_PRELOAD = "preload";
726
727 /**
728 * <code>fraction-digits</code> attribute is used with
729 * <code>i18:number</code> to
730 * indicate the number of digits behind the fraction
731 */
732 public static final String I18N_FRACTION_DIGITS_ATTRIBUTE = "fraction-digits";
733
734 //
735 // States of the transformer
736 //
737
738 private static final int STATE_OUTSIDE = 0;
739 private static final int STATE_INSIDE_TEXT = 10;
740 private static final int STATE_INSIDE_PARAM = 20;
741 private static final int STATE_INSIDE_TRANSLATE = 30;
742 private static final int STATE_INSIDE_CHOOSE = 50;
743 private static final int STATE_INSIDE_WHEN = 51;
744 private static final int STATE_INSIDE_OTHERWISE = 52;
745 private static final int STATE_INSIDE_DATE = 60;
746 private static final int STATE_INSIDE_DATE_TIME = 61;
747 private static final int STATE_INSIDE_TIME = 62;
748 private static final int STATE_INSIDE_NUMBER = 63;
749
750 // All date-time related parameter types and element names
751 private static final Set dateTypes;
752
753 // All number related parameter types and element names
754 private static final Set numberTypes;
755
756 // Date pattern types map: short, medium, long, full
757 private static final Map datePatterns;
758
759 static {
760 // initialize date types set
761 HashSet set = new HashSet(5);
762 set.add(I18N_DATE_ELEMENT);
763 set.add(I18N_TIME_ELEMENT);
764 set.add(I18N_DATE_TIME_ELEMENT);
765 dateTypes = Collections.unmodifiableSet(set);
766
767 // initialize number types set
768 set = new HashSet(9);
769 set.add(I18N_NUMBER_ELEMENT);
770 set.add(I18N_PERCENT_ELEMENT);
771 set.add(I18N_CURRENCY_ELEMENT);
772 set.add(I18N_INT_CURRENCY_ELEMENT);
773 set.add(I18N_CURRENCY_NO_UNIT_ELEMENT);
774 set.add(I18N_INT_CURRENCY_NO_UNIT_ELEMENT);
775 numberTypes = Collections.unmodifiableSet(set);
776
777 // Initialize date patterns map
778 Map map = new HashMap(7);
779 map.put("SHORT", new Integer(DateFormat.SHORT));
780 map.put("MEDIUM", new Integer(DateFormat.MEDIUM));
781 map.put("LONG", new Integer(DateFormat.LONG));
782 map.put("FULL", new Integer(DateFormat.FULL));
783 datePatterns = Collections.unmodifiableMap(map);
784 }
785
786
787 //
788 // Global configuration variables
789 //
790
791 /**
792 * Component (service) manager
793 */
794 protected ServiceManager manager;
795
796 /**
797 * Message bundle loader factory component (service)
798 */
799 protected BundleFactory factory;
800
801 /**
802 * All catalogues (keyed by catalogue id). The values are instances
803 * of {@link CatalogueInfo}.
804 */
805 private Map catalogues;
806
807 /**
808 * Default (global) catalogue
809 */
810 private CatalogueInfo defaultCatalogue;
811
812 /**
813 * Default (global) untranslated message value
814 */
815 private String defaultUntranslated;
816
817 //
818 // Local configuration variables
819 //
820
821 protected Map objectModel;
822
823 /**
824 * Locale
825 */
826 protected Locale locale;
827
828 /**
829 * Catalogue (local)
830 */
831 private CatalogueInfo catalogue;
832
833 /**
834 * Current (local) untranslated message value
835 */
836 private String untranslated;
837
838 /**
839 * {@link SaxBuffer} containing the contents of {@link #untranslated}.
840 */
841 private ParamSaxBuffer untranslatedRecorder;
842
843 //
844 // Current state of the transformer
845 //
846
847 /**
848 * Current state of the transformer. Default value is STATE_OUTSIDE.
849 */
850 private int current_state;
851
852 /**
853 * Previous state of the transformer.
854 * Used in text translation inside params and translate elements.
855 */
856 private int prev_state;
857
858 /**
859 * The i18n:key attribute is stored for the current element.
860 * If no translation found for the key then the character data of element is
861 * used as default value.
862 */
863 private String currentKey;
864
865 /**
866 * Contains the id of the current catalogue if it was explicitely mentioned
867 * on an i18n:text element, otherwise it is null.
868 */
869 private String currentCatalogueId;
870
871 /**
872 * Character data buffer. used to concat chunked character data
873 */
874 private StringBuffer strBuffer;
875
876 /**
877 * A flag for copying the node when doing in-place translation
878 */
879 private boolean translate_copy;
880
881 // A flag for copying the _GOOD_ node and not others
882 // when doing in-place translation within i18n:choose
883 private boolean translate_end;
884
885 // Translated text. Inside i18n:translate, collects character events.
886 private ParamSaxBuffer tr_text_recorder;
887
888 // Current "i18n:text" events
889 private ParamSaxBuffer text_recorder;
890
891 // Current parameter events
892 private SaxBuffer param_recorder;
893
894 // Param count when not using i18n:param name="..."
895 private int param_count;
896
897 // Param name attribute for substitution.
898 private String param_name;
899
900 // i18n:param's hashmap for substitution
901 private HashMap indexedParams;
902
903 // Current parameter value (translated or not)
904 private String param_value;
905
906 // Date and number elements and params formatting attributes with values.
907 private HashMap formattingParams;
908
909 /**
910 * Returns the current locale setting of this transformer instance.
911 * @return current Locale object
912 */
913 public Locale getLocale() {
914 return this.locale;
915 }
916
917 /**
918 * Implemenation of CacheableProcessingComponents.
919 * Generates unique key for the current locale.
920 */
921 public java.io.Serializable getKey() {
922 // TODO: Key should be composed out of used catalogues locations, and locale.
923 // Right now it is hardcoded only to default catalogue location.
924 StringBuffer key = new StringBuffer();
925 if (catalogue != null) {
926 key.append(catalogue.getLocation()[0]);
927 }
928 key.append("?");
929 if (locale != null) {
930 key.append(locale.getLanguage());
931 key.append("_");
932 key.append(locale.getCountry());
933 key.append("_");
934 key.append(locale.getVariant());
935 }
936 return key.toString();
937 }
938
939 /**
940 * Implementation of CacheableProcessingComponent.
941 * Generates validity object for this transformer or <code>null</code>
942 * if this instance is not cacheable.
943 */
944 public SourceValidity getValidity() {
945 // FIXME (KP): Cache validity should be generated by
946 // Bundle implementations.
947 return org.apache.excalibur.source.impl.validity.NOPValidity.SHARED_INSTANCE;
948 }
949
950 /**
951 * Look up the {@link BundleFactory} to be used.
952 */
953 public void service(ServiceManager manager) throws ServiceException {
954 this.manager = manager;
955 try {
956 this.factory = (BundleFactory) manager.lookup(BundleFactory.ROLE);
957 } catch (ServiceException e) {
958 getLogger().debug("Failed to lookup <" + BundleFactory.ROLE + ">", e);
959 throw e;
960 }
961 }
962
963 /**
964 * Implementation of Configurable interface.
965 * Configure this transformer.
966 */
967 public void configure(Configuration conf) throws ConfigurationException {
968 // Read in the config options from the transformer definition
969 Configuration cataloguesConf = conf.getChild("catalogues", false);
970 if (cataloguesConf == null) {
971 throw new ConfigurationException("Required <catalogues> configuration is missing.",
972 conf);
973 }
974
975 // new configuration style
976 Configuration[] catalogueConfs = cataloguesConf.getChildren("catalogue");
977 catalogues = new HashMap(catalogueConfs.length + 3);
978 for (int i = 0; i < catalogueConfs.length; i++) {
979 String id = catalogueConfs[i].getAttribute("id");
980 String name = catalogueConfs[i].getAttribute("name");
981
982 String[] locations;
983 String location = catalogueConfs[i].getAttribute("location", null);
984 Configuration[] locationConf = catalogueConfs[i].getChildren("location");
985 if (location != null) {
986 if (locationConf.length > 0) {
987 String msg = "Location attribute cannot be " +
988 "specified with location elements";
989 getLogger().error(msg);
990 throw new ConfigurationException(msg, catalogueConfs[i]);
991 }
992
993 if (getLogger().isDebugEnabled()) {
994 getLogger().debug("name=" + name + ", location=" +
995 location);
996 }
997 locations = new String[1];
998 locations[0] = location;
999 } else {
1000 if (locationConf.length == 0) {
1001 String msg = "A location attribute or location " +
1002 "elements must be specified";
1003 getLogger().error(msg);
1004 throw new ConfigurationException(msg, catalogueConfs[i]);
1005 }
1006
1007 locations = new String[locationConf.length];
1008 for (int j=0; j < locationConf.length; ++j) {
1009 locations[j] = locationConf[j].getValue();
1010 if (getLogger().isDebugEnabled()) {
1011 getLogger().debug("name=" + name + ", location=" +
1012 locations[j]);
1013 }
1014 }
1015 }
1016
1017 CatalogueInfo catalogueInfo;
1018 try {
1019 catalogueInfo = new CatalogueInfo(name, locations);
1020 } catch (PatternException e) {
1021 throw new ConfigurationException("Error in name or location attribute on catalogue " +
1022 "element with id " + id, catalogueConfs[i], e);
1023 }
1024 catalogues.put(id, catalogueInfo);
1025 }
1026
1027 String defaultCatalogueId = cataloguesConf.getAttribute("default");
1028 defaultCatalogue = (CatalogueInfo) catalogues.get(defaultCatalogueId);
1029 if (defaultCatalogue == null) {
1030 throw new ConfigurationException("Default catalogue id '" + defaultCatalogueId +
1031 "' denotes a nonexisting catalogue", cataloguesConf);
1032 }
1033
1034 // Obtain default text to use for untranslated messages
1035 defaultUntranslated = conf.getChild(I18N_UNTRANSLATED).getValue(null);
1036 if (getLogger().isDebugEnabled()) {
1037 getLogger().debug("Default untranslated text is '" + defaultUntranslated + "'");
1038 }
1039
1040 // Preload specified catalogues (if any)
1041 Configuration[] preloadConfs = conf.getChildren(I18N_PRELOAD);
1042 for (int i = 0; i < preloadConfs.length; i++) {
1043 String localeStr = preloadConfs[i].getValue();
1044 this.locale = I18nUtils.parseLocale(localeStr);
1045
1046 String id = preloadConfs[i].getAttribute("catalogue", null);
1047 if (id != null) {
1048 CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues.get(id);
1049 if (catalogueInfo == null) {
1050 throw new ConfigurationException("Invalid catalogue id '" + id +
1051 "' in preload element.", preloadConfs[i]);
1052 }
1053
1054 try {
1055 catalogueInfo.getCatalogue();
1056 } finally {
1057 catalogueInfo.releaseCatalog();
1058 }
1059 } else {
1060 for (Iterator j = catalogues.values().iterator(); j.hasNext(); ) {
1061 CatalogueInfo catalogueInfo = (CatalogueInfo) j.next();
1062 try {
1063 catalogueInfo.getCatalogue();
1064 } finally {
1065 catalogueInfo.releaseCatalog();
1066 }
1067 }
1068 }
1069 }
1070 this.locale = null;
1071 }
1072
1073 /**
1074 * Setup current instance of transformer.
1075 */
1076 public void setup(SourceResolver resolver, Map objectModel, String source,
1077 Parameters parameters)
1078 throws ProcessingException, SAXException, IOException {
1079
1080 this.objectModel = objectModel;
1081
1082 untranslated = parameters.getParameter(I18N_UNTRANSLATED, defaultUntranslated);
1083 if (untranslated != null) {
1084 untranslatedRecorder = new ParamSaxBuffer();
1085 untranslatedRecorder.characters(untranslated.toCharArray(), 0, untranslated.length());
1086 }
1087
1088 // Get current locale
1089 String lc = parameters.getParameter(I18N_LOCALE, null);
1090 Locale locale = I18nUtils.parseLocale(lc);
1091 if (getLogger().isDebugEnabled()) {
1092 getLogger().debug("Using locale '" + locale + "'");
1093 }
1094
1095 // Initialize instance state variables
1096 this.locale = locale;
1097 this.current_state = STATE_OUTSIDE;
1098 this.prev_state = STATE_OUTSIDE;
1099 this.currentKey = null;
1100 this.currentCatalogueId = null;
1101 this.translate_copy = false;
1102 this.tr_text_recorder = null;
1103 this.text_recorder = new ParamSaxBuffer();
1104 this.param_count = 0;
1105 this.param_name = null;
1106 this.param_value = null;
1107 this.param_recorder = null;
1108 this.indexedParams = new HashMap(3);
1109 this.formattingParams = null;
1110 this.strBuffer = null;
1111
1112 // give the catalogue variable its value -- first look if it's locally overridden
1113 // and otherwise use the component-wide defaults.
1114 String catalogueId = parameters.getParameter(I18N_DEFAULT_CATALOGUE_ID, null);
1115 if (catalogueId != null) {
1116 CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues.get(catalogueId);
1117 if (catalogueInfo == null) {
1118 throw new ProcessingException("I18nTransformer: '" +
1119 catalogueId +
1120 "' is not an existing catalogue id.");
1121 }
1122 catalogue = catalogueInfo;
1123 } else {
1124 catalogue = defaultCatalogue;
1125 }
1126
1127 if (getLogger().isDebugEnabled()) {
1128 getLogger().debug("Default catalogue is " + catalogue.getName());
1129 }
1130 }
1131
1132
1133 //
1134 // Standard SAX event handlers
1135 //
1136
1137 public void startElement(String uri, String name, String raw,
1138 Attributes attr)
1139 throws SAXException {
1140
1141 // Handle previously buffered characters
1142 if (current_state != STATE_OUTSIDE && strBuffer != null) {
1143 i18nCharacters(strBuffer.toString());
1144 strBuffer = null;
1145 }
1146
1147 // Process start element event
1148 if (I18nUtils.matchesI18nNamespace(uri)) {
1149 if (getLogger().isDebugEnabled()) {
1150 getLogger().debug("Starting i18n element: " + name);
1151 }
1152 startI18NElement(name, attr);
1153 } else {
1154 // We have a non i18n element event
1155 if (current_state == STATE_OUTSIDE) {
1156 super.startElement(uri, name, raw,
1157 translateAttributes(name, attr));
1158 } else if (current_state == STATE_INSIDE_PARAM) {
1159 param_recorder.startElement(uri, name, raw, attr);
1160 } else if (current_state == STATE_INSIDE_TEXT) {
1161 text_recorder.startElement(uri, name, raw, attr);
1162 } else if ((current_state == STATE_INSIDE_WHEN ||
1163 current_state == STATE_INSIDE_OTHERWISE)
1164 && translate_copy) {
1165
1166 super.startElement(uri, name, raw, attr);
1167 }
1168 }
1169 }
1170
1171 public void endElement(String uri, String name, String raw)
1172 throws SAXException {
1173
1174 // Handle previously buffered characters
1175 if (current_state != STATE_OUTSIDE && strBuffer != null) {
1176 i18nCharacters(strBuffer.toString());
1177 strBuffer = null;
1178 }
1179
1180 if (I18nUtils.matchesI18nNamespace(uri)) {
1181 endI18NElement(name);
1182 } else if (current_state == STATE_INSIDE_PARAM) {
1183 param_recorder.endElement(uri, name, raw);
1184 } else if (current_state == STATE_INSIDE_TEXT) {
1185 text_recorder.endElement(uri, name, raw);
1186 } else if (current_state == STATE_INSIDE_CHOOSE ||
1187 (current_state == STATE_INSIDE_WHEN ||
1188 current_state == STATE_INSIDE_OTHERWISE)
1189 && !translate_copy) {
1190
1191 // Output nothing
1192 } else {
1193 super.endElement(uri, name, raw);
1194 }
1195 }
1196
1197 public void characters(char[] ch, int start, int len)
1198 throws SAXException {
1199
1200 if (current_state == STATE_OUTSIDE ||
1201 ((current_state == STATE_INSIDE_WHEN ||
1202 current_state == STATE_INSIDE_OTHERWISE) && translate_copy)) {
1203
1204 super.characters(ch, start, len);
1205 } else {
1206 // Perform buffering to prevent chunked character data
1207 if (strBuffer == null) {
1208 strBuffer = new StringBuffer();
1209 }
1210 strBuffer.append(ch, start, len);
1211 }
1212 }
1213
1214 //
1215 // i18n specific event handlers
1216 //
1217
1218 private void startI18NElement(String name, Attributes attr)
1219 throws SAXException {
1220
1221 if (getLogger().isDebugEnabled()) {
1222 getLogger().debug("Start i18n element: " + name);
1223 }
1224
1225 if (I18N_TEXT_ELEMENT.equals(name)) {
1226 if (current_state != STATE_OUTSIDE
1227 && current_state != STATE_INSIDE_PARAM
1228 && current_state != STATE_INSIDE_TRANSLATE) {
1229
1230 throw new SAXException(
1231 getClass().getName()
1232 + ": nested i18n:text elements are not allowed."
1233 + " Current state: " + current_state);
1234 }
1235
1236 prev_state = current_state;
1237 current_state = STATE_INSIDE_TEXT;
1238
1239 currentKey = attr.getValue("", I18N_KEY_ATTRIBUTE);
1240 if (currentKey == null) {
1241 // Try the namespaced attribute
1242 currentKey = attr.getValue(I18N_NAMESPACE_URI, I18N_KEY_ATTRIBUTE);
1243 if (currentKey == null) {
1244 // Try the old namespace
1245 currentKey = attr.getValue(I18N_OLD_NAMESPACE_URI, I18N_KEY_ATTRIBUTE);
1246 }
1247 }
1248
1249 currentCatalogueId = attr.getValue("", I18N_CATALOGUE_ATTRIBUTE);
1250 if (currentCatalogueId == null) {
1251 // Try the namespaced attribute
1252 currentCatalogueId = attr.getValue(I18N_NAMESPACE_URI, I18N_CATALOGUE_ATTRIBUTE);
1253 }
1254
1255 if (prev_state != STATE_INSIDE_PARAM) {
1256 tr_text_recorder = null;
1257 }
1258
1259 if (currentKey != null) {
1260 tr_text_recorder = getMessage(currentKey, (ParamSaxBuffer)null);
1261 }
1262
1263 } else if (I18N_TRANSLATE_ELEMENT.equals(name)) {
1264 if (current_state != STATE_OUTSIDE) {
1265 throw new SAXException(
1266 getClass().getName()
1267 + ": i18n:translate element must be used "
1268 + "outside of other i18n elements. Current state: "
1269 + current_state);
1270 }
1271
1272 prev_state = current_state;
1273 current_state = STATE_INSIDE_TRANSLATE;
1274 } else if (I18N_PARAM_ELEMENT.equals(name)) {
1275 if (current_state != STATE_INSIDE_TRANSLATE) {
1276 throw new SAXException(
1277 getClass().getName()
1278 + ": i18n:param element can be used only inside "
1279 + "i18n:translate element. Current state: "
1280 + current_state);
1281 }
1282
1283 param_name = attr.getValue(I18N_PARAM_NAME_ATTRIBUTE);
1284 if (param_name == null) {
1285 param_name = String.valueOf(param_count++);
1286 }
1287
1288 param_recorder = new SaxBuffer();
1289 setFormattingParams(attr);
1290 current_state = STATE_INSIDE_PARAM;
1291 } else if (I18N_CHOOSE_ELEMENT.equals(name)) {
1292 if (current_state != STATE_OUTSIDE) {
1293 throw new SAXException(
1294 getClass().getName()
1295 + ": i18n:choose elements cannot be used"
1296 + "inside of other i18n elements.");
1297 }
1298
1299 translate_copy = false;
1300 translate_end = false;
1301 prev_state = current_state;
1302 current_state = STATE_INSIDE_CHOOSE;
1303 } else if (I18N_WHEN_ELEMENT.equals(name) ||
1304 I18N_IF_ELEMENT.equals(name)) {
1305
1306 if (I18N_WHEN_ELEMENT.equals(name) &&
1307 current_state != STATE_INSIDE_CHOOSE) {
1308 throw new SAXException(
1309 getClass().getName()
1310 + ": i18n:when elements are can be used only"
1311 + "inside of i18n:choose elements.");
1312 }
1313
1314 if (I18N_IF_ELEMENT.equals(name) &&
1315 current_state != STATE_OUTSIDE) {
1316 throw new SAXException(
1317 getClass().getName()
1318 + ": i18n:if elements cannot be nested.");
1319 }
1320
1321 String locale = attr.getValue(I18N_LOCALE_ATTRIBUTE);
1322 if (locale == null)
1323 throw new SAXException(
1324 getClass().getName()
1325 + ": i18n:" + name
1326 + " element cannot be used without 'locale' attribute.");
1327
1328 if ((!translate_end && current_state == STATE_INSIDE_CHOOSE)
1329 || current_state == STATE_OUTSIDE) {
1330
1331 // Perform soft locale matching
1332 if (this.locale.toString().startsWith(locale)) {
1333 if (getLogger().isDebugEnabled()) {
1334 getLogger().debug("Locale matching: " + locale);
1335 }
1336 translate_copy = true;
1337 }
1338 }
1339
1340 prev_state = current_state;
1341 current_state = STATE_INSIDE_WHEN;
1342
1343 } else if (I18N_OTHERWISE_ELEMENT.equals(name)) {
1344 if (current_state != STATE_INSIDE_CHOOSE) {
1345 throw new SAXException(
1346 getClass().getName()
1347 + ": i18n:otherwise elements are not allowed "
1348 + "only inside i18n:choose.");
1349 }
1350
1351 getLogger().debug("Matching any locale");
1352 if (!translate_end) {
1353 translate_copy = true;
1354 }
1355
1356 prev_state = current_state;
1357 current_state = STATE_INSIDE_OTHERWISE;
1358
1359 } else if (I18N_DATE_ELEMENT.equals(name)) {
1360 if (current_state != STATE_OUTSIDE
1361 && current_state != STATE_INSIDE_TEXT
1362 && current_state != STATE_INSIDE_PARAM) {
1363 throw new SAXException(
1364 getClass().getName()
1365 + ": i18n:date elements are not allowed "
1366 + "inside of other i18n elements.");
1367 }
1368
1369 setFormattingParams(attr);
1370 prev_state = current_state;
1371 current_state = STATE_INSIDE_DATE;
1372 } else if (I18N_DATE_TIME_ELEMENT.equals(name)) {
1373 if (current_state != STATE_OUTSIDE
1374 && current_state != STATE_INSIDE_TEXT
1375 && current_state != STATE_INSIDE_PARAM) {
1376 throw new SAXException(
1377 getClass().getName()
1378 + ": i18n:date-time elements are not allowed "
1379 + "inside of other i18n elements.");
1380 }
1381
1382 setFormattingParams(attr);
1383 prev_state = current_state;
1384 current_state = STATE_INSIDE_DATE_TIME;
1385 } else if (I18N_TIME_ELEMENT.equals(name)) {
1386 if (current_state != STATE_OUTSIDE
1387 && current_state != STATE_INSIDE_TEXT
1388 && current_state != STATE_INSIDE_PARAM) {
1389 throw new SAXException(
1390 getClass().getName()
1391 + ": i18n:date elements are not allowed "
1392 + "inside of other i18n elements.");
1393 }
1394
1395 setFormattingParams(attr);
1396 prev_state = current_state;
1397 current_state = STATE_INSIDE_TIME;
1398 } else if (I18N_NUMBER_ELEMENT.equals(name)) {
1399 if (current_state != STATE_OUTSIDE
1400 && current_state != STATE_INSIDE_TEXT
1401 && current_state != STATE_INSIDE_PARAM) {
1402 throw new SAXException(
1403 getClass().getName()
1404 + ": i18n:number elements are not allowed "
1405 + "inside of other i18n elements.");
1406 }
1407
1408 setFormattingParams(attr);
1409 prev_state = current_state;
1410 current_state = STATE_INSIDE_NUMBER;
1411 }
1412 }
1413
1414 // Get all possible i18n formatting attribute values and store in a Map
1415 private void setFormattingParams(Attributes attr) {
1416 // average number of attributes is 3
1417 formattingParams = new HashMap(3);
1418
1419 String attr_value = attr.getValue(I18N_SRC_PATTERN_ATTRIBUTE);
1420 if (attr_value != null) {
1421 formattingParams.put(I18N_SRC_PATTERN_ATTRIBUTE, attr_value);
1422 }
1423
1424 attr_value = attr.getValue(I18N_PATTERN_ATTRIBUTE);
1425 if (attr_value != null) {
1426 formattingParams.put(I18N_PATTERN_ATTRIBUTE, attr_value);
1427 }
1428
1429 attr_value = attr.getValue(I18N_VALUE_ATTRIBUTE);
1430 if (attr_value != null) {
1431 formattingParams.put(I18N_VALUE_ATTRIBUTE, attr_value);
1432 }
1433
1434 attr_value = attr.getValue(I18N_LOCALE_ATTRIBUTE);
1435 if (attr_value != null) {
1436 formattingParams.put(I18N_LOCALE_ATTRIBUTE, attr_value);
1437 }
1438
1439 attr_value = attr.getValue(CURRENCY_LOCALE_ATTRIBUTE);
1440 if (attr_value != null) {
1441 formattingParams.put(CURRENCY_LOCALE_ATTRIBUTE, attr_value);
1442 }
1443
1444 attr_value = attr.getValue(I18N_SRC_LOCALE_ATTRIBUTE);
1445 if (attr_value != null) {
1446 formattingParams.put(I18N_SRC_LOCALE_ATTRIBUTE, attr_value);
1447 }
1448
1449 attr_value = attr.getValue(I18N_TYPE_ATTRIBUTE);
1450 if (attr_value != null) {
1451 formattingParams.put(I18N_TYPE_ATTRIBUTE, attr_value);
1452 }
1453
1454 attr_value = attr.getValue(I18N_FRACTION_DIGITS_ATTRIBUTE);
1455 if (attr_value != null) {
1456 formattingParams.put(I18N_FRACTION_DIGITS_ATTRIBUTE, attr_value);
1457 }
1458 }
1459
1460 private void endI18NElement(String name) throws SAXException {
1461 if (getLogger().isDebugEnabled()) {
1462 getLogger().debug("End i18n element: " + name);
1463 }
1464
1465 switch (current_state) {
1466 case STATE_INSIDE_TEXT:
1467 endTextElement();
1468 break;
1469
1470 case STATE_INSIDE_TRANSLATE:
1471 endTranslateElement();
1472 break;
1473
1474 case STATE_INSIDE_CHOOSE:
1475 endChooseElement();
1476 break;
1477
1478 case STATE_INSIDE_WHEN:
1479 case STATE_INSIDE_OTHERWISE:
1480 endWhenElement();
1481 break;
1482
1483 case STATE_INSIDE_PARAM:
1484 endParamElement();
1485 break;
1486
1487 case STATE_INSIDE_DATE:
1488 case STATE_INSIDE_DATE_TIME:
1489 case STATE_INSIDE_TIME:
1490 endDate_TimeElement();
1491 break;
1492
1493 case STATE_INSIDE_NUMBER:
1494 endNumberElement();
1495 break;
1496 }
1497 }
1498
1499 private void i18nCharacters(String textValue) throws SAXException {
1500 if (getLogger().isDebugEnabled()) {
1501 getLogger().debug("i18n message text = '" + textValue + "'");
1502 }
1503
1504 SaxBuffer buffer;
1505 switch (current_state) {
1506 case STATE_INSIDE_TEXT:
1507 buffer = text_recorder;
1508 break;
1509
1510 case STATE_INSIDE_PARAM:
1511 buffer = param_recorder;
1512 break;
1513
1514 case STATE_INSIDE_WHEN:
1515 case STATE_INSIDE_OTHERWISE:
1516 // Previously handeld to avoid the String() conversion.
1517 return;
1518
1519 case STATE_INSIDE_TRANSLATE:
1520 if (tr_text_recorder == null) {
1521 tr_text_recorder = new ParamSaxBuffer();
1522 }
1523 buffer = tr_text_recorder;
1524 break;
1525
1526 case STATE_INSIDE_CHOOSE:
1527 // No characters allowed. Send an exception ?
1528 if (getLogger().isDebugEnabled()) {
1529 textValue = textValue.trim();
1530 if (textValue.length() > 0) {
1531 getLogger().debug("No characters allowed inside <i18n:choose> tag. Received: " + textValue);
1532 }
1533 }
1534 return;
1535
1536 case STATE_INSIDE_DATE:
1537 case STATE_INSIDE_DATE_TIME:
1538 case STATE_INSIDE_TIME:
1539 case STATE_INSIDE_NUMBER:
1540 // Trim text values to avoid parsing errors.
1541 textValue = textValue.trim();
1542 if (textValue.length() > 0) {
1543 if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null) {
1544 formattingParams.put(I18N_VALUE_ATTRIBUTE, textValue);
1545 } else {
1546 // ignore the text inside of date element
1547 }
1548 }
1549 return;
1550
1551 default:
1552 throw new IllegalStateException(getClass().getName() +
1553 " developer's fault: characters not handled. " +
1554 "Current state: " + current_state);
1555 }
1556
1557 char[] ch = textValue.toCharArray();
1558 buffer.characters(ch, 0, ch.length);
1559 }
1560
1561 // Translate all attributes that are listed in i18n:attr attribute
1562 private Attributes translateAttributes(final String element, Attributes attr)
1563 throws SAXException {
1564 if (attr == null) {
1565 return null;
1566 }
1567
1568 AttributesImpl tempAttr = null;
1569
1570 // Translate all attributes from i18n:attr="name1 name2 ..."
1571 // using their values as keys.
1572 int attrIndex = attr.getIndex(I18N_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE);
1573 if (attrIndex == -1) {
1574 // Try the old namespace
1575 attrIndex = attr.getIndex(I18N_OLD_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE);
1576 }
1577
1578 if (attrIndex != -1) {
1579 StringTokenizer st = new StringTokenizer(attr.getValue(attrIndex));
1580
1581 // Make a copy which we are going to modify
1582 tempAttr = new AttributesImpl(attr);
1583 // Remove the i18n:attr attribute - we don't need it anymore
1584 tempAttr.removeAttribute(attrIndex);
1585
1586 // Iterate through listed attributes and translate them
1587 while (st.hasMoreElements()) {
1588 final String name = st.nextToken();
1589
1590 int index = tempAttr.getIndex(name);
1591 if (index == -1) {
1592 getLogger().warn("Attribute " +
1593 name + " not found in element <" + element + ">");
1594 continue;
1595 }
1596
1597 String value = translateAttribute(element, name, tempAttr.getValue(index));
1598 if (value != null) {
1599 // Set the translated value. If null, do nothing.
1600 tempAttr.setValue(index, value);
1601 }
1602 }
1603
1604 attr = tempAttr;
1605 }
1606
1607 // Translate all attributes from i18n:expr="name1 name2 ..."
1608 // using their values as keys.
1609 attrIndex = attr.getIndex(I18N_NAMESPACE_URI, I18N_EXPR_ATTRIBUTE);
1610 if (attrIndex != -1) {
1611 StringTokenizer st = new StringTokenizer(attr.getValue(attrIndex));
1612
1613 if (tempAttr == null) {
1614 tempAttr = new AttributesImpl(attr);
1615 }
1616 tempAttr.removeAttribute(attrIndex);
1617
1618 // Iterate through listed attributes and evaluate them
1619 while (st.hasMoreElements()) {
1620 final String name = st.nextToken();
1621
1622 int index = tempAttr.getIndex(name);
1623 if (index == -1) {
1624 getLogger().warn("Attribute " +
1625 name + " not found in element <" + element + ">");
1626 continue;
1627 }
1628
1629 final StringBuffer translated = new StringBuffer();
1630
1631 // Evaluate {..} expression
1632 VariableExpressionTokenizer.TokenReciever tr = new VariableExpressionTokenizer.TokenReciever () {
1633 private String catalogueName;
1634
1635 public void addToken(int type, String value) {
1636 if (type == MODULE) {
1637 this.catalogueName = value;
1638 } else if (type == VARIABLE) {
1639 translated.append(translateAttribute(element, name, value));
1640 } else if (type == TEXT) {
1641 if (this.catalogueName != null) {
1642 translated.append(translateAttribute(element,
1643 name,
1644 this.catalogueName + ":" + value));
1645 this.catalogueName = null;
1646 } else if (value != null) {
1647 translated.append(value);
1648 }
1649 }
1650 }
1651 };
1652
1653 try {
1654 VariableExpressionTokenizer.tokenize(tempAttr.getValue(index), tr);
1655 } catch (PatternException e) {
1656 throw new SAXException(e);
1657 }
1658
1659 // Set the translated value.
1660 tempAttr.setValue(index, translated.toString());
1661 }
1662
1663 attr = tempAttr;
1664 }
1665
1666 // nothing to translate, just return
1667 return attr;
1668 }
1669
1670 /**
1671 * Translate attribute value.
1672 * Value can be prefixed with catalogue ID and semicolon.
1673 * @return Translated text, untranslated text, or null.
1674 */
1675 private String translateAttribute(String element, String name, String key) {
1676 // Check if the key contains a colon, if so the text before
1677 // the colon denotes a catalogue ID.
1678 int colonPos = key.indexOf(":");
1679 String catalogueID = null;
1680 if (colonPos != -1) {
1681 catalogueID = key.substring(0, colonPos);
1682 key = key.substring(colonPos + 1, key.length());
1683 }
1684
1685 final SaxBuffer text = getMessage(catalogueID, key);
1686 if (text == null) {
1687 getLogger().warn("Translation not found for attribute " +
1688 name + " in element <" + element + ">");
1689 return untranslated;
1690 }
1691 return text.toString();
1692 }
1693
1694 private void endTextElement() throws SAXException {
1695 switch (prev_state) {
1696 case STATE_OUTSIDE:
1697 if (tr_text_recorder == null) {
1698 if (currentKey == null) {
1699 // Use the text as key. Not recommended for large strings,
1700 // especially if they include markup.
1701 tr_text_recorder = getMessage(text_recorder.toString(), text_recorder);
1702 } else {
1703 // We have the key, but couldn't find a translation
1704 if (getLogger().isDebugEnabled()) {
1705 getLogger().debug("Translation not found for key '" + currentKey + "'");
1706 }
1707
1708 // Use the untranslated-text only when the content of the i18n:text
1709 // element was empty
1710 if (text_recorder.isEmpty() && untranslatedRecorder != null) {
1711 tr_text_recorder = untranslatedRecorder;
1712 } else {
1713 tr_text_recorder = text_recorder;
1714 }
1715 }
1716 }
1717
1718 if (tr_text_recorder != null) {
1719 tr_text_recorder.toSAX(this.contentHandler);
1720 }
1721
1722 text_recorder.recycle();
1723 tr_text_recorder = null;
1724 currentKey = null;
1725 currentCatalogueId = null;
1726 break;
1727
1728 case STATE_INSIDE_TRANSLATE:
1729 if (tr_text_recorder == null) {
1730 if (!text_recorder.isEmpty()) {
1731 tr_text_recorder = getMessage(text_recorder.toString(), text_recorder);
1732 if (tr_text_recorder == text_recorder) {
1733 // If the default value was returned, make a copy
1734 tr_text_recorder = new ParamSaxBuffer(text_recorder);
1735 }
1736 }
1737 }
1738
1739 text_recorder.recycle();
1740 break;
1741
1742 case STATE_INSIDE_PARAM:
1743 // We send the translated text to the param recorder, after trying to translate it.
1744 // Remember you can't give a key when inside a param, that'll be nonsense!
1745 // No need to clone. We just send the events.
1746 if (!text_recorder.isEmpty()) {
1747 getMessage(text_recorder.toString(), text_recorder).toSAX(param_recorder);
1748 text_recorder.recycle();
1749 }
1750 break;
1751 }
1752
1753 current_state = prev_state;
1754 prev_state = STATE_OUTSIDE;
1755 }
1756
1757 // Process substitution parameter
1758 private void endParamElement() throws SAXException {
1759 String paramType = (String)formattingParams.get(I18N_TYPE_ATTRIBUTE);
1760 if (paramType != null) {
1761 // We have a typed parameter
1762
1763 if (getLogger().isDebugEnabled()) {
1764 getLogger().debug("Param type: " + paramType);
1765 }
1766 if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null && param_value != null) {
1767 if (getLogger().isDebugEnabled()) {
1768 getLogger().debug("Put param value: " + param_value);
1769 }
1770 formattingParams.put(I18N_VALUE_ATTRIBUTE, param_value);
1771 }
1772
1773 // Check if we have a date or a number parameter
1774 if (dateTypes.contains(paramType)) {
1775 if (getLogger().isDebugEnabled()) {
1776 getLogger().debug("Formatting date_time param: " + formattingParams);
1777 }
1778 param_value = formatDate_Time(formattingParams);
1779 } else if (numberTypes.contains(paramType)) {
1780 if (getLogger().isDebugEnabled()) {
1781 getLogger().debug("Formatting number param: " + formattingParams);
1782 }
1783 param_value = formatNumber(formattingParams);
1784 }
1785 if (getLogger().isDebugEnabled()) {
1786 getLogger().debug("Added substitution param: " + param_value);
1787 }
1788 }
1789
1790 param_value = null;
1791 current_state = STATE_INSIDE_TRANSLATE;
1792
1793 if(param_recorder == null) {
1794 return;
1795 }
1796
1797 indexedParams.put(param_name, param_recorder);
1798 param_recorder = null;
1799 }
1800
1801 private void endTranslateElement() throws SAXException {
1802 if (tr_text_recorder != null) {
1803 if (getLogger().isDebugEnabled()) {
1804 getLogger().debug("End of translate with params. " +
1805 "Fragment for substitution : " + tr_text_recorder);
1806 }
1807 tr_text_recorder.toSAX(super.contentHandler, indexedParams);
1808 tr_text_recorder = null;
1809 text_recorder.recycle();
1810 }
1811
1812 indexedParams.clear();
1813 param_count = 0;
1814 current_state = STATE_OUTSIDE;
1815 }
1816
1817 private void endChooseElement() {
1818 current_state = STATE_OUTSIDE;
1819 }
1820
1821 private void endWhenElement() {
1822 current_state = prev_state;
1823 if (translate_copy) {
1824 translate_copy = false;
1825 translate_end = true;
1826 }
1827 }
1828
1829 private void endDate_TimeElement() throws SAXException {
1830 String result = formatDate_Time(formattingParams);
1831 switch(prev_state) {
1832 case STATE_OUTSIDE:
1833 super.contentHandler.characters(result.toCharArray(), 0,
1834 result.length());
1835 break;
1836 case STATE_INSIDE_PARAM:
1837 param_recorder.characters(result.toCharArray(), 0, result.length());
1838 break;
1839 case STATE_INSIDE_TEXT:
1840 text_recorder.characters(result.toCharArray(), 0, result.length());
1841 break;
1842 }
1843 current_state = prev_state;
1844 }
1845
1846 // Helper method: creates Locale object from a string value in a map
1847 private Locale getLocale(Map params, String attribute) {
1848 // the specific locale value
1849 String lc = (String)params.get(attribute);
1850 return I18nUtils.parseLocale(lc, this.locale);
1851 }
1852
1853 private String formatDate_Time(Map params) throws SAXException {
1854 // Check that we have not null params
1855 if (params == null) {
1856 throw new IllegalArgumentException("Nothing to format");
1857 }
1858
1859 // Formatters
1860 SimpleDateFormat to_fmt;
1861 SimpleDateFormat from_fmt;
1862
1863 // Date formatting styles
1864 int srcStyle = DateFormat.DEFAULT;
1865 int style = DateFormat.DEFAULT;
1866
1867 // Date formatting patterns
1868 boolean realPattern = false;
1869 boolean realSrcPattern = false;
1870
1871 // From locale
1872 Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE);
1873 // To locale
1874 Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE);
1875
1876 // From pattern
1877 String srcPattern = (String)params.get(I18N_SRC_PATTERN_ATTRIBUTE);
1878 // To pattern
1879 String pattern = (String)params.get(I18N_PATTERN_ATTRIBUTE);
1880 // The date value
1881 String value = (String)params.get(I18N_VALUE_ATTRIBUTE);
1882
1883 // A src-pattern attribute is present
1884 if (srcPattern != null) {
1885 // Check if we have a real pattern
1886 Integer patternValue = (Integer)datePatterns.get(srcPattern.toUpperCase());
1887 if (patternValue != null) {
1888 srcStyle = patternValue.intValue();
1889 } else {
1890 realSrcPattern = true;
1891 }
1892 }
1893
1894 // A pattern attribute is present
1895 if (pattern != null) {
1896 Integer patternValue = (Integer)datePatterns.get(pattern.toUpperCase());
1897 if (patternValue != null) {
1898 style = patternValue.intValue();
1899 } else {
1900 realPattern = true;
1901 }
1902 }
1903
1904 // If we are inside of a typed param
1905 String paramType = (String)formattingParams.get(I18N_TYPE_ATTRIBUTE);
1906
1907 // Initializing date formatters
1908 if (current_state == STATE_INSIDE_DATE ||
1909 I18N_DATE_ELEMENT.equals(paramType)) {
1910
1911 to_fmt = (SimpleDateFormat)DateFormat.getDateInstance(style, loc);
1912 from_fmt = (SimpleDateFormat)DateFormat.getDateInstance(
1913 srcStyle,
1914 srcLoc
1915 );
1916 } else if (current_state == STATE_INSIDE_DATE_TIME ||
1917 I18N_DATE_TIME_ELEMENT.equals(paramType)) {
1918 to_fmt = (SimpleDateFormat)DateFormat.getDateTimeInstance(
1919 style,
1920 style,
1921 loc
1922 );
1923 from_fmt = (SimpleDateFormat)DateFormat.getDateTimeInstance(
1924 srcStyle,
1925 srcStyle,
1926 srcLoc
1927 );
1928 } else {
1929 // STATE_INSIDE_TIME or param type='time'
1930 to_fmt = (SimpleDateFormat)DateFormat.getTimeInstance(style, loc);
1931 from_fmt = (SimpleDateFormat)DateFormat.getTimeInstance(
1932 srcStyle,
1933 srcLoc
1934 );
1935 }
1936
1937 // parsed date object
1938 Date dateValue;
1939
1940 // pattern overwrites locale format
1941 if (realSrcPattern) {
1942 from_fmt.applyPattern(srcPattern);
1943 }
1944
1945 if (realPattern) {
1946 to_fmt.applyPattern(pattern);
1947 }
1948
1949 // get current date and time by default
1950 if (value == null) {
1951 dateValue = new Date();
1952 } else {
1953 try {
1954 dateValue = from_fmt.parse(value);
1955 } catch (ParseException pe) {
1956 throw new SAXException(
1957 this.getClass().getName()
1958 + "i18n:date - parsing error.", pe
1959 );
1960 }
1961 }
1962
1963 // we have all necessary data here: do formatting.
1964 if (getLogger().isDebugEnabled()) {
1965 getLogger().debug("### Formatting date: " + dateValue + " with localized pattern " +
1966 to_fmt.toLocalizedPattern() + " for locale: " + locale);
1967 }
1968 return to_fmt.format(dateValue);
1969 }
1970
1971 private void endNumberElement() throws SAXException {
1972 String result = formatNumber(formattingParams);
1973 switch(prev_state) {
1974 case STATE_OUTSIDE:
1975 super.contentHandler.characters(result.toCharArray(), 0, result.length());
1976 break;
1977 case STATE_INSIDE_PARAM:
1978 param_recorder.characters(result.toCharArray(), 0, result.length());
1979 break;
1980 case STATE_INSIDE_TEXT:
1981 text_recorder.characters(result.toCharArray(), 0, result.length());
1982 break;
1983 }
1984 current_state = prev_state;
1985 }
1986
1987 private String formatNumber(Map params) throws SAXException {
1988 if (params == null) {
1989 throw new SAXException(
1990 this.getClass().getName()
1991 + ": i18n:number - error in element attributes."
1992 );
1993 }
1994
1995 // from pattern
1996 String srcPattern = (String)params.get(I18N_SRC_PATTERN_ATTRIBUTE);
1997 // to pattern
1998 String pattern = (String)params.get(I18N_PATTERN_ATTRIBUTE);
1999 // the number value
2000 String value = (String)params.get(I18N_VALUE_ATTRIBUTE);
2001
2002 if (value == null) return "";
2003 // type
2004 String type = (String)params.get(I18N_TYPE_ATTRIBUTE);
2005
2006 // fraction-digits
2007 int fractionDigits = -1;
2008 try {
2009 String fd = (String)params.get(I18N_FRACTION_DIGITS_ATTRIBUTE);
2010 if (fd != null)
2011 fractionDigits = Integer.parseInt(fd);
2012 } catch (NumberFormatException nfe) {
2013 getLogger().warn("Error in number format with fraction-digits", nfe);
2014 }
2015
2016 // parsed number
2017 Number numberValue;
2018
2019 // locale, may be switched locale
2020 Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE);
2021 Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE);
2022 // currency locale
2023 Locale currencyLoc = getLocale(params, CURRENCY_LOCALE_ATTRIBUTE);
2024 // decimal and grouping locale
2025 Locale dgLoc = null;
2026 if (currencyLoc != null) {
2027 // the reasoning here is: if there is a currency locale, then start from that
2028 // one but take certain properties (like decimal and grouping seperation symbols)
2029 // from the default locale (this happens further on).
2030 dgLoc = loc;
2031 loc = currencyLoc;
2032 }
2033
2034 // src format
2035 DecimalFormat from_fmt = (DecimalFormat)NumberFormat.getInstance(srcLoc);
2036 int int_currency = 0;
2037
2038 // src-pattern overwrites locale format
2039 if (srcPattern != null) {
2040 from_fmt.applyPattern(srcPattern);
2041 }
2042
2043 // to format
2044 DecimalFormat to_fmt;
2045 char dec = from_fmt.getDecimalFormatSymbols().getDecimalSeparator();
2046 int decAt = 0;
2047 boolean appendDec = false;
2048
2049 if (type == null || type.equals( I18N_NUMBER_ELEMENT )) {
2050 to_fmt = (DecimalFormat)NumberFormat.getInstance(loc);
2051 to_fmt.setMaximumFractionDigits(309);
2052 for (int i = value.length() - 1;
2053 i >= 0 && value.charAt(i) != dec; i--, decAt++) {
2054 }
2055
2056 if (decAt < value.length())to_fmt.setMinimumFractionDigits(decAt);
2057 decAt = 0;
2058 for (int i = 0; i < value.length() && value.charAt(i) != dec; i++) {
2059 if (Character.isDigit(value.charAt(i))) {
2060 decAt++;
2061 }
2062 }
2063
2064 to_fmt.setMinimumIntegerDigits(decAt);
2065 if (value.charAt(value.length() - 1) == dec) {
2066 appendDec = true;
2067 }
2068 } else if (type.equals( I18N_CURRENCY_ELEMENT )) {
2069 to_fmt = (DecimalFormat)NumberFormat.getCurrencyInstance(loc);
2070 } else if (type.equals( I18N_INT_CURRENCY_ELEMENT )) {
2071 to_fmt = (DecimalFormat)NumberFormat.getCurrencyInstance(loc);
2072 int_currency = 1;
2073 for (int i = 0; i < to_fmt.getMaximumFractionDigits(); i++) {
2074 int_currency *= 10;
2075 }
2076 } else if ( type.equals( I18N_CURRENCY_NO_UNIT_ELEMENT ) ) {
2077 DecimalFormat tmp = (DecimalFormat) NumberFormat.getCurrencyInstance( loc );
2078 to_fmt = (DecimalFormat) NumberFormat.getInstance( loc );
2079 to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits());
2080 to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits());
2081 } else if ( type.equals( I18N_INT_CURRENCY_NO_UNIT_ELEMENT ) ) {
2082 DecimalFormat tmp = (DecimalFormat) NumberFormat.getCurrencyInstance( loc );
2083 int_currency = 1;
2084 for ( int i = 0; i < tmp.getMaximumFractionDigits(); i++ )
2085 int_currency *= 10;
2086 to_fmt = (DecimalFormat) NumberFormat.getInstance( loc );
2087 to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits());
2088 to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits());
2089 } else if (type.equals( I18N_PERCENT_ELEMENT )) {
2090 to_fmt = (DecimalFormat)NumberFormat.getPercentInstance(loc);
2091 } else {
2092 throw new SAXException("<i18n:number>: unknown type: " + type);
2093 }
2094
2095 if(fractionDigits > -1) {
2096 to_fmt.setMinimumFractionDigits(fractionDigits);
2097 to_fmt.setMaximumFractionDigits(fractionDigits);
2098 }
2099
2100 if(dgLoc != null) {
2101 DecimalFormat df = (DecimalFormat)NumberFormat.getCurrencyInstance(dgLoc);
2102 DecimalFormatSymbols dfsNew = df.getDecimalFormatSymbols();
2103 DecimalFormatSymbols dfsOrig = to_fmt.getDecimalFormatSymbols();
2104 dfsOrig.setDecimalSeparator(dfsNew.getDecimalSeparator());
2105 dfsOrig.setMonetaryDecimalSeparator(dfsNew.getMonetaryDecimalSeparator());
2106 dfsOrig.setGroupingSeparator(dfsNew.getGroupingSeparator());
2107 to_fmt.setDecimalFormatSymbols(dfsOrig);
2108 }
2109
2110 // pattern overwrites locale format
2111 if (pattern != null) {
2112 to_fmt.applyPattern(pattern);
2113 }
2114
2115 try {
2116 numberValue = from_fmt.parse(value);
2117 if (int_currency > 0) {
2118 numberValue = new Double(numberValue.doubleValue() / int_currency);
2119 } else {
2120 // what?
2121 }
2122 } catch (ParseException pe) {
2123 throw new SAXException(this.getClass().getName() + "i18n:number - parsing error.", pe);
2124 }
2125
2126 // we have all necessary data here: do formatting.
2127 String result = to_fmt.format(numberValue);
2128 if (appendDec) result = result + dec;
2129 if (getLogger().isDebugEnabled()) {
2130 getLogger().debug("i18n:number result: " + result);
2131 }
2132 return result;
2133 }
2134
2135 //-- Dictionary handling routines
2136
2137 /**
2138 * Helper method to retrieve a message from the dictionary.
2139 *
2140 * @param catalogueID if not null, this catalogue will be used instead of the default one.
2141 * @return SaxBuffer containing message, or null if not found.
2142 */
2143 protected ParamSaxBuffer getMessage(String catalogueID, String key) {
2144 if (getLogger().isDebugEnabled()) {
2145 getLogger().debug("Getting key " + key + " from catalogue " + catalogueID);
2146 }
2147
2148 CatalogueInfo catalogue = this.catalogue;
2149 if (catalogueID != null) {
2150 catalogue = (CatalogueInfo)catalogues.get(catalogueID);
2151 if (catalogue == null) {
2152 if (getLogger().isWarnEnabled()) {
2153 getLogger().warn("Catalogue not found: " + catalogueID +
2154 ", will not translate key " + key);
2155 }
2156 return null;
2157 }
2158 }
2159
2160 Bundle bundle = catalogue.getCatalogue();
2161 if (bundle == null) {
2162 // Can't translate
2163 getLogger().debug("Untranslated key: '" + key + "'");
2164 return null;
2165 }
2166
2167 try {
2168 return (ParamSaxBuffer) bundle.getObject(key);
2169 } catch (MissingResourceException e) {
2170 getLogger().debug("Untranslated key: '" + key + "'");
2171 }
2172
2173 return null;
2174 }
2175
2176 /**
2177 * Helper method to retrieve a message from the current dictionary.
2178 * A default value is returned if message is not found.
2179 *
2180 * @return SaxBuffer containing message, or defaultValue if not found.
2181 */
2182 private ParamSaxBuffer getMessage(String key, ParamSaxBuffer defaultValue) {
2183 SaxBuffer value = getMessage(currentCatalogueId, key);
2184 if (value == null) {
2185 return defaultValue;
2186 }
2187
2188 return new ParamSaxBuffer(value);
2189 }
2190
2191 public void recycle() {
2192 this.untranslatedRecorder = null;
2193 this.catalogue = null;
2194 this.objectModel = null;
2195
2196 // Release catalogues which were selected for current locale
2197 Iterator i = catalogues.values().iterator();
2198 while (i.hasNext()) {
2199 CatalogueInfo catalogueInfo = (CatalogueInfo) i.next();
2200 catalogueInfo.releaseCatalog();
2201 }
2202
2203 super.recycle();
2204 }
2205
2206 public void dispose() {
2207 if (manager != null) {
2208 manager.release(factory);
2209 }
2210 factory = null;
2211 manager = null;
2212 catalogues = null;
2213 }
2214
2215
2216 /**
2217 * Holds information about one catalogue. The location and name of the catalogue
2218 * can contain references to input modules, and are resolved upon each transformer
2219 * usage. It is important that releaseCatalog is called when the transformer is recycled.
2220 */
2221 public final class CatalogueInfo {
2222 VariableResolver name;
2223 VariableResolver[] locations;
2224 String resolvedName;
2225 String[] resolvedLocations;
2226 Bundle catalogue;
2227
2228 public CatalogueInfo(String name, String[] locations) throws PatternException {
2229 this.name = VariableResolverFactory.getResolver(name, manager);
2230 this.locations = new VariableResolver[locations.length];
2231 for (int i=0; i < locations.length; ++i) {
2232 this.locations[i] = VariableResolverFactory.getResolver(locations[i], manager);
2233 }
2234 }
2235
2236 public String getName() {
2237 try {
2238 if (resolvedName == null) {
2239 resolve();
2240 }
2241 } catch (Exception e) {
2242 // Ignore the error for now
2243 }
2244 return resolvedName;
2245 }
2246
2247 public String[] getLocation() {
2248 try {
2249 if (resolvedName == null) {
2250 resolve();
2251 }
2252 } catch (Exception e) {
2253 // Ignore the error for now
2254 }
2255 return resolvedLocations;
2256 }
2257
2258 private void resolve() throws Exception {
2259 if (resolvedLocations == null) {
2260 resolvedLocations = new String[locations.length];
2261 for (int i=0; i < resolvedLocations.length; ++i) {
2262 resolvedLocations[i] = locations[i].resolve(null, objectModel);
2263 }
2264 }
2265 if (resolvedName == null) {
2266 resolvedName = name.resolve(null, objectModel);
2267 }
2268 }
2269
2270 public Bundle getCatalogue() {
2271 if (catalogue == null) {
2272 try {
2273 resolve();
2274 catalogue = factory.select(resolvedLocations, resolvedName, locale);
2275 } catch (Exception e) {
2276 getLogger().error("Error obtaining catalogue '" + getName() +
2277 "' from <" + getLocation() + "> for locale " +
2278 locale, e);
2279 }
2280 }
2281
2282 return catalogue;
2283 }
2284
2285 public void releaseCatalog() {
2286 if (catalogue != null) {
2287 factory.release(catalogue);
2288 }
2289 catalogue = null;
2290 resolvedName = null;
2291 resolvedLocations = null;
2292 }
2293 }
2294 }