Contributing Translators

Here we create two Translators and make them available to the whole application by name.

   (y, n, yes, no)

   (maximum 2 decimal places)

The first translator is called yesno and its class is YesNoTranslator.
It parses "y", "n", "yes", and "no" (ignoring case), client-side, to Boolean, server-side, and back to "yes" or "no", client-side.

The second translator is called money2 and its class is MoneyTranslator.
It parses a String, client-side, to a BigDecimal with maximum of 2 decimal places, server-side, and vice-versa.

We made both translators available to the whole application by contributing them to the TranslatorAlternatesSource service - see AppModule source below. Alternatively, a translator can be made available to the whole application and selected automatically based on the type that it translates, by contributing it to the TranslatorSource service.

This example has also used localization, which is explained later in the Localization examples.

References: Translator, TranslatorSource, TranslatorAlternatesSource, ComponentMessagesSource, Defining Tapestry IoC Services.

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>Contributing Translators</h3>

    Here we create two Translators and make them available to the whole application <strong>by name</strong>.

    <div class="eg">
        <t:form class="form-horizontal" t:id="inputs">
            <div class="form-group">
                <t:label for="newToTapestry" class="col-sm-3"/>
                <div class="col-sm-3">
                    <t:textfield t:id="newToTapestry" t:translate="yesno"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${newToTapestry}
                        &nbsp;&nbsp;
                        <span class="text-muted">(y, n, yes, no)</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <t:label for="price" class="col-sm-3"/>
                <div class="col-sm-3">
                    <t:textfield t:id="price" t:translate="money2"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${price}
                        &nbsp;&nbsp;
                        <span class="text-muted">(maximum 2 decimal places)</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-3 col-sm-offset-3">
                    <t:submit/>
                </div>
            </div>
        </t:form>
    </div>
    
    The first translator is called <em>yesno</em> and its class is <code>YesNoTranslator</code>.<br/>
    It parses "y", "n", "yes", and "no" (ignoring case), client-side, to Boolean, server-side, and back to "yes" or "no", client-side.<br/><br/>

    The second translator is called <em>money2</em> and its class is <code>MoneyTranslator</code>.<br/>
    It parses a String, client-side, to a BigDecimal with maximum of 2 decimal places, server-side, and vice-versa.<br/><br/>

    We made both translators available to the whole application by contributing them to the <code>TranslatorAlternatesSource</code> 
    service - see AppModule source below.

    Alternatively, a translator can be made available to the whole application and selected <strong>automatically</strong> based on the type 
    that it translates, by contributing it to the <code>TranslatorSource</code> service.<br/><br/>
    
    This example has also used localization, which is explained later in the Localization examples.<br/><br/> 

    References: 
    <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/services/TranslatorSource.html">TranslatorSource</a>,  
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/TranslatorAlternatesSource.html">TranslatorAlternatesSource</a>,  
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/messages/ComponentMessagesSource.html">ComponentMessagesSource</a>, 
    <a href="http://tapestry.apache.org/defining-tapestry-ioc-services.html">Defining Tapestry IoC Services</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/ContributingTranslators.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/ContributingTranslators.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/ContributingTranslators_fr.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/ContributingTranslators.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/plain.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/YesNoTranslator.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/TranslationMessages.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/TranslationMessages_fr.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/MoneyTranslator.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
    </t:tabgroup>
</body>
</html>


newToTapestry-label=Are you new to Tapestry?
price-label=A price


newToTapestry-label=Êtes-vous familier avec Tapestry?
price-label=Une prix


package jumpstart.web.pages.examples.input;

import java.math.BigDecimal;

import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;

@Import(stylesheet="css/examples/plain.css")
public class ContributingTranslators {

    // Screen fields

    @Property
    @Persist(PersistenceConstants.FLASH)
    private Boolean newToTapestry;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private BigDecimal price;

}


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


package jumpstart.web.translators;

import org.apache.tapestry5.Field;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.services.FormSupport;

public class YesNoTranslator implements Translator<Boolean> {
    private final String name;
    private final Class<Boolean> type;
    private final String messageKey;

    public YesNoTranslator(String name) {
        this.name = name;
        this.type = Boolean.class;
        this.messageKey = "yesno-format-exception";
    }

    @Override
    public String toClient(Boolean value) {
        return (value == null ? null : value == true ? "yes" : "no");
    }

    @Override
    public Boolean parseClient(Field field, String clientValue, String message) throws ValidationException {
        if (clientValue == null) {
            return null;
        }
        else {
            String s = clientValue.toLowerCase();
            if (s.equals("y") || s.equals("yes")) {
                return Boolean.TRUE;
            }
            else if (s.equals("n") || s.equals("no")) {
                return Boolean.FALSE;
            }
            else {
                throw new ValidationException(message);
            }
        }
    }

    @Override
    public void render(Field field, String message, MarkupWriter writer, FormSupport formSupport) {
        // Do nothing; we don't yet support client-side validation.
        // formSupport.addValidation(field, "yesno", message, null);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Class<Boolean> getType() {
        return type;
    }

    @Override
    public String getMessageKey() {
        return messageKey;
    }

    
}


yesno-format-exception=Answer must be y, n, yes, or no for %s.  Upper case is permitted
money-format-exception=You must provide a valid money amount for %s.  


yesno-format-exception=Réponse doit être y, n, yes, ou no pour %s. Majuscules est autorisée.
money-format-exception=Vous devez donner une monnaie valide pour %s.


// Based on an example kindly provided by George Christman. See http://tapestry.1045711.n5.nabble.com/Currency-Converter-tp5719990.html .

package jumpstart.web.translators;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;

import org.apache.tapestry5.Field;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.FormSupport;

// This class represents money with BigDecimal.
public class MoneyTranslator implements Translator<BigDecimal> {

    private final String name;
    private final int maxDecimalPlaces;
    private final ThreadLocale threadLocale;

    private final Class<BigDecimal> type;
    private final String messageKey;

    private ParsePosition parsePosition;

    public MoneyTranslator(String name, int maxDecimalPlaces, ThreadLocale threadLocale) {
        this.name = name;
        this.maxDecimalPlaces = maxDecimalPlaces;
        this.threadLocale = threadLocale;

        type = BigDecimal.class;
        messageKey = "money-format-exception";

        parsePosition = new ParsePosition(0);
    }

    @Override
    public String toClient(BigDecimal value) {

        // We want the client locale's currency format...
        
        Locale locale = threadLocale.getLocale();
        DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
        
        // ...with the right number of decimal places...
        
        decimalFormat.setMaximumFractionDigits(maxDecimalPlaces);

        // ...without a currency symbol
        
        DecimalFormatSymbols decimalFormatSymbols = decimalFormat.getDecimalFormatSymbols();
        decimalFormatSymbols.setCurrencySymbol("");
        decimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);
        
        return decimalFormat.format(value).trim();
    }

    @Override
    public BigDecimal parseClient(Field field, String clientValue, String message) throws ValidationException {
        
        if (clientValue == null) {
            return null;
        }
        else {
            BigDecimal money = parseMoney(clientValue.trim(), message);
            
            if (money.scale() > maxDecimalPlaces) {
                throw new ValidationException(message);
            }
            
            return money;
        }
        
    }

    private BigDecimal parseMoney(String clientValue, String message) throws ValidationException {

        // Based on the techniques described in http://www.ibm.com/developerworks/java/library/j-numberformat/ .

        Locale locale = threadLocale.getLocale();

        // Try parsing as a decimal number...

        DecimalFormat numberFormat = (DecimalFormat) NumberFormat.getInstance(locale);
        numberFormat.setParseBigDecimal(true);

        // ...in the format of the client locale (eg. 12,345.67 or 12345.67 or -12,345.67 or -12 345,67)

        numberFormat.setNegativePrefix("-");
        numberFormat.setNegativeSuffix("");
        parsePosition.setIndex(0);
        BigDecimal bigDecimal = (BigDecimal) numberFormat.parse(clientValue, parsePosition);

        if (parsePosition.getIndex() == clientValue.length() && bigDecimal != null) {
            return bigDecimal;
        }

        // ...with trailing negative sign (eg. 12,345.67-)

        numberFormat.setNegativePrefix("");
        numberFormat.setNegativeSuffix("-");
        parsePosition.setIndex(0);
        bigDecimal = (BigDecimal) numberFormat.parse(clientValue, parsePosition);

        if (parsePosition.getIndex() == clientValue.length() && bigDecimal != null) {
            return bigDecimal;
        }

        // ...with brackets for negative (eg. (12,345.67))

        numberFormat.setNegativePrefix("(");
        numberFormat.setNegativeSuffix(")");
        parsePosition.setIndex(0);
        bigDecimal = (BigDecimal) numberFormat.parse(clientValue, parsePosition);

        if (parsePosition.getIndex() == clientValue.length() && bigDecimal != null) {
            return bigDecimal;
        }

        throw new ValidationException(message);
    }

    @Override
    public void render(Field field, String message, MarkupWriter writer, FormSupport formSupport) {
        // Do nothing; we don't yet support client-side validation.
        // formSupport.addValidation(field, name, message, null);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Class<BigDecimal> getType() {
        return type;
    }

    @Override
    public String getMessageKey() {
        return messageKey;
    }

}


package jumpstart.web.services;

import java.util.Map;

import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptor;
import org.apache.tapestry5.internal.beanvalidator.BaseCCD;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.EagerLoad;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import org.apache.tapestry5.ioc.services.Coercion;
import org.apache.tapestry5.ioc.services.CoercionTuple;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.BeanBlockContribution;
import org.apache.tapestry5.services.ComponentRequestFilter;
import org.apache.tapestry5.services.DisplayBlockContribution;
import org.apache.tapestry5.services.EditBlockContribution;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.javascript.DataConstants;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.services.security.WhitelistAnalyzer;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.upload.services.UploadSymbols;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;

import jumpstart.business.validation.constraints.Letters;
import jumpstart.util.JodaTimeUtil;
import jumpstart.web.translators.MoneyTranslator;
import jumpstart.web.translators.YesNoTranslator;

/**
 * This module is automatically included as part of the Tapestry IoC Registry, it's a good place to configure and extend
 * Tapestry, or to place your own service definitions. See http://tapestry.apache.org/5.3.4/tapestry-ioc/module.html
 */
public class AppModule {
    private static final String UPLOADS_PATH = "jumpstart.upload-path";

    @Inject
    @Symbol(SymbolConstants.PRODUCTION_MODE)
    @Property(write = false)
    private static boolean productionMode;

    // Add 2 services to those provided by Tapestry.
    // - CountryNames, and SelectIdModelFactory are used by pages which ask Tapestry to @Inject them.

    public static void bind(ServiceBinder binder) {
        binder.bind(CountryNames.class);
        binder.bind(SelectIdModelFactory.class, SelectIdModelFactoryImpl.class);
    }

    // Tell Tapestry about our custom translators and their message file.
    // We do this by contributing configuration to Tapestry's TranslatorAlternatesSource service, FieldValidatorSource
    // service, and ComponentMessagesSource service.

    @SuppressWarnings("rawtypes")
    public static void contributeTranslatorAlternatesSource(MappedConfiguration<String, Translator> configuration,
            ThreadLocale threadLocale) {
        configuration.add("yesno", new YesNoTranslator("yesno"));
        configuration.add("money2", new MoneyTranslator("money2", 2, threadLocale));
    }

    public void contributeComponentMessagesSource(OrderedConfiguration<String> configuration) {
        configuration.add("myTranslationMessages", "jumpstart/web/translators/TranslationMessages");
    }

    // Tell Tapestry about the client-side (javascript) validators that corresponds to each server-side Bean Validator.

    public static void contributeClientConstraintDescriptorSource(final JavaScriptSupport javaScriptSupport,
            final Configuration<ClientConstraintDescriptor> configuration) {

        configuration.add(new BaseCCD(Letters.class) {

            public void applyClientValidation(MarkupWriter writer, String message, Map<String, Object> attributes) {
                javaScriptSupport.require("beanvalidation/letters");
                writer.attributes(DataConstants.VALIDATION_ATTRIBUTE, true, "data-validate-letters", true,
                        "data-letters-message", message);
            }

        });

    }

    // Tell Tapestry about our custom ValueEncoders.
    // We do this by contributing configuration to Tapestry's ValueEncoderSource service.

    // @SuppressWarnings("rawtypes")
    // public static void contributeValueEncoderSource(MappedConfiguration<Class, Object> configuration) {
    // configuration.addInstance(Person.class, PersonEncoder.class);
    // }

    // Tell Tapestry which locales we support, and tell Tapestry to use its jQuery implementation for its JavaScript.
    // We do this by contributing configuration to Tapestry's ApplicationDefaults service.

    public static void contributeApplicationDefaults(MappedConfiguration<String, String> configuration) {
        configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en_US,en_GB,fr");
        configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery");
    }

    // Tell Tapestry how to detect and protect pages that require security.
    // We do this by contributing a custom ComponentRequestFilter to Tapestry's ComponentRequestHandler service.
    // - ComponentRequestHandler is shown in
    // http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview
    // - Based on http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html

    public void contributeComponentRequestHandler(OrderedConfiguration<ComponentRequestFilter> configuration) {
        configuration.addInstance("PageProtectionFilter", PageProtectionFilter.class);
    }

    // Tell Tapestry how to handle JBoss 7's classpath URLs - JBoss uses a "virtual file system".
    // We do this by overriding Tapestry's ClasspathURLConverter service.
    // See "Running Tapestry on JBoss" in http://wiki.apache.org/tapestry/Tapestry5HowTos .

    @SuppressWarnings("rawtypes")
    public static void contributeServiceOverride(MappedConfiguration<Class, Object> configuration) {
        configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterJBoss7());
    }

    // Tell Tapestry how to handle @EJB in page and component classes.
    // We do this by contributing configuration to Tapestry's ComponentClassTransformWorker service.
    // - Based on http://wiki.apache.org/tapestry/JEE-Annotation.

    @Primary
    public static void contributeComponentClassTransformWorker(
            OrderedConfiguration<ComponentClassTransformWorker2> configuration) {
        configuration.addInstance("EJB", EJBAnnotationWorker.class, "before:Property");
    }

    // Tell Tapestry how to handle pages annotated with @WhitelistAccessOnly, eg. Tapestry's ServiceStatus and
    // PageCatalog.
    // The default WhitelistAnalyzer allows localhost only and only in non-production mode.
    // Our aim is to make the servicestatus page available to ALL clients when not in production mode.
    // We do this by contributing our own WhitelistAnalyzer to Tapestry's ClientWhitelist service.

    public static void contributeClientWhitelist(OrderedConfiguration<WhitelistAnalyzer> configuration) {
        if (!productionMode) {
            configuration.add("NonProductionWhitelistAnalyzer", new WhitelistAnalyzer() {
                @Override
                public boolean isRequestOnWhitelist(Request request) {
                    if (request.getPath().startsWith("/core/servicestatus")) {
                        return true;
                    }
                    else {
                        // This is copied from org.apache.tapestry5.internal.services.security.LocalhostOnly
                        String remoteHost = request.getRemoteHost();
                        return remoteHost.equals("localhost") || remoteHost.equals("127.0.0.1")
                                || remoteHost.equals("0:0:0:0:0:0:0:1%0") || remoteHost.equals("0:0:0:0:0:0:0:1");
                    }
                }
            }, "before:*");
        }
    }

    // Tell Tapestry how to build our Filer service (used in the FileUpload example).
    // Annotate it with EagerLoad to force resolution of symbols at startup rather than when it is first used.

    @EagerLoad
    public static IFiler buildFiler(Logger logger, @Inject @Symbol(UPLOADS_PATH) final String uploadsPath,
            @Inject @Symbol(UploadSymbols.FILESIZE_MAX) final long fileSizeMax) {
        return new Filer(logger, UPLOADS_PATH, uploadsPath, UploadSymbols.FILESIZE_MAX, fileSizeMax);
    }

    // Tell Tapestry how to coerce Joda Time types to and from Java Date types for the TypeCoercers example.
    // We do this by contributing configuration to Tapestry's TypeCoercer service.
    // - Based on http://tapestry.apache.org/typecoercer-service.html

    @SuppressWarnings("rawtypes")
    public static void contributeTypeCoercer(Configuration<CoercionTuple> configuration) {

        // From java.util.Date to DateMidnight

        Coercion<java.util.Date, DateMidnight> toDateMidnight = new Coercion<java.util.Date, DateMidnight>() {
            public DateMidnight coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toDateMidnight(input);
            }
        };

        configuration.add(new CoercionTuple<>(java.util.Date.class, DateMidnight.class, toDateMidnight));

        // From DateMidnight to java.util.Date

        Coercion<DateMidnight, java.util.Date> fromDateMidnight = new Coercion<DateMidnight, java.util.Date>() {
            public java.util.Date coerce(DateMidnight input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(DateMidnight.class, java.util.Date.class, fromDateMidnight));

        // From java.util.Date to LocalDate

        Coercion<java.util.Date, LocalDate> toLocalDate = new Coercion<java.util.Date, LocalDate>() {
            public LocalDate coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toLocalDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(java.util.Date.class, LocalDate.class, toLocalDate));

        // From LocalDate to java.util.Date

        Coercion<LocalDate, java.util.Date> fromLocalDate = new Coercion<LocalDate, java.util.Date>() {
            public java.util.Date coerce(LocalDate input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(LocalDate.class, java.util.Date.class, fromLocalDate));
    }

    // Tell Tapestry how its BeanDisplay and BeanEditor can handle the JodaTime types.
    // We do this by contributing configuration to Tapestry's DefaultDataTypeAnalyzer and BeanBlockSource services.
    // - Based on http://tapestry.apache.org/beaneditform-guide.html .

    public static void contributeDefaultDataTypeAnalyzer(
            @SuppressWarnings("rawtypes") MappedConfiguration<Class, String> configuration) {
        configuration.add(DateTime.class, "dateTime");
        configuration.add(DateMidnight.class, "dateMidnight");
        configuration.add(LocalDateTime.class, "localDateTime");
        configuration.add(LocalDate.class, "localDate");
        configuration.add(LocalTime.class, "localTime");
    }

    public static void contributeBeanBlockSource(Configuration<BeanBlockContribution> configuration) {

        configuration.add(new DisplayBlockContribution("dateTime", "infra/AppPropertyDisplayBlocks", "dateTime"));
        configuration
                .add(new DisplayBlockContribution("dateMidnight", "infra/AppPropertyDisplayBlocks", "dateMidnight"));
        configuration
                .add(new DisplayBlockContribution("localDateTime", "infra/AppPropertyDisplayBlocks", "localDateTime"));
        configuration.add(new DisplayBlockContribution("localDate", "infra/AppPropertyDisplayBlocks", "localDate"));
        configuration.add(new DisplayBlockContribution("localTime", "infra/AppPropertyDisplayBlocks", "localTime"));

        configuration.add(new EditBlockContribution("dateMidnight", "infra/AppPropertyEditBlocks", "dateMidnight"));
        configuration.add(new EditBlockContribution("localDate", "infra/AppPropertyEditBlocks", "localDate"));

    }

}