© Bauke Scholtz, Arjan Tijms 2018

Bauke Scholtz and Arjan Tijms, The Definitive Guide to JSF in Java EE 8, https://doi.org/10.1007/978-1-4842-3387-0_15

15. Extensions

Bauke Scholtz and Arjan Tijms2

(1)Willemstad, Curaçao

(2)Amsterdam, Noord-Holland, The Netherlands

If there is one single element or virtue of JSF (JavaServer Faces) to which we can attribute its lasting for so long, it’s probably its ability to be extended in a large variety of ways. From the onset JSF made it possibly to have most of its core elements replaced, decorated, or augmented.

This gave rise to a large number of extension libraries and projects. In the very early days these were A4J (Ajax4JSF), Tomahawk, RichFaces, the stand-alone Facelets project, PrettyFaces, and many, many more. A4J was merged into RichFaces, and RichFaces itself was eventually sunset in 2016. Facelets was incorporated into JSF itself, while PrettyFaces became part of the Rewrite framework. These days well-known and active extension libraries are PrimeFaces, OmniFaces, and BootsFaces, among others. While individual libraries have come and gone, the main constant is the extensibility of JSF from its first days until the present.

It’s sometimes said that all those libraries address defects or omissions in JSF, but this is not entirely accurate. In fact, JSF was explicitly designed to make such extensions possible and therefore to allow, even stimulate, such extension libraries to appear. For instance, a contemporary peer technology of JSF, EJB (Enterprise JavaBeans), had few to no extension points and, thus, despite its many shortcomings, we never saw much of an ecosystem flourish around it.

Extension Types

There are a couple of different ways by which to use the various extension points in JSF. A major distinction is between the “classical” approach and the “CDI-centric approach .”

In the latter approach there’s very little to nothing that JSF has to explicitly support extensibillity, as CDI has a number of mechanisms built in to support extending or replacing CDI artifacts. This is the planned future for JSF (making most if not everything a CDI artifact), but for the moment JSF 2.3 is in an early transitional phase and only a few artifacts are vended via CDI. Table 8-1 in Chapter 8 showed these artifacts.

Extending CDI Artifacts

One of the ways CDI augments or replaces a type fully is to provide an alternative producer. We already saw this technique being used in Chapter 13, albeit for a slightly different use case.

The easiest way is if you need to fully replace the type. If augmenting is needed, some code is necessary to obtain the previous type, which with the current version of CDI (2.0) is slightly verbose.

The following shows an example where we replace the request parameter map with a new map that has all the values of the original map , plus an additional value that we add ourselves:

@Dependent @Alternative @Priority(APPLICATION)
public class RequestParameterMapProducer {


    @Produces @RequestScoped @RequestParameterMap
    public Map<String, String> producer(BeanManager beanManager) {
        Map<String, String> previousMap = getPreviousMap(beanManager);
        Map<String, String> newMap = new HashMap<>(previousMap);
        newMap.put("test", "myTestValue");
        return newMap;
    }
}

The getPreviousMap() method is, as mentioned, somewhat verbose . It’s defined as follows:

private Map<String, String> getPreviousMap(BeanManager beanManager) {
    class RequestParameterMapAnnotationLiteral
        extends AnnotationLiteral<RequestParameterMap>
        implements RequestParameterMap
    {
        private static final long serialVersionUID = 1L;
    }


    Type MAP_TYPE = new ParameterizedType() {
        @Override
        public Type getRawType() {
            return Map.class;
        }
        @Override
        public Type[] getActualTypeArguments() {
            return new Type[] {String.class, String.class};
        }
        @Override
        public Type getOwnerType() {
            return null;
        }
    };


    return (Map<String, String>) beanManager
        .getReference(beanManager
        .resolve(beanManager
        .getBeans(MAP_TYPE, new RequestParameterMapAnnotationLiteral())
        .stream()
        .filter(bean -> bean
            .getBeanClass() != RequestParameterMapProducer.class)
        .collect(Collectors.toSet())),
            MAP_TYPE,
            beanManager.createCreationalContext(null));
    }
}

It’s expected that the task of obtaining this “previous” or “original” type will be made easier in a future revision of any of the specs involved. For instance, a future version of JSF will likely introduce ready-to-use annotation literals for its (CDI) annotations, such as the RequestParameterMapAnnotationLiteral shown here.

In order to test that this alternative producer works, consider the following backing bean :

@Named @RequestScoped
public class TestBean {


    @Inject @RequestParameterMap
    private Map<String, String> requestParameterMap;


    public String getTest() {
        return requestParameterMap.get("test");
    }


    public String getFoo() {
        return requestParameterMap.get("foo");
    }
}

And the following Facelet:

<!DOCTYPE html>
<html lang="en"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
>
    <h:head/>
    <h:body>
        <p>Test: #{testBean.test}</p>
        <p>Foo: #{testBean.foo}</p>
    </h:body>
</html>

Deploying an application containing these artifacts with a request parameter of, say, “foo=bar”, will reveal that the new map indeed contains the original request parameters as well as the value that we added ourselves.

Extending Classical Artifacts

The classical approach to augment or fully replace a type in JSF is by installing a factory for that type. The basic way such a factory works is in broad lines identical to the CDI approach demonstrated above; the factory returns an implementation of the requested type and obtains a reference to the “previous” or “original” type.

Being classical in Java EE typically means XML, and indeed the classical factory involves XML. Specifically, registering a factory entails using the <factory> element in faces-config.xml and a specific element per type for which a factory is to be provided. As of JSF 2.3 the following factories are supported:

  • <application-factory>

  • <exception-handler-factory>

  • <external-context-factory>

  • <faces-context-factory>

  • <facelet-cache-factory>

  • <partial-view-context-factory>

  • <lifecycle-factory>

  • <view-declaration-language-factory>

  • <tag-handler-delegate-factory>

  • <render-kit-factory>

  • <visit-context-factory>

  • <flash-factory>

  • <flow-handler-factory>

  • <client-window-factory>

  • <search-expression-context-factory>

Next to these, there are another number of artifacts that can be replaced/augmented in a somewhat similar but still different way; here there’s no factory returning the type, but an implementation of the type is specified directly. This variant is specified using the <application> element in faces-config.xml. As of JSF 2.3 the following types can be replaced/augmented directly:

  • <navigation-handler>

  • <view-handler>

  • <resource-handler>

  • <search-expression-handler>

  • <flow-handler>

  • <state-manager>

  • <action-listener>

Note that all of these are singletons. From the JSF runtime point of view there’s only one of each, but multiple implementations each adding something are supported by means of wrapping, which therefore forms a chain (implementation A wrapping implementation B, wrapping implementation C, etc.).

The above specific elements used for each type immediately highlight an issue with the classical approach; JSF has to provide explicit support for each specific type to be replaced/augmented in this way. By contrast, the CDI approach allows us to pretty much replace/augment any type without requiring any special support from JSF other than that JSF uses CDI for that artifact.

On the bright side, the factory implementation is currently somewhat simpler compared to the CDI version, as the “previous” or “original” type is simply being passed to it in its constructor instead of having to be looked up using verbose code.

As an example, we’ll show how to augment the external context factory . For this we start with the mentioned registration in faces-config.xml.

<factory>
    <external-context-factory>
        com.example.project.ExternalContextProducer
    </external-context-factory>
</factory>

The implementation then looks as follows:

public class ExternalContextProducer extends ExternalContextFactory {
    public ExternalContextProducer(ExternalContextFactory wrapped) {
        super(wrapped);
    }


    @Override
    public ExternalContext getExternalContext
        (Object context, Object request, Object response)
    {
        ExternalContext previousExternalContext =
            getWrapped().getExternalContext(context, request, response);
        ExternalContext newExternalContext =
            new ExternalContextWrapper(previousExternalContext) {
                @Override
                public String getAuthType() {
                    return "OurOwnAuthType";
                }
            };
        return newExternalContext;
    }
}

There are a few things to observe here. First of all, every factory of this kind has to inherit from a pre-described parent factory, which in this case is ExternalContextFactory. Second, there’s an implicit contract that must be followed, and that’s implementing the constructor exactly as shown in the example. That is, add a public constructor with a single parameter the exact same type as the superclass, and pass this parameter on to the super constructor. This instance is then available in other methods using the getWrapped() method.

Testing that this indeed works is relatively easy. We use a similar backing bean as with the CDI version :

@Named @RequestScoped
public class TestBean {


    @Inject
    private ExternalContext externalContext;


    public String getAuth() {
        return externalContext.getAuthType();
    }
}

And the following Facelet :

<!DOCTYPE html>
<html lang="en"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
>
    <h:head />
    <h:body>
        <p>Test: #{testBean.auth}</p>
    </h:body>
</html>

As the external context is also a type that’s injectable via CDI, the observant reader may wonder what happens when both a CDI alternative producer and a classic factory are provided for that type. The answer is that this is strictly speaking not specified (thus undefined behavior), yet in practice it’s strongly implied that the classic factory is used as the source to ultimately get the external context from. This means that an alternative producer for ExternalContext will only affect the direct injection of ExternalContext, and not any situation when this type is obtained in any other way, for instance, by calling FacesContext#getExternalContext(). This is something users should clearly be aware of. The expectation is, though, that a future revision of the spec will make a CDI producer the initial source.

Plug-ins

A different type of extending that JSF offers next to the alternative producers and factories is what’s essentially a plug-in. Here, no core JSF type is replaced or augmented, but an additional functionality is added to some part of the runtime. Most of these additions, therefore, have to declare in some way what it is they are exactly adding, which is different from the factories which just provided an implementation of type X or Y.

Plug-ins are added as elements of the <application> element in faces-config.xml, just as some of the factory-like types mentioned above. The following are supported:

  • <el-resolver>

  • <property-resolver> (deprecated)

  • <variable-resolver> (deprecated)

  • <search-keyword-resolver>

We already saw an example of the Search Keyword Resolver in the section “Custom Search Keywords” in Chapter 12. Characteristic for that one being a plug-in was the method isResolverForKeyWord(), by which the plug-in could indicate for which keyword, or keyword pattern, it would operate.

We’ll take a look at one other example here, namely, the EL resolver. The property resolver and variable resolver are both deprecated and have been replaced by the EL resolver. The EL resolver itself is not a JSF-specific type but originates from the Expression Language (EL) spec. This spec does, however, have important parts of its origins in JSF.

The EL resolver allows us to interpret the so-called base and property of an expression in a custom way. Considering the expression #{foo.bar.kaz}, then “foo” is the base and “bar” is the property when resolving “bar”, while “bar” is the base and “kaz” is the property when resolving “kaz”. Perhaps somewhat surprising at first is that when “foo” is being resolved, the base is null and “foo” is the property.

In practice, adding a custom EL resolver is not often needed, and we can often get by with simply defining a named CDI bean that does what we require. Custom EL resolvers could come into play when JSF is integrated in a completely different environment though, one where we’d like expressions to resolve to a completely different (managed) bean system. Even so, a CDI-to-other-beans-bridge might be a better option even there, but it’s perhaps good to know a custom EL resolver is one other tool we have in our arsenal.

Anyway, to demonstrate what an EL resolver can do we’ll show an example where the base of an EL expression is interpreted as a pattern, something we can’t do directly with a named CDI bean.

The following shows an example EL resolver :

public class CustomELResolver extends ELResolver {

    protected boolean isResolverFor(Object base, Object property) {
        return base == null
            && property instanceof String
            && ((String) property).startsWith("dev");
    }


    @Override
    public Object getValue
        (ELContext context, Object base, Object property)
    {
        if (isResolverFor(base, property)) {
            context.setPropertyResolved(true);
            return property.toString().substring(3);
        }
        return null;
    }


    @Override
    public Class<?> getType
        (ELContext context, Object base, Object property)
    {
        if (isResolverFor(base, property)) {
            context.setPropertyResolved(true);
            return String.class;
        }
        return null;
    }


    @Override
    public Class<?> getCommonPropertyType
        (ELContext context, Object base)
    {
        return base == null ? getType(context, base, null) : null;
    }


    @Override
    public boolean isReadOnly
        (ELContext context, Object base, Object property)
    {
        return true;
    }


    @Override
    public void setValue
        (ELContext context, Object base, Object property, Object value)
    {
        // NOOP;
    }


    @Override
    public Iterator<FeatureDescriptor> getFeatureDescriptors
        (ELContext context, Object base)
    {
        return null;
    }
}

If we now use the following Facelet

<!DOCTYPE html>
<html lang="en"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
>
    <h:head />
    <h:body>
        <p>Test: #{devThisIsDev}</p>
    </h:body>
</html>

we’ll see “Test: ThisIsDev” being printed when requesting it. What’s happening here is that the custom EL resolver handles every name that starts with “dev.”

Dynamic Extensions

The previous examples were mostly about registering the extensions that we needed statically, e.g., by registering a factory or the type directly in faces-config.xml. Factories give us an opportunity for some dynamic behavior. That is, at the point the factory is called we can decide what new type (if any) to return.

For even more dynamic behavior it’s frequently required to be able to dynamically add the factories, or in CDI to dynamically add the producers. CDI has an elaborate SPI (server provider interface) for this (simply called “CDI Extensions”) which are, however, somewhat outside the scope of this book.

For classic factories and actually everything that’s in faces-config.xml, there’s a somewhat low-level method to add these dynamically: the Application Configuration Populator, which we’ll discuss next.

Application Configuration Populator

The Application Configuration Populator is a mechanism to programmatically provide an additional faces-config.xml file, albeit by using the XML DOM (Document Object Model) API (application programming interface). This DOM API can be slightly obscure to use, and the power of the Application Configuration Populator is limited by its ability to only configure. There’s no SPI in JSF to directly modify other faces-config.xml at this level.

The mechanism works by implementing the abstract class javax.faces.application.ApplicationConfigurationPopulator and putting the fully qualified class name of this in a META-INF/services/javax.faces.application.ApplicationConfigurationPopulator file of a JAR library.

To demonstrate this, we’ll create another version of the ExternalContextProducer that we demonstrated earlier, this time using the mentioned ApplicationConfigurationPopulator. For this we take the same code, remove the faces-config.xml file, and add the META-INF/services entry as well as the following Java class :

public class ConfigurationProvider
    extends ApplicationConfigurationPopulator
{
    @Override
    public void populateApplicationConfiguration(Document document) {
        String ns = document.getDocumentElement().getNamespaceURI();
        Element factory = document.createElementNS(ns, "factory");
        Element externalContextFactory =
            document.createElementNS(ns, "external-context-factory");
        externalContextFactory.appendChild(
            document.createTextNode(
                ExternalContextProducer.class.getName()));
        factory.appendChild(externalContextFactory);
        document.getDocumentElement().appendChild(factory);
    }
}

A caveat is that since this uses the java.util.ServiceLoader under the hood, it really only works when ConfigurationProvider and ExternalContextProducer are packaged together in an actual JAR library placed in the /WEB-INF/lib of the WAR, instead of just being put directly in the WAR.

The Application Main Class

Above we discussed the Application Configuration Populator, which as we saw is actually a faces-config.xml provider of sorts. This means it works with fully qualified class names and elements that are still text in nature.

JSF features a variety of somewhat more traditional programmatic APIs as well, with perhaps the most well-known of them being the javax.faces.application.Application main class, which is among others a holder for the same singletons that we mentioned previously in the section “Extending Classical Artifacts.” For completeness we’ll repeat this list here.

  • javax.faces.application.NavigationHandler

  • javax.faces.application.ViewHandler

  • javax.faces.application.ResourceHandler

  • javax.faces.component.search.SearchExpressionHandler

  • javax.faces.flow.FlowHandler

  • javax.faces.application.StateManager

  • javax.faces.event.ActionListener

All of these have corresponding setters on the Application class. For instance, the following shows the Javadoc and method declaration for ActionListener:

/**
  * <p>
  * Set the default {@link ActionListener} to be registered for all
  * {@link javax.faces.component.ActionSource} components.
  * </p>
  *
  * @param listener The new default {@link ActionListener}
  *
  * @throws NullPointerException
  *            if <code>listener</code> is <code>null</code>
  */
public abstract void setActionListener(ActionListener listener);

Likewise, the Application class also has “add” methods for the plug-in types we mentioned in the section “Plug-ins.”

  • javax.el.ELResolver

  • javax.faces.el.PropertyResolver (deprecated)

  • javax.faces.el.VariableResolver (deprecated)

  • javax.faces.component.serarch.SearchKeywordResolver

A difficulty with using the Application class to set these singletons is, first of all, that it’s timing sensitive. This means we can only set such classes from a certain point, which is obviously not before the point that the Application itself is available, and for some singletons not until the first request is serviced. This first request is a somewhat difficult point to track.

Specifically, the resource handler, view handler, flow handler, and state handler, uhhh, state manager, can’t be set anymore after the first request, while the EL resolver and search keyword resolver can’t be added either after said first request.

To demonstrate this we’ll add the custom EL resolver again that we demonstrated above, but in a more dynamic way now. To do this, we remove the EL resolver from our faces-config.xml file and add a system listener instead. Our faces-config.xml file then looks as follows:

<application>
    <system-event-listener>
        <system-event-listener-class>
            com.example.project.ELResolverInstaller
        </system-event-listener-class>
        <system-event-class>
            javax.faces.event.PostConstructApplicationEvent
        </system-event-class>
    </system-event-listener>
</application>

Indeed, this doesn’t get rid of the XML and, in fact, it’s even more XML, but reducing or getting rid of XML is not the main point here, which is the ability to register the EL resolver in a more dynamic way.

The system event listener that we just registered here looks as follows:

public class ELResolverInstaller implements SystemEventListener {

    @Override
    public boolean isListenerForSource(Object source) {
        return source instanceof Application;
    }


    @Override
    public void processEvent(SystemEvent event) {
        Application application = (Application) event.getSource();
        application.addELResolver(new CustomELResolver());
    }
}

What we have here is a system event listener that listens to the PostConstructApplicationEvent. This is generally a good moment to add plug-ins like the EL resolver. The Application instance is guaranteed to be available at this point, and request processing hasn’t started yet, so we’re surely in time before the first request has been handled.

Local Extension and Wrapping

In some cases, we don’t want to override, say, a view handler globally, but only for a local invocation of, typically, a method in a component. JSF gathers for this by passing on the FacesContext, which components to use as the main entry point from which to get pretty much all other things. Components are for JSF 2.3 not CDI artifacts or otherwise injectable, so the CDI approach for the moment doesn’t hold for them.

Local extension can then be done by wrapping the faces context and passing that wrapped context to the next layer. Note that the Servlet spec uses the same pattern where the HttpServletRequest and HttpServletResponse can be wrapped by a filter and passed on to the next filter, which can wrap it again and pass it to its next filter, etc.

To illustrate this, suppose that for a certain component we’d like to augment the action URL generation used by, among others, form components in such a way that “?foo=bar” is added to this URL. If we do this by globally overriding the view handler all components and other code using the view handler would get to see this URL, while here we’d only want this for this very specific component.

To achieve just this, we use wrapping here as illustrated in the following component:

@FacesComponent(createTag = true)
public class CustomForm extends HtmlForm {


    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        super.encodeBegin(new ActionURLDecorator(context));
    }
}

Implementing the wrapper without support from JSF would be a somewhat tedious task, to say the least, as the path from FacesContext to ViewHandler is a few calls deep and the classes involved have a large amount of methods. Luckily JSF greatly eases this task by providing wrappers for most of its important artifacts, with an easy-to-use constructor.

The pattern used here is that the top-level class (ActionURLDecorator from the example above) inherits from FacesContextWrapper and pushes the original faces context to its superclass. Then the path of intermediate objects is implemented by overriding the initial method in the chain (getApplication() here), and returning a wrapper for the return type of that method, with the super version of it passed into its constructor. This wrapper then does the same thing for the next method in the chain, until the final method is reached for which custom behavior is required.

The following gives an example of this:

public class ActionURLDecorator extends FacesContextWrapper {

    public ActionURLDecorator(FacesContext context) {
        super(context);
    }


    @Override
    public Application getApplication() {
        return new ApplicationWrapper(super.getApplication()) {
            @Override
            public ViewHandler getViewHandler() {
                return new ViewHandlerWrapper(super.getViewHandler()) {
                    @Override
                    public String getActionURL
                        (FacesContext context, String viewId)
                    {
                        String url = super.getActionURL(context, viewId);
                        return url + "?foo=bar";
                    }
                };
            }
        };
    }
}

With these two classes present in the JSF application, now consider the following Facelet that makes use of our custom form component :

<!DOCTYPE html>
<html lang="en"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:test="http://xmlns.jcp.org/jsf/component"
>
    <h:head />
    <h:body>
        <test:customForm action="index">
            <h:commandButton action="index" value="Go Index" />
        </test:customForm>
    </h:body>
</html>

Remember that no XML registration is needed for the custom component when it’s annotated with @FacesComponent(createTag=true), and that its XML namespace defaults to http://xmlns.jcp.org/jsf/component and its component tag name to the simple class name. (See also Chapter 11.)

If we request the view corresponding to this Facelet and press the button, we’d indeed see “?foo=bar” appearing after the URL, meaning that our local extension of the view handler via a chain of wrappers has worked correctly.

Introspection

An important aspect of being able to extend a framework correctly is not only to be able to utilize extension points but also to be able to introspect the framework and query it for what artifacts or resources it has available.

One of the places where JSF provides support for introspection is its ability to reveal which view resources are present in the system. Remember that in JSF, views like, for example, Facelets are abstracted behind the view handler, which in turn manages one or more view declaration language (VDL) instances. A VDL, also called a templating engine, has the ability to read its views via the resource handler. As we’ve seen in this chapter, all these things can be augmented or even fully replaced.

This specifically means that views can come from anywhere (e.g., from the filesystem (most typical)) but can also be generated in memory, be loaded from a database or fetched over the network, and much more. Also, the simple physical file to logically view name mapping such as that used by Facelets doesn’t have to hold for other view declaration languages at all.

Together this means that without an explicit introspection mechanism where the view handler, VDL , and resource handler can be asked which views/resources they have available, we would not be able to reliably obtain a full list of views.

Such list of views is needed, for example, when we want to utilize so-called extensionless URLs , which are URLs without any extension such as .xhtml or .jsf, and without any extra path mapping present such as /faces/*. Lacking any hint inside the URL itself, the Servlet container on top of which JSF works has to have some other way of knowing that a certain request has to be routed to the faces servlet.

A particularly elegant way to do this is by utilizing Servlet’s “exact mapping” feature, which is a variant of URL mapping where an exact name instead of a pattern is mapped to a given servlet, which in this case would be the faces servlet. Since the Servlet spec has an API for dynamically adding Servlet mappings, and JSF has an API to dynamical introspect which views are available, we pretty much only have to combine these two to implement extensionless URLs.

The following shows an example of how to do this:

@WebListener
public class ExtensionLessURLs implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent event) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        event.getServletContext()
            .getServletRegistrations()
            .values()
            .stream()
            .filter(servlet -> servlet
                .getClassName().equals(FacesServlet.class.getName()))
            .findAny()
            .ifPresent(facesServlet -> facesContext
                .getApplication()
                .getViewHandler()
                .getViews(facesContext, "/",
                    ViewVisitOption.RETURN_AS_MINIMAL_IMPLICIT_OUTCOME)
                .forEach(view -> facesServlet.addMapping(view)));
    }
}

What happens here is that we first try to find the faces servlet , which incidentally is another example of introspection, this time in the Servlet spec. If found, we ask the view handler for all views. As mentioned above, this will internally introspect all the available view declaration instances, which in turn may introspect the resource handler. The “RETURN_AS_MINIMAL_IMPLICIT_OUTCOME” parameter is used to make sure all views are returned in their minimal form without any file extensions or other markers appended. This is the same form that can be returned from, for example, action methods or used with the action attribute of command components.

Having obtained the stream of views in the right format, we directly add each of them as exact mapping to the faces servlet that we found earlier.

For example, suppose we have a Facelet view in the web root in a folder /foo named bar.xhtml. Then getViews() will return a stream with the string “/foo/bar”. When the faces servlet is mapped to this “/foo/bar”, and assuming the JSF application is deployed to the context path /test on domain localhost port 8080, we can request http://localhost:8080/test/foo/bar to see the rendered response of that Facelet.

Note that even though it’s relatively simple to achieve extensionless URLs in JSF this way, it’s still somewhat tedious to have to do this for every application. It’s expected that a next revision of JSF will support this via a single setting.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.146.152