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    }