0001: /*
0002: * Licensed to the Apache Software Foundation (ASF) under one or more
0003: * contributor license agreements. See the NOTICE file distributed with
0004: * this work for additional information regarding copyright ownership.
0005: * The ASF licenses this file to You under the Apache License, Version 2.0
0006: * (the "License"); you may not use this file except in compliance with
0007: * the License. You may obtain a copy of the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS,
0013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014: * See the License for the specific language governing permissions and
0015: * limitations under the License.
0016: */
0017: package org.apache.cocoon.transformation;
0018:
0019: import org.apache.avalon.framework.activity.Disposable;
0020: import org.apache.avalon.framework.configuration.Configurable;
0021: import org.apache.avalon.framework.configuration.Configuration;
0022: import org.apache.avalon.framework.configuration.ConfigurationException;
0023: import org.apache.avalon.framework.parameters.Parameters;
0024: import org.apache.avalon.framework.service.ServiceException;
0025: import org.apache.avalon.framework.service.ServiceManager;
0026: import org.apache.avalon.framework.service.Serviceable;
0027:
0028: import org.apache.cocoon.ProcessingException;
0029: import org.apache.cocoon.caching.CacheableProcessingComponent;
0030: import org.apache.cocoon.components.treeprocessor.variables.VariableExpressionTokenizer;
0031: import org.apache.cocoon.components.treeprocessor.variables.VariableResolver;
0032: import org.apache.cocoon.components.treeprocessor.variables.VariableResolverFactory;
0033: import org.apache.cocoon.environment.SourceResolver;
0034: import org.apache.cocoon.i18n.Bundle;
0035: import org.apache.cocoon.i18n.BundleFactory;
0036: import org.apache.cocoon.i18n.I18nUtils;
0037: import org.apache.cocoon.sitemap.PatternException;
0038: import org.apache.cocoon.xml.ParamSaxBuffer;
0039: import org.apache.cocoon.xml.SaxBuffer;
0040:
0041: import org.apache.excalibur.source.SourceValidity;
0042: import org.xml.sax.Attributes;
0043: import org.xml.sax.SAXException;
0044: import org.xml.sax.helpers.AttributesImpl;
0045:
0046: import java.io.IOException;
0047: import java.text.DateFormat;
0048: import java.text.DecimalFormat;
0049: import java.text.DecimalFormatSymbols;
0050: import java.text.NumberFormat;
0051: import java.text.ParseException;
0052: import java.text.SimpleDateFormat;
0053: import java.util.Collections;
0054: import java.util.Date;
0055: import java.util.HashMap;
0056: import java.util.HashSet;
0057: import java.util.Iterator;
0058: import java.util.Locale;
0059: import java.util.Map;
0060: import java.util.MissingResourceException;
0061: import java.util.Set;
0062: import java.util.StringTokenizer;
0063:
0064: /**
0065: * @cocoon.sitemap.component.documentation
0066: * Internationalization transformer is used to transform i18n markup into text
0067: * based on a particular locale.
0068: *
0069: * @cocoon.sitemap.component.name i18n
0070: * @cocoon.sitemap.component.documentation.caching TBD
0071: * @cocoon.sitemap.component.logger sitemap.transformer.i18n
0072: *
0073: * <h3>I18n Transformer</h3>
0074: * <p>The i18n transformer works by finding a translation for the user's locale
0075: * in the configured catalogues. Locale is passed as parameter to the transformer,
0076: * and it can be determined based on the request, session, or a cookie data by
0077: * the {@link org.apache.cocoon.acting.LocaleAction}.</p>
0078: *
0079: * <p>For the passed local it then attempts to find a message catalogue that
0080: * satisifies the locale, and uses it for for processing text replacement
0081: * directed by i18n markup.</p>
0082: *
0083: * <p>Message catalogues are maintained in separate files, with a naming
0084: * convention similar to that of {@link java.util.ResourceBundle}. I.e.
0085: * <code>basename_locale</code>, where <i>basename</i> can be any name,
0086: * and <i>locale</i> can be any locale specified using ISO 639/3166
0087: * characters (eg. <code>en_AU</code>, <code>de_AT</code>, <code>es</code>).</p>
0088: *
0089: * <p><strong>NOTE:</strong> ISO 639 is not a stable standard; some of the
0090: * language codes it defines (specifically, iw, ji, and in) have changed
0091: * (see {@link java.util.Locale} for details).
0092: *
0093: * <h3>Message Catalogues</h3>
0094: * <p>Catalogues are of the following format:
0095: * <pre>
0096: * <?xml version="1.0"?>
0097: * <!-- message catalogue file for locale ... -->
0098: * <catalogue xml:lang="locale">
0099: * <message key="key">text <i>or</i> markup</message>
0100: * ....
0101: * </catalogue>
0102: * </pre>
0103: * Where <code>key</code> specifies a particular message for that
0104: * language.
0105: *
0106: * <h3>Usage</h3>
0107: * <p>Files to be translated contain the following markup:
0108: * <pre>
0109: * <?xml version="1.0"?>
0110: * ... some text, translate <i18n:text>key</i18n:text>
0111: * </pre>
0112: * At runtime, the i18n transformer will find a message catalogue for the
0113: * user's locale, and will appropriately replace the text between the
0114: * <code><i18n:text></code> markup, using the value between the tags as
0115: * the lookup key.</p>
0116: *
0117: * <p>If the i18n transformer cannot find an appropriate message catalogue for
0118: * the user's given locale, it will recursively try to locate a <i>parent</i>
0119: * message catalogue, until a valid catalogue can be found.
0120: * ie:
0121: * <ul>
0122: * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>_<i>variant</i>.xml
0123: * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>.xml
0124: * <li><strong>catalogue</strong>_<i>language</i>.xml
0125: * <li><strong>catalogue</strong>.xml
0126: * </ul>
0127: * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i>
0128: * (no variant), the following search will occur:
0129: * <ul>
0130: * <li><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
0131: * <li><strong>messages</strong>_<i>en</i>.xml
0132: * <li><strong>messages</strong>.xml
0133: * </ul>
0134: * This allows the developer to write a hierarchy of message catalogues,
0135: * at each defining messages with increasing depth of variation.</p>
0136: *
0137: * <p>In addition, catalogues can be split across multiple locations. For example,
0138: * there can be a default catalogue in one directory with a user or client specific
0139: * catalogue in another directory. The catalogues will be searched in the order of
0140: * the locations specified still following the locale ordering specified above.
0141: * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i>
0142: * (no variant) and locations of <i>translations/client</i> and <i>translations</i>,
0143: * the following search will occur:
0144: * <ul>
0145: * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
0146: * <li><i>translations/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml
0147: * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>.xml
0148: * <li><i>translations/</i><strong>messages</strong>_<i>en</i.xml
0149: * <li><i>translations/client/</i><strong>messages</strong>.xml
0150: * <li><i>translations/</i><strong>messages</strong>.xml
0151: * </ul>
0152: * </p>
0153: *
0154: * <p>The <code>i18n:text</code> element can optionally take an attribute
0155: * <code>i18n:catalogue</code> to indicate which specific catalogue to use.
0156: * The value of this attribute should be the id of the catalogue to use
0157: * (see sitemap configuration).
0158: *
0159: * <h3>Sitemap Configuration</h3>
0160: * <pre>
0161: * <map:transformer name="i18n"
0162: * src="org.apache.cocoon.transformation.I18nTransformer">
0163: *
0164: * <catalogues default="someId">
0165: * <catalogue id="someId" name="messages" [location="translations"]>
0166: * [<location>translations/client</location>]
0167: * [<location>translations</location>]
0168: * </catalogue>
0169: * ...
0170: * </catalogues>
0171: * <untranslated-text>untranslated</untranslated-text>
0172: * <preload>en_US</preload>
0173: * <preload catalogue="someId">fr_CA</preload>
0174: * </map:transformer>
0175: * </pre>
0176: * Where:
0177: * <ul>
0178: * <li><strong>catalogues</strong>: container element in which the catalogues
0179: * are defined. It must have an attribute 'default' whose value is one
0180: * of the id's of the catalogue elements. (<i>mandatory</i>).
0181: * <li><strong>catalogue</strong>: specifies a catalogue. It takes 2 required
0182: * attributes: id (can be wathever you like) and name (base name of the catalogue).
0183: * The location (location of the message catalogue) is also required, but can be
0184: * specified either as an attribute or as one or more subelements, but not both.
0185: * If more than one location is specified the catalogues will be searched in the
0186: * order they appear in the configuration. The name and location can contain
0187: * references to inputmodules (same syntax as in other places in the
0188: * sitemap). They are resolved on each usage of the transformer, so they can
0189: * refer to e.g. request parameters. (<i>at least 1 catalogue
0190: * element required</i>). After input module references are resolved the location
0191: * string can be the root of a URI. For example, specifying a location of
0192: * cocoon:/test with a name of messages and a locale of en_GB will cause the
0193: * sitemap to try to process cocoon:/test/messages_en_GB.xml,
0194: * cocoon:/test/messages_en.xml and cocoon:/test/messages.xml.
0195: * <li><strong>untranslated-text</strong>: text used for
0196: * untranslated keys (default is to output the key name).
0197: * <li><strong>preload</strong>: locale of the catalogue to preload. Will attempt
0198: * to resolve all configured catalogues for specified locale. If optional
0199: * <code>catalogue</code> attribute is present, will preload only specified
0200: * catalogue. Multiple <code>preload</code> elements can be specified.
0201: * </ul>
0202: *
0203: * <h3>Pipeline Usage</h3>
0204: * <p>To use the transformer in a pipeline, simply specify it in a particular
0205: * transform, and pass locale parameter:
0206: * <pre>
0207: * <map:match pattern="file">
0208: * <map:generate src="file.xml"/>
0209: * <map:transform type="i18n">
0210: * <map:parameter name="locale" value="..."/>
0211: * </map:transform>
0212: * <map:serialize/>
0213: * </map:match>
0214: * </pre>
0215: * You can use {@link org.apache.cocoon.acting.LocaleAction} or any other
0216: * way to provide transformer with a locale.</p>
0217: *
0218: * <p>If in certain pipeline, you want to use a different catalogue as the
0219: * default catalogue, you can do so by specifying a parameter called
0220: * <strong>default-catalogue-id</strong>.
0221: *
0222: * <p>The <strong>untranslated-text</strong> can also be overridden at the
0223: * pipeline level by specifying it as a parameter.</p>
0224: *
0225: *
0226: * <h3>i18n markup</h3>
0227: *
0228: * <p>For date, time and number formatting use the following tags:
0229: * <ul>
0230: * <li><strong><i18n:date/></strong> gives localized date.</li>
0231: * <li><strong><i18n:date-time/></strong> gives localized date and time.</li>
0232: * <li><strong><i18n:time/></strong> gives localized time.</li>
0233: * <li><strong><i18n:number/></strong> gives localized number.</li>
0234: * <li><strong><i18n:currency/></strong> gives localized currency.</li>
0235: * <li><strong><i18n:percent/></strong> gives localized percent.</li>
0236: * </ul>
0237: * Elements <code>date</code>, <code>date-time</code> and <code>time</code>
0238: * accept <code>pattern</code> and <code>src-pattern</code> attribute, with
0239: * values of:
0240: * <ul>
0241: * <li><code>short</code>
0242: * <li><code>medium</code>
0243: * <li><code>long</code>
0244: * <li><code>full</code>
0245: * </ul>
0246: * See {@link java.text.DateFormat} for more info on these values.</p>
0247: *
0248: * <p>Elements <code>date</code>, <code>date-time</code>, <code>time</code> and
0249: * <code>number</code>, a different <code>locale</code> and
0250: * <code>source-locale</code> can be specified:
0251: * <pre>
0252: * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE">
0253: * 12/24/01
0254: * </i18n:date>
0255: * </pre>
0256: * Will result in 24.12.2001.</p>
0257: *
0258: * <p>A given real <code>pattern</code> and <code>src-pattern</code> (not
0259: * keywords <code>short, medium, long, full</code>) overrides any value
0260: * specified by <code>locale</code> and <code>src-locale</code> attributes.</p>
0261: *
0262: * <p>Future work coming:
0263: * <ul>
0264: * <li>Introduce new <get-locale/> element
0265: * <li>Move all formatting routines to I18nUtils
0266: * </ul>
0267: *
0268: * @author <a href="mailto:kpiroumian@apache.org">Konstantin Piroumian</a>
0269: * @author <a href="mailto:mattam@netcourrier.com">Matthieu Sozeau</a>
0270: * @author <a href="mailto:crafterm@apache.org">Marcus Crafter</a>
0271: * @author <a href="mailto:Michael.Enke@wincor-nixdorf.com">Michael Enke</a>
0272: * @version $Id: I18nTransformer.java 474832 2006-11-14 15:56:43Z vgritsenko $
0273: */
0274: public class I18nTransformer extends AbstractTransformer implements
0275: CacheableProcessingComponent, Serviceable, Configurable,
0276: Disposable {
0277:
0278: /**
0279: * The namespace for i18n is "http://apache.org/cocoon/i18n/2.1".
0280: */
0281: public static final String I18N_NAMESPACE_URI = I18nUtils.NAMESPACE_URI;
0282:
0283: /**
0284: * The old namespace for i18n is "http://apache.org/cocoon/i18n/2.0".
0285: */
0286: public static final String I18N_OLD_NAMESPACE_URI = I18nUtils.OLD_NAMESPACE_URI;
0287:
0288: //
0289: // i18n elements
0290: //
0291:
0292: /**
0293: * <code>i18n:text</code> element is used to translate any text, with
0294: * or without markup. Example:
0295: * <pre>
0296: * <i18n:text>
0297: * This is <strong>translated</strong> string.
0298: * </i18n:text>
0299: * </pre>
0300: */
0301: public static final String I18N_TEXT_ELEMENT = "text";
0302:
0303: /**
0304: * <code>i18n:translate</code> element is used to translate text with
0305: * parameter substitution. Example:
0306: * <pre>
0307: * <i18n:translate>
0308: * <i18n:text>This is translated string with {0} param</i18n:text>
0309: * <i18n:param>1</i18n:param>
0310: * </i18n:translate>
0311: * </pre>
0312: * The <code>i18n:text</code> fragment can include markup and parameters
0313: * at any place. Also do parameters, which can include <code>i18n:text</code>,
0314: * <code>i18n:date</code>, etc. elements (without keys only).
0315: *
0316: * @see #I18N_TEXT_ELEMENT
0317: * @see #I18N_PARAM_ELEMENT
0318: */
0319: public static final String I18N_TRANSLATE_ELEMENT = "translate";
0320:
0321: /**
0322: * <code>i18n:choose</code> element is used to translate elements in-place.
0323: * The first <code>i18n:when</code> element matching the current locale
0324: * is selected and the others are discarded.
0325: *
0326: * <p>To specify what to do if no locale matched, simply add a node with
0327: * <code>locale="*"</code>. <em>Note that this element must be the last
0328: * child of <i18n:choose>.</em></p>
0329: * <pre>
0330: * <i18n:choose>
0331: * <i18n:when locale="en">
0332: * Good Morning
0333: * </en>
0334: * <i18n:when locale="fr">
0335: * Bonjour
0336: * </jp>
0337: * <i18n:when locale="jp">
0338: * Aligato?
0339: * </jp>
0340: * <i18n:otherwise>
0341: * Sorry, i don't know how to say hello in your language
0342: * </jp>
0343: * <i18n:translate>
0344: * </pre>
0345: * <p>You can include any markup within <code>i18n:when</code> elements,
0346: * with the exception of other <code>i18n:*</code> elements.</p>
0347: *
0348: * @see #I18N_IF_ELEMENT
0349: * @see #I18N_LOCALE_ATTRIBUTE
0350: * @since 2.1
0351: */
0352: public static final String I18N_CHOOSE_ELEMENT = "choose";
0353:
0354: /**
0355: * <code>i18n:when</code> is used to test a locale.
0356: * It can be used within <code>i18:choose</code> elements or alone.
0357: * <em>Note: Using <code>locale="*"</code> here has no sense.</em>
0358: * Example:
0359: * <pre>
0360: * <greeting>
0361: * <i18n:when locale="en">Hello</i18n:when>
0362: * <i18n:when locale="fr">Bonjour</i18n:when>
0363: * </greeting>
0364: * </pre>
0365: *
0366: * @see #I18N_LOCALE_ATTRIBUTE
0367: * @see #I18N_CHOOSE_ELEMENT
0368: * @since 2.1
0369: */
0370: public static final String I18N_WHEN_ELEMENT = "when";
0371:
0372: /**
0373: * <code>i18n:if</code> is used to test a locale. Example:
0374: * <pre>
0375: * <greeting>
0376: * <i18n:if locale="en">Hello</i18n:when>
0377: * <i18n:if locale="fr">Bonjour</i18n:when>
0378: * </greeting>
0379: * </pre>
0380: *
0381: * @see #I18N_LOCALE_ATTRIBUTE
0382: * @see #I18N_CHOOSE_ELEMENT
0383: * @see #I18N_WHEN_ELEMENT
0384: * @since 2.1
0385: */
0386: public static final String I18N_IF_ELEMENT = "if";
0387:
0388: /**
0389: * <code>i18n:otherwise</code> is used to match any locale when
0390: * no matching locale has been found inside an <code>i18n:choose</code>
0391: * block.
0392: *
0393: * @see #I18N_CHOOSE_ELEMENT
0394: * @see #I18N_WHEN_ELEMENT
0395: * @since 2.1
0396: */
0397: public static final String I18N_OTHERWISE_ELEMENT = "otherwise";
0398:
0399: /**
0400: * <code>i18n:param</code> is used with i18n:translate to provide
0401: * substitution params. The param can have <code>i18n:text</code> as
0402: * its value to provide multilungual value. Parameters can have
0403: * additional attributes to be used for formatting:
0404: * <ul>
0405: * <li><code>type</code>: can be <code>date, date-time, time,
0406: * number, currency, currency-no-unit or percent</code>.
0407: * Used to format params before substitution.</li>
0408: * <li><code>value</code>: the value of the param. If no value is
0409: * specified then the text inside of the param element will be used.</li>
0410: * <li><code>locale</code>: used only with <code>number, date, time,
0411: * date-time</code> types and used to override the current locale to
0412: * format the given value.</li>
0413: * <li><code>src-locale</code>: used with <code>number, date, time,
0414: * date-time</code> types and specify the locale that should be used to
0415: * parse the given value.</li>
0416: * <li><code>pattern</code>: used with <code>number, date, time,
0417: * date-time</code> types and specify the pattern that should be used
0418: * to format the given value.</li>
0419: * <li><code>src-pattern</code>: used with <code>number, date, time,
0420: * date-time</code> types and specify the pattern that should be used
0421: * to parse the given value.</li>
0422: * </ul>
0423: *
0424: * @see #I18N_TRANSLATE_ELEMENT
0425: * @see #I18N_DATE_ELEMENT
0426: * @see #I18N_TIME_ELEMENT
0427: * @see #I18N_DATE_TIME_ELEMENT
0428: * @see #I18N_NUMBER_ELEMENT
0429: */
0430: public static final String I18N_PARAM_ELEMENT = "param";
0431:
0432: /**
0433: * This attribute affects a name to the param that could be used
0434: * for substitution.
0435: *
0436: * @since 2.1
0437: */
0438: public static final String I18N_PARAM_NAME_ATTRIBUTE = "name";
0439:
0440: /**
0441: * <code>i18n:date</code> is used to provide a localized date string.
0442: * Allowed attributes are: <code>pattern, src-pattern, locale,
0443: * src-locale</code>. Usage examples:
0444: * <pre>
0445: * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE">
0446: * 12/24/01
0447: * </i18n:date>
0448: *
0449: * <i18n:date pattern="dd/MM/yyyy" />
0450: * </pre>
0451: *
0452: * If no value is specified then the current date will be used. E.g.:
0453: * <pre>
0454: * <i18n:date />
0455: * </pre>
0456: * Displays the current date formatted with default pattern for
0457: * the current locale.
0458: *
0459: * @see #I18N_PARAM_ELEMENT
0460: * @see #I18N_DATE_TIME_ELEMENT
0461: * @see #I18N_TIME_ELEMENT
0462: * @see #I18N_NUMBER_ELEMENT
0463: */
0464: public static final String I18N_DATE_ELEMENT = "date";
0465:
0466: /**
0467: * <code>i18n:date-time</code> is used to provide a localized date and
0468: * time string. Allowed attributes are: <code>pattern, src-pattern,
0469: * locale, src-locale</code>. Usage examples:
0470: * <pre>
0471: * <i18n:date-time src-pattern="short" src-locale="en_US" locale="de_DE">
0472: * 12/24/01 1:00 AM
0473: * </i18n:date>
0474: *
0475: * <i18n:date-time pattern="dd/MM/yyyy hh:mm" />
0476: * </pre>
0477: *
0478: * If no value is specified then the current date and time will be used.
0479: * E.g.:
0480: * <pre>
0481: * <i18n:date-time />
0482: * </pre>
0483: * Displays the current date formatted with default pattern for
0484: * the current locale.
0485: *
0486: * @see #I18N_PARAM_ELEMENT
0487: * @see #I18N_DATE_ELEMENT
0488: * @see #I18N_TIME_ELEMENT
0489: * @see #I18N_NUMBER_ELEMENT
0490: */
0491: public static final String I18N_DATE_TIME_ELEMENT = "date-time";
0492:
0493: /**
0494: * <code>i18n:time</code> is used to provide a localized time string.
0495: * Allowed attributes are: <code>pattern, src-pattern, locale,
0496: * src-locale</code>. Usage examples:
0497: * <pre>
0498: * <i18n:time src-pattern="short" src-locale="en_US" locale="de_DE">
0499: * 1:00 AM
0500: * </i18n:time>
0501: *
0502: * <i18n:time pattern="hh:mm:ss" />
0503: * </pre>
0504: *
0505: * If no value is specified then the current time will be used. E.g.:
0506: * <pre>
0507: * <i18n:time />
0508: * </pre>
0509: * Displays the current time formatted with default pattern for
0510: * the current locale.
0511: *
0512: * @see #I18N_PARAM_ELEMENT
0513: * @see #I18N_DATE_TIME_ELEMENT
0514: * @see #I18N_DATE_ELEMENT
0515: * @see #I18N_NUMBER_ELEMENT
0516: */
0517: public static final String I18N_TIME_ELEMENT = "time";
0518:
0519: /**
0520: * <code>i18n:number</code> is used to provide a localized number string.
0521: * Allowed attributes are: <code>pattern, src-pattern, locale, src-locale,
0522: * type</code>. Usage examples:
0523: * <pre>
0524: * <i18n:number src-pattern="short" src-locale="en_US" locale="de_DE">
0525: * 1000.0
0526: * </i18n:number>
0527: *
0528: * <i18n:number type="currency" />
0529: * </pre>
0530: *
0531: * If no value is specifies then 0 will be used.
0532: *
0533: * @see #I18N_PARAM_ELEMENT
0534: * @see #I18N_DATE_TIME_ELEMENT
0535: * @see #I18N_TIME_ELEMENT
0536: * @see #I18N_DATE_ELEMENT
0537: */
0538: public static final String I18N_NUMBER_ELEMENT = "number";
0539:
0540: /**
0541: * Currency element name
0542: */
0543: public static final String I18N_CURRENCY_ELEMENT = "currency";
0544:
0545: /**
0546: * Percent element name
0547: */
0548: public static final String I18N_PERCENT_ELEMENT = "percent";
0549:
0550: /**
0551: * Integer currency element name
0552: */
0553: public static final String I18N_INT_CURRENCY_ELEMENT = "int-currency";
0554:
0555: /**
0556: * Currency without unit element name
0557: */
0558: public static final String I18N_CURRENCY_NO_UNIT_ELEMENT = "currency-no-unit";
0559:
0560: /**
0561: * Integer currency without unit element name
0562: */
0563: public static final String I18N_INT_CURRENCY_NO_UNIT_ELEMENT = "int-currency-no-unit";
0564:
0565: //
0566: // i18n general attributes
0567: //
0568:
0569: /**
0570: * This attribute is used with i18n:text element to indicate the key of
0571: * the according message. The character data of the element will be used
0572: * if no message is found by this key. E.g.:
0573: * <pre>
0574: * <i18n:text i18n:key="a_key">article_text1</i18n:text>
0575: * </pre>
0576: */
0577: public static final String I18N_KEY_ATTRIBUTE = "key";
0578:
0579: /**
0580: * This attribute is used with <strong>any</strong> element (even not i18n)
0581: * to translate attribute values. Should contain whitespace separated
0582: * attribute names that should be translated:
0583: * <pre>
0584: * <para title="first" name="article" i18n:attr="title name"/>
0585: * </pre>
0586: * Attribute value considered as key in message catalogue.
0587: */
0588: public static final String I18N_ATTR_ATTRIBUTE = "attr";
0589:
0590: /**
0591: * This attribute is used with <strong>any</strong> element (even not i18n)
0592: * to evaluate attribute values. Should contain whitespace separated
0593: * attribute names that should be evaluated:
0594: * <pre>
0595: * <para title="first" name="{one} {two}" i18n:attr="name"/>
0596: * </pre>
0597: * Attribute value considered as expression containing text and catalogue
0598: * keys in curly braces.
0599: */
0600: public static final String I18N_EXPR_ATTRIBUTE = "expr";
0601:
0602: //
0603: // i18n number and date formatting attributes
0604: //
0605:
0606: /**
0607: * This attribute is used with date and number formatting elements to
0608: * indicate the pattern that should be used to parse the element value.
0609: *
0610: * @see #I18N_PARAM_ELEMENT
0611: * @see #I18N_DATE_TIME_ELEMENT
0612: * @see #I18N_DATE_ELEMENT
0613: * @see #I18N_TIME_ELEMENT
0614: * @see #I18N_NUMBER_ELEMENT
0615: */
0616: public static final String I18N_SRC_PATTERN_ATTRIBUTE = "src-pattern";
0617:
0618: /**
0619: * This attribute is used with date and number formatting elements to
0620: * indicate the pattern that should be used to format the element value.
0621: *
0622: * @see #I18N_PARAM_ELEMENT
0623: * @see #I18N_DATE_TIME_ELEMENT
0624: * @see #I18N_DATE_ELEMENT
0625: * @see #I18N_TIME_ELEMENT
0626: * @see #I18N_NUMBER_ELEMENT
0627: */
0628: public static final String I18N_PATTERN_ATTRIBUTE = "pattern";
0629:
0630: /**
0631: * This attribute is used with date and number formatting elements to
0632: * indicate the locale that should be used to format the element value.
0633: * Also used for in-place translations.
0634: *
0635: * @see #I18N_PARAM_ELEMENT
0636: * @see #I18N_DATE_TIME_ELEMENT
0637: * @see #I18N_DATE_ELEMENT
0638: * @see #I18N_TIME_ELEMENT
0639: * @see #I18N_NUMBER_ELEMENT
0640: * @see #I18N_WHEN_ELEMENT
0641: */
0642: public static final String I18N_LOCALE_ATTRIBUTE = "locale";
0643:
0644: /**
0645: * This attribute is used with date and number formatting elements to
0646: * indicate the locale that should be used to parse the element value.
0647: *
0648: * @see #I18N_PARAM_ELEMENT
0649: * @see #I18N_DATE_TIME_ELEMENT
0650: * @see #I18N_DATE_ELEMENT
0651: * @see #I18N_TIME_ELEMENT
0652: * @see #I18N_NUMBER_ELEMENT
0653: */
0654: public static final String I18N_SRC_LOCALE_ATTRIBUTE = "src-locale";
0655:
0656: /**
0657: * This attribute is used with date and number formatting elements to
0658: * indicate the value that should be parsed and formatted. If value
0659: * attribute is not used then the character data of the element will be used.
0660: *
0661: * @see #I18N_PARAM_ELEMENT
0662: * @see #I18N_DATE_TIME_ELEMENT
0663: * @see #I18N_DATE_ELEMENT
0664: * @see #I18N_TIME_ELEMENT
0665: * @see #I18N_NUMBER_ELEMENT
0666: */
0667: public static final String I18N_VALUE_ATTRIBUTE = "value";
0668:
0669: /**
0670: * This attribute is used with <code>i18:param</code> to
0671: * indicate the parameter type: <code>date, time, date-time</code> or
0672: * <code>number, currency, percent, int-currency, currency-no-unit,
0673: * int-currency-no-unit</code>.
0674: * Also used with <code>i18:translate</code> to indicate inplace
0675: * translations: <code>inplace</code>
0676: * @deprecated since 2.1. Use nested tags instead, e.g.:
0677: * <i18n:param><i18n:date/></i18n:param>
0678: */
0679: public static final String I18N_TYPE_ATTRIBUTE = "type";
0680:
0681: /**
0682: * This attribute is used to specify a different locale for the
0683: * currency. When specified, this locale will be combined with
0684: * the "normal" locale: e.g. the seperator symbols are taken from
0685: * the normal locale but the currency symbol and possition will
0686: * be taken from the currency locale.
0687: * This enables to see a currency formatted for Euro but with US
0688: * grouping and decimal char.
0689: */
0690: public static final String CURRENCY_LOCALE_ATTRIBUTE = "currency";
0691:
0692: /**
0693: * This attribute can be used on <code>i18n:text</code> to indicate the catalogue
0694: * from which the key should be retrieved. This attribute is optional,
0695: * if it is not mentioned the default catalogue is used.
0696: */
0697: public static final String I18N_CATALOGUE_ATTRIBUTE = "catalogue";
0698:
0699: //
0700: // Configuration parameters
0701: //
0702:
0703: /**
0704: * This configuration parameter specifies the default locale to be used.
0705: */
0706: public static final String I18N_LOCALE = "locale";
0707:
0708: /**
0709: * This configuration parameter specifies the id of the catalogue to be used as
0710: * default catalogue, allowing to redefine the default catalogue on the pipeline
0711: * level.
0712: */
0713: public static final String I18N_DEFAULT_CATALOGUE_ID = "default-catalogue-id";
0714:
0715: /**
0716: * This configuration parameter specifies the message that should be
0717: * displayed in case of a not translated text (message not found).
0718: */
0719: public static final String I18N_UNTRANSLATED = "untranslated-text";
0720:
0721: /**
0722: * This configuration parameter specifies locale for which catalogues should
0723: * be preloaded.
0724: */
0725: public static final String I18N_PRELOAD = "preload";
0726:
0727: /**
0728: * <code>fraction-digits</code> attribute is used with
0729: * <code>i18:number</code> to
0730: * indicate the number of digits behind the fraction
0731: */
0732: public static final String I18N_FRACTION_DIGITS_ATTRIBUTE = "fraction-digits";
0733:
0734: //
0735: // States of the transformer
0736: //
0737:
0738: private static final int STATE_OUTSIDE = 0;
0739: private static final int STATE_INSIDE_TEXT = 10;
0740: private static final int STATE_INSIDE_PARAM = 20;
0741: private static final int STATE_INSIDE_TRANSLATE = 30;
0742: private static final int STATE_INSIDE_CHOOSE = 50;
0743: private static final int STATE_INSIDE_WHEN = 51;
0744: private static final int STATE_INSIDE_OTHERWISE = 52;
0745: private static final int STATE_INSIDE_DATE = 60;
0746: private static final int STATE_INSIDE_DATE_TIME = 61;
0747: private static final int STATE_INSIDE_TIME = 62;
0748: private static final int STATE_INSIDE_NUMBER = 63;
0749:
0750: // All date-time related parameter types and element names
0751: private static final Set dateTypes;
0752:
0753: // All number related parameter types and element names
0754: private static final Set numberTypes;
0755:
0756: // Date pattern types map: short, medium, long, full
0757: private static final Map datePatterns;
0758:
0759: static {
0760: // initialize date types set
0761: HashSet set = new HashSet(5);
0762: set.add(I18N_DATE_ELEMENT);
0763: set.add(I18N_TIME_ELEMENT);
0764: set.add(I18N_DATE_TIME_ELEMENT);
0765: dateTypes = Collections.unmodifiableSet(set);
0766:
0767: // initialize number types set
0768: set = new HashSet(9);
0769: set.add(I18N_NUMBER_ELEMENT);
0770: set.add(I18N_PERCENT_ELEMENT);
0771: set.add(I18N_CURRENCY_ELEMENT);
0772: set.add(I18N_INT_CURRENCY_ELEMENT);
0773: set.add(I18N_CURRENCY_NO_UNIT_ELEMENT);
0774: set.add(I18N_INT_CURRENCY_NO_UNIT_ELEMENT);
0775: numberTypes = Collections.unmodifiableSet(set);
0776:
0777: // Initialize date patterns map
0778: Map map = new HashMap(7);
0779: map.put("SHORT", new Integer(DateFormat.SHORT));
0780: map.put("MEDIUM", new Integer(DateFormat.MEDIUM));
0781: map.put("LONG", new Integer(DateFormat.LONG));
0782: map.put("FULL", new Integer(DateFormat.FULL));
0783: datePatterns = Collections.unmodifiableMap(map);
0784: }
0785:
0786: //
0787: // Global configuration variables
0788: //
0789:
0790: /**
0791: * Component (service) manager
0792: */
0793: protected ServiceManager manager;
0794:
0795: /**
0796: * Message bundle loader factory component (service)
0797: */
0798: protected BundleFactory factory;
0799:
0800: /**
0801: * All catalogues (keyed by catalogue id). The values are instances
0802: * of {@link CatalogueInfo}.
0803: */
0804: private Map catalogues;
0805:
0806: /**
0807: * Default (global) catalogue
0808: */
0809: private CatalogueInfo defaultCatalogue;
0810:
0811: /**
0812: * Default (global) untranslated message value
0813: */
0814: private String defaultUntranslated;
0815:
0816: //
0817: // Local configuration variables
0818: //
0819:
0820: protected Map objectModel;
0821:
0822: /**
0823: * Locale
0824: */
0825: protected Locale locale;
0826:
0827: /**
0828: * Catalogue (local)
0829: */
0830: private CatalogueInfo catalogue;
0831:
0832: /**
0833: * Current (local) untranslated message value
0834: */
0835: private String untranslated;
0836:
0837: /**
0838: * {@link SaxBuffer} containing the contents of {@link #untranslated}.
0839: */
0840: private ParamSaxBuffer untranslatedRecorder;
0841:
0842: //
0843: // Current state of the transformer
0844: //
0845:
0846: /**
0847: * Current state of the transformer. Default value is STATE_OUTSIDE.
0848: */
0849: private int current_state;
0850:
0851: /**
0852: * Previous state of the transformer.
0853: * Used in text translation inside params and translate elements.
0854: */
0855: private int prev_state;
0856:
0857: /**
0858: * The i18n:key attribute is stored for the current element.
0859: * If no translation found for the key then the character data of element is
0860: * used as default value.
0861: */
0862: private String currentKey;
0863:
0864: /**
0865: * Contains the id of the current catalogue if it was explicitely mentioned
0866: * on an i18n:text element, otherwise it is null.
0867: */
0868: private String currentCatalogueId;
0869:
0870: /**
0871: * Character data buffer. used to concat chunked character data
0872: */
0873: private StringBuffer strBuffer;
0874:
0875: /**
0876: * A flag for copying the node when doing in-place translation
0877: */
0878: private boolean translate_copy;
0879:
0880: // A flag for copying the _GOOD_ node and not others
0881: // when doing in-place translation within i18n:choose
0882: private boolean translate_end;
0883:
0884: // Translated text. Inside i18n:translate, collects character events.
0885: private ParamSaxBuffer tr_text_recorder;
0886:
0887: // Current "i18n:text" events
0888: private ParamSaxBuffer text_recorder;
0889:
0890: // Current parameter events
0891: private SaxBuffer param_recorder;
0892:
0893: // Param count when not using i18n:param name="..."
0894: private int param_count;
0895:
0896: // Param name attribute for substitution.
0897: private String param_name;
0898:
0899: // i18n:param's hashmap for substitution
0900: private HashMap indexedParams;
0901:
0902: // Current parameter value (translated or not)
0903: private String param_value;
0904:
0905: // Date and number elements and params formatting attributes with values.
0906: private HashMap formattingParams;
0907:
0908: /**
0909: * Returns the current locale setting of this transformer instance.
0910: * @return current Locale object
0911: */
0912: public Locale getLocale() {
0913: return this .locale;
0914: }
0915:
0916: /**
0917: * Implemenation of CacheableProcessingComponents.
0918: * Generates unique key for the current locale.
0919: */
0920: public java.io.Serializable getKey() {
0921: // TODO: Key should be composed out of used catalogues locations, and locale.
0922: // Right now it is hardcoded only to default catalogue location.
0923: StringBuffer key = new StringBuffer();
0924: if (catalogue != null) {
0925: key.append(catalogue.getLocation()[0]);
0926: }
0927: key.append("?");
0928: if (locale != null) {
0929: key.append(locale.getLanguage());
0930: key.append("_");
0931: key.append(locale.getCountry());
0932: key.append("_");
0933: key.append(locale.getVariant());
0934: }
0935: return key.toString();
0936: }
0937:
0938: /**
0939: * Implementation of CacheableProcessingComponent.
0940: * Generates validity object for this transformer or <code>null</code>
0941: * if this instance is not cacheable.
0942: */
0943: public SourceValidity getValidity() {
0944: // FIXME (KP): Cache validity should be generated by
0945: // Bundle implementations.
0946: return org.apache.excalibur.source.impl.validity.NOPValidity.SHARED_INSTANCE;
0947: }
0948:
0949: /**
0950: * Look up the {@link BundleFactory} to be used.
0951: */
0952: public void service(ServiceManager manager) throws ServiceException {
0953: this .manager = manager;
0954: try {
0955: this .factory = (BundleFactory) manager
0956: .lookup(BundleFactory.ROLE);
0957: } catch (ServiceException e) {
0958: getLogger().debug(
0959: "Failed to lookup <" + BundleFactory.ROLE + ">", e);
0960: throw e;
0961: }
0962: }
0963:
0964: /**
0965: * Implementation of Configurable interface.
0966: * Configure this transformer.
0967: */
0968: public void configure(Configuration conf)
0969: throws ConfigurationException {
0970: // Read in the config options from the transformer definition
0971: Configuration cataloguesConf = conf.getChild("catalogues",
0972: false);
0973: if (cataloguesConf == null) {
0974: throw new ConfigurationException(
0975: "Required <catalogues> configuration is missing.",
0976: conf);
0977: }
0978:
0979: // new configuration style
0980: Configuration[] catalogueConfs = cataloguesConf
0981: .getChildren("catalogue");
0982: catalogues = new HashMap(catalogueConfs.length + 3);
0983: for (int i = 0; i < catalogueConfs.length; i++) {
0984: String id = catalogueConfs[i].getAttribute("id");
0985: String name = catalogueConfs[i].getAttribute("name");
0986:
0987: String[] locations;
0988: String location = catalogueConfs[i].getAttribute(
0989: "location", null);
0990: Configuration[] locationConf = catalogueConfs[i]
0991: .getChildren("location");
0992: if (location != null) {
0993: if (locationConf.length > 0) {
0994: String msg = "Location attribute cannot be "
0995: + "specified with location elements";
0996: getLogger().error(msg);
0997: throw new ConfigurationException(msg,
0998: catalogueConfs[i]);
0999: }
1000:
1001: if (getLogger().isDebugEnabled()) {
1002: getLogger().debug(
1003: "name=" + name + ", location=" + location);
1004: }
1005: locations = new String[1];
1006: locations[0] = location;
1007: } else {
1008: if (locationConf.length == 0) {
1009: String msg = "A location attribute or location "
1010: + "elements must be specified";
1011: getLogger().error(msg);
1012: throw new ConfigurationException(msg,
1013: catalogueConfs[i]);
1014: }
1015:
1016: locations = new String[locationConf.length];
1017: for (int j = 0; j < locationConf.length; ++j) {
1018: locations[j] = locationConf[j].getValue();
1019: if (getLogger().isDebugEnabled()) {
1020: getLogger().debug(
1021: "name=" + name + ", location="
1022: + locations[j]);
1023: }
1024: }
1025: }
1026:
1027: CatalogueInfo catalogueInfo;
1028: try {
1029: catalogueInfo = new CatalogueInfo(name, locations);
1030: } catch (PatternException e) {
1031: throw new ConfigurationException(
1032: "Error in name or location attribute on catalogue "
1033: + "element with id " + id,
1034: catalogueConfs[i], e);
1035: }
1036: catalogues.put(id, catalogueInfo);
1037: }
1038:
1039: String defaultCatalogueId = cataloguesConf
1040: .getAttribute("default");
1041: defaultCatalogue = (CatalogueInfo) catalogues
1042: .get(defaultCatalogueId);
1043: if (defaultCatalogue == null) {
1044: throw new ConfigurationException("Default catalogue id '"
1045: + defaultCatalogueId
1046: + "' denotes a nonexisting catalogue",
1047: cataloguesConf);
1048: }
1049:
1050: // Obtain default text to use for untranslated messages
1051: defaultUntranslated = conf.getChild(I18N_UNTRANSLATED)
1052: .getValue(null);
1053: if (getLogger().isDebugEnabled()) {
1054: getLogger().debug(
1055: "Default untranslated text is '"
1056: + defaultUntranslated + "'");
1057: }
1058:
1059: // Preload specified catalogues (if any)
1060: Configuration[] preloadConfs = conf.getChildren(I18N_PRELOAD);
1061: for (int i = 0; i < preloadConfs.length; i++) {
1062: String localeStr = preloadConfs[i].getValue();
1063: this .locale = I18nUtils.parseLocale(localeStr);
1064:
1065: String id = preloadConfs[i].getAttribute("catalogue", null);
1066: if (id != null) {
1067: CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues
1068: .get(id);
1069: if (catalogueInfo == null) {
1070: throw new ConfigurationException(
1071: "Invalid catalogue id '" + id
1072: + "' in preload element.",
1073: preloadConfs[i]);
1074: }
1075:
1076: try {
1077: catalogueInfo.getCatalogue();
1078: } finally {
1079: catalogueInfo.releaseCatalog();
1080: }
1081: } else {
1082: for (Iterator j = catalogues.values().iterator(); j
1083: .hasNext();) {
1084: CatalogueInfo catalogueInfo = (CatalogueInfo) j
1085: .next();
1086: try {
1087: catalogueInfo.getCatalogue();
1088: } finally {
1089: catalogueInfo.releaseCatalog();
1090: }
1091: }
1092: }
1093: }
1094: this .locale = null;
1095: }
1096:
1097: /**
1098: * Setup current instance of transformer.
1099: */
1100: public void setup(SourceResolver resolver, Map objectModel,
1101: String source, Parameters parameters)
1102: throws ProcessingException, SAXException, IOException {
1103:
1104: this .objectModel = objectModel;
1105:
1106: untranslated = parameters.getParameter(I18N_UNTRANSLATED,
1107: defaultUntranslated);
1108: if (untranslated != null) {
1109: untranslatedRecorder = new ParamSaxBuffer();
1110: untranslatedRecorder.characters(untranslated.toCharArray(),
1111: 0, untranslated.length());
1112: }
1113:
1114: // Get current locale
1115: String lc = parameters.getParameter(I18N_LOCALE, null);
1116: Locale locale = I18nUtils.parseLocale(lc);
1117: if (getLogger().isDebugEnabled()) {
1118: getLogger().debug("Using locale '" + locale + "'");
1119: }
1120:
1121: // Initialize instance state variables
1122: this .locale = locale;
1123: this .current_state = STATE_OUTSIDE;
1124: this .prev_state = STATE_OUTSIDE;
1125: this .currentKey = null;
1126: this .currentCatalogueId = null;
1127: this .translate_copy = false;
1128: this .tr_text_recorder = null;
1129: this .text_recorder = new ParamSaxBuffer();
1130: this .param_count = 0;
1131: this .param_name = null;
1132: this .param_value = null;
1133: this .param_recorder = null;
1134: this .indexedParams = new HashMap(3);
1135: this .formattingParams = null;
1136: this .strBuffer = null;
1137:
1138: // give the catalogue variable its value -- first look if it's locally overridden
1139: // and otherwise use the component-wide defaults.
1140: String catalogueId = parameters.getParameter(
1141: I18N_DEFAULT_CATALOGUE_ID, null);
1142: if (catalogueId != null) {
1143: CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues
1144: .get(catalogueId);
1145: if (catalogueInfo == null) {
1146: throw new ProcessingException("I18nTransformer: '"
1147: + catalogueId
1148: + "' is not an existing catalogue id.");
1149: }
1150: catalogue = catalogueInfo;
1151: } else {
1152: catalogue = defaultCatalogue;
1153: }
1154:
1155: if (getLogger().isDebugEnabled()) {
1156: getLogger().debug(
1157: "Default catalogue is " + catalogue.getName());
1158: }
1159: }
1160:
1161: //
1162: // Standard SAX event handlers
1163: //
1164:
1165: public void startElement(String uri, String name, String raw,
1166: Attributes attr) throws SAXException {
1167:
1168: // Handle previously buffered characters
1169: if (current_state != STATE_OUTSIDE && strBuffer != null) {
1170: i18nCharacters(strBuffer.toString());
1171: strBuffer = null;
1172: }
1173:
1174: // Process start element event
1175: if (I18nUtils.matchesI18nNamespace(uri)) {
1176: if (getLogger().isDebugEnabled()) {
1177: getLogger().debug("Starting i18n element: " + name);
1178: }
1179: startI18NElement(name, attr);
1180: } else {
1181: // We have a non i18n element event
1182: if (current_state == STATE_OUTSIDE) {
1183: super .startElement(uri, name, raw, translateAttributes(
1184: name, attr));
1185: } else if (current_state == STATE_INSIDE_PARAM) {
1186: param_recorder.startElement(uri, name, raw, attr);
1187: } else if (current_state == STATE_INSIDE_TEXT) {
1188: text_recorder.startElement(uri, name, raw, attr);
1189: } else if ((current_state == STATE_INSIDE_WHEN || current_state == STATE_INSIDE_OTHERWISE)
1190: && translate_copy) {
1191:
1192: super .startElement(uri, name, raw, attr);
1193: }
1194: }
1195: }
1196:
1197: public void endElement(String uri, String name, String raw)
1198: throws SAXException {
1199:
1200: // Handle previously buffered characters
1201: if (current_state != STATE_OUTSIDE && strBuffer != null) {
1202: i18nCharacters(strBuffer.toString());
1203: strBuffer = null;
1204: }
1205:
1206: if (I18nUtils.matchesI18nNamespace(uri)) {
1207: endI18NElement(name);
1208: } else if (current_state == STATE_INSIDE_PARAM) {
1209: param_recorder.endElement(uri, name, raw);
1210: } else if (current_state == STATE_INSIDE_TEXT) {
1211: text_recorder.endElement(uri, name, raw);
1212: } else if (current_state == STATE_INSIDE_CHOOSE
1213: || (current_state == STATE_INSIDE_WHEN || current_state == STATE_INSIDE_OTHERWISE)
1214: && !translate_copy) {
1215:
1216: // Output nothing
1217: } else {
1218: super .endElement(uri, name, raw);
1219: }
1220: }
1221:
1222: public void characters(char[] ch, int start, int len)
1223: throws SAXException {
1224:
1225: if (current_state == STATE_OUTSIDE
1226: || ((current_state == STATE_INSIDE_WHEN || current_state == STATE_INSIDE_OTHERWISE) && translate_copy)) {
1227:
1228: super .characters(ch, start, len);
1229: } else {
1230: // Perform buffering to prevent chunked character data
1231: if (strBuffer == null) {
1232: strBuffer = new StringBuffer();
1233: }
1234: strBuffer.append(ch, start, len);
1235: }
1236: }
1237:
1238: //
1239: // i18n specific event handlers
1240: //
1241:
1242: private void startI18NElement(String name, Attributes attr)
1243: throws SAXException {
1244:
1245: if (getLogger().isDebugEnabled()) {
1246: getLogger().debug("Start i18n element: " + name);
1247: }
1248:
1249: if (I18N_TEXT_ELEMENT.equals(name)) {
1250: if (current_state != STATE_OUTSIDE
1251: && current_state != STATE_INSIDE_PARAM
1252: && current_state != STATE_INSIDE_TRANSLATE) {
1253:
1254: throw new SAXException(
1255: getClass().getName()
1256: + ": nested i18n:text elements are not allowed."
1257: + " Current state: " + current_state);
1258: }
1259:
1260: prev_state = current_state;
1261: current_state = STATE_INSIDE_TEXT;
1262:
1263: currentKey = attr.getValue("", I18N_KEY_ATTRIBUTE);
1264: if (currentKey == null) {
1265: // Try the namespaced attribute
1266: currentKey = attr.getValue(I18N_NAMESPACE_URI,
1267: I18N_KEY_ATTRIBUTE);
1268: if (currentKey == null) {
1269: // Try the old namespace
1270: currentKey = attr.getValue(I18N_OLD_NAMESPACE_URI,
1271: I18N_KEY_ATTRIBUTE);
1272: }
1273: }
1274:
1275: currentCatalogueId = attr.getValue("",
1276: I18N_CATALOGUE_ATTRIBUTE);
1277: if (currentCatalogueId == null) {
1278: // Try the namespaced attribute
1279: currentCatalogueId = attr.getValue(I18N_NAMESPACE_URI,
1280: I18N_CATALOGUE_ATTRIBUTE);
1281: }
1282:
1283: if (prev_state != STATE_INSIDE_PARAM) {
1284: tr_text_recorder = null;
1285: }
1286:
1287: if (currentKey != null) {
1288: tr_text_recorder = getMessage(currentKey,
1289: (ParamSaxBuffer) null);
1290: }
1291:
1292: } else if (I18N_TRANSLATE_ELEMENT.equals(name)) {
1293: if (current_state != STATE_OUTSIDE) {
1294: throw new SAXException(
1295: getClass().getName()
1296: + ": i18n:translate element must be used "
1297: + "outside of other i18n elements. Current state: "
1298: + current_state);
1299: }
1300:
1301: prev_state = current_state;
1302: current_state = STATE_INSIDE_TRANSLATE;
1303: } else if (I18N_PARAM_ELEMENT.equals(name)) {
1304: if (current_state != STATE_INSIDE_TRANSLATE) {
1305: throw new SAXException(
1306: getClass().getName()
1307: + ": i18n:param element can be used only inside "
1308: + "i18n:translate element. Current state: "
1309: + current_state);
1310: }
1311:
1312: param_name = attr.getValue(I18N_PARAM_NAME_ATTRIBUTE);
1313: if (param_name == null) {
1314: param_name = String.valueOf(param_count++);
1315: }
1316:
1317: param_recorder = new SaxBuffer();
1318: setFormattingParams(attr);
1319: current_state = STATE_INSIDE_PARAM;
1320: } else if (I18N_CHOOSE_ELEMENT.equals(name)) {
1321: if (current_state != STATE_OUTSIDE) {
1322: throw new SAXException(getClass().getName()
1323: + ": i18n:choose elements cannot be used"
1324: + "inside of other i18n elements.");
1325: }
1326:
1327: translate_copy = false;
1328: translate_end = false;
1329: prev_state = current_state;
1330: current_state = STATE_INSIDE_CHOOSE;
1331: } else if (I18N_WHEN_ELEMENT.equals(name)
1332: || I18N_IF_ELEMENT.equals(name)) {
1333:
1334: if (I18N_WHEN_ELEMENT.equals(name)
1335: && current_state != STATE_INSIDE_CHOOSE) {
1336: throw new SAXException(getClass().getName()
1337: + ": i18n:when elements are can be used only"
1338: + "inside of i18n:choose elements.");
1339: }
1340:
1341: if (I18N_IF_ELEMENT.equals(name)
1342: && current_state != STATE_OUTSIDE) {
1343: throw new SAXException(getClass().getName()
1344: + ": i18n:if elements cannot be nested.");
1345: }
1346:
1347: String locale = attr.getValue(I18N_LOCALE_ATTRIBUTE);
1348: if (locale == null)
1349: throw new SAXException(
1350: getClass().getName()
1351: + ": i18n:"
1352: + name
1353: + " element cannot be used without 'locale' attribute.");
1354:
1355: if ((!translate_end && current_state == STATE_INSIDE_CHOOSE)
1356: || current_state == STATE_OUTSIDE) {
1357:
1358: // Perform soft locale matching
1359: if (this .locale.toString().startsWith(locale)) {
1360: if (getLogger().isDebugEnabled()) {
1361: getLogger().debug("Locale matching: " + locale);
1362: }
1363: translate_copy = true;
1364: }
1365: }
1366:
1367: prev_state = current_state;
1368: current_state = STATE_INSIDE_WHEN;
1369:
1370: } else if (I18N_OTHERWISE_ELEMENT.equals(name)) {
1371: if (current_state != STATE_INSIDE_CHOOSE) {
1372: throw new SAXException(getClass().getName()
1373: + ": i18n:otherwise elements are not allowed "
1374: + "only inside i18n:choose.");
1375: }
1376:
1377: getLogger().debug("Matching any locale");
1378: if (!translate_end) {
1379: translate_copy = true;
1380: }
1381:
1382: prev_state = current_state;
1383: current_state = STATE_INSIDE_OTHERWISE;
1384:
1385: } else if (I18N_DATE_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(getClass().getName()
1390: + ": i18n:date elements are not allowed "
1391: + "inside of other i18n elements.");
1392: }
1393:
1394: setFormattingParams(attr);
1395: prev_state = current_state;
1396: current_state = STATE_INSIDE_DATE;
1397: } else if (I18N_DATE_TIME_ELEMENT.equals(name)) {
1398: if (current_state != STATE_OUTSIDE
1399: && current_state != STATE_INSIDE_TEXT
1400: && current_state != STATE_INSIDE_PARAM) {
1401: throw new SAXException(getClass().getName()
1402: + ": i18n:date-time elements are not allowed "
1403: + "inside of other i18n elements.");
1404: }
1405:
1406: setFormattingParams(attr);
1407: prev_state = current_state;
1408: current_state = STATE_INSIDE_DATE_TIME;
1409: } else if (I18N_TIME_ELEMENT.equals(name)) {
1410: if (current_state != STATE_OUTSIDE
1411: && current_state != STATE_INSIDE_TEXT
1412: && current_state != STATE_INSIDE_PARAM) {
1413: throw new SAXException(getClass().getName()
1414: + ": i18n:date elements are not allowed "
1415: + "inside of other i18n elements.");
1416: }
1417:
1418: setFormattingParams(attr);
1419: prev_state = current_state;
1420: current_state = STATE_INSIDE_TIME;
1421: } else if (I18N_NUMBER_ELEMENT.equals(name)) {
1422: if (current_state != STATE_OUTSIDE
1423: && current_state != STATE_INSIDE_TEXT
1424: && current_state != STATE_INSIDE_PARAM) {
1425: throw new SAXException(getClass().getName()
1426: + ": i18n:number elements are not allowed "
1427: + "inside of other i18n elements.");
1428: }
1429:
1430: setFormattingParams(attr);
1431: prev_state = current_state;
1432: current_state = STATE_INSIDE_NUMBER;
1433: }
1434: }
1435:
1436: // Get all possible i18n formatting attribute values and store in a Map
1437: private void setFormattingParams(Attributes attr) {
1438: // average number of attributes is 3
1439: formattingParams = new HashMap(3);
1440:
1441: String attr_value = attr.getValue(I18N_SRC_PATTERN_ATTRIBUTE);
1442: if (attr_value != null) {
1443: formattingParams
1444: .put(I18N_SRC_PATTERN_ATTRIBUTE, attr_value);
1445: }
1446:
1447: attr_value = attr.getValue(I18N_PATTERN_ATTRIBUTE);
1448: if (attr_value != null) {
1449: formattingParams.put(I18N_PATTERN_ATTRIBUTE, attr_value);
1450: }
1451:
1452: attr_value = attr.getValue(I18N_VALUE_ATTRIBUTE);
1453: if (attr_value != null) {
1454: formattingParams.put(I18N_VALUE_ATTRIBUTE, attr_value);
1455: }
1456:
1457: attr_value = attr.getValue(I18N_LOCALE_ATTRIBUTE);
1458: if (attr_value != null) {
1459: formattingParams.put(I18N_LOCALE_ATTRIBUTE, attr_value);
1460: }
1461:
1462: attr_value = attr.getValue(CURRENCY_LOCALE_ATTRIBUTE);
1463: if (attr_value != null) {
1464: formattingParams.put(CURRENCY_LOCALE_ATTRIBUTE, attr_value);
1465: }
1466:
1467: attr_value = attr.getValue(I18N_SRC_LOCALE_ATTRIBUTE);
1468: if (attr_value != null) {
1469: formattingParams.put(I18N_SRC_LOCALE_ATTRIBUTE, attr_value);
1470: }
1471:
1472: attr_value = attr.getValue(I18N_TYPE_ATTRIBUTE);
1473: if (attr_value != null) {
1474: formattingParams.put(I18N_TYPE_ATTRIBUTE, attr_value);
1475: }
1476:
1477: attr_value = attr.getValue(I18N_FRACTION_DIGITS_ATTRIBUTE);
1478: if (attr_value != null) {
1479: formattingParams.put(I18N_FRACTION_DIGITS_ATTRIBUTE,
1480: attr_value);
1481: }
1482: }
1483:
1484: private void endI18NElement(String name) throws SAXException {
1485: if (getLogger().isDebugEnabled()) {
1486: getLogger().debug("End i18n element: " + name);
1487: }
1488:
1489: switch (current_state) {
1490: case STATE_INSIDE_TEXT:
1491: endTextElement();
1492: break;
1493:
1494: case STATE_INSIDE_TRANSLATE:
1495: endTranslateElement();
1496: break;
1497:
1498: case STATE_INSIDE_CHOOSE:
1499: endChooseElement();
1500: break;
1501:
1502: case STATE_INSIDE_WHEN:
1503: case STATE_INSIDE_OTHERWISE:
1504: endWhenElement();
1505: break;
1506:
1507: case STATE_INSIDE_PARAM:
1508: endParamElement();
1509: break;
1510:
1511: case STATE_INSIDE_DATE:
1512: case STATE_INSIDE_DATE_TIME:
1513: case STATE_INSIDE_TIME:
1514: endDate_TimeElement();
1515: break;
1516:
1517: case STATE_INSIDE_NUMBER:
1518: endNumberElement();
1519: break;
1520: }
1521: }
1522:
1523: private void i18nCharacters(String textValue) throws SAXException {
1524: if (getLogger().isDebugEnabled()) {
1525: getLogger()
1526: .debug("i18n message text = '" + textValue + "'");
1527: }
1528:
1529: SaxBuffer buffer;
1530: switch (current_state) {
1531: case STATE_INSIDE_TEXT:
1532: buffer = text_recorder;
1533: break;
1534:
1535: case STATE_INSIDE_PARAM:
1536: buffer = param_recorder;
1537: break;
1538:
1539: case STATE_INSIDE_WHEN:
1540: case STATE_INSIDE_OTHERWISE:
1541: // Previously handeld to avoid the String() conversion.
1542: return;
1543:
1544: case STATE_INSIDE_TRANSLATE:
1545: if (tr_text_recorder == null) {
1546: tr_text_recorder = new ParamSaxBuffer();
1547: }
1548: buffer = tr_text_recorder;
1549: break;
1550:
1551: case STATE_INSIDE_CHOOSE:
1552: // No characters allowed. Send an exception ?
1553: if (getLogger().isDebugEnabled()) {
1554: textValue = textValue.trim();
1555: if (textValue.length() > 0) {
1556: getLogger().debug(
1557: "No characters allowed inside <i18n:choose> tag. Received: "
1558: + textValue);
1559: }
1560: }
1561: return;
1562:
1563: case STATE_INSIDE_DATE:
1564: case STATE_INSIDE_DATE_TIME:
1565: case STATE_INSIDE_TIME:
1566: case STATE_INSIDE_NUMBER:
1567: // Trim text values to avoid parsing errors.
1568: textValue = textValue.trim();
1569: if (textValue.length() > 0) {
1570: if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null) {
1571: formattingParams.put(I18N_VALUE_ATTRIBUTE,
1572: textValue);
1573: } else {
1574: // ignore the text inside of date element
1575: }
1576: }
1577: return;
1578:
1579: default:
1580: throw new IllegalStateException(getClass().getName()
1581: + " developer's fault: characters not handled. "
1582: + "Current state: " + current_state);
1583: }
1584:
1585: char[] ch = textValue.toCharArray();
1586: buffer.characters(ch, 0, ch.length);
1587: }
1588:
1589: // Translate all attributes that are listed in i18n:attr attribute
1590: private Attributes translateAttributes(final String element,
1591: Attributes attr) throws SAXException {
1592: if (attr == null) {
1593: return null;
1594: }
1595:
1596: AttributesImpl tempAttr = null;
1597:
1598: // Translate all attributes from i18n:attr="name1 name2 ..."
1599: // using their values as keys.
1600: int attrIndex = attr.getIndex(I18N_NAMESPACE_URI,
1601: I18N_ATTR_ATTRIBUTE);
1602: if (attrIndex == -1) {
1603: // Try the old namespace
1604: attrIndex = attr.getIndex(I18N_OLD_NAMESPACE_URI,
1605: I18N_ATTR_ATTRIBUTE);
1606: }
1607:
1608: if (attrIndex != -1) {
1609: StringTokenizer st = new StringTokenizer(attr
1610: .getValue(attrIndex));
1611:
1612: // Make a copy which we are going to modify
1613: tempAttr = new AttributesImpl(attr);
1614: // Remove the i18n:attr attribute - we don't need it anymore
1615: tempAttr.removeAttribute(attrIndex);
1616:
1617: // Iterate through listed attributes and translate them
1618: while (st.hasMoreElements()) {
1619: final String name = st.nextToken();
1620:
1621: int index = tempAttr.getIndex(name);
1622: if (index == -1) {
1623: getLogger().warn(
1624: "Attribute " + name
1625: + " not found in element <"
1626: + element + ">");
1627: continue;
1628: }
1629:
1630: String value = translateAttribute(element, name,
1631: tempAttr.getValue(index));
1632: if (value != null) {
1633: // Set the translated value. If null, do nothing.
1634: tempAttr.setValue(index, value);
1635: }
1636: }
1637:
1638: attr = tempAttr;
1639: }
1640:
1641: // Translate all attributes from i18n:expr="name1 name2 ..."
1642: // using their values as keys.
1643: attrIndex = attr.getIndex(I18N_NAMESPACE_URI,
1644: I18N_EXPR_ATTRIBUTE);
1645: if (attrIndex != -1) {
1646: StringTokenizer st = new StringTokenizer(attr
1647: .getValue(attrIndex));
1648:
1649: if (tempAttr == null) {
1650: tempAttr = new AttributesImpl(attr);
1651: }
1652: tempAttr.removeAttribute(attrIndex);
1653:
1654: // Iterate through listed attributes and evaluate them
1655: while (st.hasMoreElements()) {
1656: final String name = st.nextToken();
1657:
1658: int index = tempAttr.getIndex(name);
1659: if (index == -1) {
1660: getLogger().warn(
1661: "Attribute " + name
1662: + " not found in element <"
1663: + element + ">");
1664: continue;
1665: }
1666:
1667: final StringBuffer translated = new StringBuffer();
1668:
1669: // Evaluate {..} expression
1670: VariableExpressionTokenizer.TokenReciever tr = new VariableExpressionTokenizer.TokenReciever() {
1671: private String catalogueName;
1672:
1673: public void addToken(int type, String value) {
1674: if (type == MODULE) {
1675: this .catalogueName = value;
1676: } else if (type == VARIABLE) {
1677: translated.append(translateAttribute(
1678: element, name, value));
1679: } else if (type == TEXT) {
1680: if (this .catalogueName != null) {
1681: translated.append(translateAttribute(
1682: element, name,
1683: this .catalogueName + ":"
1684: + value));
1685: this .catalogueName = null;
1686: } else if (value != null) {
1687: translated.append(value);
1688: }
1689: }
1690: }
1691: };
1692:
1693: try {
1694: VariableExpressionTokenizer.tokenize(tempAttr
1695: .getValue(index), tr);
1696: } catch (PatternException e) {
1697: throw new SAXException(e);
1698: }
1699:
1700: // Set the translated value.
1701: tempAttr.setValue(index, translated.toString());
1702: }
1703:
1704: attr = tempAttr;
1705: }
1706:
1707: // nothing to translate, just return
1708: return attr;
1709: }
1710:
1711: /**
1712: * Translate attribute value.
1713: * Value can be prefixed with catalogue ID and semicolon.
1714: * @return Translated text, untranslated text, or null.
1715: */
1716: private String translateAttribute(String element, String name,
1717: String key) {
1718: // Check if the key contains a colon, if so the text before
1719: // the colon denotes a catalogue ID.
1720: int colonPos = key.indexOf(":");
1721: String catalogueID = null;
1722: if (colonPos != -1) {
1723: catalogueID = key.substring(0, colonPos);
1724: key = key.substring(colonPos + 1, key.length());
1725: }
1726:
1727: final SaxBuffer text = getMessage(catalogueID, key);
1728: if (text == null) {
1729: getLogger().warn(
1730: "Translation not found for attribute " + name
1731: + " in element <" + element + ">");
1732: return untranslated;
1733: }
1734: return text.toString();
1735: }
1736:
1737: private void endTextElement() throws SAXException {
1738: switch (prev_state) {
1739: case STATE_OUTSIDE:
1740: if (tr_text_recorder == null) {
1741: if (currentKey == null) {
1742: // Use the text as key. Not recommended for large strings,
1743: // especially if they include markup.
1744: tr_text_recorder = getMessage(text_recorder
1745: .toString(), text_recorder);
1746: } else {
1747: // We have the key, but couldn't find a translation
1748: if (getLogger().isDebugEnabled()) {
1749: getLogger().debug(
1750: "Translation not found for key '"
1751: + currentKey + "'");
1752: }
1753:
1754: // Use the untranslated-text only when the content of the i18n:text
1755: // element was empty
1756: if (text_recorder.isEmpty()
1757: && untranslatedRecorder != null) {
1758: tr_text_recorder = untranslatedRecorder;
1759: } else {
1760: tr_text_recorder = text_recorder;
1761: }
1762: }
1763: }
1764:
1765: if (tr_text_recorder != null) {
1766: tr_text_recorder.toSAX(this .contentHandler);
1767: }
1768:
1769: text_recorder.recycle();
1770: tr_text_recorder = null;
1771: currentKey = null;
1772: currentCatalogueId = null;
1773: break;
1774:
1775: case STATE_INSIDE_TRANSLATE:
1776: if (tr_text_recorder == null) {
1777: if (!text_recorder.isEmpty()) {
1778: tr_text_recorder = getMessage(text_recorder
1779: .toString(), text_recorder);
1780: if (tr_text_recorder == text_recorder) {
1781: // If the default value was returned, make a copy
1782: tr_text_recorder = new ParamSaxBuffer(
1783: text_recorder);
1784: }
1785: }
1786: }
1787:
1788: text_recorder.recycle();
1789: break;
1790:
1791: case STATE_INSIDE_PARAM:
1792: // We send the translated text to the param recorder, after trying to translate it.
1793: // Remember you can't give a key when inside a param, that'll be nonsense!
1794: // No need to clone. We just send the events.
1795: if (!text_recorder.isEmpty()) {
1796: getMessage(text_recorder.toString(), text_recorder)
1797: .toSAX(param_recorder);
1798: text_recorder.recycle();
1799: }
1800: break;
1801: }
1802:
1803: current_state = prev_state;
1804: prev_state = STATE_OUTSIDE;
1805: }
1806:
1807: // Process substitution parameter
1808: private void endParamElement() throws SAXException {
1809: String paramType = (String) formattingParams
1810: .get(I18N_TYPE_ATTRIBUTE);
1811: if (paramType != null) {
1812: // We have a typed parameter
1813:
1814: if (getLogger().isDebugEnabled()) {
1815: getLogger().debug("Param type: " + paramType);
1816: }
1817: if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null
1818: && param_value != null) {
1819: if (getLogger().isDebugEnabled()) {
1820: getLogger()
1821: .debug("Put param value: " + param_value);
1822: }
1823: formattingParams.put(I18N_VALUE_ATTRIBUTE, param_value);
1824: }
1825:
1826: // Check if we have a date or a number parameter
1827: if (dateTypes.contains(paramType)) {
1828: if (getLogger().isDebugEnabled()) {
1829: getLogger().debug(
1830: "Formatting date_time param: "
1831: + formattingParams);
1832: }
1833: param_value = formatDate_Time(formattingParams);
1834: } else if (numberTypes.contains(paramType)) {
1835: if (getLogger().isDebugEnabled()) {
1836: getLogger().debug(
1837: "Formatting number param: "
1838: + formattingParams);
1839: }
1840: param_value = formatNumber(formattingParams);
1841: }
1842: if (getLogger().isDebugEnabled()) {
1843: getLogger().debug(
1844: "Added substitution param: " + param_value);
1845: }
1846: }
1847:
1848: param_value = null;
1849: current_state = STATE_INSIDE_TRANSLATE;
1850:
1851: if (param_recorder == null) {
1852: return;
1853: }
1854:
1855: indexedParams.put(param_name, param_recorder);
1856: param_recorder = null;
1857: }
1858:
1859: private void endTranslateElement() throws SAXException {
1860: if (tr_text_recorder != null) {
1861: if (getLogger().isDebugEnabled()) {
1862: getLogger().debug(
1863: "End of translate with params. "
1864: + "Fragment for substitution : "
1865: + tr_text_recorder);
1866: }
1867: tr_text_recorder.toSAX(super .contentHandler, indexedParams);
1868: tr_text_recorder = null;
1869: text_recorder.recycle();
1870: }
1871:
1872: indexedParams.clear();
1873: param_count = 0;
1874: current_state = STATE_OUTSIDE;
1875: }
1876:
1877: private void endChooseElement() {
1878: current_state = STATE_OUTSIDE;
1879: }
1880:
1881: private void endWhenElement() {
1882: current_state = prev_state;
1883: if (translate_copy) {
1884: translate_copy = false;
1885: translate_end = true;
1886: }
1887: }
1888:
1889: private void endDate_TimeElement() throws SAXException {
1890: String result = formatDate_Time(formattingParams);
1891: switch (prev_state) {
1892: case STATE_OUTSIDE:
1893: super .contentHandler.characters(result.toCharArray(), 0,
1894: result.length());
1895: break;
1896: case STATE_INSIDE_PARAM:
1897: param_recorder.characters(result.toCharArray(), 0, result
1898: .length());
1899: break;
1900: case STATE_INSIDE_TEXT:
1901: text_recorder.characters(result.toCharArray(), 0, result
1902: .length());
1903: break;
1904: }
1905: current_state = prev_state;
1906: }
1907:
1908: // Helper method: creates Locale object from a string value in a map
1909: private Locale getLocale(Map params, String attribute) {
1910: // the specific locale value
1911: String lc = (String) params.get(attribute);
1912: return I18nUtils.parseLocale(lc, this .locale);
1913: }
1914:
1915: private String formatDate_Time(Map params) throws SAXException {
1916: // Check that we have not null params
1917: if (params == null) {
1918: throw new IllegalArgumentException("Nothing to format");
1919: }
1920:
1921: // Formatters
1922: SimpleDateFormat to_fmt;
1923: SimpleDateFormat from_fmt;
1924:
1925: // Date formatting styles
1926: int srcStyle = DateFormat.DEFAULT;
1927: int style = DateFormat.DEFAULT;
1928:
1929: // Date formatting patterns
1930: boolean realPattern = false;
1931: boolean realSrcPattern = false;
1932:
1933: // From locale
1934: Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE);
1935: // To locale
1936: Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE);
1937:
1938: // From pattern
1939: String srcPattern = (String) params
1940: .get(I18N_SRC_PATTERN_ATTRIBUTE);
1941: // To pattern
1942: String pattern = (String) params.get(I18N_PATTERN_ATTRIBUTE);
1943: // The date value
1944: String value = (String) params.get(I18N_VALUE_ATTRIBUTE);
1945:
1946: // A src-pattern attribute is present
1947: if (srcPattern != null) {
1948: // Check if we have a real pattern
1949: Integer patternValue = (Integer) datePatterns
1950: .get(srcPattern.toUpperCase());
1951: if (patternValue != null) {
1952: srcStyle = patternValue.intValue();
1953: } else {
1954: realSrcPattern = true;
1955: }
1956: }
1957:
1958: // A pattern attribute is present
1959: if (pattern != null) {
1960: Integer patternValue = (Integer) datePatterns.get(pattern
1961: .toUpperCase());
1962: if (patternValue != null) {
1963: style = patternValue.intValue();
1964: } else {
1965: realPattern = true;
1966: }
1967: }
1968:
1969: // If we are inside of a typed param
1970: String paramType = (String) formattingParams
1971: .get(I18N_TYPE_ATTRIBUTE);
1972:
1973: // Initializing date formatters
1974: if (current_state == STATE_INSIDE_DATE
1975: || I18N_DATE_ELEMENT.equals(paramType)) {
1976:
1977: to_fmt = (SimpleDateFormat) DateFormat.getDateInstance(
1978: style, loc);
1979: from_fmt = (SimpleDateFormat) DateFormat.getDateInstance(
1980: srcStyle, srcLoc);
1981: } else if (current_state == STATE_INSIDE_DATE_TIME
1982: || I18N_DATE_TIME_ELEMENT.equals(paramType)) {
1983: to_fmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(
1984: style, style, loc);
1985: from_fmt = (SimpleDateFormat) DateFormat
1986: .getDateTimeInstance(srcStyle, srcStyle, srcLoc);
1987: } else {
1988: // STATE_INSIDE_TIME or param type='time'
1989: to_fmt = (SimpleDateFormat) DateFormat.getTimeInstance(
1990: style, loc);
1991: from_fmt = (SimpleDateFormat) DateFormat.getTimeInstance(
1992: srcStyle, srcLoc);
1993: }
1994:
1995: // parsed date object
1996: Date dateValue;
1997:
1998: // pattern overwrites locale format
1999: if (realSrcPattern) {
2000: from_fmt.applyPattern(srcPattern);
2001: }
2002:
2003: if (realPattern) {
2004: to_fmt.applyPattern(pattern);
2005: }
2006:
2007: // get current date and time by default
2008: if (value == null) {
2009: dateValue = new Date();
2010: } else {
2011: try {
2012: dateValue = from_fmt.parse(value);
2013: } catch (ParseException pe) {
2014: throw new SAXException(this .getClass().getName()
2015: + "i18n:date - parsing error.", pe);
2016: }
2017: }
2018:
2019: // we have all necessary data here: do formatting.
2020: if (getLogger().isDebugEnabled()) {
2021: getLogger().debug(
2022: "### Formatting date: " + dateValue
2023: + " with localized pattern "
2024: + to_fmt.toLocalizedPattern()
2025: + " for locale: " + locale);
2026: }
2027: return to_fmt.format(dateValue);
2028: }
2029:
2030: private void endNumberElement() throws SAXException {
2031: String result = formatNumber(formattingParams);
2032: switch (prev_state) {
2033: case STATE_OUTSIDE:
2034: super .contentHandler.characters(result.toCharArray(), 0,
2035: result.length());
2036: break;
2037: case STATE_INSIDE_PARAM:
2038: param_recorder.characters(result.toCharArray(), 0, result
2039: .length());
2040: break;
2041: case STATE_INSIDE_TEXT:
2042: text_recorder.characters(result.toCharArray(), 0, result
2043: .length());
2044: break;
2045: }
2046: current_state = prev_state;
2047: }
2048:
2049: private String formatNumber(Map params) throws SAXException {
2050: if (params == null) {
2051: throw new SAXException(this .getClass().getName()
2052: + ": i18n:number - error in element attributes.");
2053: }
2054:
2055: // from pattern
2056: String srcPattern = (String) params
2057: .get(I18N_SRC_PATTERN_ATTRIBUTE);
2058: // to pattern
2059: String pattern = (String) params.get(I18N_PATTERN_ATTRIBUTE);
2060: // the number value
2061: String value = (String) params.get(I18N_VALUE_ATTRIBUTE);
2062:
2063: if (value == null)
2064: return "";
2065: // type
2066: String type = (String) params.get(I18N_TYPE_ATTRIBUTE);
2067:
2068: // fraction-digits
2069: int fractionDigits = -1;
2070: try {
2071: String fd = (String) params
2072: .get(I18N_FRACTION_DIGITS_ATTRIBUTE);
2073: if (fd != null)
2074: fractionDigits = Integer.parseInt(fd);
2075: } catch (NumberFormatException nfe) {
2076: getLogger().warn(
2077: "Error in number format with fraction-digits", nfe);
2078: }
2079:
2080: // parsed number
2081: Number numberValue;
2082:
2083: // locale, may be switched locale
2084: Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE);
2085: Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE);
2086: // currency locale
2087: Locale currencyLoc = getLocale(params,
2088: CURRENCY_LOCALE_ATTRIBUTE);
2089: // decimal and grouping locale
2090: Locale dgLoc = null;
2091: if (currencyLoc != null) {
2092: // the reasoning here is: if there is a currency locale, then start from that
2093: // one but take certain properties (like decimal and grouping seperation symbols)
2094: // from the default locale (this happens further on).
2095: dgLoc = loc;
2096: loc = currencyLoc;
2097: }
2098:
2099: // src format
2100: DecimalFormat from_fmt = (DecimalFormat) NumberFormat
2101: .getInstance(srcLoc);
2102: int int_currency = 0;
2103:
2104: // src-pattern overwrites locale format
2105: if (srcPattern != null) {
2106: from_fmt.applyPattern(srcPattern);
2107: }
2108:
2109: // to format
2110: DecimalFormat to_fmt;
2111: char dec = from_fmt.getDecimalFormatSymbols()
2112: .getDecimalSeparator();
2113: int decAt = 0;
2114: boolean appendDec = false;
2115:
2116: if (type == null || type.equals(I18N_NUMBER_ELEMENT)) {
2117: to_fmt = (DecimalFormat) NumberFormat.getInstance(loc);
2118: to_fmt.setMaximumFractionDigits(309);
2119: for (int i = value.length() - 1; i >= 0
2120: && value.charAt(i) != dec; i--, decAt++) {
2121: }
2122:
2123: if (decAt < value.length())
2124: to_fmt.setMinimumFractionDigits(decAt);
2125: decAt = 0;
2126: for (int i = 0; i < value.length()
2127: && value.charAt(i) != dec; i++) {
2128: if (Character.isDigit(value.charAt(i))) {
2129: decAt++;
2130: }
2131: }
2132:
2133: to_fmt.setMinimumIntegerDigits(decAt);
2134: if (value.charAt(value.length() - 1) == dec) {
2135: appendDec = true;
2136: }
2137: } else if (type.equals(I18N_CURRENCY_ELEMENT)) {
2138: to_fmt = (DecimalFormat) NumberFormat
2139: .getCurrencyInstance(loc);
2140: } else if (type.equals(I18N_INT_CURRENCY_ELEMENT)) {
2141: to_fmt = (DecimalFormat) NumberFormat
2142: .getCurrencyInstance(loc);
2143: int_currency = 1;
2144: for (int i = 0; i < to_fmt.getMaximumFractionDigits(); i++) {
2145: int_currency *= 10;
2146: }
2147: } else if (type.equals(I18N_CURRENCY_NO_UNIT_ELEMENT)) {
2148: DecimalFormat tmp = (DecimalFormat) NumberFormat
2149: .getCurrencyInstance(loc);
2150: to_fmt = (DecimalFormat) NumberFormat.getInstance(loc);
2151: to_fmt.setMinimumFractionDigits(tmp
2152: .getMinimumFractionDigits());
2153: to_fmt.setMaximumFractionDigits(tmp
2154: .getMaximumFractionDigits());
2155: } else if (type.equals(I18N_INT_CURRENCY_NO_UNIT_ELEMENT)) {
2156: DecimalFormat tmp = (DecimalFormat) NumberFormat
2157: .getCurrencyInstance(loc);
2158: int_currency = 1;
2159: for (int i = 0; i < tmp.getMaximumFractionDigits(); i++)
2160: int_currency *= 10;
2161: to_fmt = (DecimalFormat) NumberFormat.getInstance(loc);
2162: to_fmt.setMinimumFractionDigits(tmp
2163: .getMinimumFractionDigits());
2164: to_fmt.setMaximumFractionDigits(tmp
2165: .getMaximumFractionDigits());
2166: } else if (type.equals(I18N_PERCENT_ELEMENT)) {
2167: to_fmt = (DecimalFormat) NumberFormat
2168: .getPercentInstance(loc);
2169: } else {
2170: throw new SAXException("<i18n:number>: unknown type: "
2171: + type);
2172: }
2173:
2174: if (fractionDigits > -1) {
2175: to_fmt.setMinimumFractionDigits(fractionDigits);
2176: to_fmt.setMaximumFractionDigits(fractionDigits);
2177: }
2178:
2179: if (dgLoc != null) {
2180: DecimalFormat df = (DecimalFormat) NumberFormat
2181: .getCurrencyInstance(dgLoc);
2182: DecimalFormatSymbols dfsNew = df.getDecimalFormatSymbols();
2183: DecimalFormatSymbols dfsOrig = to_fmt
2184: .getDecimalFormatSymbols();
2185: dfsOrig.setDecimalSeparator(dfsNew.getDecimalSeparator());
2186: dfsOrig.setMonetaryDecimalSeparator(dfsNew
2187: .getMonetaryDecimalSeparator());
2188: dfsOrig.setGroupingSeparator(dfsNew.getGroupingSeparator());
2189: to_fmt.setDecimalFormatSymbols(dfsOrig);
2190: }
2191:
2192: // pattern overwrites locale format
2193: if (pattern != null) {
2194: to_fmt.applyPattern(pattern);
2195: }
2196:
2197: try {
2198: numberValue = from_fmt.parse(value);
2199: if (int_currency > 0) {
2200: numberValue = new Double(numberValue.doubleValue()
2201: / int_currency);
2202: } else {
2203: // what?
2204: }
2205: } catch (ParseException pe) {
2206: throw new SAXException(this .getClass().getName()
2207: + "i18n:number - parsing error.", pe);
2208: }
2209:
2210: // we have all necessary data here: do formatting.
2211: String result = to_fmt.format(numberValue);
2212: if (appendDec)
2213: result = result + dec;
2214: if (getLogger().isDebugEnabled()) {
2215: getLogger().debug("i18n:number result: " + result);
2216: }
2217: return result;
2218: }
2219:
2220: //-- Dictionary handling routines
2221:
2222: /**
2223: * Helper method to retrieve a message from the dictionary.
2224: *
2225: * @param catalogueID if not null, this catalogue will be used instead of the default one.
2226: * @return SaxBuffer containing message, or null if not found.
2227: */
2228: protected ParamSaxBuffer getMessage(String catalogueID, String key) {
2229: if (getLogger().isDebugEnabled()) {
2230: getLogger().debug(
2231: "Getting key " + key + " from catalogue "
2232: + catalogueID);
2233: }
2234:
2235: CatalogueInfo catalogue = this .catalogue;
2236: if (catalogueID != null) {
2237: catalogue = (CatalogueInfo) catalogues.get(catalogueID);
2238: if (catalogue == null) {
2239: if (getLogger().isWarnEnabled()) {
2240: getLogger()
2241: .warn(
2242: "Catalogue not found: "
2243: + catalogueID
2244: + ", will not translate key "
2245: + key);
2246: }
2247: return null;
2248: }
2249: }
2250:
2251: Bundle bundle = catalogue.getCatalogue();
2252: if (bundle == null) {
2253: // Can't translate
2254: getLogger().debug("Untranslated key: '" + key + "'");
2255: return null;
2256: }
2257:
2258: try {
2259: return (ParamSaxBuffer) bundle.getObject(key);
2260: } catch (MissingResourceException e) {
2261: getLogger().debug("Untranslated key: '" + key + "'");
2262: }
2263:
2264: return null;
2265: }
2266:
2267: /**
2268: * Helper method to retrieve a message from the current dictionary.
2269: * A default value is returned if message is not found.
2270: *
2271: * @return SaxBuffer containing message, or defaultValue if not found.
2272: */
2273: private ParamSaxBuffer getMessage(String key,
2274: ParamSaxBuffer defaultValue) {
2275: SaxBuffer value = getMessage(currentCatalogueId, key);
2276: if (value == null) {
2277: return defaultValue;
2278: }
2279:
2280: return new ParamSaxBuffer(value);
2281: }
2282:
2283: public void recycle() {
2284: this .untranslatedRecorder = null;
2285: this .catalogue = null;
2286: this .objectModel = null;
2287:
2288: // Release catalogues which were selected for current locale
2289: Iterator i = catalogues.values().iterator();
2290: while (i.hasNext()) {
2291: CatalogueInfo catalogueInfo = (CatalogueInfo) i.next();
2292: catalogueInfo.releaseCatalog();
2293: }
2294:
2295: super .recycle();
2296: }
2297:
2298: public void dispose() {
2299: if (manager != null) {
2300: manager.release(factory);
2301: }
2302: factory = null;
2303: manager = null;
2304: catalogues = null;
2305: }
2306:
2307: /**
2308: * Holds information about one catalogue. The location and name of the catalogue
2309: * can contain references to input modules, and are resolved upon each transformer
2310: * usage. It is important that releaseCatalog is called when the transformer is recycled.
2311: */
2312: public final class CatalogueInfo {
2313: VariableResolver name;
2314: VariableResolver[] locations;
2315: String resolvedName;
2316: String[] resolvedLocations;
2317: Bundle catalogue;
2318:
2319: public CatalogueInfo(String name, String[] locations)
2320: throws PatternException {
2321: this .name = VariableResolverFactory.getResolver(name,
2322: manager);
2323: this .locations = new VariableResolver[locations.length];
2324: for (int i = 0; i < locations.length; ++i) {
2325: this .locations[i] = VariableResolverFactory
2326: .getResolver(locations[i], manager);
2327: }
2328: }
2329:
2330: public String getName() {
2331: try {
2332: if (resolvedName == null) {
2333: resolve();
2334: }
2335: } catch (Exception e) {
2336: // Ignore the error for now
2337: }
2338: return resolvedName;
2339: }
2340:
2341: public String[] getLocation() {
2342: try {
2343: if (resolvedName == null) {
2344: resolve();
2345: }
2346: } catch (Exception e) {
2347: // Ignore the error for now
2348: }
2349: return resolvedLocations;
2350: }
2351:
2352: private void resolve() throws Exception {
2353: if (resolvedLocations == null) {
2354: resolvedLocations = new String[locations.length];
2355: for (int i = 0; i < resolvedLocations.length; ++i) {
2356: resolvedLocations[i] = locations[i].resolve(null,
2357: objectModel);
2358: }
2359: }
2360: if (resolvedName == null) {
2361: resolvedName = name.resolve(null, objectModel);
2362: }
2363: }
2364:
2365: public Bundle getCatalogue() {
2366: if (catalogue == null) {
2367: try {
2368: resolve();
2369: catalogue = factory.select(resolvedLocations,
2370: resolvedName, locale);
2371: } catch (Exception e) {
2372: getLogger().error(
2373: "Error obtaining catalogue '" + getName()
2374: + "' from <" + getLocation()
2375: + "> for locale " + locale, e);
2376: }
2377: }
2378:
2379: return catalogue;
2380: }
2381:
2382: public void releaseCatalog() {
2383: if (catalogue != null) {
2384: factory.release(catalogue);
2385: }
2386: catalogue = null;
2387: resolvedName = null;
2388: resolvedLocations = null;
2389: }
2390: }
2391: }
|