Save This Page
Home » cocoon-2.1.11-src » org.apache » cocoon » transformation » [javadoc | source]
    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    * &lt;?xml version="1.0"?&gt;
   97    * &lt;!-- message catalogue file for locale ... --&gt;
   98    * &lt;catalogue xml:lang=&quot;locale&quot;&gt;
   99    *   &lt;message key="key"&gt;text &lt;i&gt;or&lt;/i&gt; markup&lt;/message&gt;
  100    *   ....
  101    * &lt;/catalogue&gt;
  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    * &lt;?xml version="1.0"?&gt;
  110    * ... some text, translate &lt;i18n:text&gt;key&lt;/i18n:text&gt;
  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>&lt;i18n:text&gt;</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    * &lt;map:transformer name="i18n"
  162    *     src="org.apache.cocoon.transformation.I18nTransformer"&gt;
  163    *
  164    *     &lt;catalogues default="someId"&gt;
  165    *       &lt;catalogue id="someId" name="messages" [location="translations"]&gt;
  166    *         [&lt;location&gt;translations/client&lt;/location&gt;]
  167    *         [&lt;location&gt;translations&lt;/location&gt;]
  168    *       &lt;/catalogue&gt;
  169    *       ...
  170    *     &lt;/catalogues&gt;
  171    *     &lt;untranslated-text&gt;untranslated&lt;/untranslated-text&gt;
  172    *     &lt;preload&gt;en_US&lt;/preload&gt;
  173    *     &lt;preload catalogue="someId"&gt;fr_CA&lt;/preload&gt;
  174    * &lt;/map:transformer&gt;
  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    * &lt;map:match pattern="file"&gt;
  208    *   &lt;map:generate src="file.xml"/&gt;
  209    *   &lt;map:transform type="i18n"&gt;
  210    *     &lt;map:parameter name="locale" value="..."/&gt;
  211    *   &lt;/map:transform&gt;
  212    *   &lt;map:serialize/&gt;
  213    * &lt;/map:match&gt;
  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>&lt;i18n:date/&gt;</strong> gives localized date.</li>
  231    *  <li><strong>&lt;i18n:date-time/&gt;</strong> gives localized date and time.</li>
  232    *  <li><strong>&lt;i18n:time/&gt;</strong> gives localized time.</li>
  233    *  <li><strong>&lt;i18n:number/&gt;</strong> gives localized number.</li>
  234    *  <li><strong>&lt;i18n:currency/&gt;</strong> gives localized currency.</li>
  235    *  <li><strong>&lt;i18n:percent/&gt;</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    * &lt;i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
  253    *   12/24/01
  254    * &lt;/i18n:date&gt;
  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 &lt;get-locale/&gt; 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        *   &lt;i18n:text&gt;
  297        *     This is &lt;strong&gt;translated&lt;/strong&gt; string.
  298        *   &lt;/i18n:text&gt;
  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        * &lt;i18n:translate&gt;
  308        *   &lt;i18n:text&gt;This is translated string with {0} param&lt;/i18n:text&gt;
  309        *   &lt;i18n:param&gt;1&lt;/i18n:param&gt;
  310        * &lt;/i18n:translate&gt;
  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 &lt;i18n:choose&gt;.</em></p>
  329        * <pre>
  330        * &lt;i18n:choose&gt;
  331        *   &lt;i18n:when locale="en"&gt;
  332        *     Good Morning
  333        *   &lt;/en&gt;
  334        *   &lt;i18n:when locale="fr"&gt;
  335        *     Bonjour
  336        *   &lt;/jp&gt;
  337        *   &lt;i18n:when locale="jp"&gt;
  338        *     Aligato?
  339        *   &lt;/jp&gt;
  340        *   &lt;i18n:otherwise&gt;
  341        *     Sorry, i don't know how to say hello in your language
  342        *   &lt;/jp&gt;
  343        * &lt;i18n:translate&gt;
  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        * &lt;greeting&gt;
  361        *   &lt;i18n:when locale="en"&gt;Hello&lt;/i18n:when&gt;
  362        *   &lt;i18n:when locale="fr"&gt;Bonjour&lt;/i18n:when&gt;
  363        * &lt;/greeting&gt;
  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        * &lt;greeting&gt;
  376        *   &lt;i18n:if locale="en"&gt;Hello&lt;/i18n:when&gt;
  377        *   &lt;i18n:if locale="fr"&gt;Bonjour&lt;/i18n:when&gt;
  378        * &lt;/greeting&gt;
  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        *  &lt;i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
  446        *    12/24/01
  447        *  &lt;/i18n:date&gt;
  448        *
  449        *  &lt;i18n:date pattern="dd/MM/yyyy" /&gt;
  450        * </pre>
  451        *
  452        * If no value is specified then the current date will be used. E.g.:
  453        * <pre>
  454        *   &lt;i18n:date /&gt;
  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        *  &lt;i18n:date-time src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
  472        *    12/24/01 1:00 AM
  473        *  &lt;/i18n:date&gt;
  474        *
  475        *  &lt;i18n:date-time pattern="dd/MM/yyyy hh:mm" /&gt;
  476        * </pre>
  477        *
  478        * If no value is specified then the current date and time will be used.
  479        * E.g.:
  480        * <pre>
  481        *  &lt;i18n:date-time /&gt;
  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        *  &lt;i18n:time src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
  499        *    1:00 AM
  500        *  &lt;/i18n:time&gt;
  501        *
  502        * &lt;i18n:time pattern="hh:mm:ss" /&gt;
  503        * </pre>
  504        *
  505        * If no value is specified then the current time will be used. E.g.:
  506        * <pre>
  507        *  &lt;i18n:time /&gt;
  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        *  &lt;i18n:number src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
  525        *    1000.0
  526        *  &lt;/i18n:number&gt;
  527        *
  528        * &lt;i18n:number type="currency" /&gt;
  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        * &lt;i18n:text i18n:key="a_key"&gt;article_text1&lt;/i18n:text&gt;
  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        * &lt;para title="first" name="article" i18n:attr="title name"/&gt;
  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        * &lt;para title="first" name="{one} {two}" i18n:attr="name"/&gt;
  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        * &lt;i18n:param&gt;&lt;i18n:date/&gt;&lt;/i18n:param&gt;
  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("&lt;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   }