| /* |
| * Copyright 1999-2004 The Apache Software Foundation. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.cocoon.transformation; |
| |
| import org.apache.avalon.framework.activity.Disposable; |
| import org.apache.avalon.framework.configuration.Configurable; |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.caching.CacheableProcessingComponent; |
| import org.apache.cocoon.components.treeprocessor.variables.VariableExpressionTokenizer; |
| import org.apache.cocoon.components.treeprocessor.variables.VariableResolver; |
| import org.apache.cocoon.components.treeprocessor.variables.VariableResolverFactory; |
| import org.apache.cocoon.environment.SourceResolver; |
| import org.apache.cocoon.i18n.Bundle; |
| import org.apache.cocoon.i18n.BundleFactory; |
| import org.apache.cocoon.i18n.I18nUtils; |
| import org.apache.cocoon.sitemap.PatternException; |
| import org.apache.cocoon.xml.ParamSaxBuffer; |
| import org.apache.cocoon.xml.SaxBuffer; |
| |
| import org.apache.excalibur.source.SourceValidity; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.AttributesImpl; |
| |
| import java.io.IOException; |
| import java.text.DateFormat; |
| import java.text.DecimalFormat; |
| import java.text.DecimalFormatSymbols; |
| import java.text.NumberFormat; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.MissingResourceException; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| |
| /** |
| * @cocoon.sitemap.component.documentation |
| * Internationalization transformer is used to transform i18n markup into text |
| * based on a particular locale. |
| * |
| * @cocoon.sitemap.component.name i18n |
| * @cocoon.sitemap.component.documentation.caching TBD |
| * @cocoon.sitemap.component.logger sitemap.transformer.i18n |
| * |
| * <h3>i18n transformer</h3> |
| * <p>The i18n transformer works by finding a translation for the user's locale |
| * in the configured catalogues. Locale is determined based on the request, |
| * session, or a cookie data. See {@link org.apache.cocoon.acting.LocaleAction} |
| * for details.</p> |
| * |
| * <p>For the passed local it then attempts to find a message catalogue that |
| * satisifies the locale, and uses it for for processing text replacement |
| * directed by i18n markup.</p> |
| * |
| * <p>Message catalogues are maintained in separate files, with a naming |
| * convention similar to that of {@link java.util.ResourceBundle}. I.e. |
| * <code>basename_locale</code>, where <i>basename</i> can be any name, |
| * and <i>locale</i> can be any locale specified using ISO 639/3166 |
| * characters (eg. <code>en_AU</code>, <code>de_AT</code>, <code>es</code>).</p> |
| * |
| * <p><strong>NOTE:</strong> ISO 639 is not a stable standard; some of the |
| * language codes it defines (specifically, iw, ji, and in) have changed |
| * (see {@link java.util.Locale} for details). |
| * |
| * <h3>Message Catalogues</h3> |
| * <p>Catalogues are of the following format: |
| * <pre> |
| * <?xml version="1.0"?> |
| * <!-- message catalogue file for locale ... --> |
| * <catalogue xml:lang="locale"> |
| * <message key="key">text <i>or</i> markup</message> |
| * .... |
| * </catalogue> |
| * </pre> |
| * Where <code>key</code> specifies a particular message for that |
| * language. |
| * |
| * <h3>Usage</h3> |
| * <p>Files to be translated contain the following markup: |
| * <pre> |
| * <?xml version="1.0"?> |
| * ... some text, translate <i18n:text>key</i18n:text> |
| * </pre> |
| * At runtime, the i18n transformer will find a message catalogue for the |
| * user's locale, and will appropriately replace the text between the |
| * <code><i18n:text></code> markup, using the value between the tags as |
| * the lookup key.</p> |
| * |
| * <p>If the i18n transformer cannot find an appropriate message catalogue for |
| * the user's given locale, it will recursively try to locate a <i>parent</i> |
| * message catalogue, until a valid catalogue can be found. |
| * ie: |
| * <ul> |
| * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>_<i>variant</i>.xml |
| * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>.xml |
| * <li><strong>catalogue</strong>_<i>language</i>.xml |
| * <li><strong>catalogue</strong>.xml |
| * </ul> |
| * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i> |
| * (no variant), the following search will occur: |
| * <ul> |
| * <li><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml |
| * <li><strong>messages</strong>_<i>en</i>.xml |
| * <li><strong>messages</strong>.xml |
| * </ul> |
| * This allows the developer to write a hierarchy of message catalogues, |
| * at each defining messages with increasing depth of variation.</p> |
| * |
| * <p>In addition, catalogues can be split across multiple locations. For example, |
| * there can be a default catalogue in one directory with a user or client specific |
| * catalogue in another directory. The catalogues will be searched in the order of |
| * the locations specified still following the locale ordering specified above. |
| * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i> |
| * (no variant) and locations of <i>translations/client</i> and <i>translations</i>, |
| * the following search will occur: |
| * <ul> |
| * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml |
| * <li><i>translations/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml |
| * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>.xml |
| * <li><i>translations/</i><strong>messages</strong>_<i>en</i.xml |
| * <li><i>translations/client/</i><strong>messages</strong>.xml |
| * <li><i>translations/</i><strong>messages</strong>.xml |
| * </ul> |
| * </p> |
| * |
| * <p>The <code>i18n:text</code> element can optionally take an attribute |
| * <code>i18n:catalogue</code> to indicate which specific catalogue to use. |
| * The value of this attribute should be the id of the catalogue to use |
| * (see sitemap configuration). |
| * |
| * <h3>Sitemap configuration</h3> |
| * <pre> |
| * <map:transformer name="i18n" |
| * src="org.apache.cocoon.transformation.I18nTransformer"> |
| * |
| * <catalogues default="someId"> |
| * <catalogue id="someId" name="messages" [location="translations"]> |
| * [<location>translations/client</location>] |
| * [<location>translations</location>] |
| * </catalogue> |
| * ... |
| * </catalogues> |
| * <untranslated-text>untranslated</untranslated-text> |
| * <cache-at-startup>true</cache-at-startup> |
| * </map:transformer> |
| * </pre> |
| * Where: |
| * <ul> |
| * <li><strong>catalogues</strong>: container element in which the catalogues |
| * are defined. It must have an attribute 'default' whose value is one |
| * of the id's of the catalogue elements. (<i>mandatory</i>). |
| * <li><strong>catalogue</strong>: specifies a catalogue. It takes 2 required |
| * attributes: id (can be wathever you like) and name (base name of the catalogue). |
| * The location (location of the message catalogue) is also required, but can be |
| * specified either as an attribute or as one or more subelements, but not both. |
| * If more than one location is specified the catalogues will be searched in the |
| * order they appear in the configuration. The name and location can contain |
| * references to inputmodules (same syntax as in other places in the |
| * sitemap). They are resolved on each usage of the transformer, so they can |
| * refer to e.g. request parameters. (<i>at least 1 catalogue |
| * element required</i>). After input module references are resolved the location |
| * string can be the root of a URI. For example, specifying a location of |
| * cocoon:/test with a name of messages and a locale of en_GB will cause the |
| * sitemap to try to process cocoon:/test/messages_en_GB.xml, |
| * cocoon:/test/messages_en.xml and cocoon:/test/messages.xml. |
| * <li><strong>untranslated-text</strong>: text used for |
| * untranslated keys (default is to output the key name). |
| * <li><strong>cache-at-startup</strong>: flag whether to cache |
| * messages at startup (false by default). |
| * </ul> |
| * |
| * <h3>Pipeline Usage</h3> |
| * <p>To use the transformer in a pipeline, simply specify it in a particular |
| * transform, and pass locale parameter: |
| * <pre> |
| * <map:match pattern="file"> |
| * <map:generate src="file.xml"/> |
| * <map:transform type="i18n"> |
| * <map:parameter name="locale" value="..."/> |
| * </map:transform> |
| * <map:serialize/> |
| * </map:match> |
| * </pre> |
| * You can use {@link org.apache.cocoon.acting.LocaleAction} or any other |
| * way to provide transformer with a locale.</p> |
| * |
| * <p>If in certain pipeline, you want to use a different catalogue as the |
| * default catalogue, you can do so by specifying a parameter called |
| * <strong>default-catalogue-id</strong>. |
| * |
| * <p>The <strong>untranslated-text</strong> can also be overridden at the |
| * pipeline level by specifying it as a parameter.</p> |
| * |
| * |
| * <h3>i18n markup</h3> |
| * |
| * <p>For date, time and number formatting use the following tags: |
| * <ul> |
| * <li><strong><i18n:date/></strong> gives localized date.</li> |
| * <li><strong><i18n:date-time/></strong> gives localized date and time.</li> |
| * <li><strong><i18n:time/></strong> gives localized time.</li> |
| * <li><strong><i18n:number/></strong> gives localized number.</li> |
| * <li><strong><i18n:currency/></strong> gives localized currency.</li> |
| * <li><strong><i18n:percent/></strong> gives localized percent.</li> |
| * </ul> |
| * Elements <code>date</code>, <code>date-time</code> and <code>time</code> |
| * accept <code>pattern</code> and <code>src-pattern</code> attribute, with |
| * values of: |
| * <ul> |
| * <li><code>short</code> |
| * <li><code>medium</code> |
| * <li><code>long</code> |
| * <li><code>full</code> |
| * </ul> |
| * See {@link java.text.DateFormat} for more info on these values.</p> |
| * |
| * <p>Elements <code>date</code>, <code>date-time</code>, <code>time</code> and |
| * <code>number</code>, a different <code>locale</code> and |
| * <code>source-locale</code> can be specified: |
| * <pre> |
| * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"> |
| * 12/24/01 |
| * </i18n:date> |
| * </pre> |
| * Will result in 24.12.2001.</p> |
| * |
| * <p>A given real <code>pattern</code> and <code>src-pattern</code> (not |
| * keywords <code>short, medium, long, full</code>) overrides any value |
| * specified by <code>locale</code> and <code>src-locale</code> attributes.</p> |
| * |
| * <p>Future work coming: |
| * <ul> |
| * <li>Introduce new <get-locale/> element |
| * <li>Move all formatting routines to I18nUtils |
| * </ul> |
| * |
| * @version $Id$ |
| */ |
| public class I18nTransformer extends AbstractTransformer |
| implements CacheableProcessingComponent, |
| Serviceable, Configurable, Disposable { |
| |
| /** |
| * The namespace for i18n is "http://apache.org/cocoon/i18n/2.1". |
| */ |
| public static final String I18N_NAMESPACE_URI = |
| "http://apache.org/cocoon/i18n/2.1"; |
| |
| /** |
| * The old namespace for i18n is "http://apache.org/cocoon/i18n/2.0". |
| */ |
| public static final String I18N_OLD_NAMESPACE_URI = |
| "http://apache.org/cocoon/i18n/2.0"; |
| |
| // |
| // i18n elements |
| // |
| |
| /** |
| * <code>i18n:text</code> element is used to translate any text, with |
| * or without markup. Example: |
| * <pre> |
| * <i18n:text> |
| * This is <strong>translated</strong> string. |
| * </i18n:text> |
| * </pre> |
| */ |
| public static final String I18N_TEXT_ELEMENT = "text"; |
| |
| /** |
| * <code>i18n:translate</code> element is used to translate text with |
| * parameter substitution. Example: |
| * <pre> |
| * <i18n:translate> |
| * <i18n:text>This is translated string with {0} param</i18n:text> |
| * <i18n:param>1</i18n:param> |
| * </i18n:translate> |
| * </pre> |
| * The <code>i18n:text</code> fragment can include markup and parameters |
| * at any place. Also do parameters, which can include <code>i18n:text</code>, |
| * <code>i18n:date</code>, etc. elements (without keys only). |
| * |
| * @see #I18N_TEXT_ELEMENT |
| * @see #I18N_PARAM_ELEMENT |
| */ |
| public static final String I18N_TRANSLATE_ELEMENT = "translate"; |
| |
| /** |
| * <code>i18n:choose</code> element is used to translate elements in-place. |
| * The first <code>i18n:when</code> element matching the current locale |
| * is selected and the others are discarded. |
| * |
| * <p>To specify what to do if no locale matched, simply add a node with |
| * <code>locale="*"</code>. <em>Note that this element must be the last |
| * child of <i18n:choose>.</em></p> |
| * <pre> |
| * <i18n:choose> |
| * <i18n:when locale="en"> |
| * Good Morning |
| * </en> |
| * <i18n:when locale="fr"> |
| * Bonjour |
| * </jp> |
| * <i18n:when locale="jp"> |
| * Aligato? |
| * </jp> |
| * <i18n:otherwise> |
| * Sorry, i don't know how to say hello in your language |
| * </jp> |
| * <i18n:translate> |
| * </pre> |
| * <p>You can include any markup within <code>i18n:when</code> elements, |
| * with the exception of other <code>i18n:*</code> elements.</p> |
| * |
| * @see #I18N_IF_ELEMENT |
| * @see #I18N_LOCALE_ATTRIBUTE |
| * @since 2.1 |
| */ |
| public static final String I18N_CHOOSE_ELEMENT = "choose"; |
| |
| /** |
| * <code>i18n:when</code> is used to test a locale. |
| * It can be used within <code>i18:choose</code> elements or alone. |
| * <em>Note: Using <code>locale="*"</code> here has no sense.</em> |
| * Example: |
| * <pre> |
| * <greeting> |
| * <i18n:when locale="en">Hello</i18n:when> |
| * <i18n:when locale="fr">Bonjour</i18n:when> |
| * </greeting> |
| * </pre> |
| * |
| * @see #I18N_LOCALE_ATTRIBUTE |
| * @see #I18N_CHOOSE_ELEMENT |
| * @since 2.1 |
| */ |
| public static final String I18N_WHEN_ELEMENT = "when"; |
| |
| /** |
| * <code>i18n:if</code> is used to test a locale. Example: |
| * <pre> |
| * <greeting> |
| * <i18n:if locale="en">Hello</i18n:when> |
| * <i18n:if locale="fr">Bonjour</i18n:when> |
| * </greeting> |
| * </pre> |
| * |
| * @see #I18N_LOCALE_ATTRIBUTE |
| * @see #I18N_CHOOSE_ELEMENT |
| * @see #I18N_WHEN_ELEMENT |
| * @since 2.1 |
| */ |
| public static final String I18N_IF_ELEMENT = "if"; |
| |
| /** |
| * <code>i18n:otherwise</code> is used to match any locale when |
| * no matching locale has been found inside an <code>i18n:choose</code> |
| * block. |
| * |
| * @see #I18N_CHOOSE_ELEMENT |
| * @see #I18N_WHEN_ELEMENT |
| * @since 2.1 |
| */ |
| public static final String I18N_OTHERWISE_ELEMENT = "otherwise"; |
| |
| /** |
| * <code>i18n:param</code> is used with i18n:translate to provide |
| * substitution params. The param can have <code>i18n:text</code> as |
| * its value to provide multilungual value. Parameters can have |
| * additional attributes to be used for formatting: |
| * <ul> |
| * <li><code>type</code>: can be <code>date, date-time, time, |
| * number, currency, currency-no-unit or percent</code>. |
| * Used to format params before substitution.</li> |
| * <li><code>value</code>: the value of the param. If no value is |
| * specified then the text inside of the param element will be used.</li> |
| * <li><code>locale</code>: used only with <code>number, date, time, |
| * date-time</code> types and used to override the current locale to |
| * format the given value.</li> |
| * <li><code>src-locale</code>: used with <code>number, date, time, |
| * date-time</code> types and specify the locale that should be used to |
| * parse the given value.</li> |
| * <li><code>pattern</code>: used with <code>number, date, time, |
| * date-time</code> types and specify the pattern that should be used |
| * to format the given value.</li> |
| * <li><code>src-pattern</code>: used with <code>number, date, time, |
| * date-time</code> types and specify the pattern that should be used |
| * to parse the given value.</li> |
| * </ul> |
| * |
| * @see #I18N_TRANSLATE_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_PARAM_ELEMENT = "param"; |
| |
| /** |
| * This attribute affects a name to the param that could be used |
| * for substitution. |
| * |
| * @since 2.1 |
| */ |
| public static final String I18N_PARAM_NAME_ATTRIBUTE = "name"; |
| |
| /** |
| * <code>i18n:date</code> is used to provide a localized date string. |
| * Allowed attributes are: <code>pattern, src-pattern, locale, |
| * src-locale</code>. Usage examples: |
| * <pre> |
| * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"> |
| * 12/24/01 |
| * </i18n:date> |
| * |
| * <i18n:date pattern="dd/MM/yyyy" /> |
| * </pre> |
| * |
| * If no value is specified then the current date will be used. E.g.: |
| * <pre> |
| * <i18n:date /> |
| * </pre> |
| * Displays the current date formatted with default pattern for |
| * the current locale. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_DATE_ELEMENT = "date"; |
| |
| /** |
| * <code>i18n:date-time</code> is used to provide a localized date and |
| * time string. Allowed attributes are: <code>pattern, src-pattern, |
| * locale, src-locale</code>. Usage examples: |
| * <pre> |
| * <i18n:date-time src-pattern="short" src-locale="en_US" locale="de_DE"> |
| * 12/24/01 1:00 AM |
| * </i18n:date> |
| * |
| * <i18n:date-time pattern="dd/MM/yyyy hh:mm" /> |
| * </pre> |
| * |
| * If no value is specified then the current date and time will be used. |
| * E.g.: |
| * <pre> |
| * <i18n:date-time /> |
| * </pre> |
| * Displays the current date formatted with default pattern for |
| * the current locale. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_DATE_TIME_ELEMENT = "date-time"; |
| |
| /** |
| * <code>i18n:time</code> is used to provide a localized time string. |
| * Allowed attributes are: <code>pattern, src-pattern, locale, |
| * src-locale</code>. Usage examples: |
| * <pre> |
| * <i18n:time src-pattern="short" src-locale="en_US" locale="de_DE"> |
| * 1:00 AM |
| * </i18n:time> |
| * |
| * <i18n:time pattern="hh:mm:ss" /> |
| * </pre> |
| * |
| * If no value is specified then the current time will be used. E.g.: |
| * <pre> |
| * <i18n:time /> |
| * </pre> |
| * Displays the current time formatted with default pattern for |
| * the current locale. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_TIME_ELEMENT = "time"; |
| |
| /** |
| * <code>i18n:number</code> is used to provide a localized number string. |
| * Allowed attributes are: <code>pattern, src-pattern, locale, src-locale, |
| * type</code>. Usage examples: |
| * <pre> |
| * <i18n:number src-pattern="short" src-locale="en_US" locale="de_DE"> |
| * 1000.0 |
| * </i18n:number> |
| * |
| * <i18n:number type="currency" /> |
| * </pre> |
| * |
| * If no value is specifies then 0 will be used. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| */ |
| public static final String I18N_NUMBER_ELEMENT = "number"; |
| |
| /** |
| * Currency element name |
| */ |
| public static final String I18N_CURRENCY_ELEMENT = "currency"; |
| |
| /** |
| * Percent element name |
| */ |
| public static final String I18N_PERCENT_ELEMENT = "percent"; |
| |
| /** |
| * Integer currency element name |
| */ |
| public static final String I18N_INT_CURRENCY_ELEMENT = "int-currency"; |
| |
| /** |
| * Currency without unit element name |
| */ |
| public static final String I18N_CURRENCY_NO_UNIT_ELEMENT = "currency-no-unit"; |
| |
| /** |
| * Integer currency without unit element name |
| */ |
| public static final String I18N_INT_CURRENCY_NO_UNIT_ELEMENT = "int-currency-no-unit"; |
| |
| // |
| // i18n general attributes |
| // |
| |
| /** |
| * This attribute is used with i18n:text element to indicate the key of |
| * the according message. The character data of the element will be used |
| * if no message is found by this key. E.g.: |
| * <pre> |
| * <i18n:text i18n:key="a_key">article_text1</i18n:text> |
| * </pre> |
| */ |
| public static final String I18N_KEY_ATTRIBUTE = "key"; |
| |
| /** |
| * This attribute is used with <strong>any</strong> element (even not i18n) |
| * to translate attribute values. Should contain whitespace separated |
| * attribute names that should be translated: |
| * <pre> |
| * <para title="first" name="article" i18n:attr="title name"/> |
| * </pre> |
| * Attribute value considered as key in message catalogue. |
| */ |
| public static final String I18N_ATTR_ATTRIBUTE = "attr"; |
| |
| /** |
| * This attribute is used with <strong>any</strong> element (even not i18n) |
| * to evaluate attribute values. Should contain whitespace separated |
| * attribute names that should be evaluated: |
| * <pre> |
| * <para title="first" name="{one} {two}" i18n:attr="name"/> |
| * </pre> |
| * Attribute value considered as expression containing text and catalogue |
| * keys in curly braces. |
| */ |
| public static final String I18N_EXPR_ATTRIBUTE = "expr"; |
| |
| // |
| // i18n number and date formatting attributes |
| // |
| |
| /** |
| * This attribute is used with date and number formatting elements to |
| * indicate the pattern that should be used to parse the element value. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_SRC_PATTERN_ATTRIBUTE = "src-pattern"; |
| |
| /** |
| * This attribute is used with date and number formatting elements to |
| * indicate the pattern that should be used to format the element value. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_PATTERN_ATTRIBUTE = "pattern"; |
| |
| /** |
| * This attribute is used with date and number formatting elements to |
| * indicate the locale that should be used to format the element value. |
| * Also used for in-place translations. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| * @see #I18N_WHEN_ELEMENT |
| */ |
| public static final String I18N_LOCALE_ATTRIBUTE = "locale"; |
| |
| /** |
| * This attribute is used with date and number formatting elements to |
| * indicate the locale that should be used to parse the element value. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_SRC_LOCALE_ATTRIBUTE = "src-locale"; |
| |
| /** |
| * This attribute is used with date and number formatting elements to |
| * indicate the value that should be parsed and formatted. If value |
| * attribute is not used then the character data of the element will be used. |
| * |
| * @see #I18N_PARAM_ELEMENT |
| * @see #I18N_DATE_TIME_ELEMENT |
| * @see #I18N_DATE_ELEMENT |
| * @see #I18N_TIME_ELEMENT |
| * @see #I18N_NUMBER_ELEMENT |
| */ |
| public static final String I18N_VALUE_ATTRIBUTE = "value"; |
| |
| /** |
| * This attribute is used with <code>i18:param</code> to |
| * indicate the parameter type: <code>date, time, date-time</code> or |
| * <code>number, currency, percent, int-currency, currency-no-unit, |
| * int-currency-no-unit</code>. |
| * Also used with <code>i18:translate</code> to indicate inplace |
| * translations: <code>inplace</code> |
| * @deprecated since 2.1. Use nested tags instead, e.g.: |
| * <i18n:param><i18n:date/></i18n:param> |
| */ |
| public static final String I18N_TYPE_ATTRIBUTE = "type"; |
| |
| /** |
| * This attribute is used to specify a different locale for the |
| * currency. When specified, this locale will be combined with |
| * the "normal" locale: e.g. the seperator symbols are taken from |
| * the normal locale but the currency symbol and possition will |
| * be taken from the currency locale. |
| * This enables to see a currency formatted for Euro but with US |
| * grouping and decimal char. |
| */ |
| public static final String CURRENCY_LOCALE_ATTRIBUTE = "currency"; |
| |
| /** |
| * This attribute can be used on <code>i18n:text</code> to indicate the catalogue |
| * from which the key should be retrieved. This attribute is optional, |
| * if it is not mentioned the default catalogue is used. |
| */ |
| public static final String I18N_CATALOGUE_ATTRIBUTE = "catalogue"; |
| |
| // |
| // Configuration parameters |
| // |
| |
| /** |
| * This configuration parameter specifies the default locale to be used. |
| */ |
| public static final String I18N_LOCALE = "locale"; |
| |
| /** |
| * This configuration parameter specifies the id of the catalogue to be used as |
| * default catalogue, allowing to redefine the default catalogue on the pipeline |
| * level. |
| */ |
| public static final String I18N_DEFAULT_CATALOGUE_ID = "default-catalogue-id"; |
| |
| /** |
| * This configuration parameter specifies the message that should be |
| * displayed in case of a not translated text (message not found). |
| */ |
| public static final String I18N_UNTRANSLATED = "untranslated-text"; |
| |
| /** |
| * This configuration parameter specifies if the message catalog should be |
| * cached at startup. |
| */ |
| public static final String I18N_CACHE_STARTUP = "cache-at-startup"; |
| |
| /** |
| * <code>fraction-digits</code> attribute is used with |
| * <code>i18:number</code> to |
| * indicate the number of digits behind the fraction |
| */ |
| public static final String I18N_FRACTION_DIGITS_ATTRIBUTE = "fraction-digits"; |
| |
| // |
| // States of the transformer |
| // |
| |
| private static final int STATE_OUTSIDE = 0; |
| private static final int STATE_INSIDE_TEXT = 10; |
| private static final int STATE_INSIDE_PARAM = 20; |
| private static final int STATE_INSIDE_TRANSLATE = 30; |
| private static final int STATE_INSIDE_CHOOSE = 50; |
| private static final int STATE_INSIDE_WHEN = 51; |
| private static final int STATE_INSIDE_OTHERWISE = 52; |
| private static final int STATE_INSIDE_DATE = 60; |
| private static final int STATE_INSIDE_DATE_TIME = 61; |
| private static final int STATE_INSIDE_TIME = 62; |
| private static final int STATE_INSIDE_NUMBER = 63; |
| |
| // All date-time related parameter types and element names |
| private static final Set dateTypes; |
| |
| // All number related parameter types and element names |
| private static final Set numberTypes; |
| |
| // Date pattern types map: short, medium, long, full |
| private static final Map datePatterns; |
| |
| static { |
| // initialize date types set |
| HashSet set = new HashSet(5); |
| set.add(I18N_DATE_ELEMENT); |
| set.add(I18N_TIME_ELEMENT); |
| set.add(I18N_DATE_TIME_ELEMENT); |
| dateTypes = Collections.unmodifiableSet(set); |
| |
| // initialize number types set |
| set = new HashSet(9); |
| set.add(I18N_NUMBER_ELEMENT); |
| set.add(I18N_PERCENT_ELEMENT); |
| set.add(I18N_CURRENCY_ELEMENT); |
| set.add(I18N_INT_CURRENCY_ELEMENT); |
| set.add(I18N_CURRENCY_NO_UNIT_ELEMENT); |
| set.add(I18N_INT_CURRENCY_NO_UNIT_ELEMENT); |
| numberTypes = Collections.unmodifiableSet(set); |
| |
| // Initialize date patterns map |
| Map map = new HashMap(7); |
| map.put("SHORT", new Integer(DateFormat.SHORT)); |
| map.put("MEDIUM", new Integer(DateFormat.MEDIUM)); |
| map.put("LONG", new Integer(DateFormat.LONG)); |
| map.put("FULL", new Integer(DateFormat.FULL)); |
| datePatterns = Collections.unmodifiableMap(map); |
| } |
| |
| |
| // |
| // Global configuration variables |
| // |
| |
| /** |
| * Component (service) manager |
| */ |
| protected ServiceManager manager; |
| |
| /** |
| * Message bundle loader factory component (service) |
| */ |
| protected BundleFactory factory; |
| |
| /** |
| * All catalogues (keyed by catalogue id). The values are instances |
| * of {@link CatalogueInfo}. |
| */ |
| private Map catalogues; |
| |
| /** |
| * Default (global) catalogue |
| */ |
| private CatalogueInfo defaultCatalogue; |
| |
| /** |
| * Default (global) untranslated message value |
| */ |
| private String defaultUntranslated; |
| |
| // |
| // Local configuration variables |
| // |
| |
| protected Map objectModel; |
| |
| /** |
| * Locale |
| */ |
| protected Locale locale; |
| |
| /** |
| * Catalogue (local) |
| */ |
| private CatalogueInfo catalogue; |
| |
| /** |
| * Current (local) untranslated message value |
| */ |
| private String untranslated; |
| |
| /** |
| * {@link SaxBuffer} containing the contents of {@link #untranslated}. |
| */ |
| private ParamSaxBuffer untranslatedRecorder; |
| |
| // |
| // Current state of the transformer |
| // |
| |
| /** |
| * Current state of the transformer. Default value is STATE_OUTSIDE. |
| */ |
| private int current_state; |
| |
| /** |
| * Previous state of the transformer. |
| * Used in text translation inside params and translate elements. |
| */ |
| private int prev_state; |
| |
| /** |
| * The i18n:key attribute is stored for the current element. |
| * If no translation found for the key then the character data of element is |
| * used as default value. |
| */ |
| private String currentKey; |
| |
| /** |
| * Contains the id of the current catalogue if it was explicitely mentioned |
| * on an i18n:text element, otherwise it is null. |
| */ |
| private String currentCatalogueId; |
| |
| /** |
| * Character data buffer. used to concat chunked character data |
| */ |
| private StringBuffer strBuffer; |
| |
| /** |
| * A flag for copying the node when doing in-place translation |
| */ |
| private boolean translate_copy; |
| |
| // A flag for copying the _GOOD_ node and not others |
| // when doing in-place translation within i18n:choose |
| private boolean translate_end; |
| |
| // Translated text. Inside i18n:translate, collects character events. |
| private ParamSaxBuffer tr_text_recorder; |
| |
| // Current "i18n:text" events |
| private ParamSaxBuffer text_recorder; |
| |
| // Current parameter events |
| private SaxBuffer param_recorder; |
| |
| // Param count when not using i18n:param name="..." |
| private int param_count; |
| |
| // Param name attribute for substitution. |
| private String param_name; |
| |
| // i18n:param's hashmap for substitution |
| private HashMap indexedParams; |
| |
| // Current parameter value (translated or not) |
| private String param_value; |
| |
| // Date and number elements and params formatting attributes with values. |
| private HashMap formattingParams; |
| |
| /** |
| * Returns the current locale setting of this transformer instance. |
| * @return current Locale object |
| */ |
| public Locale getLocale() { |
| return this.locale; |
| } |
| |
| /** |
| * Implemenation of CacheableProcessingComponents. |
| * Generates unique key for the current locale. |
| */ |
| public java.io.Serializable getKey() { |
| // TODO: Key should be composed out of used catalogues locations, and locale. |
| // Right now it is hardcoded only to default catalogue location. |
| StringBuffer key = new StringBuffer(); |
| if (catalogue != null) { |
| key.append(catalogue.getLocation()[0]); |
| } |
| key.append("?"); |
| if (locale != null) { |
| key.append(locale.getLanguage()); |
| key.append("_"); |
| key.append(locale.getCountry()); |
| key.append("_"); |
| key.append(locale.getVariant()); |
| } |
| return key.toString(); |
| } |
| |
| /** |
| * Implementation of CacheableProcessingComponent. |
| * Generates validity object for this transformer or <code>null</code> |
| * if this instance is not cacheable. |
| */ |
| public SourceValidity getValidity() { |
| // FIXME (KP): Cache validity should be generated by |
| // Bundle implementations. |
| return org.apache.excalibur.source.impl.validity.NOPValidity.SHARED_INSTANCE; |
| } |
| |
| /** |
| * Look up the {@link BundleFactory} to be used. |
| */ |
| public void service(ServiceManager manager) throws ServiceException { |
| this.manager = manager; |
| try { |
| this.factory = (BundleFactory) manager.lookup(BundleFactory.ROLE); |
| } catch (ServiceException e) { |
| getLogger().debug("Failed to lookup <" + BundleFactory.ROLE + ">", e); |
| throw e; |
| } |
| } |
| |
| /** |
| * Implementation of Configurable interface. |
| * Configure this transformer. |
| */ |
| public void configure(Configuration conf) throws ConfigurationException { |
| // Read in the config options from the transformer definition |
| Configuration cataloguesConf = conf.getChild("catalogues", false); |
| if (cataloguesConf == null) { |
| throw new ConfigurationException("I18nTransformer requires <catalogues> configuration at " + |
| conf.getLocation()); |
| } |
| |
| // new configuration style |
| Configuration[] catalogueConfs = cataloguesConf.getChildren("catalogue"); |
| catalogues = new HashMap(catalogueConfs.length + 3); |
| for (int i = 0; i < catalogueConfs.length; i++) { |
| String id = catalogueConfs[i].getAttribute("id"); |
| String name = catalogueConfs[i].getAttribute("name"); |
| |
| String[] locations; |
| String location = catalogueConfs[i].getAttribute("location", null); |
| Configuration[] locationConf = |
| catalogueConfs[i].getChildren("location"); |
| if (location != null) { |
| if (locationConf.length > 0) { |
| String msg = "I18nTransformer: Location attribute cannot be " + |
| "specified with location elements"; |
| getLogger().error(msg); |
| throw new ConfigurationException(msg); |
| } |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("name=" + name + ", location=" + |
| location); |
| } |
| locations = new String[1]; |
| locations[0] = location; |
| } else { |
| if (locationConf.length == 0) { |
| String msg = "I18nTransformer: A location attribute or location " + |
| "elements must be specified"; |
| getLogger().error(msg); |
| throw new ConfigurationException(msg); |
| } |
| |
| locations = new String[locationConf.length]; |
| for (int j=0; j < locationConf.length; ++j) { |
| locations[j] = locationConf[j].getValue(); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("name=" + name + ", location=" + |
| locations[j]); |
| } |
| } |
| } |
| |
| CatalogueInfo catalogueInfo; |
| try { |
| catalogueInfo = new CatalogueInfo(name, locations); |
| } catch (PatternException e) { |
| throw new ConfigurationException("I18nTransformer: Error in name or location " + |
| "attribute on catalogue element with id " + id, e); |
| } |
| catalogues.put(id, catalogueInfo); |
| } |
| |
| String defaultCatalogueId = cataloguesConf.getAttribute("default"); |
| defaultCatalogue = (CatalogueInfo) catalogues.get(defaultCatalogueId); |
| if (defaultCatalogue == null) { |
| throw new ConfigurationException("I18nTransformer: Default catalogue id '" + |
| defaultCatalogueId + "' denotes a nonexisting catalogue"); |
| } |
| |
| // Obtain default text to use for untranslated messages |
| defaultUntranslated = conf.getChild(I18N_UNTRANSLATED).getValue(null); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Default untranslated text is '" + defaultUntranslated + "'"); |
| } |
| } |
| |
| /** |
| * Setup current instance of transformer. |
| */ |
| public void setup(SourceResolver resolver, Map objectModel, String source, |
| Parameters parameters) |
| throws ProcessingException, SAXException, IOException { |
| |
| this.objectModel = objectModel; |
| |
| untranslated = parameters.getParameter(I18N_UNTRANSLATED, defaultUntranslated); |
| if (untranslated != null) { |
| untranslatedRecorder = new ParamSaxBuffer(); |
| untranslatedRecorder.characters(untranslated.toCharArray(), 0, untranslated.length()); |
| } |
| |
| // Get current locale |
| String lc = parameters.getParameter(I18N_LOCALE, null); |
| Locale locale = I18nUtils.parseLocale(lc); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Using locale '" + locale + "'"); |
| } |
| |
| // Initialize instance state variables |
| this.locale = locale; |
| this.current_state = STATE_OUTSIDE; |
| this.prev_state = STATE_OUTSIDE; |
| this.currentKey = null; |
| this.currentCatalogueId = null; |
| this.translate_copy = false; |
| this.tr_text_recorder = null; |
| this.text_recorder = new ParamSaxBuffer(); |
| this.param_count = 0; |
| this.param_name = null; |
| this.param_value = null; |
| this.param_recorder = null; |
| this.indexedParams = new HashMap(3); |
| this.formattingParams = null; |
| this.strBuffer = null; |
| |
| // give the catalogue variable its value -- first look if it's locally overridden |
| // and otherwise use the component-wide defaults. |
| String catalogueId = parameters.getParameter(I18N_DEFAULT_CATALOGUE_ID, null); |
| if (catalogueId != null) { |
| CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues.get(catalogueId); |
| if (catalogueInfo == null) { |
| throw new ProcessingException("I18nTransformer: '" + |
| catalogueId + |
| "' is not an existing catalogue id."); |
| } |
| catalogue = catalogueInfo; |
| } else { |
| catalogue = defaultCatalogue; |
| } |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Default catalogue is " + catalogue.getName()); |
| } |
| } |
| |
| |
| // |
| // Standard SAX event handlers |
| // |
| |
| public void startElement(String uri, String name, String raw, |
| Attributes attr) |
| throws SAXException { |
| |
| // Handle previously buffered characters |
| if (current_state != STATE_OUTSIDE && strBuffer != null) { |
| i18nCharacters(strBuffer.toString()); |
| strBuffer = null; |
| } |
| |
| // Process start element event |
| if (I18nUtils.matchesI18nNamespace(uri)) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Starting i18n element: " + name); |
| } |
| startI18NElement(name, attr); |
| } else { |
| // We have a non i18n element event |
| if (current_state == STATE_OUTSIDE) { |
| super.startElement(uri, name, raw, |
| translateAttributes(name, attr)); |
| } else if (current_state == STATE_INSIDE_PARAM) { |
| param_recorder.startElement(uri, name, raw, attr); |
| } else if (current_state == STATE_INSIDE_TEXT) { |
| text_recorder.startElement(uri, name, raw, attr); |
| } else if ((current_state == STATE_INSIDE_WHEN || |
| current_state == STATE_INSIDE_OTHERWISE) |
| && translate_copy) { |
| |
| super.startElement(uri, name, raw, attr); |
| } |
| } |
| } |
| |
| public void endElement(String uri, String name, String raw) |
| throws SAXException { |
| |
| // Handle previously buffered characters |
| if (current_state != STATE_OUTSIDE && strBuffer != null) { |
| i18nCharacters(strBuffer.toString()); |
| strBuffer = null; |
| } |
| |
| if (I18nUtils.matchesI18nNamespace(uri)) { |
| endI18NElement(name); |
| } else if (current_state == STATE_INSIDE_PARAM) { |
| param_recorder.endElement(uri, name, raw); |
| } else if (current_state == STATE_INSIDE_TEXT) { |
| text_recorder.endElement(uri, name, raw); |
| } else if (current_state == STATE_INSIDE_CHOOSE || |
| (current_state == STATE_INSIDE_WHEN || |
| current_state == STATE_INSIDE_OTHERWISE) |
| && !translate_copy) { |
| |
| // Output nothing |
| } else { |
| super.endElement(uri, name, raw); |
| } |
| } |
| |
| public void characters(char[] ch, int start, int len) |
| throws SAXException { |
| |
| if (current_state == STATE_OUTSIDE || |
| ((current_state == STATE_INSIDE_WHEN || |
| current_state == STATE_INSIDE_OTHERWISE) && translate_copy)) { |
| |
| super.characters(ch, start, len); |
| } else { |
| // Perform buffering to prevent chunked character data |
| if (strBuffer == null) { |
| strBuffer = new StringBuffer(); |
| } |
| strBuffer.append(ch, start, len); |
| } |
| } |
| |
| // |
| // i18n specific event handlers |
| // |
| |
| private void startI18NElement(String name, Attributes attr) |
| throws SAXException { |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Start i18n element: " + name); |
| } |
| |
| if (I18N_TEXT_ELEMENT.equals(name)) { |
| if (current_state != STATE_OUTSIDE |
| && current_state != STATE_INSIDE_PARAM |
| && current_state != STATE_INSIDE_TRANSLATE) { |
| |
| throw new SAXException( |
| getClass().getName() |
| + ": nested i18n:text elements are not allowed." |
| + " Current state: " + current_state); |
| } |
| |
| prev_state = current_state; |
| current_state = STATE_INSIDE_TEXT; |
| |
| currentKey = attr.getValue("", I18N_KEY_ATTRIBUTE); |
| if (currentKey == null) { |
| // Try the namespaced attribute |
| currentKey = attr.getValue(I18N_NAMESPACE_URI, I18N_KEY_ATTRIBUTE); |
| if (currentKey == null) { |
| // Try the old namespace |
| currentKey = attr.getValue(I18N_OLD_NAMESPACE_URI, I18N_KEY_ATTRIBUTE); |
| } |
| } |
| |
| currentCatalogueId = attr.getValue("", I18N_CATALOGUE_ATTRIBUTE); |
| if (currentCatalogueId == null) { |
| // Try the namespaced attribute |
| currentCatalogueId = attr.getValue(I18N_NAMESPACE_URI, I18N_CATALOGUE_ATTRIBUTE); |
| } |
| |
| if (prev_state != STATE_INSIDE_PARAM) { |
| tr_text_recorder = null; |
| } |
| |
| if (currentKey != null) { |
| tr_text_recorder = getMessage(currentKey, (ParamSaxBuffer)null); |
| } |
| |
| } else if (I18N_TRANSLATE_ELEMENT.equals(name)) { |
| if (current_state != STATE_OUTSIDE) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:translate element must be used " |
| + "outside of other i18n elements. Current state: " |
| + current_state); |
| } |
| |
| prev_state = current_state; |
| current_state = STATE_INSIDE_TRANSLATE; |
| } else if (I18N_PARAM_ELEMENT.equals(name)) { |
| if (current_state != STATE_INSIDE_TRANSLATE) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:param element can be used only inside " |
| + "i18n:translate element. Current state: " |
| + current_state); |
| } |
| |
| param_name = attr.getValue(I18N_PARAM_NAME_ATTRIBUTE); |
| if (param_name == null) { |
| param_name = String.valueOf(param_count++); |
| } |
| |
| param_recorder = new SaxBuffer(); |
| setFormattingParams(attr); |
| current_state = STATE_INSIDE_PARAM; |
| } else if (I18N_CHOOSE_ELEMENT.equals(name)) { |
| if (current_state != STATE_OUTSIDE) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:choose elements cannot be used" |
| + "inside of other i18n elements."); |
| } |
| |
| translate_copy = false; |
| translate_end = false; |
| prev_state = current_state; |
| current_state = STATE_INSIDE_CHOOSE; |
| } else if (I18N_WHEN_ELEMENT.equals(name) || |
| I18N_IF_ELEMENT.equals(name)) { |
| |
| if (I18N_WHEN_ELEMENT.equals(name) && |
| current_state != STATE_INSIDE_CHOOSE) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:when elements are can be used only" |
| + "inside of i18n:choose elements."); |
| } |
| |
| if (I18N_IF_ELEMENT.equals(name) && |
| current_state != STATE_OUTSIDE) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:if elements cannot be nested."); |
| } |
| |
| String locale = attr.getValue(I18N_LOCALE_ATTRIBUTE); |
| if (locale == null) |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:" + name |
| + " element cannot be used without 'locale' attribute."); |
| |
| if ((!translate_end && current_state == STATE_INSIDE_CHOOSE) |
| || current_state == STATE_OUTSIDE) { |
| |
| // Perform soft locale matching |
| if (this.locale.toString().startsWith(locale)) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Locale matching: " + locale); |
| } |
| translate_copy = true; |
| } |
| } |
| |
| prev_state = current_state; |
| current_state = STATE_INSIDE_WHEN; |
| |
| } else if (I18N_OTHERWISE_ELEMENT.equals(name)) { |
| if (current_state != STATE_INSIDE_CHOOSE) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:otherwise elements are not allowed " |
| + "only inside i18n:choose."); |
| } |
| |
| getLogger().debug("Matching any locale"); |
| if (!translate_end) { |
| translate_copy = true; |
| } |
| |
| prev_state = current_state; |
| current_state = STATE_INSIDE_OTHERWISE; |
| |
| } else if (I18N_DATE_ELEMENT.equals(name)) { |
| if (current_state != STATE_OUTSIDE |
| && current_state != STATE_INSIDE_TEXT |
| && current_state != STATE_INSIDE_PARAM) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:date elements are not allowed " |
| + "inside of other i18n elements."); |
| } |
| |
| setFormattingParams(attr); |
| prev_state = current_state; |
| current_state = STATE_INSIDE_DATE; |
| } else if (I18N_DATE_TIME_ELEMENT.equals(name)) { |
| if (current_state != STATE_OUTSIDE |
| && current_state != STATE_INSIDE_TEXT |
| && current_state != STATE_INSIDE_PARAM) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:date-time elements are not allowed " |
| + "inside of other i18n elements."); |
| } |
| |
| setFormattingParams(attr); |
| prev_state = current_state; |
| current_state = STATE_INSIDE_DATE_TIME; |
| } else if (I18N_TIME_ELEMENT.equals(name)) { |
| if (current_state != STATE_OUTSIDE |
| && current_state != STATE_INSIDE_TEXT |
| && current_state != STATE_INSIDE_PARAM) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:date elements are not allowed " |
| + "inside of other i18n elements."); |
| } |
| |
| setFormattingParams(attr); |
| prev_state = current_state; |
| current_state = STATE_INSIDE_TIME; |
| } else if (I18N_NUMBER_ELEMENT.equals(name)) { |
| if (current_state != STATE_OUTSIDE |
| && current_state != STATE_INSIDE_TEXT |
| && current_state != STATE_INSIDE_PARAM) { |
| throw new SAXException( |
| getClass().getName() |
| + ": i18n:number elements are not allowed " |
| + "inside of other i18n elements."); |
| } |
| |
| setFormattingParams(attr); |
| prev_state = current_state; |
| current_state = STATE_INSIDE_NUMBER; |
| } |
| } |
| |
| // Get all possible i18n formatting attribute values and store in a Map |
| private void setFormattingParams(Attributes attr) { |
| // average number of attributes is 3 |
| formattingParams = new HashMap(3); |
| |
| String attr_value = attr.getValue(I18N_SRC_PATTERN_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(I18N_SRC_PATTERN_ATTRIBUTE, attr_value); |
| } |
| |
| attr_value = attr.getValue(I18N_PATTERN_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(I18N_PATTERN_ATTRIBUTE, attr_value); |
| } |
| |
| attr_value = attr.getValue(I18N_VALUE_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(I18N_VALUE_ATTRIBUTE, attr_value); |
| } |
| |
| attr_value = attr.getValue(I18N_LOCALE_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(I18N_LOCALE_ATTRIBUTE, attr_value); |
| } |
| |
| attr_value = attr.getValue(CURRENCY_LOCALE_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(CURRENCY_LOCALE_ATTRIBUTE, attr_value); |
| } |
| |
| attr_value = attr.getValue(I18N_SRC_LOCALE_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(I18N_SRC_LOCALE_ATTRIBUTE, attr_value); |
| } |
| |
| attr_value = attr.getValue(I18N_TYPE_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(I18N_TYPE_ATTRIBUTE, attr_value); |
| } |
| |
| attr_value = attr.getValue(I18N_FRACTION_DIGITS_ATTRIBUTE); |
| if (attr_value != null) { |
| formattingParams.put(I18N_FRACTION_DIGITS_ATTRIBUTE, attr_value); |
| } |
| } |
| |
| private void endI18NElement(String name) throws SAXException { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("End i18n element: " + name); |
| } |
| |
| switch (current_state) { |
| case STATE_INSIDE_TEXT: |
| endTextElement(); |
| break; |
| |
| case STATE_INSIDE_TRANSLATE: |
| endTranslateElement(); |
| break; |
| |
| case STATE_INSIDE_CHOOSE: |
| endChooseElement(); |
| break; |
| |
| case STATE_INSIDE_WHEN: |
| case STATE_INSIDE_OTHERWISE: |
| endWhenElement(); |
| break; |
| |
| case STATE_INSIDE_PARAM: |
| endParamElement(); |
| break; |
| |
| case STATE_INSIDE_DATE: |
| case STATE_INSIDE_DATE_TIME: |
| case STATE_INSIDE_TIME: |
| endDate_TimeElement(); |
| break; |
| |
| case STATE_INSIDE_NUMBER: |
| endNumberElement(); |
| break; |
| } |
| } |
| |
| private void i18nCharacters(String textValue) throws SAXException { |
| // Trim text values to avoid parsing errors. |
| textValue = textValue.trim(); |
| if (textValue.length() == 0) { |
| return; |
| } |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug( "i18n message text = '" + textValue + "'" ); |
| } |
| |
| char[] ch = textValue.toCharArray(); |
| switch (current_state) { |
| case STATE_INSIDE_TEXT: |
| text_recorder.characters(ch, 0, ch.length); |
| break; |
| |
| case STATE_INSIDE_PARAM: |
| param_recorder.characters(ch, 0, ch.length); |
| break; |
| |
| case STATE_INSIDE_WHEN: |
| case STATE_INSIDE_OTHERWISE: |
| // Previously handeld to avoid the String() conversion. |
| break; |
| |
| case STATE_INSIDE_TRANSLATE: |
| if(tr_text_recorder == null) { |
| tr_text_recorder = new ParamSaxBuffer(); |
| } |
| tr_text_recorder.characters(ch, 0, ch.length); |
| break; |
| |
| case STATE_INSIDE_CHOOSE: |
| // No characters allowed. Send an exception ? |
| getLogger().debug("No characters allowed inside <i18n:choose> tags"); |
| break; |
| |
| case STATE_INSIDE_DATE: |
| case STATE_INSIDE_DATE_TIME: |
| case STATE_INSIDE_TIME: |
| case STATE_INSIDE_NUMBER: |
| if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null) { |
| formattingParams.put(I18N_VALUE_ATTRIBUTE, textValue); |
| } else { |
| // ignore the text inside of date element |
| } |
| break; |
| |
| default: |
| throw new IllegalStateException(getClass().getName() + |
| " developer's fault: characters not handled. " + |
| "Current state: " + current_state); |
| } |
| } |
| |
| // Translate all attributes that are listed in i18n:attr attribute |
| private Attributes translateAttributes(final String element, Attributes attr) |
| throws SAXException { |
| if (attr == null) { |
| return null; |
| } |
| |
| AttributesImpl tempAttr = null; |
| |
| // Translate all attributes from i18n:attr="name1 name2 ..." |
| // using their values as keys. |
| int attrIndex = attr.getIndex(I18N_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE); |
| if (attrIndex == -1) { |
| // Try the old namespace |
| attrIndex = attr.getIndex(I18N_OLD_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE); |
| } |
| |
| if (attrIndex != -1) { |
| StringTokenizer st = new StringTokenizer(attr.getValue(attrIndex)); |
| |
| // Make a copy which we are going to modify |
| tempAttr = new AttributesImpl(attr); |
| // Remove the i18n:attr attribute - we don't need it anymore |
| tempAttr.removeAttribute(attrIndex); |
| |
| // Iterate through listed attributes and translate them |
| while (st.hasMoreElements()) { |
| final String name = st.nextToken(); |
| |
| int index = tempAttr.getIndex(name); |
| if (index == -1) { |
| getLogger().warn("Attribute " + |
| name + " not found in element <" + element + ">"); |
| continue; |
| } |
| |
| String value = translateAttribute(element, name, tempAttr.getValue(index)); |
| if (value != null) { |
| // Set the translated value. If null, do nothing. |
| tempAttr.setValue(index, value); |
| } |
| } |
| |
| attr = tempAttr; |
| } |
| |
| // Translate all attributes from i18n:expr="name1 name2 ..." |
| // using their values as keys. |
| attrIndex = attr.getIndex(I18N_NAMESPACE_URI, I18N_EXPR_ATTRIBUTE); |
| if (attrIndex != -1) { |
| StringTokenizer st = new StringTokenizer(attr.getValue(attrIndex)); |
| |
| if (tempAttr == null) { |
| tempAttr = new AttributesImpl(attr); |
| } |
| tempAttr.removeAttribute(attrIndex); |
| |
| // Iterate through listed attributes and evaluate them |
| while (st.hasMoreElements()) { |
| final String name = st.nextToken(); |
| |
| int index = tempAttr.getIndex(name); |
| if (index == -1) { |
| getLogger().warn("Attribute " + |
| name + " not found in element <" + element + ">"); |
| continue; |
| } |
| |
| final StringBuffer translated = new StringBuffer(); |
| |
| // Evaluate {..} expression |
| VariableExpressionTokenizer.TokenReciever tr = new VariableExpressionTokenizer.TokenReciever () { |
| private String catalogueName; |
| |
| public void addToken(int type, String value) { |
| if (type == MODULE) { |
| this.catalogueName = value; |
| } else if (type == VARIABLE) { |
| translated.append(translateAttribute(element, name, value)); |
| } else if (type == TEXT) { |
| if (this.catalogueName != null) { |
| translated.append(translateAttribute(element, |
| name, |
| this.catalogueName + ":" + value)); |
| this.catalogueName = null; |
| } else if (value != null) { |
| translated.append(value); |
| } |
| } |
| } |
| }; |
| |
| try { |
| VariableExpressionTokenizer.tokenize(tempAttr.getValue(index), tr); |
| } catch (PatternException e) { |
| throw new SAXException(e); |
| } |
| |
| // Set the translated value. |
| tempAttr.setValue(index, translated.toString()); |
| } |
| |
| attr = tempAttr; |
| } |
| |
| // nothing to translate, just return |
| return attr; |
| } |
| |
| /** |
| * Translate attribute value. |
| * Value can be prefixed with catalogue ID and semicolon. |
| * @return Translated text, untranslated text, or null. |
| */ |
| private String translateAttribute(String element, String name, String key) { |
| // Check if the key contains a colon, if so the text before |
| // the colon denotes a catalogue ID. |
| int colonPos = key.indexOf(":"); |
| String catalogueID = null; |
| if (colonPos != -1) { |
| catalogueID = key.substring(0, colonPos); |
| key = key.substring(colonPos + 1, key.length()); |
| } |
| |
| final SaxBuffer text = getMessage(catalogueID, key); |
| if (text == null) { |
| getLogger().warn("Translation not found for attribute " + |
| name + " in element <" + element + ">"); |
| return untranslated; |
| } |
| return text.toString(); |
| } |
| |
| private void endTextElement() throws SAXException { |
| switch (prev_state) { |
| case STATE_OUTSIDE: |
| if (tr_text_recorder == null) { |
| if (currentKey == null) { |
| // Use the text as key. Not recommended for large strings, |
| // especially if they include markup. |
| tr_text_recorder = getMessage(text_recorder.toString(), text_recorder); |
| } else { |
| // We have the key, but couldn't find a translation |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Translation not found for key '" + currentKey + "'"); |
| } |
| |
| // Use the untranslated-text only when the content of the i18n:text |
| // element was empty |
| if (text_recorder.isEmpty() && untranslatedRecorder != null) { |
| tr_text_recorder = untranslatedRecorder; |
| } else { |
| tr_text_recorder = text_recorder; |
| } |
| } |
| } |
| |
| if (tr_text_recorder != null) { |
| tr_text_recorder.toSAX(this.contentHandler); |
| } |
| |
| text_recorder.recycle(); |
| tr_text_recorder = null; |
| currentKey = null; |
| currentCatalogueId = null; |
| break; |
| |
| case STATE_INSIDE_TRANSLATE: |
| if (tr_text_recorder == null) { |
| if (!text_recorder.isEmpty()) { |
| tr_text_recorder = getMessage(text_recorder.toString(), text_recorder); |
| if (tr_text_recorder == text_recorder) { |
| // If the default value was returned, make a copy |
| tr_text_recorder = new ParamSaxBuffer(text_recorder); |
| } |
| } |
| } |
| |
| text_recorder.recycle(); |
| break; |
| |
| case STATE_INSIDE_PARAM: |
| // We send the translated text to the param recorder, after trying to translate it. |
| // Remember you can't give a key when inside a param, that'll be nonsense! |
| // No need to clone. We just send the events. |
| if (!text_recorder.isEmpty()) { |
| getMessage(text_recorder.toString(), text_recorder).toSAX(param_recorder); |
| text_recorder.recycle(); |
| } |
| break; |
| } |
| |
| current_state = prev_state; |
| prev_state = STATE_OUTSIDE; |
| } |
| |
| // Process substitution parameter |
| private void endParamElement() throws SAXException { |
| String paramType = (String)formattingParams.get(I18N_TYPE_ATTRIBUTE); |
| if (paramType != null) { |
| // We have a typed parameter |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Param type: " + paramType); |
| } |
| if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null && param_value != null) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Put param value: " + param_value); |
| } |
| formattingParams.put(I18N_VALUE_ATTRIBUTE, param_value); |
| } |
| |
| // Check if we have a date or a number parameter |
| if (dateTypes.contains(paramType)) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Formatting date_time param: " + formattingParams); |
| } |
| param_value = formatDate_Time(formattingParams); |
| } else if (numberTypes.contains(paramType)) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Formatting number param: " + formattingParams); |
| } |
| param_value = formatNumber(formattingParams); |
| } |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Added substitution param: " + param_value); |
| } |
| } |
| |
| param_value = null; |
| current_state = STATE_INSIDE_TRANSLATE; |
| |
| if(param_recorder == null) { |
| return; |
| } |
| |
| indexedParams.put(param_name, param_recorder); |
| param_recorder = null; |
| } |
| |
| private void endTranslateElement() throws SAXException { |
| if (tr_text_recorder != null) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("End of translate with params. " + |
| "Fragment for substitution : " + tr_text_recorder); |
| } |
| tr_text_recorder.toSAX(super.contentHandler, indexedParams); |
| tr_text_recorder = null; |
| text_recorder.recycle(); |
| } |
| |
| indexedParams.clear(); |
| param_count = 0; |
| current_state = STATE_OUTSIDE; |
| } |
| |
| private void endChooseElement() { |
| current_state = STATE_OUTSIDE; |
| } |
| |
| private void endWhenElement() { |
| current_state = prev_state; |
| if (translate_copy) { |
| translate_copy = false; |
| translate_end = true; |
| } |
| } |
| |
| private void endDate_TimeElement() throws SAXException { |
| String result = formatDate_Time(formattingParams); |
| switch(prev_state) { |
| case STATE_OUTSIDE: |
| super.contentHandler.characters(result.toCharArray(), 0, |
| result.length()); |
| break; |
| case STATE_INSIDE_PARAM: |
| param_recorder.characters(result.toCharArray(), 0, result.length()); |
| break; |
| case STATE_INSIDE_TEXT: |
| text_recorder.characters(result.toCharArray(), 0, result.length()); |
| break; |
| } |
| current_state = prev_state; |
| } |
| |
| // Helper method: creates Locale object from a string value in a map |
| private Locale getLocale(Map params, String attribute) { |
| // the specific locale value |
| String lc = (String)params.get(attribute); |
| return I18nUtils.parseLocale(lc, this.locale); |
| } |
| |
| private String formatDate_Time(Map params) throws SAXException { |
| // Check that we have not null params |
| if (params == null) { |
| throw new IllegalArgumentException("Nothing to format"); |
| } |
| |
| // Formatters |
| SimpleDateFormat to_fmt; |
| SimpleDateFormat from_fmt; |
| |
| // Date formatting styles |
| int srcStyle = DateFormat.DEFAULT; |
| int style = DateFormat.DEFAULT; |
| |
| // Date formatting patterns |
| boolean realPattern = false; |
| boolean realSrcPattern = false; |
| |
| // From locale |
| Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE); |
| // To locale |
| Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE); |
| |
| // From pattern |
| String srcPattern = (String)params.get(I18N_SRC_PATTERN_ATTRIBUTE); |
| // To pattern |
| String pattern = (String)params.get(I18N_PATTERN_ATTRIBUTE); |
| // The date value |
| String value = (String)params.get(I18N_VALUE_ATTRIBUTE); |
| |
| // A src-pattern attribute is present |
| if (srcPattern != null) { |
| // Check if we have a real pattern |
| Integer patternValue = (Integer)datePatterns.get(srcPattern.toUpperCase()); |
| if (patternValue != null) { |
| srcStyle = patternValue.intValue(); |
| } else { |
| realSrcPattern = true; |
| } |
| } |
| |
| // A pattern attribute is present |
| if (pattern != null) { |
| Integer patternValue = (Integer)datePatterns.get(pattern.toUpperCase()); |
| if (patternValue != null) { |
| style = patternValue.intValue(); |
| } else { |
| realPattern = true; |
| } |
| } |
| |
| // If we are inside of a typed param |
| String paramType = (String)formattingParams.get(I18N_TYPE_ATTRIBUTE); |
| |
| // Initializing date formatters |
| if (current_state == STATE_INSIDE_DATE || |
| I18N_DATE_ELEMENT.equals(paramType)) { |
| |
| to_fmt = (SimpleDateFormat)DateFormat.getDateInstance(style, loc); |
| from_fmt = (SimpleDateFormat)DateFormat.getDateInstance( |
| srcStyle, |
| srcLoc |
| ); |
| } else if (current_state == STATE_INSIDE_DATE_TIME || |
| I18N_DATE_TIME_ELEMENT.equals(paramType)) { |
| to_fmt = (SimpleDateFormat)DateFormat.getDateTimeInstance( |
| style, |
| style, |
| loc |
| ); |
| from_fmt = (SimpleDateFormat)DateFormat.getDateTimeInstance( |
| srcStyle, |
| srcStyle, |
| srcLoc |
| ); |
| } else { |
| // STATE_INSIDE_TIME or param type='time' |
| to_fmt = (SimpleDateFormat)DateFormat.getTimeInstance(style, loc); |
| from_fmt = (SimpleDateFormat)DateFormat.getTimeInstance( |
| srcStyle, |
| srcLoc |
| ); |
| } |
| |
| // parsed date object |
| Date dateValue; |
| |
| // pattern overwrites locale format |
| if (realSrcPattern) { |
| from_fmt.applyPattern(srcPattern); |
| } |
| |
| if (realPattern) { |
| to_fmt.applyPattern(pattern); |
| } |
| |
| // get current date and time by default |
| if (value == null) { |
| dateValue = new Date(); |
| } else { |
| try { |
| dateValue = from_fmt.parse(value); |
| } catch (ParseException pe) { |
| throw new SAXException( |
| this.getClass().getName() |
| + "i18n:date - parsing error.", pe |
| ); |
| } |
| } |
| |
| // we have all necessary data here: do formatting. |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("### Formatting date: " + dateValue + " with localized pattern " + |
| to_fmt.toLocalizedPattern() + " for locale: " + locale); |
| } |
| return to_fmt.format(dateValue); |
| } |
| |
| private void endNumberElement() throws SAXException { |
| String result = formatNumber(formattingParams); |
| switch(prev_state) { |
| case STATE_OUTSIDE: |
| super.contentHandler.characters(result.toCharArray(), 0, result.length()); |
| break; |
| case STATE_INSIDE_PARAM: |
| param_recorder.characters(result.toCharArray(), 0, result.length()); |
| break; |
| case STATE_INSIDE_TEXT: |
| text_recorder.characters(result.toCharArray(), 0, result.length()); |
| break; |
| } |
| current_state = prev_state; |
| } |
| |
| private String formatNumber(Map params) throws SAXException { |
| if (params == null) { |
| throw new SAXException( |
| this.getClass().getName() |
| + ": i18n:number - error in element attributes." |
| ); |
| } |
| |
| // from pattern |
| String srcPattern = (String)params.get(I18N_SRC_PATTERN_ATTRIBUTE); |
| // to pattern |
| String pattern = (String)params.get(I18N_PATTERN_ATTRIBUTE); |
| // the number value |
| String value = (String)params.get(I18N_VALUE_ATTRIBUTE); |
| |
| if (value == null) return ""; |
| // type |
| String type = (String)params.get(I18N_TYPE_ATTRIBUTE); |
| |
| // fraction-digits |
| int fractionDigits = -1; |
| try { |
| String fd = (String)params.get(I18N_FRACTION_DIGITS_ATTRIBUTE); |
| if (fd != null) |
| fractionDigits = Integer.parseInt(fd); |
| } catch (NumberFormatException nfe) { |
| getLogger().warn("Error in number format with fraction-digits", nfe); |
| } |
| |
| // parsed number |
| Number numberValue; |
| |
| // locale, may be switched locale |
| Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE); |
| Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE); |
| // currency locale |
| Locale currencyLoc = getLocale(params, CURRENCY_LOCALE_ATTRIBUTE); |
| // decimal and grouping locale |
| Locale dgLoc = null; |
| if (currencyLoc != null) { |
| // the reasoning here is: if there is a currency locale, then start from that |
| // one but take certain properties (like decimal and grouping seperation symbols) |
| // from the default locale (this happens further on). |
| dgLoc = loc; |
| loc = currencyLoc; |
| } |
| |
| // src format |
| DecimalFormat from_fmt = (DecimalFormat)NumberFormat.getInstance(srcLoc); |
| int int_currency = 0; |
| |
| // src-pattern overwrites locale format |
| if (srcPattern != null) { |
| from_fmt.applyPattern(srcPattern); |
| } |
| |
| // to format |
| DecimalFormat to_fmt; |
| char dec = from_fmt.getDecimalFormatSymbols().getDecimalSeparator(); |
| int decAt = 0; |
| boolean appendDec = false; |
| |
| if (type == null || type.equals( I18N_NUMBER_ELEMENT )) { |
| to_fmt = (DecimalFormat)NumberFormat.getInstance(loc); |
| to_fmt.setMaximumFractionDigits(309); |
| for (int i = value.length() - 1; |
| i >= 0 && value.charAt(i) != dec; i--, decAt++) { |
| } |
| |
| if (decAt < value.length())to_fmt.setMinimumFractionDigits(decAt); |
| decAt = 0; |
| for (int i = 0; i < value.length() && value.charAt(i) != dec; i++) { |
| if (Character.isDigit(value.charAt(i))) { |
| decAt++; |
| } |
| } |
| |
| to_fmt.setMinimumIntegerDigits(decAt); |
| if (value.charAt(value.length() - 1) == dec) { |
| appendDec = true; |
| } |
| } else if (type.equals( I18N_CURRENCY_ELEMENT )) { |
| to_fmt = (DecimalFormat)NumberFormat.getCurrencyInstance(loc); |
| } else if (type.equals( I18N_INT_CURRENCY_ELEMENT )) { |
| to_fmt = (DecimalFormat)NumberFormat.getCurrencyInstance(loc); |
| int_currency = 1; |
| for (int i = 0; i < to_fmt.getMaximumFractionDigits(); i++) { |
| int_currency *= 10; |
| } |
| } else if ( type.equals( I18N_CURRENCY_NO_UNIT_ELEMENT ) ) { |
| DecimalFormat tmp = (DecimalFormat) NumberFormat.getCurrencyInstance( loc ); |
| to_fmt = (DecimalFormat) NumberFormat.getInstance( loc ); |
| to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits()); |
| to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits()); |
| } else if ( type.equals( I18N_INT_CURRENCY_NO_UNIT_ELEMENT ) ) { |
| DecimalFormat tmp = (DecimalFormat) NumberFormat.getCurrencyInstance( loc ); |
| int_currency = 1; |
| for ( int i = 0; i < tmp.getMaximumFractionDigits(); i++ ) |
| int_currency *= 10; |
| to_fmt = (DecimalFormat) NumberFormat.getInstance( loc ); |
| to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits()); |
| to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits()); |
| } else if (type.equals( I18N_PERCENT_ELEMENT )) { |
| to_fmt = (DecimalFormat)NumberFormat.getPercentInstance(loc); |
| } else { |
| throw new SAXException("<i18n:number>: unknown type: " + type); |
| } |
| |
| if(fractionDigits > -1) { |
| to_fmt.setMinimumFractionDigits(fractionDigits); |
| to_fmt.setMaximumFractionDigits(fractionDigits); |
| } |
| |
| if(dgLoc != null) { |
| DecimalFormat df = (DecimalFormat)NumberFormat.getCurrencyInstance(dgLoc); |
| DecimalFormatSymbols dfsNew = df.getDecimalFormatSymbols(); |
| DecimalFormatSymbols dfsOrig = to_fmt.getDecimalFormatSymbols(); |
| dfsOrig.setDecimalSeparator(dfsNew.getDecimalSeparator()); |
| dfsOrig.setMonetaryDecimalSeparator(dfsNew.getMonetaryDecimalSeparator()); |
| dfsOrig.setGroupingSeparator(dfsNew.getGroupingSeparator()); |
| to_fmt.setDecimalFormatSymbols(dfsOrig); |
| } |
| |
| // pattern overwrites locale format |
| if (pattern != null) { |
| to_fmt.applyPattern(pattern); |
| } |
| |
| if (value == null) { |
| numberValue = new Long(0); |
| } else { |
| try { |
| numberValue = from_fmt.parse(value); |
| if (int_currency > 0) { |
| numberValue = new Double(numberValue.doubleValue() / |
| int_currency); |
| } else { |
| // what? |
| } |
| } catch (ParseException pe) { |
| throw new SAXException( |
| this.getClass().getName() |
| + "i18n:number - parsing error.", pe |
| ); |
| } |
| } |
| |
| // we have all necessary data here: do formatting. |
| String result = to_fmt.format(numberValue); |
| if (appendDec) result = result + dec; |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("i18n:number result: " + result); |
| } |
| return result; |
| } |
| |
| //-- Dictionary handling routines |
| |
| /** |
| * Helper method to retrieve a message from the dictionary. |
| * |
| * @param catalogueID if not null, this catalogue will be used instead of the default one. |
| * @return SaxBuffer containing message, or null if not found. |
| */ |
| private ParamSaxBuffer getMessage(String catalogueID, String key) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Getting key " + key + " from catalogue " + catalogueID); |
| } |
| |
| CatalogueInfo catalogue = this.catalogue; |
| if (catalogueID != null) { |
| catalogue = (CatalogueInfo)catalogues.get(catalogueID); |
| if (catalogue == null) { |
| if (getLogger().isWarnEnabled()) { |
| getLogger().warn("Catalogue not found: " + catalogueID + |
| ", will not translate key " + key); |
| } |
| return null; |
| } |
| } |
| |
| Bundle bundle = catalogue.getCatalogue(); |
| if (bundle == null) { |
| // Can't translate |
| getLogger().debug("Untranslated key: '" + key + "'"); |
| return null; |
| } |
| |
| try { |
| return (ParamSaxBuffer) bundle.getObject(key); |
| } catch (MissingResourceException e) { |
| getLogger().debug("Untranslated key: '" + key + "'"); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Helper method to retrieve a message from the current dictionary. |
| * A default value is returned if message is not found. |
| * |
| * @return SaxBuffer containing message, or defaultValue if not found. |
| */ |
| private ParamSaxBuffer getMessage(String key, ParamSaxBuffer defaultValue) { |
| SaxBuffer value = getMessage(currentCatalogueId, key); |
| if (value == null) { |
| return defaultValue; |
| } |
| |
| return new ParamSaxBuffer(value); |
| } |
| |
| public void recycle() { |
| this.untranslatedRecorder = null; |
| this.catalogue = null; |
| this.objectModel = null; |
| |
| // Release catalogues which were selected for current locale |
| Iterator i = catalogues.values().iterator(); |
| while (i.hasNext()) { |
| CatalogueInfo catalogueInfo = (CatalogueInfo) i.next(); |
| catalogueInfo.releaseCatalog(); |
| } |
| |
| super.recycle(); |
| } |
| |
| public void dispose() { |
| if (manager != null) { |
| manager.release(factory); |
| } |
| factory = null; |
| manager = null; |
| catalogues = null; |
| } |
| |
| |
| /** |
| * Holds information about one catalogue. The location and name of the catalogue |
| * can contain references to input modules, and are resolved upon each transformer |
| * usage. It is important that releaseCatalog is called when the transformer is recycled. |
| */ |
| private final class CatalogueInfo { |
| VariableResolver name; |
| VariableResolver[] locations; |
| String resolvedName; |
| String[] resolvedLocations; |
| Bundle catalogue; |
| |
| public CatalogueInfo(String name, String[] locations) throws PatternException { |
| this.name = VariableResolverFactory.getResolver(name, manager); |
| this.locations = new VariableResolver[locations.length]; |
| for (int i=0; i < locations.length; ++i) { |
| this.locations[i] = VariableResolverFactory.getResolver(locations[i], manager); |
| } |
| } |
| |
| public String getName() { |
| try { |
| if (resolvedName == null) { |
| resolve(); |
| } |
| } catch (Exception e) { |
| // Ignore the error for now |
| } |
| return resolvedName; |
| } |
| |
| public String[] getLocation() { |
| try { |
| if (resolvedName == null) { |
| resolve(); |
| } |
| } catch (Exception e) { |
| // Ignore the error for now |
| } |
| return resolvedLocations; |
| } |
| |
| private void resolve() throws Exception { |
| if (resolvedLocations == null) { |
| resolvedLocations = new String[locations.length]; |
| for (int i=0; i < resolvedLocations.length; ++i) { |
| resolvedLocations[i] = locations[i].resolve(null, objectModel); |
| } |
| } |
| if (resolvedName == null) { |
| resolvedName = name.resolve(null, objectModel); |
| } |
| } |
| |
| public Bundle getCatalogue() { |
| if (catalogue == null) { |
| try { |
| resolve(); |
| catalogue = factory.select(resolvedLocations, resolvedName, locale); |
| } catch (Exception e) { |
| getLogger().error("Error obtaining catalogue '" + getName() + |
| "' from <" + getLocation() + "> for locale " + |
| locale, e); |
| } |
| } |
| |
| return catalogue; |
| } |
| |
| public void releaseCatalog() { |
| if (catalogue != null) { |
| factory.release(catalogue); |
| } |
| catalogue = null; |
| resolvedName = null; |
| resolvedLocations = null; |
| } |
| } |
| |
| } |