-
Available since OmniFaces 3.7
This ResourceHandler
generates the manifest.json
and also an offline-aware sw.js
based on any WebAppManifest
found in the runtime classpath. Historical note: this class was introduced in 3.6 as WebAppManifestResourceHandler
without any service worker related logic and since 3.7 renamed to PWAResourceHandler
after having service worker related logic added.
Usage
- Create a class which extends
WebAppManifest
in your web application project. - Give it the appropriate CDI scope annotation, e.g
ApplicationScoped
,SessionScoped
or evenRequestScoped
(note that aViewScoped
won't work). - Override properties accordingly conform javadoc and the rules in the W3 spec.
- Reference it as
#{resource['omnifaces:manifest.webmanifest']}
in your template.
Here's a concrete example:
package com.example;
import java.util.Arrays;
import java.util.Collection;
import jakarta.enterprise.context.ApplicationScoped;
import org.omnifaces.resourcehandler.WebAppManifest;
@ApplicationScoped
public class ExampleWebAppManifest extends WebAppManifest {
@Override
public String getName() {
return "Example Application Name";
}
@Override
public String getShortName() {
return "EAN";
}
@Override
public Collection<ImageResource> getIcons() {
return Arrays.asList(
ImageResource.of("logo.svg"),
ImageResource.of("logo-120x120.png", Size.SIZE_120),
ImageResource.of("logo-180x180.png", Size.SIZE_180),
ImageResource.of("logo-192x192.png", Size.SIZE_192),
ImageResource.of("logo-512x512.png", Size.SIZE_512)
);
}
@Override
public String getThemeColor() {
return "#cc9900";
}
@Override
public String getBackgroundColor() {
return "#ffffff";
}
@Override
public Display getDisplay() {
return Display.STANDALONE;
}
@Override
public Collection<Category> getCategories() {
return Arrays.asList(Category.BUSINESS, Category.FINANCE);
}
@Override
public Collection<RelatedApplication> getRelatedApplications() {
return Arrays.asList(
RelatedApplication.of(Platform.PLAY, "https://play.google.com/store/apps/details?id=com.example.app1", "com.example.app1"),
RelatedApplication.of(Platform.ITUNES, "https://itunes.apple.com/app/example-app1/id123456789")
);
}
}
Reference it in your template exactly as follows, with the exact library name of omnifaces
and exact resource name of manifest.webmanifest
. You cannot change these values.
<link rel="manifest" href="#{resource['omnifaces:manifest.webmanifest']}" crossorigin="use-credentials" />
The crossorigin
attribute is optional, you can drop it, but it's mandatory if you've put the SessionScoped
annotation on your WebAppManifest
bean, else the browser won't retain the session cookies while downloading the manifest.json
and then this resource handler won't be able to maintain the server side cache, see also next section.
For backwards compatibility with earlier OmniFaces versions, the resource name of manifest.json
is also supported. The default resource name has changed fo manifest.webmanifest
since version 4.2 in order to comply the new change in W3C spec. The resource will use the application/manifest+json
content type. In case you face the following warning in server logs or something similar,
WARNING: JSF1091: No mime type could be found for file manifest.webmanifest. To resolve this, add a mime-type mapping to the applications web.xml.then take action accordingly by adding the following entry to your
web.xml
:
<mime-mapping>
<extension>webmanifest</extension>
<mime-type>application/manifest+json</mime-type>
</mime-mapping>
Note: you do not need to explicitly register this resource handler in your faces-config.xml
. It's already automatically registered.
Server side caching
Basically, the CDI scope annotation being used is determinative for the autogenerated v=
query parameter indicating the last modified timestamp. If you make your WebAppManifest
bean RequestScoped
, then it'll change on every request and the browser will be forced to re-download it. If you can however guarantee that the properties of your WebAppManifest
are static, and thus you can safely make it ApplicationScoped
, then the v=
query parameter will basically represent the timestamp of the first time the bean is instantiated.
Offline-aware service worker
The generated sw.js
will by default auto-register the WebAppManifest.getStartUrl()
and all welcome files from web.xml
as cacheable resources which are also available offline. You can override the welcome files with WebAppManifest.getCacheableViewIds()
. E.g.
@Override
public Collection<String> getCacheableViewIds() {
return Arrays.asList("/index.xhtml", "/contact.xhtml", "/support.xhtml");
}
If this method returns an empty collection, i.e. there are no cacheable resources at all, and thus also no offline resources at all, then no service worker file will be generated as it won't have any use then.
In case you want to show a custom page as "You are offline!" error page, then you can specify it by overriding the WebAppManifest.getOfflineViewId()
.
@Override
public String getOfflineViewId() {
return "/offline.xhtml";
}
Whereby the offline.xhtml
should contain something like this:
<h1>Whoops! You appear to be offline!</h1>
<p>Please check your connection and then try refreshing this page.</p>
For each of those "cacheable view IDs" and "offline view IDs", the Faces view briefly will be built in in order to extract all <x:outputStylesheet>
,<x:outputScript>
and <x:graphicImage>
resources and add them to cacheable resources of the service worker as well.
If the WebAppManifest.getCacheableViewIds()
returns an empty collection, then no sw.js
will be generated, and WebAppManifest.getOfflineViewId()
will also be ignored.
Client side events
In the client side, you can listen on omnifaces.offline
and omnifaces.online
events in the window
whether the client is currently online or offline.
window.addEventListener("omnifaces.online", function(event) {
var url = event.detail.url;
// ..
});
window.addEventListener("omnifaces.offline", function(event) {
var url = event.detail.url;
var error = event.detail.error;
// ...
});
Or when you're using jQuery:
$(window).on("omnifaces.online", function(event) {
var url = event.detail.url;
// ..
});
$(window).on("omnifaces.offline", function(event) {
var url = event.detail.url;
var error = event.detail.error;
// ...
});
This gives you the opportunity to set a global flag and/or show some sort of notification. The event.detail
will contain at least the url
which was being requested through the service worker, and in case of the omnifaces.offline
event, there will also be an error
which represents the original network error object thrown by fetch()
.
Demo
The #{resource['omnifaces:manifest.json']}
is already
configured
on this showcase web application via an application scoped ManifestJson
bean as shown below.
Rightclick the page and View Source and explore the <link rel="manifest">
.
It should point to the autogenerated manifest.json
file.
At the bottom there should be a OmniFaces.ServiceWorker.init()
script pointing to the autogenerated sw.js
file.
/*
* Copyright 2020 OmniFaces.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.omnifaces.showcase;
import static java.util.Arrays.asList;
import java.util.Collection;
import jakarta.enterprise.context.ApplicationScoped;
import org.omnifaces.resourcehandler.WebAppManifest;
@ApplicationScoped
public class ManifestJson extends WebAppManifest {
@Override
public String getName() {
return "OmniFaces Showcase";
}
@Override
public String getShortName() {
return "OFS";
}
@Override
public Collection<ImageResource> getIcons() {
return asList(
ImageResource.of("layout:img/OmniFaces-icon-192x192.png", Size.SIZE_192),
ImageResource.of("layout:img/OmniFaces-icon-512x512.png", Size.SIZE_512)
);
}
@Override
public Display getDisplay() {
return Display.STANDALONE;
}
@Override
public String getThemeColor() {
return "#373737";
}
@Override
public String getBackgroundColor() {
return "#f2f2f2";
}
@Override
protected String getOfflineViewId() {
return "/offline.xhtml";
}
}
<ui:composition template="/WEB-INF/templates/errorpage.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
>
<ui:define name="contentTitle">Internet has gone</ui:define>
<ui:define name="errorContent">
<p>
This site appears to be offline.
Please verify if you're connected to the Internet and retry.
If you're connected, then this site is probably either down or having an expired SSL certificate.
</p>
</ui:define>
</ui:composition>