@EJB

@EJB is a Java EE annotation to indicate a dependency on a Enterprise JavaBean. In JumpStart, we use the @EJB annotation in pages, components, and mixins to indicate we want a Session Bean injected. To detect @EJB and inject the Session Bean, we created a class called EJBAnnotationWorker. It implements ComponentClassTransformWorker2 and is contributed to Tapestry in AppModule.

Here we've injected a local Session Bean, IPersonFinderServiceLocal, to find entity Person with id = 1:
Id
1
Version
76
First Name
Acme2
Last Name
456
Region
East Coast
Start Date
Jun 19, 1952
References: @EJB, ComponentClassTransformWorker2, 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>@EJB</h3>
    
    @EJB is a Java EE annotation to indicate a dependency on a Enterprise JavaBean. 
    In JumpStart, we use the @EJB annotation in pages, components, and mixins to indicate we want a Session Bean injected. 
    To detect @EJB and inject the Session Bean, we created a class called EJBAnnotationWorker. 
    It implements ComponentClassTransformWorker2 and is contributed to Tapestry in AppModule.<br/><br/>
    
    Here we've injected a local Session Bean, IPersonFinderServiceLocal, to find entity Person with id = 1:<br/>
    
    <div class="eg">
        <t:beandisplay object="person"/>
    </div>

    References: <a href="http://docs.oracle.com/javaee/6/api/javax/ejb/EJB.html">@EJB</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/transform/ComponentClassTransformWorker2.html">
    ComponentClassTransformWorker2</a>, 
    <a href="http://tapestry.apache.org/defining-tapestry-ioc-services.html">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/state/AtEjb.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/state/AtEjb.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/plain.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/EJBAnnotationWorker.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/client/IBusinessServicesLocator.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/client/BusinessServicesLocator.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/commons/jndi/JNDIObjectLocator.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/util/EJBProviderEnum.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/util/EJBProviderUtil.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/commons/exception/SystemUnavailableException.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.examples.state;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;

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

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

    // Screen fields

    @Property
    private Person person;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    // The code
    
    void setupRender() throws Exception {
        person = personFinderService.findPerson(1L);

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

}


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


// Based on http://wiki.apache.org/tapestry/JEE-Annotation.

package jumpstart.web.services;

import javax.ejb.EJB;

import jumpstart.client.BusinessServicesLocator;
import jumpstart.client.IBusinessServicesLocator;
import jumpstart.util.StringUtil;

import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.plastic.PlasticClass;
import org.apache.tapestry5.plastic.PlasticField;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.services.transform.TransformationSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Inject an EJB into tapestry sources
 */
public class EJBAnnotationWorker implements ComponentClassTransformWorker2 {
    private static final Logger logger = LoggerFactory.getLogger(EJBAnnotationWorker.class);

    private IBusinessServicesLocator locator;

    public EJBAnnotationWorker() {
        locator = new BusinessServicesLocator(logger);
    }

    @Override
    public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) {

        for (PlasticField field : plasticClass.getFieldsWithAnnotation(EJB.class)) {
            final EJB annotation = field.getAnnotation(EJB.class);

            if (StringUtil.isNotEmpty(annotation.name())) {
                throw new RuntimeException(
                        "This implementation of the @EJB annotation does not support the name parameter. Found name = \""
                                + annotation.name() + "\".");
            }
            else if (StringUtil.isNotEmpty(annotation.beanName())) {
                throw new RuntimeException(
                        "This implementation of the @EJB annotation does not support the beanName parameter. Found beanName = \""
                                + annotation.beanName() + "\".");
            }
            else if (StringUtil.isNotEmpty(annotation.mappedName())) {
                throw new RuntimeException(
                        "This implementation of the @EJB annotation does not support the mappedName parameter. Found mappedName = \""
                                + annotation.mappedName() + "\".");
            }

            String fieldType = field.getTypeName();
            Object injectionValue = locator.getService(fieldType);

            if (injectionValue != null) {
                field.inject(injectionValue);
                field.claim(annotation);
            }
        }
    }

}


package jumpstart.client;

/**
 * BusinessServicesLocator is used to centralize all lookups of business services in JNDI.
 */
public interface IBusinessServicesLocator {

    /**
     * @param interfaceClass eg. IPersonServiceLocal.class .
     * @return eg. An instance of IPersonServiceLocal as found by JNDI.
     */
    public abstract Object getService(Class<?> interfaceClass);

    /**
     * @param canonicalInterfaceName eg. "jumpstart.business.domain.examples.iface.IPersonServiceLocal".
     * @return eg. An instance of IPersonServiceLocal as found by JNDI.
     */
    public abstract Object getService(String canonicalInterfaceName);

    /**
     * Invoked after any kind of naming or remote exception. All cached naming contexts and interfaces are discarded.
     */
    public abstract void clear();

}


package jumpstart.client;

import java.util.regex.Pattern;

import jumpstart.business.commons.jndi.JNDIObjectLocator;
import jumpstart.util.EJBProviderEnum;
import jumpstart.util.EJBProviderUtil;

import org.slf4j.Logger;

/**
 * BusinessServicesLocator is used to centralize all lookups of business services in JNDI. At the time of writing it is
 * used by the business tier's BaseTest and the web tier's EJBAnnotationWorker and PageProtectionFilter.
 * 
 * This version knows the formats of JNDI names assigned by OpenEJB, GlassFish, and JBoss (the EJB 3.1 specification
 * only "almost" standardized them). It minimises the overhead of JNDI lookups by caching the objects it looks up. If
 * this class becomes a bottleneck, then you may need to decentralise it.
 */
public class BusinessServicesLocator extends JNDIObjectLocator implements IBusinessServicesLocator {
    private static final String REMOTE = "REMOTE";
    private static final String LOCAL = "LOCAL";
    private static final Pattern StripLocalPattern = Pattern.compile(LOCAL + "|" + REMOTE, Pattern.CASE_INSENSITIVE);

    private final EJBProviderEnum ejbProvider;

    public BusinessServicesLocator(Logger logger) {
        super(logger);
        // You wouldn't normally have to do this but JumpStart has to deal with many types of environment...
        this.ejbProvider = EJBProviderUtil.detectEJBProvider(logger);
    }

    /**
     * An example of interfaceClass is IPersonServiceLocal.
     */
    @Override
    public Object getService(Class<?> interfaceClass) {
        return getService(interfaceClass.getCanonicalName());
    }

    /**
     * We expect canonicalInterfaceName to be like "jumpstart.business.domain.examples.iface.IPersonServiceLocal", ie.
     * its simple name has a leading "I" and trailing "Local" or "Remote".
     */
    @Override
    public Object getService(String canonicalInterfaceName) {
        String jndiName = null;

        // You wouldn't normally have to do all this work but JumpStart has to deal with many types of environment and
        // EJB 3.1 still hasn't quite standardised JNDI names.

        if (ejbProvider == EJBProviderEnum.OPENEJB_7_LOCAL || ejbProvider == EJBProviderEnum.TOMCAT_7_OPENEJB_7_LOCAL
                || ejbProvider == EJBProviderEnum.OPENEJB_7_REMOTE) {
            // Uses the implementation class name eg. "PersonServiceLocal".
            jndiName = getSimpleName(canonicalInterfaceName).substring(1);
        }
        else if (ejbProvider == EJBProviderEnum.GLASSFISH_3_LOCAL || ejbProvider == EJBProviderEnum.GLASSFISH_3_REMOTE) {
            // Use the EJB3.1 global name eg.
            // "java:global/jumpstart/jumpstartejb/PersonService!jumpstart.business.domain.examples.iface.IPersonServiceLocal".
            jndiName = "java:global/jumpstart/jumpstartejb/"
                    + stripOffLocalOrRemote(getSimpleName(canonicalInterfaceName).substring(1)) + "!"
                    + canonicalInterfaceName;
        }
        else if (ejbProvider == EJBProviderEnum.WILDFLY_11_LOCAL) {
            // Uses one of the ways found early in the JBoss log (logger is
            // org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor).
            jndiName = "java:global/jumpstart/jumpstart.jar/"
                    + stripOffLocalOrRemote(getSimpleName(canonicalInterfaceName).substring(1)) + "!"
                    + canonicalInterfaceName;
        }
        else if (ejbProvider == EJBProviderEnum.WILDFLY_11_REMOTE) {
            // Uses an odd format - similar to the EJB3.1 global name, eg.
            // "ejb/jumpstart/jumpstart.jar/PersonService!jumpstart.business.domain.examples.iface.IPersonServiceRemote".
            jndiName = "ejb:jumpstart/jumpstart.jar/"
                    + stripOffLocalOrRemote(getSimpleName(canonicalInterfaceName).substring(1)) + "!"
                    + canonicalInterfaceName;
        }
        else {
            throw new IllegalStateException("Don't know how to use ejbProvider = " + ejbProvider);
        }

        return getJNDIObject(jndiName);
    }

    private static String getSimpleName(String s) {
        return s.substring(s.lastIndexOf(".") + 1);
    }

    private static String stripOffLocalOrRemote(String s) {
        String stripped = s;
        String uc = s.toUpperCase();

        if (uc.endsWith(LOCAL) || uc.endsWith(REMOTE)) {
            stripped = StripLocalPattern.matcher(s).replaceFirst("");
        }

        return stripped;
    }
}


package jumpstart.business.commons.jndi;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameClassPair;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;

import org.slf4j.Logger;

import jumpstart.business.commons.exception.SystemUnavailableException;
import jumpstart.util.EJBProviderEnum;
import jumpstart.util.EJBProviderUtil;

/**
 * JNDIObjectLocator is used to centralize all JNDI lookups. It minimises the overhead of JNDI lookups by caching the
 * objects it looks up.
 */
public class JNDIObjectLocator {
    protected Logger logger;
    private InitialContext initialContext;
    private Map<String, Object> jndiObjectCache = Collections.synchronizedMap(new HashMap<String, Object>());

    public JNDIObjectLocator(Logger logger) {
        this.logger = logger;
    }

    public synchronized void clear() {
        jndiObjectCache.clear();
    }

    public Object getJNDIObject(String jndiName) {

        Object jndiObject = jndiObjectCache.get(jndiName);

        if (jndiObject == null && !jndiObjectCache.containsKey(jndiName)) {
            try {
                jndiObject = lookup(jndiName);
                jndiObjectCache.put(jndiName, jndiObject);
            }
            catch (RuntimeException e) {
                clear();
                throw e;
            }
        }
        return jndiObject;
    }

    private synchronized Object lookup(String name) {

        // Recheck the cache because the name we're looking for may have been added while we were waiting for sync.

        if (!jndiObjectCache.containsKey(name)) {
            try {
                return getInitialContext().lookup(name);
            }
            catch (NameNotFoundException e) {
                clear();
                throw new SystemUnavailableException(
                        "JNDI lookup failed for \"" + name
                                + "\".  Is ejb server not started? Has the ejb.provider property been specified correctly",
                        e);
            }
            catch (NamingException e) {
                clear();
                throw new SystemUnavailableException("JNDI lookup failed for \"" + name
                        + "\".  Is ejb server not started?  If using jboss, is jbossall-client.jar missing from classpath?"
                        + " Error looking up " + e.getRemainingName() + " because of " + e.getCause() + " while "
                        + e.getExplanation(), e);
            }
        }
        else {
            return jndiObjectCache.get(name);
        }
    }

    private synchronized InitialContext getInitialContext() {
        if (initialContext == null) {

            try {
                EJBProviderEnum ejbProvider = EJBProviderUtil.detectEJBProvider(logger);

                initialContext = new InitialContext();

                // Glassfish 3.1.1 can't list context - see http://java.net/jira/browse/GLASSFISH-17220
                if (ejbProvider != EJBProviderEnum.GLASSFISH_3_REMOTE) {
                    logger.info("InitialContext environment = " + initialContext.getEnvironment());
                    logger.info("InitialContext contains:");
                    listContext("   ", initialContext);
                }
            }
            catch (NamingException e) {
                clear();
                throw new SystemUnavailableException("Cannot get initial context."
                        + " Is JNDI server not started?  If using wildfly, is jbossall-client.jar missing from classpath?"
                        + " Error looking up " + e.getRemainingName() + " because of " + e.getCause() + " while "
                        + e.getExplanation(), e);
            }

        }

        return initialContext;
    }

    /**
     * This is not essential, but it can be handy when things go wrong to have the objects in the context listed in the
     * log.
     * 
     * @param s
     * @param c
     * @throws NamingException
     */
    private final void listContext(String s, Context c) throws NamingException {
        NamingEnumeration<NameClassPair> pairs = c.list("");
        for (; pairs.hasMoreElements();) {
            NameClassPair p = pairs.next();
            logger.info(s + "/" + p.getName() + " : " + p.getClassName());

            try {
                Object o = c.lookup(p.getName());

                if (o instanceof Context) {
                    Context child = (Context) o;
                    listContext(s + "/" + p.getName(), child);
                }
            }
            catch (Throwable t) {
                // Not really a problem so just log it.
                logger.debug("      " + t.getClass().getName() + ": " + t.getMessage());
            }
        }
    }
}


package jumpstart.util;

public enum EJBProviderEnum {
    OPENEJB_7_LOCAL, OPENEJB_7_REMOTE, WILDFLY_11_LOCAL, WILDFLY_11_REMOTE, GLASSFISH_3_LOCAL, GLASSFISH_3_REMOTE, TOMCAT_7_OPENEJB_7_LOCAL;
}


package jumpstart.util;

import org.slf4j.Logger;

public class EJBProviderUtil {
    private static String PROPERTY_EJB_PROVIDER = "jumpstart.ejb-provider";

    public static EJBProviderEnum detectEJBProvider(Logger logger) {
        EJBProviderEnum ejbProvider = null;

        String ejbProviderStr = null;

        try {
            ejbProviderStr = System.getProperty(PROPERTY_EJB_PROVIDER);

            if (ejbProviderStr == null) {
                throw new IllegalStateException("System property \"" + PROPERTY_EJB_PROVIDER
                        + "\" not found. Please set it to one of: {" + getAllowedValuesAsStr() + "}.");
            }

            ejbProvider = EJBProviderEnum.valueOf(ejbProviderStr);
        }
        catch (IllegalStateException e) {
            throw e;
        }
        catch (SecurityException e) {
            throw new IllegalStateException("Failed to get system property \"" + PROPERTY_EJB_PROVIDER + "\": " + e);
        }
        catch (Exception e) {
            throw new IllegalStateException("Found system property \"" + PROPERTY_EJB_PROVIDER + "\" equals \""
                    + ejbProviderStr + "\", expected one of: {" + getAllowedValuesAsStr() + "}.");
        }

        return ejbProvider;
    }

    private static String getAllowedValuesAsStr() {
        String valuesStr = "";

        EJBProviderEnum[] values = EJBProviderEnum.values();

        for (EJBProviderEnum ejbProviderEnum : values) {
            valuesStr += "\"" + ejbProviderEnum.name() + "\", ";
        }

        return valuesStr.substring(0, valuesStr.lastIndexOf(","));
    }

}


package jumpstart.business.commons.exception;

import jumpstart.business.MessageUtil;

@SuppressWarnings("serial")
public class SystemUnavailableException extends SystemException {
    String symptom;

    /**
     * Throw this exception when the system becomes unavailable eg. due to database connection failure.
     */
    public SystemUnavailableException(Throwable throwable) {
        super(throwable);
    }

    /**
     * Throw this exception when the system becomes unavailable eg. due to database connection failure.
     */
    public SystemUnavailableException(String symptom, Throwable throwable) {
        super(throwable);
        this.symptom = symptom;
    }

    @Override
    public String getMessage() {
        String msg = MessageUtil.toText("SystemUnavailableException", symptom);
        return msg;
    }

    public String getSymptom() {
        return symptom;
    }

}


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 WildFly 11's classpath URLs - WildFly uses a "virtual file system".
    // We do this by overriding Tapestry's ClasspathURLConverter service.
    // See "Running Tapestry on JBoss" (sic) in http://wiki.apache.org/tapestry/Tapestry5HowTos .

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

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

    }

}