Sunday, July 29, 2018

OmniFaces 3.2 adds o:hashParam, CDNResource and UUID in exception logging

OmniFaces 3.2 has been released!

Next to a bunch of utility methods, this version adds a <o:hashParam> and a CDNResource, and the FullAjaxExceptionhandler and FacesExceptionFilter will from now log the exceptions with an UUID and user IP.

You can find the complete list of additions, changes and fixes at What's new in OmniFaces 3.2? list in showcase.

Installation

Non-Maven users: download OmniFaces 3.2 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: use <version>3.2</version>.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>3.2</version>
</dependency>

o:hashParam

This new brother of <f|o:viewParam> will less or more do the same things, but then with hash query string parameters instead of request query string parameters.

The "hash query string" is the part in URL after the # which could be formatted in the same format as a regular request query string (the part in URL after the ?). An example:

http://example.com/page.xhtml#foo=baz&bar=kaz

This specific part of the URL (also called hash fragment identifier) is by default not sent to the server. The <o:hashParam> will on page load and on every window.onhashchange event send it anyway so that the JSF model gets updated, and on every JSF ajax request update the hash query string when the corresponding JSF model value has changed.

Check out the live demo!

CDNResource

This is a new javax.faces.application.Resource subclass which can be used by your custom ResourceHandler implementation which automatically uploads the local resources to a CDN and then returns the CDN URL instead of the local URL. The CDNResource offers the CombinedResourceHandler the opportunity to automatically generate a fallback URL to the local resource into the <script> and <link rel="stylesheet"> elements generated by the associated <h:outputScript>, <o:deferredScript> and <h:outputStylesheet> components. This is for now indeed only useful when you have enabled the CombinedResourceHandler. A more general appliance may come in a future OmniFaces version which should also cover non-combined resources and images.

Imagine that you have the below custom resource handler configured as <resource-handler> in faces-config.xml which automatically uploads all local CSS/JS/image resources to a Amazon S3 based CDN:

public class AmazonS3ResourceHandler extends DefaultResourceHandler {

    @Inject
    private AmazonS3Service s3;

    public AmazonS3ResourceHandler(ResourceHandler wrapped) {
        super(wrapped);
    }

    @Override
    public Resource decorateResource(Resource resource, String resourceName, String libraryName) {
        if (resource != null && Utils.endsWithOneOf(resourceName, ".js", ".css", ".jpg", ".gif", ".png", ".svg")) {
            return new CDNResource(resource, s3.getURL(resource, resourceName, libraryName));
        }
        else {
            return resource;
        }
    }
}

Whereby the your custom AmazonS3Service looks something like this, using the com.amazonaws:aws-java-sdk-s3 library:

@ApplicationScoped
public class AmazonS3Service {

    // key = resource path, value = last modified timestamp
    private static final Map<String, Long> RESOURCES = new ConcurrentHashMap<>();

    private AmazonS3 client;
    private String bucket;
    private String baseURL;

    @PostConstruct
    private void init() {
        client = AmazonS3ClientBuilder.standard().build(); // Use your own builder of course.
        bucket = "cdn"; // Use your own S3 bucket name of course.
        baseURL = "https://cdn.example.com/"; // Use your own CDN URL of course.
    }

    public String getURL(Resource resource, String resourceName, String libraryName) {
        String path = Paths.get(Utils.coalesce(libraryName, "")).resolve(resourceName).toString();
        Long lastModified = RESOURCES.computeIfAbsent(path, k -> uploadIfNecessary(resource, path));
        return baseURL + path + "?v=" + lastModified;
    }

    private long uploadIfNecessary(Resource resource, String path) {
        Long s3LastModified = null;
        long localLastModified;

        try {
            if (resource instanceof DynamicResource) { // E.g. combined resource.
                localLastModified = ((DynamicResource) resource).getLastModified();
            }
            else {
                localLastModified = resource.getURL().openConnection().getLastModified();
            }

            if (client.doesObjectExist(bucket, path)) {
                s3LastModified = client.getObjectMetadata(bucket, path).getLastModified().getTime();

                if (localLastModified > s3LastModified) {
                    s3LastModified = null;
                }
            }

            if (s3LastModified == null) {
                s3LastModified = localLastModified;
                upload(resource, path, s3LastModified);
            }
        }
        catch (Exception e) {
            throw new FacesException(e);
        }

        return s3LastModified;
    }

    private void upload(Resource resource, String path, long lastModified) throws Exception {
        String filename = Paths.get(path).getFileName().toString();
        byte[] content = Utils.toByteArray(resource.getInputStream());

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(resource.getContentType());
        metadata.setContentLength(content.length);
        metadata.setContentDisposition(Servlets.formatContentDispositionHeader(filename, false));
        metadata.setLastModified(new Date(lastModified));

        client.putObject(bucket, path, new ByteArrayInputStream(content), metadata);
    }

}

Then the CDNResource marker class will automatically force the CombinedResourceHandler to generate the following <script> and <link rel="stylesheet"> markup:

<script type="text/javascript" src="https://cdn.example.com/omnifaces.combined/XYZ.js?v=123"
    onerror="document.write('&lt;script src=&quot;/javax.faces.resource/XYZ.js.xhtml?ln=omnifaces.combined&v=123&quot;&gt;&lt;/script&gt;')"></script>

<link rel="stylesheet" type="text/css" href="https://cdn.example.com/omnifaces.combined/XYZ.css?v=123"
    onerror="this.onerror=null;this.href='/javax.faces.resource/XYZ.css.xhtml?ln=omnifaces.combined&v=123'" />

You see, the CombinedResourceHandler will automatically include the onerror attribute which points to the local URL as a fallback.

UUID and IP in exception logs

A very common requirement in real world projects is that any logging of the exception stack trace should also include an unique identifier (UUID) which in turn is also included in the error page and/or an automatic exception email to the administrator. This will make it easier to find back the stack trace in the server logs.

The OmniFaces FullAjaxExceptionHandler and FacesExceptionFilter will from now on also include the UUID and client IP address in the exception logs. Besides, the FacesExceptionFilter will now also start logging exception stack traces wheres it didn't do it. The UUID is in turn available as a request attribute with the name org.omnifaces.exception_uuid which you can if necessary easily include in your custom error page.

<li>Error ID: #{requestScope['org.omnifaces.exception_uuid']}</li>

These improvements will make it unnecessary to customize the FullAjaxExceptionHandler and/or FacesExceptionFilter to include the UUID.

How about OmniFaces 2.x and 1.1x?

The 2.x got on special request also the CDNResource and hence steps from 2.6.9 to 2.7 instead of to 2.6.10. For the remainder only bugfixes are done and no other new things. As long as you're still on JSF 2.2 with CDI, you can continue using latest 2.x.

The 1.1x is basically already since 2.5 in maintenance mode. I.e. only critical bugfix versions will be released. It's currently still at 1.14.1 (May 2017), featuring the same features as OmniFaces 2.4, but without any JSF 2.2 and CDI things and therefore compatible with CDI-less JSF 2.0/2.1.

Maven download stats

Here are the 2018's Maven download stats so far:

  • January 2018: 14646
  • February 2018: 14786
  • March 2018: 18059
  • April 2018: 16642
  • May 2018: 17876
  • June 2018: 17432
  • July 2018: TBD

The Definitive Guide to JSF in Java EE 8

Just in case if you have missed it .. I have finally written a book!

The Definitive Guide to JSF in Java EE 8 is since July 11, 2018 available at Amazon.com. This book is definitely a must read for anyone working with JSF or interested in JSF. It uncovers the history, inner workings, best practices and hidden gems of JSF. The source code of the book's examples can be found at GitHub.

No comments: