Property Editors

This example demonstrates adding your own property editors, allowing Grid, BeanDisplay, and BeanEditor to display/edit unusual property types.

A property editor is made up of two parts:
  1. A data type analyzer, able to recognise when a property is of a type that the editor can handle.
  2. A display block and/or edit block, to display and/or edit the property.
As Grid or BeanDisplay tries to render each readable property of an object, it analyzes the property's data type, then renders the corresponding display block if one exists.
Tapestry comes with display blocks for enum, date, calendar, password, and more. See here.

As Grid or BeanEditor tries to render each read-write property of an object, it analyzes the property's data type, then renders the corresponding edit block if one exists.
Tapestry comes with edit blocks for text, number, enum, date, and more. See here.

In JumpStart we have added to the list of supported types by contributing property editors for certain Joda Time properties:
  1. We contributed DataTypeAnalyzers to detect 5 types: DateTime, DateMidnight, LocalDateTime, LocalDate, and LocalTime.
  2. We contributed BeanBlockSource display blocks for all 5 types and edit blocks for 2 of them: DateMidnight and LocalDate.
You can see the contributions in AppModule, below.

Here is what BeanDisplay can do with a DatesExample object. It makes use of our 5 contributed display blocks.
Id
1
Version
3
ADate Time
July 31, 2001 10:35:17 AM EST
ADate Midnight
July 10, 2001
ALocal Date Time
July 31, 2001 10:35:17 AM
ALocal Date
July 31, 2001
ALocal Time As Time
12:35:17 AM
Here is what BeanEditForm can do with the same DatesExample object. It makes use of our 2 contributed edit blocks.
[Show]
[Show]
References: BeanEditForm Guide, DataTypeAnalyzer, BeanBlockSource, DisplayBlockContribution, EditBlockContribution, PropertyOutputContext, PropertyEditContext, DateField, TextField, @Component, Joda Time.

Home

The source for @EJB handling, etc. is shown in the @EJB example.

PropertyEditors.tml


<!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_3.xsd">
<head>
    <link rel="stylesheet" type="text/css" href="${context:css/examples/examples.css}"/>
</head>
<body>
    <h1>Property Editors</h1>
    
    This example demonstrates adding your own property editors, allowing Grid, BeanDisplay, and BeanEditor to display/edit unusual property types.<br/><br/>
    
    A property editor is made up of two parts:
    <ol>
        <li>A <strong>data type analyzer</strong>, able to recognise when a property is of a type that the editor can handle.</li>
        <li>A <strong>display block</strong> and/or <strong>edit block</strong>, to display and/or edit the property.</li>
    </ol>
    
    As Grid or BeanDisplay tries to render each <strong>readable property</strong> of an object, it analyzes the property's data type, then renders the 
    corresponding <strong>display block</strong> if one exists. <br/>
    Tapestry comes with display blocks for enum, date, calendar, password, and more. 
    See <a href="http://tapestry.apache.org/beaneditform-guide.html#BeanEditFormGuide-SupportedTypes">here</a>.<br/><br/>
    
    As Grid or BeanEditor tries to render each <strong>read-write property</strong> of an object, it analyzes the property's data type, then renders the 
    corresponding <strong>edit block</strong> if one exists. <br/>
    Tapestry comes with edit blocks for text, number, enum, date, and more. 
    See <a href="http://tapestry.apache.org/beaneditform-guide.html#BeanEditFormGuide-SupportedTypes">here</a>.<br/><br/>

    In JumpStart we have added to the list of supported types by contributing property editors for certain Joda Time properties:
    <ol>
        <li>We contributed <strong>DataTypeAnalyzers</strong> to detect 5 types: DateTime, DateMidnight, LocalDateTime, LocalDate, and LocalTime.</li>
        <li>We contributed BeanBlockSource <strong>display blocks</strong> for all 5 types and <strong>edit blocks</strong> for 2 of them: DateMidnight and LocalDate.</li>
    </ol>
    You can see the contributions in AppModule, below.<br/><br/>
    
    Here is what BeanDisplay can do with a DatesExample object. It makes use of our 5 contributed display blocks. 
        
    <div class="eg">
        <t:beandisplay object="datesExample" include="id,version,adatetime,adatemidnight,alocaldatetime,alocaldate,alocaltimeastime">
            [BeanDisplayForm here]
        </t:beandisplay>
    </div>
            
    Here is what BeanEditForm can do with the same DatesExample object. It makes use of our 2 contributed edit blocks.
    
    <div class="eg">    
        <t:beaneditform t:id="updateDates" object="datesExample" submitLabel="Save" include="adatemidnight,alocaldate">
            [BeanEditForm here]
        </t:beaneditform>
    </div>
        
    References: 
    <a href="http://tapestry.apache.org/beaneditform-guide.html">BeanEditForm Guide</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/DataTypeAnalyzer.html">DataTypeAnalyzer</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/BeanBlockSource.html">BeanBlockSource</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/DisplayBlockContribution.html">DisplayBlockContribution</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/EditBlockContribution.html">EditBlockContribution</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/PropertyOutputContext.html">PropertyOutputContext</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/PropertyEditContext.html">PropertyEditContext</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/components/DateField.html">DateField</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/components/TextField.html">TextField</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/annotations/Component.html">@Component</a>, 
    <a href="http://joda-time.sourceforge.net/">Joda Time</a>.<br/><br/>
    
    <a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>
    
    The source for @EJB handling, etc. is shown in the @EJB example.<br/><br/>

    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/lang/PropertyEditors.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/lang/PropertyEditors.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/examples.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/infra/AppPropertyDisplayBlocks.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/infra/AppPropertyDisplayBlocks.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/infra/AppPropertyEditBlocks.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/infra/AppPropertyEditBlocks.java"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/datestuff/DateStuffService.java"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/datestuff/DatesExample.java"/>
</body>
</html>

PropertyEditors.java


package jumpstart.web.pages.examples.lang;

import javax.ejb.EJB;

import jumpstart.business.domain.datestuff.DatesExample;
import jumpstart.business.domain.datestuff.iface.IDateStuffServiceLocal;
import jumpstart.util.ExceptionUtil;

import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.BeanEditForm;

public class PropertyEditors {

    // Screen fields

    @Property
    private DatesExample datesExample;
    
    // Generally useful bits and pieces

    @EJB
    private IDateStuffServiceLocal dateStuffService;

    @Component(id = "updateDates")
    private BeanEditForm form;

    // The code

    void setupRender() throws Exception {
        datesExample = findDatesExample(1L);
    }

    void onPrepareForSubmit() throws Exception {
        datesExample = findDatesExample(1L);
    }

    void onValidateFromUpdateDates() {
        try {
            if (datesExample.getADateMidnight() == null || datesExample.getALocalDate() == null) {
                form.recordError("Both dates are required.");
                return;
            }
            dateStuffService.changeDatesExample(datesExample);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            form.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    private DatesExample findDatesExample(Long id) throws Exception {
        // Ask business service to find DatesExample
        DatesExample datesExample = dateStuffService.findDatesExample(id);

        if (datesExample == null) {
            throw new IllegalStateException("Database data has not been set up!");
        }

        return datesExample;
    }
    
}

examples.css


body            { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333; line-height: 17px; }
h1              { font-size: 26px; line-height: 20px; } /* For IE 7 */
form            { margin: 0; }                  

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

a               { text-decoration: none; color: #3D69B6; }
a:hover         { text-decoration: underline; }

/* For BeanDisplay */
.eg dl          { margin: 0; color: #333; }
.eg dl.t-beandisplay dd.id  { display: inline; margin-left: 0px; }  /* IE 7 hack */

AppModule.java


package jumpstart.web.services;

import java.util.Arrays;
import java.util.HashSet;

import jumpstart.util.JodaTimeUtil;
import jumpstart.web.translators.MoneyTranslator;
import jumpstart.web.translators.YesNoTranslator;
import jumpstart.web.validators.Letters;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.Validator;
import org.apache.tapestry5.annotations.Property;
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.PageRenderLinkSource;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.RequestFilter;
import org.apache.tapestry5.services.security.WhitelistAnalyzer;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.upload.services.UploadSymbols;
import org.got5.tapestry5.jquery.JQuerySymbolConstants;
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;

/**
 * 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);

        // This next line addresses an issue affecting GlassFish and JBoss - see http://blog.progs.be/?p=52
        javassist.runtime.Desc.useContextClassLoader = true;
    }

    // Tell Tapestry about our custom translators, validators, and their message files.
    // 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));
    }

    @SuppressWarnings("rawtypes")
    public static void contributeFieldValidatorSource(MappedConfiguration<String, Validator> configuration) {
        configuration.add("letters", new Letters());
    }

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

    // 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 Tapestry5jQuery not to suppress Tapestry's built-in Prototype
    // and Scriptaculous (see the JQuery example for more information).
    // 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");
        // We have Tapestry5jQuery installed. Tell it we don't want it to suppress Prototype and Scriptaculous.
        configuration.add(JQuerySymbolConstants.SUPPRESS_PROTOTYPE, "false");
        // We don't use $j in our javascript - instead we use function scoping (see
        // http://api.jquery.com/jQuery.noConflict/)
        // but we need this next line to keep Tapestry happy (since Tapestry 5.3.4).
        configuration.add(JQuerySymbolConstants.JQUERY_ALIAS, "$j");
    }

    // Tell Tapestry how to block access to WEB-INF/, META-INF/, and assets that are not in our assets "whitelist".
    // We do this by contributing a custom RequestFilter to Tapestry's RequestHandler service.
    // - This is necessary due to https://issues.apache.org/jira/browse/TAP5-815 .
    // - RequestHandler is shown in http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview .
    // - RequestHandler is described in http://tapestry.apache.org/request-processing.html
    // - Based on an entry in the Tapestry Users mailing list by martijn.list on 15 Aug 09.

    public void contributeRequestHandler(OrderedConfiguration<RequestFilter> configuration,
            PageRenderLinkSource pageRenderLinkSource) {
        final HashSet<String> ASSETS_WHITE_LIST = new HashSet<String>(Arrays.asList("jpg", "jpeg", "png", "gif", "js",
                "css", "ico"));
        configuration.add("AssetProtectionFilter", new AssetProtectionFilter(ASSETS_WHITE_LIST, pageRenderLinkSource),
                "before:*");
    }

    // 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".
    // 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"));

    }

}

AppPropertyDisplayBlocks.tml


<t:container xml:space="default" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">

    <t:block id="dateTime">
        <t:jodatimeoutput value="context.propertyValue" style="LL"/>
    </t:block>

    <t:block id="dateMidnight">
        <t:jodatimeoutput value="context.propertyValue" style="L-"/>
    </t:block>

    <t:block id="localDateTime">
        <t:jodatimeoutput value="context.propertyValue" style="LL"/>
    </t:block>

    <t:block id="localDate">
        <t:jodatimeoutput value="context.propertyValue" style="L-"/>
    </t:block>

    <t:block id="localTime">
        <t:jodatimeoutput value="context.propertyValue" style="-L"/>
    </t:block>

</t:container>

AppPropertyDisplayBlocks.java


// Based on http://tapestry.apache.org/tapestry5/guide/beaneditform.html

package jumpstart.web.pages.infra;

import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.services.PropertyOutputContext;

public class AppPropertyDisplayBlocks {

    @Property
    @Environmental
    private PropertyOutputContext context;

}

AppPropertyEditBlocks.tml


<t:container xml:space="default" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">

    <t:block id="dateMidnight">
        <t:label for="dateMidnight"/>
        <input t:id="dateMidnight" t:type="DateField" value="context.propertyValue" label="prop:context.label" 
            format="prop:dateInputFormat" translate="prop:dateMidnightTranslator" validate="prop:dateMidnightValidator" 
            clientId="prop:context.propertyId" annotationProvider="context"/>
    </t:block>
    
    <t:block id="localDate">
        <t:label for="localDate"/>
        <input t:id="localDate" t:type="DateField" value="context.propertyValue" label="prop:context.label" 
            format="prop:dateInputFormat" translate="prop:localDateTranslator" validate="prop:localDateValidator" 
            clientId="prop:context.propertyId" annotationProvider="context"/>
    </t:block>

</t:container>

AppPropertyEditBlocks.java


// Based on http://tapestry.apache.org/tapestry5/guide/beaneditform.html

package jumpstart.web.pages.infra;

import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.apache.tapestry5.FieldTranslator;
import org.apache.tapestry5.FieldValidator;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.DateField;
import org.apache.tapestry5.services.PropertyEditContext;

public class AppPropertyEditBlocks {

    @Property
    @Environmental
    private PropertyEditContext context;

    @Component
    private DateField dateMidnight;

    @Component
    private DateField localDate;
    
    public DateFormat getDateInputFormat() {
        return new SimpleDateFormat("dd MMMM yyyy");
    }
    
    public FieldTranslator<?> getDateMidnightTranslator() {
        return context.getTranslator(dateMidnight);
    }
    
    public FieldValidator<?> getDateMidnightValidator() {
        return context.getValidator(dateMidnight);
    }

    public FieldTranslator<?> getLocalDateTranslator() {
        return context.getTranslator(localDate);
    }

    public FieldValidator<?> getLocalDateValidator() {
        return context.getValidator(localDate);
    }

}

DateStuffService.java


package jumpstart.business.domain.datestuff;

import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import jumpstart.business.domain.datestuff.iface.IDateStuffServiceLocal;
import jumpstart.business.domain.datestuff.iface.IDateStuffServiceRemote;

@Stateless
@Local(IDateStuffServiceLocal.class)
@Remote(IDateStuffServiceRemote.class)
public class DateStuffService implements IDateStuffServiceLocal, IDateStuffServiceRemote {

    @PersistenceContext(unitName = "jumpstart")
    private EntityManager em;

    public DatesExample findDatesExample(Long id) {
        return em.find(DatesExample.class, id);
    }

    public DatesExample changeDatesExample(DatesExample datesExample) {
        return em.merge(datesExample);
    }

}

DatesExample.java


package jumpstart.business.domain.datestuff;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;

import jumpstart.util.JodaTimeUtil;

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;

/**
 * The DatesExample entity.
 */
@Entity
@SuppressWarnings("serial")
public class DatesExample implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false)
    private Long id;

    @Version
    @Column(nullable = false)
    private Integer version;

    // Traditional java-style Date fields

    @Temporal(TemporalType.TIMESTAMP)
    private java.util.Date aTimestamp;

    @Temporal(TemporalType.DATE)
    private java.util.Date aDate;

    @Temporal(TemporalType.TIME)
    private java.util.Date aTime;

    // These fields are exposed as Joda Time types but persisted as standard Java types (Date, Timestamp, String, Integer).

    private java.sql.Timestamp aDateTime;

    private java.sql.Timestamp aDateTimeWithTZ;

    private String aDateTimeTZ;

    private java.sql.Date aDateMidnight;

    private java.sql.Date aDateMidnightWithTZ;

    private String aDateMidnightTZ;

    private java.sql.Timestamp aLocalDateTime;

    private java.sql.Date aLocalDate;

    private java.sql.Time aLocalTimeAsTime;

    private Integer aLocalTimeAsMillis;

    private String aLocalTimeAsString;

    public String toString() {
        final String DIVIDER = ", ";
        
        StringBuilder buf = new StringBuilder();
        buf.append(this.getClass().getSimpleName() + ": ");
        buf.append("[");
        buf.append("id=" + id + DIVIDER);
        buf.append("aTimestamp=" + aTimestamp + DIVIDER);
        buf.append("aDate=" + aDate + DIVIDER);
        buf.append("aTime=" + aTime + DIVIDER);
        buf.append("aDateTime=" + aDateTime + DIVIDER);
        buf.append("aDateTimeWithTZ=" + aDateTimeWithTZ + DIVIDER);
        buf.append("aDateTimeTZ=" + aDateTimeTZ + DIVIDER);
        buf.append("aDateMidnight=" + aDateMidnight + DIVIDER);
        buf.append("aLocalDateTime=" + aLocalDateTime + DIVIDER);
        buf.append("aLocalDate=" + aLocalDate + DIVIDER);
        buf.append("aLocalTimeAsTime=" + aLocalTimeAsTime + DIVIDER);
        buf.append("aLocalTimeAsMillis=" + aLocalTimeAsMillis + DIVIDER);
        buf.append("aLocalTimeAsString=" + aLocalTimeAsString + DIVIDER);
        buf.append("version=" + version);
        buf.append("]");
        return buf.toString();
    }

    // The need for an equals() method is discussed at http://www.hibernate.org/109.html

    @Override
    public boolean equals(Object obj) {
        return (obj == this) || (obj instanceof DatesExample) && id != null && id.equals(((DatesExample) obj).getId());
    }

    // The need for a hashCode() method is discussed at http://www.hibernate.org/109.html

    @Override
    public int hashCode() {
        return id == null ? super.hashCode() : id.hashCode();
    }

    public Long getId() {
        return id;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

    public Date getADate() {
        return aDate;
    }

    public void setADate(Date aDate) {
        this.aDate = aDate;
    }

    public Date getATime() {
        return aTime;
    }

    public void setATime(Date aTime) {
        this.aTime = aTime;
    }

    public Date getATimestamp() {
        return aTimestamp;
    }

    public void setATimestamp(Date aTimestamp) {
        this.aTimestamp = aTimestamp;
    }

    public DateTime getADateTime() {
        return JodaTimeUtil.toDateTime(aDateTime);
    }

    public void setADateTime(DateTime dt) {
        this.aDateTime = JodaTimeUtil.toSQLTimestamp(dt);
    }

    public DateTime getADateTimeWithTZ() {
        return JodaTimeUtil.toDateTime(aDateTimeWithTZ, aDateTimeTZ);
    }

    public void setADateTimeWithTZ(DateTime dt) {
        this.aDateTimeWithTZ = JodaTimeUtil.toSQLTimestamp(dt);
        this.aDateTimeTZ = JodaTimeUtil.toTimeZoneID(dt);
    }

    public DateMidnight getADateMidnight() {
        return JodaTimeUtil.toDateMidnight(aDateMidnight);
    }

    public void setADateMidnight(DateMidnight dm) {
        this.aDateMidnight = JodaTimeUtil.toSQLDate(dm);
    }

    public DateMidnight getADateMidnightWithTZ() {
        return JodaTimeUtil.toDateMidnight(aDateMidnightWithTZ, aDateMidnightTZ);
    }

    public void setADateMidnightWithTZ(DateMidnight dm) {
        this.aDateMidnightWithTZ = JodaTimeUtil.toSQLDate(dm);
        this.aDateMidnightTZ = JodaTimeUtil.toTimeZoneID(dm);
    }

    public LocalDateTime getALocalDateTime() {
        return JodaTimeUtil.toLocalDateTime(aLocalDateTime);
    }

    public void setALocalDateTime(LocalDateTime ldt) {
        this.aLocalDateTime = JodaTimeUtil.toSQLTimestamp(ldt);
    }

    public LocalDate getALocalDate() {
        return JodaTimeUtil.toLocalDate(aLocalDate);
    }

    public void setALocalDate(LocalDate ld) {
        this.aLocalDate = JodaTimeUtil.toSQLDate(ld);
    }

    public LocalTime getALocalTimeAsTime() {
        return JodaTimeUtil.toLocalTime(aLocalTimeAsTime);
    }

    public void setALocalTimeAsTime(LocalTime lt) {
        this.aLocalTimeAsTime = JodaTimeUtil.toSQLTime(lt);
    }

    public LocalTime getALocalTimeAsMillis() {
        return JodaTimeUtil.toLocalTime(aLocalTimeAsMillis);
    }

    public void setALocalTimeAsMillis(LocalTime lt) {
        this.aLocalTimeAsMillis = JodaTimeUtil.toIntegerMillis(lt);
    }

    public LocalTime getALocalTimeAsString() {
        return JodaTimeUtil.toLocalTime(aLocalTimeAsString);
    }

    public void setALocalTimeAsString(LocalTime lt) {
        this.aLocalTimeAsString = JodaTimeUtil.toString(lt);
    }

}