You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2009/02/26 02:54:34 UTC

svn commit: r747983 - in /tapestry/tapestry5/trunk: src/site/apt/ tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/ tapestry-core/src/main/java/org/apache/tapestry5/services/ tapestry-core/src/main/resources/org/apache/tapestry5/ ta...

Author: hlship
Date: Thu Feb 26 01:54:33 2009
New Revision: 747983

URL: http://svn.apache.org/viewvc?rev=747983&view=rev
Log:
TAP5-211: Client-side validation of numeric user input does not take into account the user's locale which causes spurious client- and server-side exceptions when users enter numbers "naturally"

Added:
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupport.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupportImpl.java
    tapestry/tapestry5/trunk/tapestry-core/src/test/app1/ClientNumericValidationDemo.tml
    tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ClientNumericValidationDemo.java
Removed:
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/ByteTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/DecimalNumberTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/DoubleTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/FloatTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/IntegerNumberTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/IntegerTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/LongTranslator.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/ShortTranslator.java
Modified:
    tapestry/tapestry5/trunk/src/site/apt/index.apt
    tapestry/tapestry5/trunk/src/site/apt/upgrade.apt
    tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/services/TapestryModule.java
    tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
    tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/IntegrationTests.java
    tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
    tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
    tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/TranslatorSourceImplTest.java

Modified: tapestry/tapestry5/trunk/src/site/apt/index.apt
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/src/site/apt/index.apt?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/src/site/apt/index.apt (original)
+++ tapestry/tapestry5/trunk/src/site/apt/index.apt Thu Feb 26 01:54:33 2009
@@ -34,6 +34,8 @@
 
 New And Of Note
 
+  * Client-side numeric validation is now locale-sensitive.
+
   * Some significant performance improvements over Tapestry 5.0.18: the time to initially load a page,
     and the time to render a page have decreased.
 

Modified: tapestry/tapestry5/trunk/src/site/apt/upgrade.apt
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/src/site/apt/upgrade.apt?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/src/site/apt/upgrade.apt (original)
+++ tapestry/tapestry5/trunk/src/site/apt/upgrade.apt Thu Feb 26 01:54:33 2009
@@ -4,7 +4,7 @@
 
 Upgrade Notes
 
-  This is a quick guide to changes since Tapestry 5.0.  This is meant to provide information
+  This is a quick guide to changes <since> Tapestry 5.0.18.  This is meant to provide information
   on any additions or changes that developers will face after upgrading from Tapestry 5.0 to Tapestry 5.1, or from one
   5.1 snapshot release to another.
 
@@ -13,6 +13,13 @@
   You should also check the {{{release-notes.html}project-wide release notes}} for information
   about bugs fixes and other improvements.
 
+Release 5.1.0.1
+
+  Tapestry's client-side JavaScript relating to input field validation has changed somewhat
+  to account for
+  {{{https://issues.apache.org/jira/browse/TAP5-211}TAP5-211}}. This will only affect users
+  who have created their own client-side validations.
+
 Release 5.1.0.0
 
 * Primary Key Encoder

Added: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslator.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslator.java?rev=747983&view=auto
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslator.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslator.java Thu Feb 26 01:54:33 2009
@@ -0,0 +1,57 @@
+// Copyright 2009 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.translator;
+
+import org.apache.tapestry5.Field;
+import org.apache.tapestry5.MarkupWriter;
+import org.apache.tapestry5.ValidationException;
+import org.apache.tapestry5.services.FormSupport;
+
+import java.text.ParseException;
+
+public class NumericTranslator<T> extends AbstractTranslator<T>
+{
+    private final NumericTranslatorSupport support;
+
+    public NumericTranslator(String name, Class<T> type, NumericTranslatorSupport support)
+    {
+        super(name, type, support.getMessageKey(type));
+
+        this.support = support;
+    }
+
+    public void render(Field field, String message, MarkupWriter writer, FormSupport formSupport)
+    {
+        if (formSupport.isClientValidationEnabled())
+            support.addValidation(getType(), field, message);
+    }
+
+    public T parseClient(Field field, String clientValue, String message) throws ValidationException
+    {
+        try
+        {
+            return support.parseClient(getType(), clientValue);
+        }
+        catch (ParseException ex)
+        {
+            throw new ValidationException(message);
+        }
+    }
+
+    public String toClient(T value)
+    {
+        return support.toClient(getType(), value);
+    }
+}

Added: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupport.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupport.java?rev=747983&view=auto
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupport.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupport.java Thu Feb 26 01:54:33 2009
@@ -0,0 +1,70 @@
+// Copyright 2009 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.translator;
+
+import org.apache.tapestry5.Field;
+
+import java.text.ParseException;
+
+/**
+ * Used to generate the client-side JSON specification for how a number-based validator operates. Uses {@link
+ * org.apache.tapestry5.ioc.services.ThreadLocale} to determine the locale for any locale-specific operations.
+ *
+ * @since 5.1.0.1
+ */
+public interface NumericTranslatorSupport
+{
+    /**
+     * Parses a client-submitted value in a localized manner.
+     *
+     * @param type        desired type of value
+     * @param clientValue value from client; this will be trimmed of leading/trailing whitespace
+     * @param <T>
+     * @return the parsed value
+     * @throws ParseException
+     * @see org.apache.tapestry5.Translator#parseClient(org.apache.tapestry5.Field, String, String)
+     */
+    <T> T parseClient(Class<T> type, String clientValue) throws ParseException;
+
+    /**
+     * Converts a server-side value to a client-side string. Integer types are formatted simply; decimal types may be
+     * formatted using thousands-seperator commas.
+     *
+     * @param type  type of value to convert
+     * @param value current (non-null) value
+     * @param <T>
+     * @return value formatted
+     */
+    <T> String toClient(Class<T> type, T value);
+
+    /**
+     * Returns the default message key for parse failures for the indicated type.
+     *
+     * @param type
+     * @param <T>
+     * @return a message key: either "integer-format-exception" or "number-format-exception"
+     */
+    <T> String getMessageKey(Class<T> type);
+
+    /**
+     * Adds client-side format validation for the field, appropriate to the indicated type.
+     *
+     * @param type    value type
+     * @param field   field to which validation should be added
+     * @param message message if the client-side value can't be parsed as a number
+     * @param <T>
+     */
+    <T> void addValidation(Class<T> type, Field field, String message);
+}

Added: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupportImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupportImpl.java?rev=747983&view=auto
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupportImpl.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/translator/NumericTranslatorSupportImpl.java Thu Feb 26 01:54:33 2009
@@ -0,0 +1,172 @@
+// Copyright 2009 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.translator;
+
+import org.apache.tapestry5.Field;
+import org.apache.tapestry5.RenderSupport;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.ioc.services.ThreadLocale;
+import org.apache.tapestry5.ioc.services.TypeCoercer;
+import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.ClientBehaviorSupport;
+import org.apache.tapestry5.services.Request;
+
+import java.math.BigInteger;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+public class NumericTranslatorSupportImpl implements NumericTranslatorSupport
+{
+    private final TypeCoercer typeCoercer;
+
+    private final ThreadLocale threadLocale;
+
+    private final Request request;
+
+    private final RenderSupport renderSupport;
+
+    private final ClientBehaviorSupport clientBehaviorSupport;
+
+    private final Map<Locale, DecimalFormatSymbols> symbolsCache = CollectionFactory.newConcurrentMap();
+
+    private final Set<Class> integerTypes = CollectionFactory.newSet();
+
+    private static final String DECIMAL_FORMAT_SYMBOLS_PROVIDED = "tapestry.decimal-format-symbols-provided";
+
+    public NumericTranslatorSupportImpl(TypeCoercer typeCoercer, ThreadLocale threadLocale, Request request,
+                                        RenderSupport renderSupport, ClientBehaviorSupport clientBehaviorSupport)
+    {
+        this.typeCoercer = typeCoercer;
+        this.threadLocale = threadLocale;
+        this.request = request;
+        this.renderSupport = renderSupport;
+        this.clientBehaviorSupport = clientBehaviorSupport;
+
+        Class[] integerTypes = {
+                Byte.class, Short.class, Integer.class, Long.class, BigInteger.class
+        };
+
+        for (Class c : integerTypes)
+            this.integerTypes.add(c);
+
+    }
+
+    public <T> void addValidation(Class<T> type, Field field, String message)
+    {
+        if (request.getAttribute(DECIMAL_FORMAT_SYMBOLS_PROVIDED) == null)
+        {
+            renderSupport.addScript("Tapestry.decimalFormatSymbols = %s;", createJSONDecimalFormatSymbols());
+
+            request.setAttribute(DECIMAL_FORMAT_SYMBOLS_PROVIDED, true);
+        }
+
+        clientBehaviorSupport.addValidation(field, "numericformat", message, isIntegerType(type));
+    }
+
+    private JSONObject createJSONDecimalFormatSymbols()
+    {
+        Locale locale = threadLocale.getLocale();
+
+        DecimalFormatSymbols symbols = getSymbols(locale);
+
+        JSONObject result = new JSONObject();
+
+        result.put("groupingSeparator", toString(symbols.getGroupingSeparator()));
+        result.put("minusSign", toString(symbols.getMinusSign()));
+        result.put("decimalSeparator", toString(symbols.getDecimalSeparator()));
+
+        return result;
+    }
+
+    private DecimalFormatSymbols getSymbols(Locale locale)
+    {
+        DecimalFormatSymbols symbols = symbolsCache.get(locale);
+
+        if (symbols == null)
+        {
+            symbols = new DecimalFormatSymbols(locale);
+            symbolsCache.put(locale, symbols);
+        }
+
+        return symbols;
+    }
+
+    private boolean isIntegerType(Class type)
+    {
+        return integerTypes.contains(type);
+    }
+
+    public <T> T parseClient(Class<T> type, String clientValue) throws ParseException
+    {
+        NumberFormat formatter = getParseFormatter(type);
+
+        Number number = formatter.parse(clientValue.trim());
+
+        return typeCoercer.coerce(number, type);
+    }
+
+    private NumberFormat getParseFormatter(Class type)
+    {
+        Locale locale = threadLocale.getLocale();
+
+        boolean isInteger = isIntegerType(type);
+
+        // We don't cache NumberFormat instances because they are not thread safe.
+        // Perhaps we should turn this service into a perthread so that we can cache
+        // (for the duration of a request)?
+
+        return isInteger ? NumberFormat.getIntegerInstance(locale)
+                         : NumberFormat.getNumberInstance(locale);
+
+    }
+
+    private NumberFormat getOutputFormatter(Class type)
+    {
+        Locale locale = threadLocale.getLocale();
+
+        boolean isInteger = isIntegerType(type);
+
+        if (!isInteger) return NumberFormat.getNumberInstance(locale);
+
+        DecimalFormatSymbols symbols = getSymbols(locale);
+
+        return new DecimalFormat(toString(symbols.getZeroDigit()), symbols);
+    }
+
+    public <T> String toClient(Class<T> type, T value)
+    {
+        // TODO: The default includes the comma (thousands grouping) seperators.
+        // I don't think most people want that!
+
+        return getOutputFormatter(type).format(value);
+    }
+
+    public <T> String getMessageKey(Class<T> type)
+    {
+        return isIntegerType(type)
+               ? "integer-format-exception"
+               : "number-format-exception";
+    }
+
+    private static String toString(char ch)
+    {
+        return String.valueOf(ch);
+    }
+}

Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/services/TapestryModule.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/services/TapestryModule.java?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/services/TapestryModule.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/services/TapestryModule.java Thu Feb 26 01:54:33 2009
@@ -31,7 +31,9 @@
 import org.apache.tapestry5.internal.renderers.*;
 import org.apache.tapestry5.internal.services.*;
 import org.apache.tapestry5.internal.transform.*;
-import org.apache.tapestry5.internal.translator.*;
+import org.apache.tapestry5.internal.translator.NumericTranslator;
+import org.apache.tapestry5.internal.translator.NumericTranslatorSupport;
+import org.apache.tapestry5.internal.translator.StringTranslator;
 import org.apache.tapestry5.internal.util.PrimaryKeyEncoder2ValueEncoder;
 import org.apache.tapestry5.internal.util.RenderableAsBlock;
 import org.apache.tapestry5.internal.util.StringRenderable;
@@ -294,6 +296,7 @@
         binder.bind(ApplicationStatePersistenceStrategy.class, SessionApplicationStatePersistenceStrategy.class).withId(
                 "SessionApplicationStatePersistenceStrategy");
         binder.bind(AssetPathConverter.class, IdentityAssetPathConverter.class);
+        binder.bind(NumericTranslatorSupport.class);
     }
 
     // ========================================================================
@@ -779,21 +782,26 @@
     }
 
     /**
-     * Contributes the basic set of named translators: <ul>  <li>string</li>  <li>byte</li> <li>integer</li>
-     * <li>long</li> <li>float</li> <li>double</li> <li>short</li> </ul>
+     * Contributes the basic set of translators: <ul>  <li>string</li>  <li>byte</li> <li>short</li> <li>integer</li>
+     * <li>long</li> <li>float</li> <li>double</li>  </ul>
      */
-    public static void contributeTranslatorSource(Configuration<Translator> configuration)
+    public static void contributeTranslatorSource(Configuration<Translator> configuration,
+                                                  NumericTranslatorSupport support)
     {
 
         configuration.add(new StringTranslator());
-        configuration.add(new ByteTranslator());
-        configuration.add(new IntegerTranslator());
-        configuration.add(new LongTranslator());
-        configuration.add(new FloatTranslator());
-        configuration.add(new DoubleTranslator());
-        configuration.add(new ShortTranslator());
+
+        Class[] types = new Class[] { Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class };
+
+        for (Class type : types)
+        {
+            String name = type.getSimpleName().toLowerCase();
+
+            configuration.add(new NumericTranslator(name, type, support));
+        }
     }
 
+
     /**
      * Adds coercions: <ul> <li>String to {@link org.apache.tapestry5.SelectModel} <li>String to {@link
      * org.apache.tapestry5.corelib.data.InsertPosition} <li>Map to {@link org.apache.tapestry5.SelectModel}
@@ -1072,7 +1080,7 @@
 
     /**
      * Builds a proxy to the current {@link org.apache.tapestry5.RenderSupport} inside this thread's {@link
-     * Environment}.
+     * org.apache.tapestry5.services.Environment}.
      */
     public RenderSupport buildRenderSupport()
     {
@@ -1080,6 +1088,18 @@
     }
 
     /**
+     * Builds a proxy to the current {@link org.apache.tapestry5.services.ClientBehaviorSupport} inside this thread's
+     * {@link org.apache.tapestry5.services.Environment}.
+     *
+     * @since 5.1.0.1
+     */
+
+    public ClientBehaviorSupport buildClientBehaviorSupport()
+    {
+        return environmentalBuilder.build(ClientBehaviorSupport.class);
+    }
+
+    /**
      * Builds a proxy to the current {@link org.apache.tapestry5.services.FormSupport} inside this thread's {@link
      * org.apache.tapestry5.services.Environment}.
      */

Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js Thu Feb 26 01:54:33 2009
@@ -1,4 +1,4 @@
-// Copyright 2007, 2008 The Apache Software Foundation
+// Copyright 2007, 2008, 2009 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -32,15 +32,8 @@
      */
     FORM_PROCESS_SUBMIT_EVENT : "tapestry:formprocesssubmit",
 
-    /** Event, triggered on a field element, to cause observers validate the format of the input, and potentially
-     *  reformat it. The field will be passed to observers as the event memo.  This event will be followed by
-     *  FIELD_VALIDATE_EVENT, if the field's value is non-blank. Observers may invoke Element.showValidationMessage()
-     *  to identify that the field is in error (and decorate the field and show a popup error message).
-     */
-    FIELD_FORMAT_EVENT : "tapestry:fieldformat",
-
-    /** Event, triggered on a field element, to cause observers to validate the input. The field will be passed
-     * to observers as the event memo.    Observers may invoke Element.showValidationMessage()
+    /** Event, triggered on a field element, to cause observers to validate the input. The field's translated
+     * value will be passed to observers as the event memo.  Observers may invoke Element.showValidationMessage()
      *  to identify that the field is in error (and decorate the field and show a popup error message).
      */
     FIELD_VALIDATE_EVENT : "tapestry:fieldvalidate",
@@ -64,7 +57,7 @@
     },
 
     /** Find all elements marked with the "t-invisible" CSS class and hide()s them, so that
-     * Prototype's visible() method operates correctly.                                    In addition,
+     * Prototype's visible() method operates correctly. In addition,
      * finds form control elements and adds additional listeners to them to support
      * form field input validation.
      *
@@ -434,6 +427,51 @@
         var slashx = URL.lastIndexOf("/");
 
         return URL.substring(0, slashx + 1);
+    },
+
+    /**
+     * Convert a user-provided localized number to an ordinary number (not a string).
+     * Removes seperators and leading/trailing whitespace. Disallows the decimal point if isInteger is true.
+     * @param number string provided by user
+     * @param isInteger if true, disallow decimal point
+     */
+    formatLocalizedNumber : function(number, isInteger)
+    {
+        // We convert from localized string to a canonical string,
+        // stripping out  group seperators (normally commas). If isInteger is
+        // true, we don't allow a decimal point.
+
+        var minus = Tapestry.decimalFormatSymbols.minusSign;
+        var grouping = Tapestry.decimalFormatSymbols.groupingSeparator;
+        var decimal = Tapestry.decimalFormatSymbols.decimalSeparator;
+
+        var canonical = "";
+
+        number.strip().toArray().each(function(ch)
+        {
+            if (ch == minus)
+            {
+                canonical += "-";
+                return;
+            }
+
+            if (ch == grouping)
+            {
+                return;
+            }
+
+            if (ch == decimal)
+            {
+                if (isInteger) throw "Not an integer";
+
+                ch = ".";
+            }
+            else if (ch < "0" || ch > "9") throw "Invalid character";
+
+            canonical += ch;
+        });
+
+        return Number(canonical);
     }
 
 };
@@ -582,9 +620,6 @@
     {
         element = $(element);
 
-        $T(element).validationError = true;
-        $T(element.form).validationError = true;
-
         element.getFieldEventManager().showValidationMessage(message);
 
         return element;
@@ -601,19 +636,24 @@
         return element;
     },
 
-    /** Utility method to add a validator function as an observer as an event.
-     *
-     * @param element element to observe events on
-     * @param eventName name of event to observe
-     * @param validator function passed the field's value
+
+    /**
+     * Adds a standard validator for the element, an observer of
+     * Tapestry.FIELD_VALIDATE_EVENT. The validator function will be
+     * passed the current field value and should throw an error message if
+     * the field's value is not valid.
+     * @param element field element to validate
+     * @param validator function to be passed the field value
      */
-    addValidatorAsObserver : function(element, eventName, validator)
+    addValidator : function(element, validator)
     {
-        element.observe(eventName, function(event)
+        element.observe(Tapestry.FIELD_VALIDATE_EVENT, function(event)
         {
             try
             {
-                validator.call(this, $F(element));
+                // event.memo is the translated value, ready to be validated.
+                // For numeric fields, this will be a number.
+                validator.call(this, event.memo);
             }
             catch (message)
             {
@@ -622,32 +662,6 @@
         });
 
         return element;
-    },
-
-    /**
-     * Adds a standard validator for the element, an observer of
-     * Tapestry.FIELD_VALIDATE_EVENT. The validator function will be
-     * passed the current field value and should throw an error message if
-     * the field's value is not valid.
-     * @param element field element to validate
-     * @param validator function to be passed the field value
-     */
-    addValidator : function(element, validator)
-    {
-        return element.addValidatorAsObserver(Tapestry.FIELD_VALIDATE_EVENT, validator);
-    },
-
-    /**
-     * Adds a standard validator for the element, an observer of
-     * Tapestry.FIELD_FORMAT_EVENT. The validator function will be
-     * passed the current field value and should throw an error message if
-     * the field's value is not valid.
-     * @param element field element to validate
-     * @param validator function to be passed the field value
-     */
-    addFormatValidator : function(element, validator)
-    {
-        return element.addValidatorAsObserver(Tapestry.FIELD_FORMAT_EVENT, validator);
     }
 });
 
@@ -868,33 +882,29 @@
 
 Tapestry.Validator = {
 
-    INT_REGEXP : /^(\+|-)?\d+$/,
-
-    FLOAT_REGEXP : /^(\+|-)?((\.\d+)|(\d+(\.\d*)?))$/,
-
     required : function(field, message)
     {
-        field.addFormatValidator(function(value)
-        {
-            if (value.strip() == '') throw message;
-        });
-    },
-
-    /** Validate that the input is a numeric integer. */
-    integernumber : function(field, message)
-    {
-        field.addFormatValidator(function(value)
+        $(field).getFieldEventManager().requiredCheck = function(value)
         {
-            if (value != '' && ! value.match(Tapestry.Validator.INT_REGEXP)) throw message;
-        });
+            if (value.strip() == '')
+                $(field).showValidationMessage(message);
+        };
     },
 
-    decimalnumber : function(field, message)
+    /** Supplies a client-side numeric translator for the field. */
+    numericformat : function (field, message, isInteger)
     {
-        field.addFormatValidator(function(value)
+        $(field).getFieldEventManager().translator = function(input)
         {
-            if (value != '' && ! value.match(Tapestry.Validator.FLOAT_REGEXP)) throw message;
-        });
+            try
+            {
+                return Tapestry.formatLocalizedNumber(input, isInteger);
+            }
+            catch (e)
+            {
+                $(field).showValidationMessage(message);
+            }
+        };
     },
 
     minlength : function(field, message, length)
@@ -1170,6 +1180,9 @@
         this.label = $(id + ':label');
         this.icon = $(id + ':icon');
 
+        this.translator = Prototype.K;
+        // this.requiredCheck = Prototype.emptyFunction;
+
         document.observe(Tapestry.FOCUS_CHANGE_EVENT, function(event)
         {
             // If changing focus *within the same form* then
@@ -1210,6 +1223,9 @@
      */
     showValidationMessage : function(message)
     {
+        $T(this.field).validationError = true;
+        $T(this.field.form).validationError = true;
+
         this.field.addClassName("t-error");
 
         if (this.label)
@@ -1238,30 +1254,38 @@
      */
     validateInput : function()
     {
-        if (this.field.disabled) return;
+        if (this.field.disabled) return false;
 
-        if (! this.field.isDeepVisible()) return;
+        if (! this.field.isDeepVisible()) return false;
 
         var t = $T(this.field);
 
-        t.validationError = false;
+        var value = $F(this.field);
 
-        this.field.fire(Tapestry.FIELD_FORMAT_EVENT, this.field);
+        t.validationError = false;
 
-        // If Format went ok, perhaps do the other validations.
+        this.requiredCheck.call(this, value);
 
-        if (! t.validationError)
+        if (!t.validationError)
         {
-            var value = $F(this.field);
+            var translated = this.translator(value);
 
-            if (value != '')
-                this.field.fire(Tapestry.FIELD_VALIDATE_EVENT, this.field);
-        }
+            // If Format went ok, perhaps do the other validations.
 
-        // Lastly, if no validation errors were found, remove the decorations.
+            if (! t.validationError)
+            {
+                // Pass the translated value (a string or a number)
+                // to each event handler as event.memo.
+
+                if (translated != '')
+                    this.field.fire(Tapestry.FIELD_VALIDATE_EVENT, translated);
+            }
 
-        if (! t.validationError)
-            this.field.removeDecorations();
+            // Lastly, if no validation errors were found, remove the decorations.
+
+            if (! t.validationError)
+                this.field.removeDecorations();
+        }
 
         return t.validationError;
     }

Added: tapestry/tapestry5/trunk/tapestry-core/src/test/app1/ClientNumericValidationDemo.tml
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/app1/ClientNumericValidationDemo.tml?rev=747983&view=auto
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/app1/ClientNumericValidationDemo.tml (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/app1/ClientNumericValidationDemo.tml Thu Feb 26 01:54:33 2009
@@ -0,0 +1,23 @@
+<t:border xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"
+          xmlns:p="tapestry:parameter">
+
+    <h1>Client-Side Locale-specific Numeric Validation</h1>
+
+    <t:beaneditform t:id="form" object="this"/>
+
+    <hr/>
+
+    <dl>
+        <dt>longValue</dt>
+        <dd id="outputLongValue">${longValue}</dd>
+        <dt>doubleValue</dt>
+        <dd id="outputDoubleValue">${doubleValue}</dd>
+    </dl>
+
+    <p>
+        <t:actionlink t:id="reset">reset</t:actionlink>
+        |
+        <t:actionlink t:id="german">switch to German</t:actionlink>
+    </p>
+
+</t:border>
\ No newline at end of file

Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/IntegrationTests.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/IntegrationTests.java?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/IntegrationTests.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/IntegrationTests.java Thu Feb 26 01:54:33 2009
@@ -1992,12 +1992,12 @@
 
         assertText("sum", "0.0");
     }
-    
+
     @Test
     public void submit_with_context()
     {
         start("Submit With Context");
-        
+
         clickAndWait(SUBMIT);
 
         assertTextPresent("Result: 10.14159");
@@ -2630,4 +2630,73 @@
         assertTextPresent("Exception assembling root component of page UnsupportedParameterBlockDemo:",
                           "Component UnsupportedParameterBlockDemo:outputraw does not include a formal parameter 'unexpected' (and does not support informal parameters).");
     }
+
+    /**
+     * TAP5-211
+     */
+    public void client_side_numeric_validation()
+    {
+        start("Client-Side Numeric Validation", "reset");
+
+        assertText("outputLongValue", "1000");
+        assertText("outputDoubleValue", "1234.67");
+
+        assertFieldValue("longValue", "1000");
+        assertFieldValue("doubleValue", "1,234.67");
+
+        type("longValue", "2,000 ");
+        type("doubleValue", " -456,789.12");
+
+        clickAndWait(SUBMIT);
+
+        assertText("outputLongValue", "2000");
+        assertText("outputDoubleValue", "-456789.12");
+
+        assertFieldValue("longValue", "2000");
+        assertFieldValue("doubleValue", "-456,789.12");
+
+        clickAndWait("link=switch to German");
+
+        assertText("outputLongValue", "2000");
+        assertText("outputDoubleValue", "-456789.12");
+
+        assertFieldValue("longValue", "2000");
+        assertFieldValue("doubleValue", "-456.789,12");
+
+        type("longValue", "3.000");
+        type("doubleValue", "5.444.333,22");
+
+        clickAndWait(SUBMIT);
+
+        assertFieldValue("longValue", "3000");
+        assertFieldValue("doubleValue", "5.444.333,22");
+
+        assertText("outputLongValue", "3000");
+        assertText("outputDoubleValue", "5444333.22");
+
+        clickAndWait("link=reset");
+
+        type("longValue", "4000.");
+        click(SUBMIT);
+
+        assertBubbleMessage("longValue", "You must provide an integer value for Long Value.");
+
+        type("doubleValue", "abc");
+
+        click(SUBMIT);
+
+        assertBubbleMessage("doubleValue", "You must provide a numeric value for Double Value.");
+    }
+
+    private void assertBubbleMessage(String fieldId, String expected)
+    {
+        String condition = String.format(
+                "selenium.browserbot.getCurrentWindow().document.getElementById('%s:errorpopup')",
+                fieldId);
+
+        waitForCondition(condition, PAGE_LOAD_TIMEOUT);
+
+        assertText(String.format("//div[@id='%s:errorpopup']/span", fieldId), expected);
+
+    }
 }
\ No newline at end of file

Added: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ClientNumericValidationDemo.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ClientNumericValidationDemo.java?rev=747983&view=auto
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ClientNumericValidationDemo.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/ClientNumericValidationDemo.java Thu Feb 26 01:54:33 2009
@@ -0,0 +1,49 @@
+// Copyright 2009 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.integration.app1.pages;
+
+import org.apache.tapestry5.annotations.Persist;
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.services.PersistentLocale;
+
+import java.util.Locale;
+
+public class ClientNumericValidationDemo
+{
+    @Persist
+    @Property
+    private long longValue;
+
+    @Persist
+    @Property
+    private double doubleValue;
+
+    @Inject
+    private PersistentLocale persistentLocale;
+
+    void onActionFromReset()
+    {
+        longValue = 1000;
+        doubleValue = 1234.67;
+
+        persistentLocale.set(Locale.ENGLISH);
+    }
+
+    void onActionFromGerman()
+    {
+        persistentLocale.set(Locale.GERMAN);
+    }
+}

Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java Thu Feb 26 01:54:33 2009
@@ -65,6 +65,9 @@
 
     private static final List<Item> ITEMS = CollectionFactory.newList(
 
+            new Item("ClientNumericValidationDemo", "Client-Side Numeric Validation",
+                     "Client-side locale-specific validation"),
+
             new Item("PublishParametersDemo", "Publish Parameters Demo",
                      "Use of @Component.publishParameters attribute."),
 
@@ -320,7 +323,7 @@
                      "Nice exception message for common problem of form fields outside forms"),
 
             new Item("SubmitWithContext", "Submit With Context",
-                              "Providing a context for Submit component")
+                     "Providing a context for Submit component")
     );
 
     static

Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java Thu Feb 26 01:54:33 2009
@@ -119,7 +119,7 @@
 
     public static void contributeApplicationDefaults(MappedConfiguration<String, String> configuration)
     {
-        configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en,fr");
+        configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en,fr,de");
         configuration.add(SymbolConstants.PRODUCTION_MODE, "false");
         configuration.add(SymbolConstants.COMPRESS_WHITESPACE, "false");
 

Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/TranslatorSourceImplTest.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/TranslatorSourceImplTest.java?rev=747983&r1=747982&r2=747983&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/TranslatorSourceImplTest.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/TranslatorSourceImplTest.java Thu Feb 26 01:54:33 2009
@@ -19,25 +19,25 @@
 import org.apache.tapestry5.ValidationException;
 import org.apache.tapestry5.internal.test.InternalBaseTestCase;
 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.ioc.services.ThreadLocale;
 import org.apache.tapestry5.services.TranslatorSource;
-import org.apache.tapestry5.services.ValidationMessagesSource;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import java.util.Collection;
+import java.util.Locale;
 
 public class TranslatorSourceImplTest extends InternalBaseTestCase
 {
     private TranslatorSource source;
 
-    private ValidationMessagesSource messagesSource;
-
     @BeforeClass
     public void setup()
     {
         source = getService(TranslatorSource.class);
-        messagesSource = getService(ValidationMessagesSource.class);
+
+        getService(ThreadLocale.class).setLocale(Locale.ENGLISH);
     }
 
 
@@ -125,6 +125,8 @@
 
                 { Integer.class, " 123 ", 123 },
 
+                { Integer.class, " 20,000 ", 20000 },
+
                 { Long.class, "  -1234567 ", -1234567l },
 
                 { Double.class, " 3.14 ", 3.14d },