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 }