Prevent suffix from being added to resources when page loads
Asked Answered
B

2

14

I have a JSF2 application running and working no problem. The issue I am having with JSF is with the resource bundle. All resources have the .xhtml suffix appended to it. So main.css becomes main.css.xhtml when loaded in the browser. I would like to have it so the .xhtml isn't apended to the resources (don't mind about the pages themselves).

Is there a way where we can NOT have .xhtml appended to resources?

I would ideally not have to change the internal workings of the site. I have listed ideas below, but I have to say I don't really like these. Hoping for a solution somewhere?

I am using Majorra v.2.1.17 on Glassfish 3.1.2.2.

Current Faces Servlet loading as in web.xml (updated)

<servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/javax.faces.resource/*</url-pattern>
</servlet-mapping>

Why this questions is different from others

Reasoning

Sure you might be asking me why I need this. Well, we are moving our application to be served by the Akamai CDN.

The issue we are having with the integration of the site is that we are trying to cache static content on the edge servers. This is done by matching file extensions (ie: .js, .doc, .png, css, etc). We cannot match xhtml because this would be caching all pages as well as static content. Which by that would cause problems with sessions and such.

Attempted Solution

In line with the answer by BalusC, I have implemented the resource handler as suggested. I will not rewrite code here, since it is in answer below.

However, I am getting an error when loading composite components. I am getting an error as such :

WARNING: StandardWrapperValve[Faces Servlet]: PWC1406: Servlet.service() for servlet Faces Servlet threw exception
java.lang.NullPointerException
    at com.sun.faces.application.ApplicationImpl.createComponent(ApplicationImpl.java:975)
    at com.sun.faces.facelets.tag.jsf.CompositeComponentTagHandler.createComponent(CompositeComponentTagHandler.java:162)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.createComponent(ComponentTagHandlerDelegateImpl.java:494)
    at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:169)
...

Composite component is loaded correctly because if I "unregister" the new ResourceHandler we just created it will load. The stack trace leads me to believe that it is trying to find this component in a java class, instead of finding it in the resources. According to grepcode this would be at this last line (975) where the error happens :

String packageName = componentResource.getLibraryName();
String className = componentResource.getResourceName();
className = packageName + '.' + className.substring(0, className.lastIndexOf('.'));

Meaning that the resourceName, aka className is null since the error I am getting is java.lang.NullPointerException. I can't seem to figure out how/where the ResourceHandler is called vis-a-vis a composite component. Any help figuring out this last issue?

Bumkin answered 19/2, 2013 at 17:30 Comment(8)
Create a custom resource handler.Leeward
@Leeward something of the sort would then make (within a css file for example) something like this : background: #14311b url("#{resource['templateImages:background.jpg']}") no-repeat 0 0; not working, since jsf wouldn't be running on "non-xhtml" files. Would it then be a good idea add "*.css" as a pattern for the jsf servlet? Or simply use static fetching? What about resources located in ln=javax.faces?Bumkin
A custom resource handler would allow you to add /javax.faces.resource/* to faces servlet mapping.Leeward
@BalusC. If I do this I thought I'd be able to access my resources as such : http://localhost:8080/myApp/javax.faces.resource/main03.css?ln=styles. This does not work, but http://localhost:8080/myApp/javax.faces.resource/main03.css.xhtml?ln=styles does. I know I am missing something.Bumkin
Correction. So it was my misunderstanding that just adding the prefix I would be still using it the same way. So /javax.faces.resource/* is added to servlet mapping. But now going to : javax.faces.resource/resources/styles/main03.css to access this sheet, the stylesheet is still not being processed by jsfBumkin
As said in the 1st comment, create a custom resource handler.Leeward
@Leeward I've edited the question to show a bit better what I did and my implementation of what I think my custom resource handler would look like. From the comments I feel like I'm missing something and I appologize for this, which might seem trivial. (all help is greatly appreciated).Bumkin
No, it's not trivial :)Leeward
L
14

This is doable with a custom ResourceHandler which returns in createResource() a Resource which in turn returns an "unmapped" URL on Resource#getRequestPath(). You only need to add the default JSF resource prefix /javax.faces.resource/* to the <url-pattern> list of the FacesServlet mapping in order to get it to be triggered anyway.

Further, you need to override isResourceRequest() to check if the URL starts with the JSF resource prefix and also the handleResourceRequest() to locate and stream the proper resource.

All with all, this should do:

public class UnmappedResourceHandler extends ResourceHandlerWrapper {

    private ResourceHandler wrapped;

    public UnmappedResourceHandler(ResourceHandler wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public Resource createResource(final String resourceName, final String libraryName) {
        final Resource resource = super.createResource(resourceName, libraryName);

        if (resource == null) {
            return null;
        }

        return new ResourceWrapper() {

            @Override
            public String getRequestPath() {
                ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
                String mapping = externalContext.getRequestServletPath();

                if (externalContext.getRequestPathInfo() == null) {
                    mapping = mapping.substring(mapping.lastIndexOf('.'));
                }

                String path = super.getRequestPath();

                if (mapping.charAt(0) == '/') {
                    return path.replaceFirst(mapping, "");
                }
                else if (path.contains("?")) {
                    return path.replace(mapping + "?", "?");
                }
                else {
                    return path.substring(0, path.length() - mapping.length());
                }
            }

            @Override // Necessary because this is missing in ResourceWrapper (will be fixed in JSF 2.2).
            public String getResourceName() {
                return resource.getResourceName();
            }

            @Override // Necessary because this is missing in ResourceWrapper (will be fixed in JSF 2.2).
            public String getLibraryName() {
                return resource.getLibraryName();
            }

            @Override // Necessary because this is missing in ResourceWrapper (will be fixed in JSF 2.2).
            public String getContentType() {
                return resource.getContentType();
            }

            @Override
            public Resource getWrapped() {
                return resource;
            }
        };
    }

    @Override
    public boolean isResourceRequest(FacesContext context) {
        return ResourceHandler.RESOURCE_IDENTIFIER.equals(context.getExternalContext().getRequestServletPath());
    }

    @Override
    public void handleResourceRequest(FacesContext context) throws IOException {
        ExternalContext externalContext = context.getExternalContext();
        String resourceName = externalContext.getRequestPathInfo();
        String libraryName = externalContext.getRequestParameterMap().get("ln");
        Resource resource = context.getApplication().getResourceHandler().createResource(resourceName, libraryName);

        if (resource == null) {
            super.handleResourceRequest(context);
            return;
        }

        if (!resource.userAgentNeedsUpdate(context)) {
            externalContext.setResponseStatus(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        externalContext.setResponseContentType(resource.getContentType());

        for (Entry<String, String> header : resource.getResponseHeaders().entrySet()) {
            externalContext.setResponseHeader(header.getKey(), header.getValue());
        }

        ReadableByteChannel input = null;
        WritableByteChannel output = null;

        try {
            input = Channels.newChannel(resource.getInputStream());
            output = Channels.newChannel(externalContext.getResponseOutputStream());

            for (ByteBuffer buffer = ByteBuffer.allocateDirect(10240); input.read(buffer) != -1; buffer.clear()) {
                output.write((ByteBuffer) buffer.flip());
            }
        }
        finally {
            if (output != null) try { output.close(); } catch (IOException ignore) {}
            if (input != null) try { input.close(); } catch (IOException ignore) {}
        }
    }

    @Override
    public ResourceHandler getWrapped() {
        return wrapped;
    }

}

Register it as follows in faces-config.xml:

<application>
    <resource-handler>com.example.UnmappedResourceHandler</resource-handler>
</application>

Extend the FacesServlet URL pattern with ResourceHandler.RESOURCE_IDENTIFIER:

<servlet-mapping>
    <servlet-name>facesServlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
    <url-pattern>/javax.faces.resource/*</url-pattern>
</servlet-mapping>
Leeward answered 21/2, 2013 at 11:16 Comment(9)
This is awesome. I want to say that I was close, but only in structure. I would of never thought of overriding the handleResourceRequest, nor would I have known how. I am getting this error tho : WARNING: StandardWrapperValve[Faces Servlet]: PWC1406: Servlet.service() for servlet Faces Servlet threw exception java.lang.NullPointerException at com.sun.faces.application.ApplicationImpl.createComponent(ApplicationImpl.java:974) but only when loading composite components... any ideas?Bumkin
I have updated the question with more information about this error and when/how I'm getting it (in attempted Solution).Bumkin
Just upgraded to Majorra 2.1.17 and am getting the exact same situation. As soon as I remove my composite component, everything works. Seems that for some reason, it will not use it as a resource... I'm trying to debug, but I might be in over my head... will continue to update the question with newly found information.Bumkin
do composite coponents have their own resource handler? This custom resource handler we (you) wrote doesn't seem to be called at all when inserting a composite component. I know this is the issue I am having since as soon as I remove it, no issues.Bumkin
Seems like the composite component tag handler cannot get the required resource (updated question with this information). I'm reading/learning quite a bit... any help in direction to take?Bumkin
I took a second look and it turns out that when you use <cc:interface componentType> (as I had in my test composite), then it works fine. Removing it indeed yields exactly the problem. The cause is actually embarrassing: ResourceWrapper doesn't wrap getResourceName() and getLibraryName() (and getContentType()) at all! This oversight is fixed in JSF 2.2. In the meanwhile, you've to wrap it yourself. I've updated the answer.Leeward
You good sir are a gentleman and a scholar. I whish I say I would of gotten here, but I was way off. I have to say I've learned a lot on this question. Many thanks!!Bumkin
FYI: this will be in OmniFaces 1.4: showcase.omnifaces.org/resourcehandlers/UnmappedResourceHandlerLeeward
This is awesome. I feel as if this was a real issue. I am happy to see that this might be helping others in the future. Once again, I thank you. Strong supporter of OmniFaces.Bumkin
M
3

You could have a look at Rewrite. Rewrite allows to modify URLs that are rendered to the page and modify them in any way you want. You could do something like this to add a CDN To your site:

.addRule(CDN.relocate("{p}foo-{version}.css")
         .where("p").matches(".*")
         .where("version").matches(".*")
         .to("http://mycdn.com/foo-{version}.css"));

I think it should be easy to implement your requirement using Rewrite.

Have a look at the example configurations to learn about the features of rewrite.

Mellophone answered 21/2, 2013 at 6:15 Comment(3)
The problem with just re-writing the URL is that it then isn't processed by JSF. Also, I am not looking to put my files to a CDN, but to use my server as origin for the CDN. The files need to remain on my server.Bumkin
With Rewrite you could also build custom rules that would serve the resources as before for the fetching server of the CDN (identified by IP range or something like this) and rewrites the resources correctly for all other users. Just a thought. :)Mellophone
I might not have explained myself properly or not understanding properly. But, I am not fetching a CDN resource. Our server will BECOME a CDN resource. With JSF, by default, *.xhtml is appended to all resources, therefore you cannot identify static resources from dynamic ones.Bumkin

© 2022 - 2024 — McMap. All rights reserved.