File Upload

This example demonstrates Tapestry's Upload component for uploading files.

To improve the user experience, we've added some JavaScript. It hides stale results messages, shows a progress message, and enables cancel during long uploads.

To keep the page class simple, we've moved all file handling into a service that we've called Filer.
Maximum file size is 2.0MB.


Sorry, but this function is not allowed in Demo mode.
The symbols jumpstart.upload-path and upload.filesize-max were provided at runtime as Java options, eg. -Dupload.filesize-max=2097152

The Upload component is NOT part of Tapestry's corelib. To use it, put the following jars in the classpath: For an AJAX uploader try Ajax Upload for Tapestry.

References: Uploading Files, Upload, Tapestry 5 and JavaScript Explained, @Import, JavaScriptSupport, Defining Tapestry IOC Services.

Home

FileUpload.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/javascript/fileupload.css}"/>
</head>
<body>
    <h1>File Upload</h1>

    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     
    
    This example demonstrates Tapestry's Upload component for uploading files. <br/><br/>
    
    To improve the user experience, we've added some JavaScript. It hides stale results messages, shows a progress message, 
    and enables cancel during long uploads.<br/><br/>
    
    To keep the page class simple, we've moved all file handling into a service that we've called Filer.<br/>
    
    <div class="eg">
        <form t:type="CustomForm" t:id="uploadForm">

            <div id="notes">
                ${fileSizeMaxMessage} 
            </div>

            <input t:type="Upload" t:id="file" t:clientid="prop:fileId" t:validate="required" onClick="fileUpload.hideResults();" t:disabled="demoMode"/><br/>
            <input t:type="Submit" t:id="upload" value="Upload" onClick="fileUpload.showProgress();" t:disabled="demoMode"/><br/>

            <div id="${progressId}" style="display: none;">
                ${message:progress}
                <button t:type="chenillekit/Button" class="cancel" type="button" t:pageName="prop:thisPageName" 
                    title="Cancel" onClick="this.form.reset(); return true;">Cancel</button>
            </div>

            <div id="${resultId}">
                <t:customerror for="file"/>
                <div id="success">
                    ${successMessage}
                </div>
                <t:errors />
            </div>
        
            <t:if test="demoMode">
                <div id="demo-mode">Sorry, but this function is not allowed in Demo mode.</div>
            </t:if>

        </form>
    </div>
    
    The symbols <em>jumpstart.upload-path</em> and <em>upload.filesize-max</em> were provided at runtime as Java options, 
    eg. <code>-Dupload.filesize-max=${fileSizemax}</code><br/><br/>

    The Upload component is NOT part of Tapestry's corelib. To use it, put the following jars in the classpath:
    <ul>
        <li>tapestry-upload-5.3.n.jar, at compile and runtime.</li>
        <li>commons-fileupload-1.2.2.jar, at compile and runtime.</li>
        <li>commons-io-2.0.1.jar, at runtime.</li>
    </ul>
    
    For an AJAX uploader try <a href="http://tawus.wordpress.com/2011/06/25/ajax-upload-for-tapestry/">Ajax Upload for Tapestry</a>.<br/><br/>
    
    References: 
    <a href="http://tapestry.apache.org/uploading-files.html">Uploading Files</a>, 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/upload/components/Upload.html">Upload</a>,
    <a href="http://wiki.apache.org/tapestry/Tapestry5AndJavaScriptExplained">Tapestry 5 and JavaScript Explained</a>, 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/annotations/Import.html">@Import</a>, 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/services/javascript/JavaScriptSupport.html">JavaScriptSupport</a>,
    <a href="http://tapestry.apache.org/defining-tapestry-ioc-services.html">Defining Tapestry IOC Services</a>.<br/><br/> 

    <a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>

    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/javascript/FileUpload.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/javascript/FileUpload.properties"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/javascript/FileUpload.js"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/javascript/FileUpload.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/javascript/fileupload.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/services/Filer.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
</body>
</html>

FileUpload.properties


file-size-max=Maximum file size is %2.1fMB.
file-required-message=Choose file to upload.
progress=Uploading now...  This may take a few minutes if the file is large (over, say, 1MB).
file-name-illegal-characters=File name "%s" contains illegal characters.
file-already-exists=File "%s" has been uploaded previously.

FileUpload.js


// A FileUpload provides functions that show/hide the progress and results blocks. 
// Written in Protoype style because Tapestry includes the Protoype library (http://www.prototypejs.org/).

var fileUpload;

FileUpload = Class.create({

    initialize : function(fileId, progressId, resultId) {
        this.fileElem = this.resolve(fileId);
        this.progressElem = this.resolve(progressId);
        this.resultElem = this.resolve(resultId);

        // Can't use Event.observe on the submit button because it conflicts with the form submission.
        // So instead, of this... Event.observe(this.submitElem, 'click', this.showProgress.bindAsEventListener(this));
        // ...add this to the submit button: onclick="fileUpload.showProgress(); return true;".

        this.hideProgress();
    },

    hideProgress: function() {
        this.progressElem.hide();
        this.resultElem.show();
        return true;
    },

    showProgress: function() {
    
        // If a file has been chosen, then hide any previous results and show the progress
    
        if (this.fileElem.value != "") {
            this.progressElem.show();
            this.resultElem.hide();
        }
        else {
            this.resultElem.show();
        }
    
        return true;
    },
    
    hideResults: function() {
        this.resultElem.hide();
        return true;
    },

    resolve: function(elementId) {
        var element = $(elementId);
        if (!element) {
            alert("To the developer: element id \"" + elementId + "\" does not exist.");
        }
        return element;
    }

})

// Extend the Tapestry.Initializer with a function that instantiates a FileUpload object.

Tapestry.Initializer.fileUpload = function(spec) {
    fileUpload = new FileUpload(spec.fileId, spec.progressId, spec.resultId);
}

FileUpload.java


package jumpstart.web.pages.examples.javascript;

import jumpstart.util.ExceptionUtil;
import jumpstart.web.components.CustomForm;
import jumpstart.web.services.IFiler;

import org.apache.commons.fileupload.FileUploadException;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.PersistenceConstants;
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.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.upload.services.UploadedFile;

// The @Import tells Tapestry to put a link to the file in the head of the page so that the browser will pull it in. 
@Import(library = "FileUpload.js")
public class FileUpload {

    private final String demoModeStr = System.getProperty("jumpstart.demo-mode");

    // Screen fields

    @Property
    private UploadedFile file;

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

    @Property
    private String fileId;

    @Property
    private String progressId;

    @Property
    private String resultId;

    // Generally useful bits and pieces.
    
    @Inject
    private IFiler filer;

    @Inject
    private ComponentResources componentResources;

    @InjectComponent
    private CustomForm uploadForm;

    @Inject
    private Messages messages;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    // The code
    
    void setupRender() {
        fileId = "file";
        progressId = "progress";
        resultId = "result";
    }

    void afterRender() {
        // Tell Tapestry to add some javascript that sets up our event handling.
        // Tapestry will put it at the end of the page in a section that runs once the DOM has been loaded.
        JSONObject spec = new JSONObject();
        spec.put("fileId", fileId);
        spec.put("progressId", progressId);
        spec.put("resultId", resultId);
        javaScriptSupport.addInitializerCall("fileUpload", spec);
    }

    void onValidateFromUploadForm() {
        try {
            String savedAsFileName = filer.save(file, messages);
            successMessage = "Successfully uploaded file \"" + savedAsFileName + "\".";
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            uploadForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    Object onUploadException(FileUploadException e) {
        // Display the cause. In a real system we would try harder to get a user-friendly message.
        uploadForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        return this;
    }
    
    public String getFileSizeMaxMessage() {
        double fileSizeMaxMB = filer.getFileSizeMax() / 1048576.0;
        return messages.format("file-size-max", (Double) fileSizeMaxMB);
    }
    
    public boolean isDemoMode() {
        return (demoModeStr != null && demoModeStr.equals("true"));
    }

    public String getThisPageName() {
        return componentResources.getPageName();
    }
    
    public long getFileSizeMax() {
        return filer.getFileSizeMax();
    }

}

fileupload.css


body, td        { 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 */

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

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

.js-required    { color: red; display: block; margin-bottom: 14px; }

#notes              { margin: 0 0 10px 0; color: gray; }
input[type="file"]  { color: gray; font-size: 13px; }
#result             { margin: 10px 0 0 0; }
#success            { color: green; }
.error-msg          { color: red; }
#progress           { margin: 10px 0 0 0; padding: 5px 10px; display: inline-block;
                        background-color: #999; color: white; font-weight: bold; }
.cancel             { margin-left: 5px; }
#demo-mode          { margin: 0 0 10px 0; color: red; }


Filer.java


package jumpstart.web.services;

import java.io.File;

import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.upload.services.UploadedFile;
import org.slf4j.Logger;

public class Filer implements IFiler {
    static final String FILE_SEPARATOR = System.getProperty("file.separator");

    @SuppressWarnings("unused")
    private final Logger logger;
    private String dirPathSymbol;
    private String dirPath;
    private String fileSizeMaxSymbol;
    private long fileSizeMax;

    public Filer(Logger logger, String dirPathSymbol, String dirPath, String fileSizeMaxSymbol, long fileSizeMax) {
        super();
        this.logger = logger;
        this.dirPathSymbol = dirPathSymbol;
        this.dirPath = sanitiseDirPath(dirPath);
        this.fileSizeMaxSymbol = fileSizeMaxSymbol;
        this.fileSizeMax = sanitiseFileSizeMax(fileSizeMax);
    }

    @Override
    public String save(UploadedFile uploadedFile, Messages messages) throws Exception {
        try {
            String targetFileName = sanitiseFileName(uploadedFile.getFileName());

            // This check is optional: Error if sanitised file name is different to original file name.

            if (!targetFileName.equals(uploadedFile.getFileName())) {
                // In a real system we would throw a exception of our own
                throw new Exception(messages.format("file-name-illegal-characters", uploadedFile.getFileName()));
            }

            File targetFile = new File(dirPath + targetFileName);

            // This check is optional: Error if the file already exists (else it will be overwritten).

            if (targetFile.exists()) {
                // In a real system we would throw a exception of our own
                throw new Exception(messages.format("file-already-exists", uploadedFile.getFileName(), targetFileName));
            }

            uploadedFile.write(targetFile);
            return targetFileName;
        }
        catch (Exception e) {
            // In a real system we would throw a user-friendly message
            throw e;
        }
    }

    @Override
    public long getFileSizeMax() {
        return fileSizeMax;
    }

    private String sanitiseDirPath(String dirPath) {
        String path = dirPath.trim();

        if (!path.endsWith("/") || path.endsWith("\\") || path.endsWith(":")) {
            path += FILE_SEPARATOR;
        }

        File dir = new File(path);

        if (!dir.exists()) {
            throw new IllegalStateException(
                    "File uploads cannot proceed because silly directory specified by system property " + dirPathSymbol
                            + " does not exist. Value = " + path + ".");
        }

        return path;
    }

    private long sanitiseFileSizeMax(long fileSizeMax) {

        if (fileSizeMax <= 10240 || fileSizeMax > 100000000) {
            throw new IllegalStateException(
                    "File uploads cannot proceed because silly value found for system property " + fileSizeMaxSymbol
                            + ", value = " + fileSizeMax + ".");
        }

        return fileSizeMax;
    }

    private String sanitiseFileName(String fileName) {
        String s = fileName.replaceAll("[\\:\\*\\?\\<\\>\\|\\'\\\"\\/\\\\]+", "_");
        return s;
    }

}

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, DateMidnight>(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, java.util.Date>(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, LocalDate>(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, java.util.Date>(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/tapestry5/guide/beaneditform.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"));

    }

}