From c6b26ac2d9ffdae8c3ab6c0118cc61741ec33219 Mon Sep 17 00:00:00 2001 From: "octavian.marcu" Date: Wed, 7 Apr 2021 19:14:45 +0200 Subject: [PATCH] added service workers support --- vaadin-touchkit-agpl/pom.xml | 12 +- .../TouchKitConnectionStateHandler.java | 11 +- .../CacheManifestStatusIndicator.java | 5 +- .../settings/ApplicationCacheSettings.java | 2 +- .../touchkit/settings/TouchKitSettings.java | 222 +++++++++++++++++- .../touchkit/settings/WebAppSettings.java | 62 +++++ .../com/vaadin/addon/touchkit/TestServer.java | 2 +- .../addon/touchkit/TestTouchKitServlet.java | 29 +++ .../vaadin/addon/touchkit/TouchkitTestUI.java | 3 + vaadin-touchkit-cval/pom.xml | 5 +- vaadin-touchkit-integration-tests/pom.xml | 4 +- vaadin-touchkit-offline-tests/pom.xml | 4 +- 12 files changed, 334 insertions(+), 27 deletions(-) create mode 100644 vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestTouchKitServlet.java diff --git a/vaadin-touchkit-agpl/pom.xml b/vaadin-touchkit-agpl/pom.xml index 564ac755..51af151a 100644 --- a/vaadin-touchkit-agpl/pom.xml +++ b/vaadin-touchkit-agpl/pom.xml @@ -16,7 +16,7 @@ org.codehaus.mojo properties-maven-plugin - 1.0-alpha-2 + 1.0.0 initialize @@ -34,6 +34,7 @@ org.apache.maven.plugins maven-gpg-plugin + 1.6 sign-artifacts @@ -63,6 +64,7 @@ org.apache.maven.plugins maven-javadoc-plugin + 2.10.4 false @@ -157,8 +159,8 @@ agpl 3.0 - [7.6.0.beta1,7.99.9999] - 7.6.0.beta1 + 7.6.0 + 7.6.0 empty.properties true http://oss.sonatype.org/content/repositories/vaadin-snapshots/ @@ -253,8 +255,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + 1.8 + 1.8 diff --git a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/communication/TouchKitConnectionStateHandler.java b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/communication/TouchKitConnectionStateHandler.java index 6697e2ec..f8049719 100644 --- a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/communication/TouchKitConnectionStateHandler.java +++ b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/communication/TouchKitConnectionStateHandler.java @@ -2,11 +2,11 @@ import static com.vaadin.addon.touchkit.gwt.client.offlinemode.OfflineMode.ActivationReason.BAD_RESPONSE; import static com.vaadin.addon.touchkit.gwt.client.offlinemode.OfflineMode.ActivationReason.SERVER_AVAILABLE; - +import com.vaadin.addon.touchkit.gwt.client.offlinemode.OfflineModeEntrypoint; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.Response; -import com.vaadin.addon.touchkit.gwt.client.offlinemode.OfflineModeEntrypoint; +import com.google.gwt.user.client.Window; import com.vaadin.client.communication.ConnectionStateHandler; import com.vaadin.client.communication.DefaultConnectionStateHandler; import com.vaadin.client.communication.PushConnection; @@ -40,6 +40,13 @@ public void heartbeatException(Request request, Throwable exception) { @Override public void heartbeatInvalidStatusCode(Request request, Response response) { + if(response.getStatusCode() == 410) { + // This has broken at some point, session expired don't properly + // restart the application. Do a manual reload as I don't know how + // it should be done these days... + // TODO ask somebody who knows about this mess + Window.Location.reload(); + } if (!offlineModeEntrypoint.isOfflineModeEnabled()) { super.heartbeatInvalidStatusCode(request, response); } else { diff --git a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/offlinemode/CacheManifestStatusIndicator.java b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/offlinemode/CacheManifestStatusIndicator.java index 48a80213..8607454d 100644 --- a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/offlinemode/CacheManifestStatusIndicator.java +++ b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/gwt/client/offlinemode/CacheManifestStatusIndicator.java @@ -257,7 +257,10 @@ protected final native void hookAllListeners( */ private static native int getStatus() /*-{ - return $wnd.applicationCache.status; + if($wnd.applicationCache) { + return $wnd.applicationCache.status; + } + return 99; }-*/; /** diff --git a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/ApplicationCacheSettings.java b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/ApplicationCacheSettings.java index 3c21511e..fb55a1a0 100644 --- a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/ApplicationCacheSettings.java +++ b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/ApplicationCacheSettings.java @@ -117,7 +117,7 @@ protected String generateManifestFileName(BootstrapPageResponse response) { * enabled. */ public boolean isCacheManifestEnabled() { - return cacheManifestEnabled; + return cacheManifestEnabled && !TouchKitSettings.supportsGooglePWA(); } /** diff --git a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/TouchKitSettings.java b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/TouchKitSettings.java index 688e4c38..6b1fadec 100644 --- a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/TouchKitSettings.java +++ b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/TouchKitSettings.java @@ -10,14 +10,32 @@ import com.vaadin.server.BootstrapListener; import com.vaadin.server.BootstrapPageResponse; import com.vaadin.server.CustomizedSystemMessages; +import com.vaadin.server.RequestHandler; import com.vaadin.server.ServiceException; import com.vaadin.server.SessionInitEvent; import com.vaadin.server.SessionInitListener; import com.vaadin.server.SystemMessages; import com.vaadin.server.SystemMessagesInfo; import com.vaadin.server.SystemMessagesProvider; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinServletService; +import com.vaadin.server.VaadinSession; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.io.IOUtils; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import com.vaadin.addon.touchkit.service.ApplicationIcon; /** * TouchKitSettings is a collection of tools that help modify various touch @@ -31,15 +49,18 @@ public class TouchKitSettings implements BootstrapListener, SessionInitListener, SystemMessagesProvider { + private List stronglyCachedResources; + /** * Interface to select different settings for different kind of devices * based on e.g. HTTP header inspection. */ interface SettingSelector extends Serializable { + /** * @param response * @return used settings or null if settings shouldn't be used for this - * request. + * request. */ T select(BootstrapPageResponse response); } @@ -63,8 +84,8 @@ public TouchKitSettings() { * Creates a new instance of TouchKitSettings and binds it to the given * {@link VaadinService}. * - * @param vaadinService - * the vaadin service to which the new instance should be bound. + * @param vaadinService the vaadin service to which the new instance should + * be bound. */ public TouchKitSettings(VaadinService vaadinService) { setViewPortSettings(new ViewPortSettings()); @@ -185,11 +206,175 @@ public void modifyBootstrapPage(BootstrapPageResponse response) { offline == null || offline.value()); getApplicationCacheSettings().modifyBootstrapPage(response); } + + if (supportsGooglePWA()) { + + String contextPath = response.getRequest().getContextPath(); + + Document document = response.getDocument(); + // manifest.json + // + Element element = document.createElement("link"); + element.attr("rel", "manifest"); + element.attr("href", contextPath + "/manifest.json"); + document.getElementsByTag("head").get(0).appendChild(element); + // This meta tag is for some weird reason needed for 100% google PWA ;-) + element = document.createElement("meta"); + element.attr("name", "theme-color"); + element.attr("content", getWebAppSettings().getThemeColor()); + document.getElementsByTag("head").get(0).appendChild(element); + + if (stronglyCachedResources == null) { + // TODO make this somehow more stable and cleaner + String text = document.body().toString().replaceAll("\n", ""); + + Pattern p = Pattern.compile(".*widgetset\": *\"([^\"]+)\"", Pattern.DOTALL); + Matcher matcher = p.matcher(text); + boolean find = matcher.find(); + String wsname = matcher.group(1); + + InputStream resourceAsStream = getClass().getResourceAsStream("/VAADIN/widgetsets/" + wsname + "/" + "safari.manifest"); + + // Might be null in case of for example fallback UI + if (resourceAsStream != null) { + List resources = new ArrayList<>(); + + try { + final URI base = new URI(contextPath + "/VAADIN/widgetsets/" + wsname + "/"); + // the safari permutation is used by most mobile web apps + // Read the cached files from the GWT generated manifest file for service workers as well + List readLines = IOUtils.readLines(resourceAsStream); + boolean cachedFilesStarted = false; + for (String readLine : readLines) { + if (readLine.startsWith("CACHE:")) { + cachedFilesStarted = true; + resources.add(contextPath + "/"); + continue; + } + if (readLine.startsWith("NETWORK:")) { + cachedFilesStarted = false; + continue; + } + if (cachedFilesStarted) { + readLine = readLine.trim(); + if (!readLine.isEmpty()) { + URI resolved = base.resolve(readLine); + resources.add(resolved.toString()); + } + } + } + } catch (IOException ex) { + Logger.getLogger(TouchKitSettings.class.getName()).log(Level.SEVERE, null, ex); + } catch (URISyntaxException ex) { + Logger.getLogger(TouchKitSettings.class.getName()).log(Level.SEVERE, null, ex); + } + this.stronglyCachedResources = resources; + } + + } + if (stronglyCachedResources != null && !stronglyCachedResources.isEmpty()) { + Element serviceworkerregistration = document.createElement("script"); + serviceworkerregistration.attr("type", "text/javascript"); + serviceworkerregistration.appendText("if ('serviceWorker' in navigator) {\n" + + " navigator.serviceWorker.register('"+contextPath+"/service-worker.js', { scope: '"+contextPath+"/' }).then(function(reg) {\n" + + "\n" + + " if(reg.installing) {\n" + + " console.log('Service worker installing');\n" + + " } else if(reg.waiting) {\n" + + " console.log('Service worker installed');\n" + + " } else if(reg.active) {\n" + + " console.log('Service worker active');\n" + + " }\n" + + "\n" + + " }).catch(function(error) {\n" + + " console.log('Registration failed with ' + error);\n" + + " });\n" + + "}"); + document.body().appendChild(serviceworkerregistration); + } + + } } @Override public void sessionInit(SessionInitEvent event) throws ServiceException { event.getSession().addBootstrapListener(this); + event.getSession().addRequestHandler(new RequestHandler() { + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { + final String pathInfo = request.getPathInfo(); + String contextPath = request.getContextPath(); + if(pathInfo != null){ + if (pathInfo.endsWith("manifest.json")) { + // TODO write manifest.json using Jackson or similar + PrintWriter writer = response.getWriter(); + writer.append("{\n" + + " \"short_name\": \"" + getWebAppSettings().getApplicationShortName() + "\",\n" + + " \"name\": \"" + getWebAppSettings().getApplicationName() + "\",\n" + + " \"display\": \"" + getWebAppSettings().getDisplay() + "\",\n" + + " \"start_url\": \"" + contextPath + getWebAppSettings().getStartUrl() + "\",\n" + + " \"background_color\": \"" + getWebAppSettings().getBackgroundColor() + "\",\n" + + " \"theme_color\": \"" + getWebAppSettings().getThemeColor() + "\",\n" + + " \"icons\": [\n"); + + final ApplicationIcon[] icons = getApplicationIcons().getApplicationIcons(); + for (int i = 0; i < icons.length; i++) { + ApplicationIcon icon = icons[i]; + if (i > 0) { + writer.println(","); + } + writer.print(" {\n"); + writer.print("\"src\": \"" + icon.getHref() + "\","); + writer.print("\"sizes\": \"" + icon.getSizes() + "\""); + writer.print(" }\n"); + } + writer.print(" ]\n"); + writer.append("}"); + + return true; + } else if (pathInfo.endsWith("service-worker.js")) { + response.setContentType("text/javascript"); + response.setCacheTime(-1); + PrintWriter writer = response.getWriter(); + writer.write("self.addEventListener('install', e => {\n" + + " e.waitUntil(\n" + + " caches.open(\"tkcache\").then(c => {\n" + + " return c.addAll([\n"); + + // TODO consider using https://github.com/GoogleChromeLabs/sw-precache + if (stronglyCachedResources != null) { + for (String stronglyCachedResource : stronglyCachedResources) { + writer.write("'"); + writer.write(stronglyCachedResource); + writer.write("',\n"); + } + } else { + Logger.getLogger(TouchKitSettings.class.getName()).log(Level.SEVERE, "strongly cached resources could not be found"); + } + + writer.write("" + + " ]).then(() => self.skipWaiting());\n" + + " })\n" + + " );\n" + + "});\n" + + "\n" + + "self.addEventListener('fetch', e => {\n" + + " e.respondWith(\n" + + " caches.open(\"tkcache\").then(c => {\n" + + " return c.match(e.request).then(res => {\n" + + " return res || fetch(e.request)\n" + + " });\n" + + " })\n" + + " );\n" + + "});"); + return true; + } + } + return false; + } + + } + ); } /** @@ -202,8 +387,8 @@ public ApplicationCacheSettings getApplicationCacheSettings() { /** * Sets the {@link ApplicationCacheSettings} instance to use. * - * @param applicationCacheSettings - * the {@link ApplicationCacheSettings} instance to use. + * @param applicationCacheSettings the {@link ApplicationCacheSettings} + * instance to use. */ public void setApplicationCacheSettings( ApplicationCacheSettings applicationCacheSettings) { @@ -213,8 +398,7 @@ public void setApplicationCacheSettings( /** * Sets the {@link ApplicationIcons} instance to use. * - * @param applicationIcons - * the {@link ApplicationIcons} instance to use. + * @param applicationIcons the {@link ApplicationIcons} instance to use. */ public void setApplicationIcons(ApplicationIcons applicationIcons) { this.applicationIcons = applicationIcons; @@ -223,8 +407,7 @@ public void setApplicationIcons(ApplicationIcons applicationIcons) { /** * Sets the {@link ViewPortSettings} instance to use by default. * - * @param viewPortSettings - * the {@link ViewPortSettings} instance to use. + * @param viewPortSettings the {@link ViewPortSettings} instance to use. * @see #addViewPortSettings(SettingSelector) */ public void setViewPortSettings(ViewPortSettings viewPortSettings) { @@ -245,8 +428,7 @@ public void addViewPortSettings( /** * Sets the {@link WebAppSettings} instance to use. * - * @param iosWebAppSettings - * the {@link WebAppSettings} instance to use. + * @param iosWebAppSettings the {@link WebAppSettings} instance to use. */ public void setWebAppSettings(WebAppSettings iosWebAppSettings) { webAppSettings = iosWebAppSettings; @@ -261,4 +443,22 @@ public SystemMessages getSystemMessages( customizedSystemMessages.setSessionExpiredNotificationEnabled(false); return customizedSystemMessages; } + + /** + * @return true if Google style service worker + json manifest style PWA + * should be used instead of original iOS style home screen web app thingies. + */ + public static boolean supportsGooglePWA() { + try { + VaadinRequest currentRequest = VaadinServletService.getCurrentRequest(); + String useragentheader = currentRequest.getHeader("User-Agent").toLowerCase(); + // Simply expect all chromes to support + if (useragentheader.contains("chrome")) { + return true; + } + } catch (Exception e) { + System.err.println("Detecting sw support failed!"); + } + return false; + } } diff --git a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/WebAppSettings.java b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/WebAppSettings.java index aa828a7a..18157073 100644 --- a/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/WebAppSettings.java +++ b/vaadin-touchkit-agpl/src/main/java/com/vaadin/addon/touchkit/settings/WebAppSettings.java @@ -23,6 +23,44 @@ public class WebAppSettings implements BootstrapListener { private boolean webAppCapable = true; private String statusBarStyle = "black"; private String startupImage; + private String applicationName; + private String applicationShortName; + private String display = "fullscreen"; + private String start_url = "/"; + private String background_color = "white"; + private String theme_color = "gray"; + + public void setThemeColor(String theme_color) { + this.theme_color = theme_color; + } + + public String getThemeColor() { + return theme_color; + } + + public String getBackgroundColor() { + return background_color; + } + + public void setBackgroundColor(String background_color) { + this.background_color = background_color; + } + + public String getStartUrl() { + return start_url; + } + + public void setStartUrl(String start_url) { + this.start_url = start_url; + } + + public String getDisplay() { + return display; + } + + public void setDisplay(String display) { + this.display = display; + } /** * Sets a header that tells the client whether the application is designed @@ -92,6 +130,22 @@ public String getStartupImage() { return startupImage; } + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + public String getApplicationShortName() { + return applicationShortName; + } + + public void setApplicationShortName(String applicationShortName) { + this.applicationShortName = applicationShortName; + } + @Override public void modifyBootstrapFragment(BootstrapFragmentResponse response) { // NOP @@ -125,6 +179,14 @@ public void modifyBootstrapPage(BootstrapPageResponse response) { element.attr("href", getStartupImage()); head.appendChild(element); } + + if(getApplicationShortName() != null) { + element = document.createElement("meta"); + element.attr("name", "apple-mobile-web-app-title"); + element.attr("content", getApplicationShortName()); + head.appendChild(element); + // + } /* * Ensure window has "stable name", in case PreserveOnRefresh is used. diff --git a/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestServer.java b/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestServer.java index 54dbbb68..8de26ffa 100644 --- a/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestServer.java +++ b/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestServer.java @@ -32,7 +32,7 @@ public static void main(String[] args) throws Exception { WebAppContext context = new WebAppContext(); - ServletHolder servletHolder = new ServletHolder(TouchKitServlet.class); + ServletHolder servletHolder = new ServletHolder(TestTouchKitServlet.class); servletHolder.setInitParameter("UI", TouchkitTestUI.class.getName()); servletHolder.setInitParameter("widgetset", "com.vaadin.addon.touchkit.gwt.TouchKitWidgetSet"); diff --git a/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestTouchKitServlet.java b/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestTouchKitServlet.java new file mode 100644 index 00000000..466b0eba --- /dev/null +++ b/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TestTouchKitServlet.java @@ -0,0 +1,29 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.vaadin.addon.touchkit; + +import com.vaadin.addon.touchkit.server.TouchKitServlet; +import com.vaadin.addon.touchkit.settings.TouchKitSettings; + +import javax.servlet.ServletException; + +/** + * + * @author mstahv + */ +public class TestTouchKitServlet extends TouchKitServlet { + + @Override + protected void servletInitialized() throws ServletException { + super.servletInitialized(); + TouchKitSettings touchKitSettings = getTouchKitSettings(); + + touchKitSettings.getWebAppSettings().setApplicationName("TouchKit test app"); + touchKitSettings.getWebAppSettings().setApplicationShortName("TouchKitTest"); + touchKitSettings.getApplicationIcons().addApplicationIcon(128, 128, "image.png", true); + + } +} diff --git a/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TouchkitTestUI.java b/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TouchkitTestUI.java index 72711403..8909a499 100644 --- a/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TouchkitTestUI.java +++ b/vaadin-touchkit-agpl/src/test/java/com/vaadin/addon/touchkit/TouchkitTestUI.java @@ -43,6 +43,9 @@ protected void init(VaadinRequest request) { className = getClass().getPackage().getName() + ".itest." + requestPathInfo.substring(1); } + if(className.contains("/")) { + className = className.substring(0, className.indexOf("/")); + } Class forName = Class.forName(className); if (forName != null) { AbstractComponentContainer newInstance = (AbstractComponentContainer) forName diff --git a/vaadin-touchkit-cval/pom.xml b/vaadin-touchkit-cval/pom.xml index 1bf1e0b3..ecdbdf7a 100644 --- a/vaadin-touchkit-cval/pom.xml +++ b/vaadin-touchkit-cval/pom.xml @@ -136,8 +136,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + 1.8 + 1.8 false @@ -202,6 +202,7 @@ org.apache.maven.plugins maven-javadoc-plugin + 2.10.4 false diff --git a/vaadin-touchkit-integration-tests/pom.xml b/vaadin-touchkit-integration-tests/pom.xml index 24072f78..c098b238 100644 --- a/vaadin-touchkit-integration-tests/pom.xml +++ b/vaadin-touchkit-integration-tests/pom.xml @@ -133,8 +133,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + 1.8 + 1.8 UTF-8 diff --git a/vaadin-touchkit-offline-tests/pom.xml b/vaadin-touchkit-offline-tests/pom.xml index 5fa92f47..6b46b3ce 100644 --- a/vaadin-touchkit-offline-tests/pom.xml +++ b/vaadin-touchkit-offline-tests/pom.xml @@ -119,8 +119,8 @@ maven-compiler-plugin 2.3.2 - 1.5 - 1.5 + 1.8 + 1.8 UTF-8