    001    // Copyright 2009, 2010 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.internal.translator;
    016
    017    import java.math.BigDecimal;
    018    import java.math.BigInteger;
    019    import java.text.DecimalFormat;
    020    import java.text.DecimalFormatSymbols;
    021    import java.text.NumberFormat;
    022    import java.text.ParseException;
    023    import java.util.Locale;
    024    import java.util.Map;
    025    import java.util.Set;
    026
    027    import org.apache.tapestry5.Field;
    028    import org.apache.tapestry5.SymbolConstants;
    029    import org.apache.tapestry5.ioc.annotations.Symbol;
    030    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    031    import org.apache.tapestry5.ioc.services.ThreadLocale;
    032    import org.apache.tapestry5.ioc.services.TypeCoercer;
    033    import org.apache.tapestry5.json.JSONObject;
    034    import org.apache.tapestry5.services.ClientBehaviorSupport;
    035    import org.apache.tapestry5.services.Request;
    036    import org.apache.tapestry5.services.javascript.InitializationPriority;
    037    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    038
    039    public class NumericTranslatorSupportImpl implements NumericTranslatorSupport
    040    {
    041        private final TypeCoercer typeCoercer;
    042
    043        private final ThreadLocale threadLocale;
    044
    045        private final Request request;
    046
    047        private final JavaScriptSupport javascriptSupport;
    048
    049        private final ClientBehaviorSupport clientBehaviorSupport;
    050
    051        private final boolean compactJSON;
    052
    053        private final Map<Locale, DecimalFormatSymbols> symbolsCache = CollectionFactory.newConcurrentMap();
    054
    055        private final Set<Class> integerTypes = CollectionFactory.newSet();
    056
    057        private static final String DECIMAL_FORMAT_SYMBOLS_PROVIDED = "tapestry.decimal-format-symbols-provided";
    058
    059        public NumericTranslatorSupportImpl(TypeCoercer typeCoercer, ThreadLocale threadLocale, Request request,
    060                JavaScriptSupport javascriptSupport, ClientBehaviorSupport clientBehaviorSupport,
    061                @Symbol(SymbolConstants.COMPACT_JSON)
    062                boolean compactJSON)
    063        {
    064            this.typeCoercer = typeCoercer;
    065            this.threadLocale = threadLocale;
    066            this.request = request;
    067            this.javascriptSupport = javascriptSupport;
    068            this.clientBehaviorSupport = clientBehaviorSupport;
    069            this.compactJSON = compactJSON;
    070
    071            Class[] integerTypes =
    072            { Byte.class, Short.class, Integer.class, Long.class, BigInteger.class };
    073
    074            for (Class c : integerTypes)
    075                this.integerTypes.add(c);
    076
    077        }
    078
    079        public <T extends Number> void addValidation(Class<T> type, Field field, String message)
    080        {
    081            if (request.getAttribute(DECIMAL_FORMAT_SYMBOLS_PROVIDED) == null)
    082            {
    083                javascriptSupport.addScript(InitializationPriority.IMMEDIATE, "Tapestry.decimalFormatSymbols = %s;",
    084                        createJSONDecimalFormatSymbols().toString(compactJSON));
    085
    086                request.setAttribute(DECIMAL_FORMAT_SYMBOLS_PROVIDED, true);
    087            }
    088
    089            clientBehaviorSupport.addValidation(field, "numericformat", message, isIntegerType(type));
    090        }
    091
    092        private JSONObject createJSONDecimalFormatSymbols()
    093        {
    094            Locale locale = threadLocale.getLocale();
    095
    096            DecimalFormatSymbols symbols = getSymbols(locale);
    097
    098            JSONObject result = new JSONObject();
    099
    100            result.put("groupingSeparator", toString(symbols.getGroupingSeparator()));
    101            result.put("minusSign", toString(symbols.getMinusSign()));
    102            result.put("decimalSeparator", toString(symbols.getDecimalSeparator()));
    103
    104            return result;
    105        }
    106
    107        private DecimalFormatSymbols getSymbols(Locale locale)
    108        {
    109            DecimalFormatSymbols symbols = symbolsCache.get(locale);
    110
    111            if (symbols == null)
    112            {
    113                symbols = new DecimalFormatSymbols(locale);
    114                symbolsCache.put(locale, symbols);
    115            }
    116
    117            return symbols;
    118        }
    119
    120        private boolean isIntegerType(Class type)
    121        {
    122            return integerTypes.contains(type);
    123        }
    124
    125        public <T extends Number> T parseClient(Class<T> type, String clientValue) throws ParseException
    126        {
    127            NumericFormatter formatter = getParseFormatter(type);
    128
    129            Number number = formatter.parse(clientValue.trim());
    130
    131            return typeCoercer.coerce(number, type);
    132        }
    133
    134        private NumericFormatter getParseFormatter(Class type)
    135        {
    136            Locale locale = threadLocale.getLocale();
    137            DecimalFormatSymbols symbols = getSymbols(locale);
    138
    139            if (type.equals(BigInteger.class))
    140                return new BigIntegerNumericFormatter(symbols);
    141
    142            if (type.equals(BigDecimal.class))
    143                return new BigDecimalNumericFormatter(symbols);
    144
    145            // We don't cache NumberFormat instances because they are not thread safe.
    146            // Perhaps we should turn this service into a perthread so that we can cache
    147            // (for the duration of a request)?
    148
    149            // We don't cache the rest of these, because they are built on DecimalFormat which is
    150            // not thread safe.
    151
    152            if (isIntegerType(type))
    153            {
    154                NumberFormat format = NumberFormat.getIntegerInstance(locale);
    155                return new NumericFormatterImpl(format);
    156            }
    157
    158            DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
    159
    160            if (type.equals(BigDecimal.class))
    161                df.setParseBigDecimal(true);
    162
    163            return new NumericFormatterImpl(df);
    164        }
    165
    166        private NumericFormatter getOutputFormatter(Class type)
    167        {
    168            Locale locale = threadLocale.getLocale();
    169
    170            DecimalFormatSymbols symbols = getSymbols(locale);
    171
    172            if (type.equals(BigInteger.class))
    173                return new BigIntegerNumericFormatter(symbols);
    174
    175            if (type.equals(BigDecimal.class))
    176                return new BigDecimalNumericFormatter(symbols);
    177
    178            // We don't cache the rest of these, because they are built on DecimalFormat which is
    179            // not thread safe.
    180
    181            if (!isIntegerType(type))
    182            {
    183                NumberFormat format = NumberFormat.getNumberInstance(locale);
    184
    185                return new NumericFormatterImpl(format);
    186            }
    187
    188            DecimalFormat df = new DecimalFormat(toString(symbols.getZeroDigit()), symbols);
    189
    190            return new NumericFormatterImpl(df);
    191        }
    192
    193        public <T extends Number> String toClient(Class<T> type, T value)
    194        {
    195            return getOutputFormatter(type).toClient(value);
    196        }
    197
    198        public <T extends Number> String getMessageKey(Class<T> type)
    199        {
    200            return isIntegerType(type) ? "integer-format-exception" : "number-format-exception";
    201        }
    202
    203        private static String toString(char ch)
    204        {
    205            return String.valueOf(ch);
    206        }
    207    }