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 }