Skip to content

Commit ee4e84b

Browse files
authored
feat: backport of PWA icons generation at build time (#21279)
Backport #20516 to Flow 2.11 Fixes #21254
1 parent d466470 commit ee4e84b

File tree

15 files changed

+971
-45
lines changed

15 files changed

+971
-45
lines changed

flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ private void runNodeUpdater()
197197
// @formatter:off
198198
new NodeTasks.Builder(getClassFinder(project),
199199
npmFolder, generatedFolder, frontendDirectory)
200+
.withWebpack(webpackOutputDirectory, null,
201+
null)
200202
.runNpmInstall(runNpmInstall)
201203
.enablePackagesUpdate(true)
202204
.useByteCodeScanner(optimizeBundle)

flow-server/src/main/java/com/vaadin/flow/server/Constants.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,13 +377,29 @@ public final class Constants implements Serializable {
377377
*/
378378
public static final String VAADIN_MAPPING = "VAADIN/";
379379

380+
/**
381+
* The static resources root folder.
382+
*/
383+
public static final String VAADIN_WEBAPP = "webapp/";
384+
385+
/**
386+
* The generated PWA icons folder.
387+
*/
388+
public static final String VAADIN_PWA_ICONS = "pwa-icons/";
389+
380390
/**
381391
* The path to meta-inf/VAADIN/ where static resources are put on the
382392
* servlet.
383393
*/
384394
public static final String VAADIN_SERVLET_RESOURCES = META_INF
385395
+ VAADIN_MAPPING;
386396

397+
/**
398+
* The path to webapp/ public resources root.
399+
*/
400+
public static final String VAADIN_WEBAPP_RESOURCES = VAADIN_SERVLET_RESOURCES
401+
+ VAADIN_WEBAPP;
402+
387403
/**
388404
* The static build resources folder.
389405
*/

flow-server/src/main/java/com/vaadin/flow/server/PwaConfiguration.java

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
*/
99
package com.vaadin.flow.server;
1010

11+
import javax.servlet.ServletContext;
12+
1113
import java.io.Serializable;
1214
import java.util.Arrays;
1315
import java.util.Collections;
1416
import java.util.List;
1517

16-
import javax.servlet.ServletContext;
17-
1818
/**
1919
* Holds the configuration from the {@link PWA} annotation.
2020
*
@@ -48,40 +48,83 @@ public class PwaConfiguration implements Serializable {
4848
private final List<String> offlineResources;
4949
private final boolean enableInstallPrompt;
5050

51+
/**
52+
* Creates the configuration using default PWA parameters.
53+
*/
54+
public PwaConfiguration() {
55+
this(false, "/", DEFAULT_NAME, "Flow PWA", "", DEFAULT_BACKGROUND_COLOR,
56+
DEFAULT_THEME_COLOR, DEFAULT_ICON, DEFAULT_PATH,
57+
DEFAULT_OFFLINE_PATH, DEFAULT_DISPLAY, "", new String[0],
58+
false);
59+
}
60+
5161
protected PwaConfiguration(PWA pwa, ServletContext servletContext) {
52-
rootUrl = hasContextPath(servletContext)
62+
this(true, rootUrlPath(servletContext), pwa.name(),
63+
pwa.shortName().substring(0,
64+
Math.min(pwa.shortName().length(), 12)),
65+
pwa.description(), pwa.backgroundColor(), pwa.themeColor(),
66+
checkPath(pwa.iconPath()), checkPath(pwa.manifestPath()),
67+
checkPath(pwa.offlinePath()), pwa.display(),
68+
pwa.startPath().replaceAll("^/+", ""), pwa.offlineResources(),
69+
pwa.enableInstallPrompt());
70+
}
71+
72+
/**
73+
* Constructs a configuration from individual values.
74+
*
75+
* @param enabled
76+
* is PWA enabled
77+
* @param name
78+
* the application name
79+
* @param shortName
80+
* the application short name
81+
* @param description
82+
* the description of the application
83+
* @param backgroundColor
84+
* the background color
85+
* @param themeColor
86+
* the theme color
87+
* @param iconPath
88+
* the icon file path
89+
* @param manifestPath
90+
* the `manifest.webmanifest` file path
91+
* @param offlinePath
92+
* the static offline HTML file path
93+
* @param display
94+
* the display mode
95+
* @param startPath
96+
* the start path
97+
* @param offlineResources
98+
* the list of files to add for pre-caching
99+
* @param enableInstallPrompt
100+
* is install prompt resources injection enabled.
101+
*/
102+
public PwaConfiguration(boolean enabled, String rootUrl, String name,
103+
String shortName, String description, String backgroundColor,
104+
String themeColor, String iconPath, String manifestPath,
105+
String offlinePath, String display, String startPath,
106+
String[] offlineResources, boolean enableInstallPrompt) {
107+
this.rootUrl = rootUrl;
108+
this.appName = name;
109+
this.shortName = shortName.substring(0,
110+
Math.min(shortName.length(), 12));
111+
this.description = description;
112+
this.backgroundColor = backgroundColor;
113+
this.themeColor = themeColor;
114+
this.iconPath = iconPath;
115+
this.manifestPath = manifestPath;
116+
this.offlinePath = offlinePath;
117+
this.display = display;
118+
this.startPath = startPath;
119+
this.enabled = enabled;
120+
this.offlineResources = Arrays.asList(offlineResources);
121+
this.enableInstallPrompt = enableInstallPrompt;
122+
}
123+
124+
private static String rootUrlPath(ServletContext servletContext) {
125+
return hasContextPath(servletContext)
53126
? servletContext.getContextPath() + "/"
54127
: "/";
55-
if (pwa != null) {
56-
appName = pwa.name();
57-
shortName = pwa.shortName().substring(0,
58-
Math.min(pwa.shortName().length(), 12));
59-
description = pwa.description();
60-
backgroundColor = pwa.backgroundColor();
61-
themeColor = pwa.themeColor();
62-
iconPath = checkPath(pwa.iconPath());
63-
manifestPath = checkPath(pwa.manifestPath());
64-
offlinePath = checkPath(pwa.offlinePath());
65-
display = pwa.display();
66-
startPath = pwa.startPath().replaceAll("^/+", "");
67-
enabled = true;
68-
offlineResources = Arrays.asList(pwa.offlineResources());
69-
enableInstallPrompt = pwa.enableInstallPrompt();
70-
} else {
71-
appName = DEFAULT_NAME;
72-
shortName = "Flow PWA";
73-
description = "";
74-
backgroundColor = DEFAULT_BACKGROUND_COLOR;
75-
themeColor = DEFAULT_THEME_COLOR;
76-
iconPath = DEFAULT_ICON;
77-
manifestPath = DEFAULT_PATH;
78-
offlinePath = DEFAULT_OFFLINE_PATH;
79-
display = DEFAULT_DISPLAY;
80-
startPath = "";
81-
enabled = false;
82-
offlineResources = Collections.emptyList();
83-
enableInstallPrompt = false;
84-
}
85128
}
86129

87130
private static boolean hasContextPath(ServletContext servletContext) {

flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
import java.awt.image.BufferedImage;
1616
import java.io.ByteArrayOutputStream;
1717
import java.io.IOException;
18+
import java.io.InputStream;
1819
import java.io.OutputStream;
1920
import java.io.Serializable;
2021
import java.io.UncheckedIOException;
2122
import java.util.Arrays;
2223
import java.util.HashMap;
2324
import java.util.Map;
2425

26+
import org.apache.commons.io.IOUtils;
2527
import org.jsoup.nodes.Element;
2628

2729
/**
@@ -41,6 +43,7 @@
4143
* @since 1.2
4244
*/
4345
public class PwaIcon implements Serializable {
46+
4447
/**
4548
* Place where icon belongs to (header or manifest.webmanifest).
4649
*/
@@ -92,6 +95,15 @@ public enum Domain {
9295
setRelativeName();
9396
}
9497

98+
protected PwaIcon(PwaIcon icon) {
99+
this.width = icon.width;
100+
this.height = icon.height;
101+
this.baseName = icon.baseName;
102+
this.domain = icon.domain;
103+
this.shouldBeCached = icon.shouldBeCached;
104+
this.attributes.putAll(icon.attributes);
105+
}
106+
95107
/**
96108
* Gets an {@link Element} presentation of the icon.
97109
*
@@ -229,6 +241,25 @@ public void setImage(BufferedImage image) {
229241
}
230242
}
231243

244+
void setImage(InputStream image) throws IOException {
245+
if (image != null) {
246+
data = IOUtils.toByteArray(image);
247+
fileHash = Arrays.hashCode(data);
248+
setRelativeName();
249+
}
250+
}
251+
252+
/**
253+
* Gets if the icon can be written on a stream or not.
254+
*
255+
* @return {@literal true} if the icon can be written, otherwise
256+
* {@literal false}.
257+
* @see #write(OutputStream)
258+
*/
259+
boolean isAvailable() {
260+
return data != null || registry.getBaseImage() != null;
261+
}
262+
232263
/**
233264
* Writes the icon image to output stream.
234265
*

flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
import java.nio.charset.StandardCharsets;
2525
import java.util.ArrayList;
2626
import java.util.List;
27+
import java.util.Optional;
2728
import java.util.stream.Collectors;
2829

2930
import org.slf4j.Logger;
3031
import org.slf4j.LoggerFactory;
3132

33+
import com.vaadin.flow.di.Lookup;
34+
import com.vaadin.flow.di.ResourceProvider;
3235
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;
3336

3437
import elemental.json.Json;
@@ -69,6 +72,7 @@ public class PwaRegistry implements Serializable {
6972
private long offlineHash;
7073
private List<PwaIcon> icons = new ArrayList<>();
7174
private final PwaConfiguration pwaConfiguration;
75+
private URL baseImageUrl;
7276
private BufferedImage baseImage;
7377

7478
private PwaRegistry(PWA pwa, ServletContext servletContext)
@@ -80,13 +84,28 @@ private PwaRegistry(PWA pwa, ServletContext servletContext)
8084

8185
// set basic configuration by given PWA annotation
8286
// fall back to defaults if unavailable
83-
pwaConfiguration = new PwaConfiguration(pwa, servletContext);
87+
pwaConfiguration = pwa == null ? new PwaConfiguration()
88+
: new PwaConfiguration(pwa, servletContext);
8489

8590
// Build pwa elements only if they are enabled
8691
initializeResources(servletContext);
8792
}
8893

94+
// Lazy load base image to prevent using AWT api unless icon
95+
// generation is required at runtime.
96+
// baseImageUrl is computed during registry initialization and used on to
97+
// load the image.
8998
BufferedImage getBaseImage() {
99+
if (baseImage == null && baseImageUrl != null) {
100+
try {
101+
baseImage = getBaseImage(baseImageUrl);
102+
} catch (IOException ex) {
103+
getLogger().error("Image is not found or can't be loaded: {}",
104+
baseImageUrl);
105+
} finally {
106+
baseImageUrl = null;
107+
}
108+
}
90109
return baseImage;
91110
}
92111

@@ -96,21 +115,21 @@ private void initializeResources(ServletContext servletContext)
96115
return;
97116
}
98117
long start = System.currentTimeMillis();
118+
119+
// Load base logo from servlet context if available
120+
// fall back to local image if unavailable
99121
URL logo = getResourceUrl(servletContext,
100122
pwaConfiguration.relIconPath());
123+
baseImageUrl = logo != null ? logo
124+
: BootstrapHandler.class.getResource("default-logo.png");
101125

102126
URL offlinePage = getResourceUrl(servletContext,
103127
pwaConfiguration.relOfflinePath());
104128
// Load base logo from servlet context if available
105129
// fall back to local image if unavailable
106130
baseImage = getBaseImage(logo);
107131

108-
if (baseImage == null) {
109-
getLogger().error("Image is not found or can't be loaded: " + logo);
110-
} else {
111-
// initialize icons
112-
icons = initializeIcons();
113-
}
132+
icons = initializeIcons(servletContext);
114133

115134
// Load offline page as string, from servlet context if
116135
// available, fall back to default page
@@ -146,14 +165,44 @@ private URL getResourceUrl(ServletContext context, String path)
146165
return resourceUrl;
147166
}
148167

149-
private List<PwaIcon> initializeIcons() {
168+
private List<PwaIcon> initializeIcons(ServletContext servletContext) {
169+
VaadinServletContext vaadinContext = new VaadinServletContext(
170+
servletContext);
171+
Optional<ResourceProvider> optionalResourceProvider = Optional
172+
.ofNullable(vaadinContext.getAttribute(Lookup.class))
173+
.map(lookup -> lookup.lookup(ResourceProvider.class));
150174
for (PwaIcon icon : getIconTemplates(pwaConfiguration.getIconPath())) {
151175
icon.setRegistry(this);
152-
icons.add(icon);
176+
// Try to find a pre-generated image
177+
String iconPath = Constants.VAADIN_WEBAPP_RESOURCES
178+
+ Constants.VAADIN_PWA_ICONS
179+
+ icon.getRelHref().substring(1);
180+
optionalResourceProvider.ifPresent(provider -> tryLoadGeneratedIcon(
181+
provider.getApplicationResource(vaadinContext, iconPath),
182+
icon, iconPath));
183+
if (icon.isAvailable()) {
184+
icons.add(icon);
185+
}
153186
}
154187
return icons;
155188
}
156189

190+
private static void tryLoadGeneratedIcon(URL iconResource, PwaIcon icon,
191+
String iconPath) {
192+
if (iconResource != null) {
193+
try (InputStream data = iconResource.openStream()) {
194+
icon.setImage(data);
195+
getLogger().trace("Loading generated PWA image from {}",
196+
iconPath);
197+
} catch (IOException ex) {
198+
// Ignore, icon will be generated at runtime
199+
getLogger().debug(
200+
"Cannot load generated PWA image from {}. Icon will be regenerated at runtime.",
201+
iconPath, ex);
202+
}
203+
}
204+
}
205+
157206
/**
158207
* Creates manifest.webmanifest json object.
159208
*
@@ -444,7 +493,14 @@ public PwaConfiguration getPwaConfiguration() {
444493
return pwaConfiguration;
445494
}
446495

447-
private static List<PwaIcon> getIconTemplates(String baseName) {
496+
/**
497+
* Gets all PWA icon variants for the give base icon.
498+
*
499+
* @param baseName
500+
* path of the base icon.
501+
* @return list of PWA icons variants.
502+
*/
503+
public static List<PwaIcon> getIconTemplates(String baseName) {
448504
List<PwaIcon> icons = new ArrayList<>();
449505
// Basic manifest icons for android support
450506
icons.add(

0 commit comments

Comments
 (0)