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
015package org.apache.tapestry5.internal.translator;
016
017import java.math.BigDecimal;
018import java.math.BigInteger;
019import java.text.DecimalFormat;
020import java.text.DecimalFormatSymbols;
021import java.text.NumberFormat;
022import java.text.ParseException;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.tapestry5.Field;
028import org.apache.tapestry5.SymbolConstants;
029import org.apache.tapestry5.ioc.annotations.Symbol;
030import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
031import org.apache.tapestry5.ioc.services.ThreadLocale;
032import org.apache.tapestry5.ioc.services.TypeCoercer;
033import org.apache.tapestry5.json.JSONObject;
034import org.apache.tapestry5.services.ClientBehaviorSupport;
035import org.apache.tapestry5.services.Request;
036import org.apache.tapestry5.services.javascript.InitializationPriority;
037import org.apache.tapestry5.services.javascript.JavaScriptSupport;
038
039public 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}