Augmenting Translators

The built-in translators bubble up events toClient and parseClient before doing their translation.
We can handle those events, which allows us to augment, or even override, the translator.

0   

  

  

   (eg. 2k=2000,5m=5000000)  

References: Overriding the Translator with Events, Translator, translator package, TextField, PasswordField, TextArea, NullFieldStrategy, DefaultNullFieldStrategy, ZeroNullFieldStrategy.

Home


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<body class="container">
    <h3>Augmenting Translators</h3>

    The built-in translators bubble up events <em>toClient</em> and <em>parseClient</em> before doing their translation. <br/>
    We can handle those events, which allows us to augment, or even override, the translator.

    <div class="eg">
        <t:form class="form-horizontal" t:id="inputs">
            <div class="form-group">
                <t:label for="primitiveWithZeroSuppressed" class="col-sm-4"/>
                <div class="col-sm-2">
                    <t:textfield t:id="primitiveWithZeroSuppressed"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${primitiveWithZeroSuppressed}
                        &nbsp;&nbsp;
                        <span class="text-muted">${primitiveWithZeroSuppressedMessage}</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <t:label for="objectDisplayingNullAsZero" class="col-sm-4"/>
                <div class="col-sm-2">
                    <t:textfield t:id="objectDisplayingNullAsZero"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${objectDisplayingNullAsZero}
                        &nbsp;&nbsp;
                        <span class="text-muted">${objectDisplayingNullAsZeroMessage}</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <t:label for="objectUsingZeroNullFieldStrategy" class="col-sm-4">
                    Object Using ZeroNullFieldStrategy
                </t:label>
                <div class="col-sm-2">
                    <t:textfield t:id="objectUsingZeroNullFieldStrategy" t:nulls="zero"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${objectUsingZeroNullFieldStrategy}
                        &nbsp;&nbsp;
                        <span class="text-muted">${objectUsingZeroNullFieldStrategyMessage}</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <t:label for="objectAllowingShorthandInput" class="col-sm-4"/>
                <div class="col-sm-2">
                    <t:textfield t:id="objectAllowingShorthandInput" t:nulls="zero" t:mixins="ClientTranslatorDisabler"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${objectAllowingShorthandInput}
                        &nbsp;&nbsp;
                        <span class="text-muted">(eg. 2k=2000,5m=5000000)&nbsp;&nbsp;${objectAllowingShorthandInputMessage}</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-2 col-sm-offset-4">
                    <t:submit/>
                </div>
            </div>
        </t:form>
    </div>
    
    <ul>
        <li>(1) Uses <em>toClient</em> to translate <em>0</em> server-side to empty string client-side. 
            The client-side translator will not let you leave the field empty because it knows that server-side is a <em>long</em>.</li>
        <li>(2) Uses <em>toClient</em> to translate <em>null</em> server-side to <em>"0"</em> client-side. 
            Notice that if you empty it, <em>parseClient</em> will NOT bubble up.</li>
        <li>(3) Uses <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/ZeroNullFieldStrategy.html">ZeroNullFieldStrategy</a> 
            which translates <em>null</em> server-side to <em>"0"</em> client-side, and empty field client-side to <em>0</em> server-side.</li>
        <li>(4) Uses <em>parseClient</em> to translate shorthand input into numerics (eg. <em>"2k"</em> client-side to <em>2000</em> server-side).</li>
        <li>(4) Also uses a <em>mixin</em>, to disable the client-side translator which would prevent non-numeric input. 
            A better mixin might be one that allows a numeric with a <em>k</em> or <em>m</em> suffix. 
            For more about mixins see the Mixin examples.</li>
    </ul>
    
    References: 
    <a href="http://tapestry.apache.org/forms-and-validation.html#FormsandValidation-OverridingtheTranslatorwithEvents">Overriding the Translator with Events</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/Translator.html">Translator</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/translator/package-summary.html">translator package</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/TextField.html">TextField</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/PasswordField.html">PasswordField</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/TextArea.html">TextArea</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/NullFieldStrategy.html">NullFieldStrategy</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/DefaultNullFieldStrategy.html">DefaultNullFieldStrategy</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/ZeroNullFieldStrategy.html">ZeroNullFieldStrategy</a>.
    <br/><br/>

    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
        
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/AugmentingTranslators.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/AugmentingTranslators.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/plain.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/mixins/ClientTranslatorDisabler.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/modules/client-translator-disabler.js"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.examples.input;

import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;

import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.TextField;
import org.apache.tapestry5.internal.translator.NumericTranslatorSupport;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;

@Import(stylesheet="css/examples/plain.css")
public class AugmentingTranslators {
    private static final String PARSED = "(parseClient handler was invoked)";

    // Screen fields

    @Property
    @Persist(PersistenceConstants.FLASH)
    private long primitiveWithZeroSuppressed;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String primitiveWithZeroSuppressedMessage;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private Long objectDisplayingNullAsZero;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String objectDisplayingNullAsZeroMessage;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private Long objectUsingZeroNullFieldStrategy;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String objectUsingZeroNullFieldStrategyMessage;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private Long objectAllowingShorthandInput;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String objectAllowingShorthandInputMessage;

    // Generally useful bits and pieces

    @Inject
    private Locale locale;

    @Inject
    private NumericTranslatorSupport numericTranslatorSupport;

    @Inject
    private Messages messages;

    @InjectComponent("objectAllowingShorthandInput")
    private TextField objectAllowingShorthandInputField;

    // The code

    /* 1st field. */

    String onToClientFromPrimitiveWithZeroSuppressed() {
        if (primitiveWithZeroSuppressed == 0) {
            return "";
        }
        else {
            // Return control to the normal translator.
            return null;
        }
    }

    Object onParseClientFromPrimitiveWithZeroSuppressed(String input) {
        // We included this handler only to set the message. Return control to the normal translator.
        primitiveWithZeroSuppressedMessage = PARSED;
        return null;
    }

    /* 2nd field. */

    String onToClientFromObjectDisplayingNullAsZero() {
        if (objectDisplayingNullAsZero == null) {
            return "0";
        }
        else {
            // Return control to the normal translator.
            return null;
        }
    }

    Object onParseClientFromObjectDisplayingNullAsZero(String input) {
        // We included this handler only to set the message. Return control to the normal translator.
        objectDisplayingNullAsZeroMessage = PARSED;
        return null;
    }

    /* 3rd field. */

    Object onParseClientFromObjectUsingZeroNullFieldStrategy(String input) {
        // We included this handler only to set the message. Return control to the normal translator.
        objectUsingZeroNullFieldStrategyMessage = PARSED;
        return null;
    }

    /* 4th field. */

    Object onParseClientFromObjectAllowingShorthandInput(String input) throws ValidationException {
        objectAllowingShorthandInputMessage = PARSED;
        String trimmed = input.trim();

        // If the trimmed input has a suffix of "k", "K", "m", or "M", then replace it with 3 or 6 zeroes.

        if (trimmed.length() > 1) {
            String lastChar = trimmed.substring(trimmed.length() - 1);

            if (lastChar.equalsIgnoreCase("k")) {
                trimmed = trimmed.substring(0, trimmed.length() - 1) + "000";
            }
            else if (lastChar.equalsIgnoreCase("m")) {
                trimmed = trimmed.substring(0, trimmed.length() - 1) + "000000";
            }
        }

        try {

            // Convert to a canonical form, stripping out grouping separators and disallowing decimal separators.
            // We can't leave this to NumericTranslatorSupport because it uses Java's NumberFormat.parse(String) which
            // is very lenient.

            String canonical = toCanonical(trimmed, true);

            Long l = numericTranslatorSupport.parseClient(Long.class, canonical);
            return l;
        }
        catch (ParseException e) {
            String message = messages.format(numericTranslatorSupport.getMessageKey(Long.class),
                    objectAllowingShorthandInputField.getLabel());
            throw new ValidationException(message);
        }
    }

    /**
     * This is the same pre-processing that Tapestry 5.3's client-side translator does (in tapestry.js).
     */
    private String toCanonical(String s, boolean anInteger) throws ParseException {
        DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);

        char minusSign = symbols.getMinusSign();
        char groupingSeparator = symbols.getGroupingSeparator();
        char decimalSeparator = symbols.getDecimalSeparator();

        // Convert non-breaking space to space. Necessary for French and other locales.
        if ((int) groupingSeparator == 160) {
            groupingSeparator = ' ';
        }

        StringBuilder canonical = new StringBuilder("");

        for (char ch : s.toCharArray()) {

            if (ch == minusSign) {
                canonical.append("-");
            }
            else if (ch == groupingSeparator) {
                continue;
            }
            else if (ch == decimalSeparator) {
                if (anInteger) {
                    throw new ParseException("Integer contains a decimal separator.", -1);
                }
                canonical.append(".");
            }
            else if (ch >= '0' && ch <= '9') {
                canonical.append(ch);
            }
            else {
                // System.out.println("ch = " + (int) ch + ", groupingSeparator = " + (int) groupingSeparator + ".");
                throw new ParseException(
                        "Contains character other than digit, minus sign, grouping separator, or decimal separator", -1);
            }

        }

        return canonical.toString();
    }

}


.eg {
                margin: 20px 0;
                padding: 14px;
                border: 1px solid #ddd;
                border-radius: 6px;
                -webkit-border-radius: 6px;
                -mox-border-radius: 6px;
}


/**
 * A simple mixin that can be applied to a field that has a Translator, eg. a TextField, PasswordField, or TextArea. 
 * It disables the field's client-side translator assigned by Tapestry.
 */
package jumpstart.web.mixins;

import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

public class ClientTranslatorDisabler {

    // Generally useful bits and pieces

    @InjectContainer
    private ClientElement attachedTo;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    // The code

    public void afterRender() {
        javaScriptSupport.require("client-translator-disabler").with(attachedTo.getClientId());
    }

}


// For a field, disables the field's default translator assigned by Tapestry.

define(["jquery"], function($) {

    return function(fieldId) {
        var $field = $("#" + fieldId);
        $field.removeAttr("data-translation");
    }
    
});