diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml
index 82962a3fa52..408bd4d91b5 100644
--- a/.github/workflows/validation.yml
+++ b/.github/workflows/validation.yml
@@ -1,7 +1,7 @@
name: Flow Validation
on:
push:
- branches: [main, '24.5', '24.4', '24.3', '23.5', '9.1']
+ branches: [main, '24.6', '24.5', '24.4', '23.5']
workflow_dispatch:
pull_request_target:
types: [opened, synchronize, reopened, edited]
diff --git a/README.md b/README.md
index 6a111427861..0e80d833d64 100644
--- a/README.md
+++ b/README.md
@@ -12,12 +12,12 @@ Ask questions about Vaadin Flow in our [forum](https://vaadin.com/forum/c/flow/8
Since [Vaadin platform 23.0](https://github.com/vaadin/platform), Flow major and minor versions are aligned with platform versions:
-| Branch | [Platform Version](https://github.com/vaadin/platform/releases) | [Flow Version](https://github.com/vaadin/flow/releases) |
-|--------|-----------------------------------------------------------------|---------------------------------------------------------|
-| 1.0 | 10 (LTS) | 1.0 |
-| 2.10 | 14.11 (LATEST free with Java 8+ support and Servlet) | 2.10 |
-| 2.11 | 14.12 (LATEST commercial with Java 8+ support and Servlet 3) | 2.11 |
-| 23.4 | 23.4 (LATEST free with Java 11+ support and Servlet 3) | 23.4 |
-| 23.5 | 23.5 (LATEST commercial with Java 11+ support and Servlet 3) | 23.5 |
-| 24.5 | 24.5 (LATEST release, Java 17+, Jakarta EE 10, Spring-boot 3) | 24.5 |
-| main | 24.6 (Vaadin 24.6 preparations) | 24.6 |
+| Branch | [Platform Version](https://github.com/vaadin/platform/releases) | [Flow Version](https://github.com/vaadin/flow/releases) |
+|--------|-------------------------------------------------------------------------|---------------------------------------------------------|
+| 1.0 | 10 (LTS) | 1.0 |
+| 2.11 | 14.12 (LATEST commercial with Java 8+ support and Servlet 3) | 2.11 |
+| 23.5 | 23.5 (LATEST commercial with Java 11+ support and Servlet 3) | 23.5 |
+| 24.4 | 24.4 (maintained minor release, Java 17+, Jakarta EE 10, Spring-boot 3) | 24.4 |
+| 24.5 | 24.5 (LATEST release, Java 17+, Jakarta EE 10, Spring-boot 3) | 24.5 |
+| 24.6 | 24.6 (Vaadin 24.6 pre-release) | 24.6 |
+| main | 24.7 (Vaadin 24.7 preparations) | 24.7 |
diff --git a/flow-bom/pom.xml b/flow-bom/pom.xml
index d172d70542b..ec65a4025a2 100644
--- a/flow-bom/pom.xml
+++ b/flow-bom/pom.xml
@@ -4,7 +4,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-bompom
diff --git a/flow-client/pom.xml b/flow-client/pom.xml
index 92c79dea5a1..858f9ec64e9 100644
--- a/flow-client/pom.xml
+++ b/flow-client/pom.xml
@@ -6,7 +6,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-clientFlow Client
diff --git a/flow-client/src/main/frontend/Flow.ts b/flow-client/src/main/frontend/Flow.ts
index d9f7515657d..79d82c9dd2a 100644
--- a/flow-client/src/main/frontend/Flow.ts
+++ b/flow-client/src/main/frontend/Flow.ts
@@ -351,7 +351,7 @@ export class Flow {
script.onload = () => resolve();
script.onerror = reject;
script.src = url;
- const nonce = $wnd.Vaadin.Flow.nonce;
+ const { nonce } = $wnd.Vaadin.Flow;
if (nonce !== undefined) {
script.setAttribute('nonce', nonce);
}
@@ -376,7 +376,7 @@ export class Flow {
const scriptAppId = document.createElement('script');
scriptAppId.type = 'module';
scriptAppId.setAttribute('data-app-id', appIdWithoutHashCode);
- const nonce = $wnd.Vaadin.Flow.nonce;
+ const { nonce } = $wnd.Vaadin.Flow;
if (nonce !== undefined) {
scriptAppId.setAttribute('nonce', nonce);
}
diff --git a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java
index 9a563ff153b..6182fde8123 100644
--- a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java
+++ b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java
@@ -209,7 +209,7 @@ public void send(final JsonObject payload) {
pushPendingMessage = payload;
push.push(payload);
} else {
- Console.log("send XHR");
+ Console.debug("send XHR");
registry.getXhrConnection().send(payload);
}
}
diff --git a/flow-data/pom.xml b/flow-data/pom.xml
index 9e2cb8ffe4a..d540c992ac0 100644
--- a/flow-data/pom.xml
+++ b/flow-data/pom.xml
@@ -6,7 +6,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-dataFlow Data
diff --git a/flow-dnd/pom.xml b/flow-dnd/pom.xml
index 4fbb4629fc5..f88d53d8cc6 100644
--- a/flow-dnd/pom.xml
+++ b/flow-dnd/pom.xml
@@ -6,7 +6,7 @@
flow-projectcom.vaadin
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-dnd
diff --git a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java
index 7e799cef943..b3fa8e28227 100644
--- a/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java
+++ b/flow-dnd/src/main/java/com/vaadin/flow/component/dnd/DragSource.java
@@ -17,6 +17,8 @@
import java.util.Locale;
+import org.slf4j.LoggerFactory;
+
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.ComponentUtil;
@@ -25,6 +27,7 @@
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dnd.internal.DndUtil;
import com.vaadin.flow.dom.Element;
+import com.vaadin.flow.dom.Style;
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
import com.vaadin.flow.shared.Registration;
@@ -344,6 +347,10 @@ default void setDragImage(Component dragImage) {
* the y-offset of the drag image
*/
default void setDragImage(Component dragImage, int offsetX, int offsetY) {
+ if (dragImage != null && !dragImage.isVisible()) {
+ throw new IllegalStateException(
+ "Drag image element is not visible and will not show.\nMake element visible to use as drag image!");
+ }
if (getDragImage() != null && getDragImage() != dragImage) {
// Remove drag image from the virtual children list if it's there.
if (getDraggableElement().getNode()
@@ -362,14 +369,12 @@ default void setDragImage(Component dragImage, int offsetX, int offsetY) {
getDragSourceComponent().addAttachListener(event -> {
if (!dragImage.isAttached()
&& dragImage.getParent().isEmpty()) {
- getDraggableElement()
- .appendVirtualChild(dragImage.getElement());
+ appendDragElement(dragImage.getElement());
}
event.unregisterListener();
});
} else {
- getDraggableElement()
- .appendVirtualChild(dragImage.getElement());
+ appendDragElement(dragImage.getElement());
}
}
ComponentUtil.setData(getDragSourceComponent(),
@@ -380,6 +385,21 @@ default void setDragImage(Component dragImage, int offsetX, int offsetY) {
(dragImage == null ? 0 : offsetY), getDraggableElement());
}
+ private void appendDragElement(Element dragElement) {
+ if (dragElement.getTag().equals("img")) {
+ getDraggableElement().appendVirtualChild(dragElement);
+ } else {
+ LoggerFactory.getLogger(DragSource.class).debug(
+ "Attaching child to dom in position -100,-100. Consider adding the component manually to not get overlapping components on drag for element.");
+ getDraggableElement().appendChild(dragElement);
+ Style style = dragElement.getStyle();
+ style.set("position", "absolute");
+ style.set("top", "-100px");
+ style.set("left", "-100px");
+ style.set("display", "none");
+ }
+ }
+
/**
* Get server side drag image. This image is applied automatically in the
* next drag start event in the browser.
diff --git a/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js b/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js
index 853dec45483..5450db3083b 100644
--- a/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js
+++ b/flow-dnd/src/main/resources/META-INF/resources/frontend/dndConnector.js
@@ -93,6 +93,10 @@ window.Vaadin.Flow.dndConnector = {
event.currentTarget.classList.add('v-dragged');
}
if(event.currentTarget.__dragImage) {
+ if(event.currentTarget.__dragImage.style.display === "none") {
+ event.currentTarget.__dragImage.style.display = "block";
+ event.currentTarget.classList.add('shown');
+ }
event.dataTransfer.setDragImage(
event.currentTarget.__dragImage,
event.currentTarget.__dragImageOffsetX,
@@ -102,6 +106,10 @@ window.Vaadin.Flow.dndConnector = {
__dragendListener: function (event) {
event.currentTarget.classList.remove('v-dragged');
+ if(event.currentTarget.classList.contains('shown')) {
+ event.currentTarget.classList.remove('shown');
+ event.currentTarget.__dragImage.style.display = "none";
+ }
},
updateDragSource: function (element) {
diff --git a/flow-html-components-testbench/pom.xml b/flow-html-components-testbench/pom.xml
index 3ee8d24d11a..346e63a4b0b 100644
--- a/flow-html-components-testbench/pom.xml
+++ b/flow-html-components-testbench/pom.xml
@@ -5,7 +5,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-html-components-testbenchTestBench elements for Flow HTML Components
diff --git a/flow-html-components/pom.xml b/flow-html-components/pom.xml
index cadcfdba62f..5b42c27b5bd 100644
--- a/flow-html-components/pom.xml
+++ b/flow-html-components/pom.xml
@@ -6,7 +6,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-html-componentsFlow HTML Components
diff --git a/flow-jandex/pom.xml b/flow-jandex/pom.xml
index 9c7169266b8..ccb4bc2282b 100644
--- a/flow-jandex/pom.xml
+++ b/flow-jandex/pom.xml
@@ -22,7 +22,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-jandex
diff --git a/flow-lit-template/pom.xml b/flow-lit-template/pom.xml
index b376029ca87..dcae134c8fa 100644
--- a/flow-lit-template/pom.xml
+++ b/flow-lit-template/pom.xml
@@ -6,7 +6,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-lit-templateFlow Lit Templates Support
diff --git a/flow-plugins/flow-dev-bundle-plugin/pom.xml b/flow-plugins/flow-dev-bundle-plugin/pom.xml
index d2af60aeda4..3d14d33c3df 100644
--- a/flow-plugins/flow-dev-bundle-plugin/pom.xml
+++ b/flow-plugins/flow-dev-bundle-plugin/pom.xml
@@ -5,7 +5,7 @@
com.vaadinflow-plugins
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-dev-bundle-pluginmaven-plugin
diff --git a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java
index ede91310d0d..643bef5c7bd 100644
--- a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java
+++ b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java
@@ -17,27 +17,40 @@
import java.io.File;
import java.io.IOException;
+import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.dependency.JsModule;
@@ -53,7 +66,9 @@
import com.vaadin.flow.server.frontend.installer.NodeInstaller;
import com.vaadin.flow.server.frontend.installer.Platform;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
+import com.vaadin.flow.server.scanner.ReflectionsClassFinder;
import com.vaadin.flow.theme.Theme;
+import com.vaadin.flow.utils.FlowFileUtils;
import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
import static com.vaadin.flow.server.Constants.VAADIN_WEBAPP_RESOURCES;
@@ -131,6 +146,9 @@ public class BuildDevBundleMojo extends AbstractMojo
@Parameter(defaultValue = "${project}", readonly = true, required = true)
MavenProject project;
+ @Parameter(defaultValue = "${mojoExecution}")
+ MojoExecution mojoExecution;
+
/**
* The folder where `package.json` file is located. Default is project root
* dir.
@@ -175,8 +193,33 @@ public class BuildDevBundleMojo extends AbstractMojo
@Parameter(property = InitParameters.NPM_EXCLUDE_WEB_COMPONENTS, defaultValue = "false")
private boolean npmExcludeWebComponents;
+ static final String CLASSFINDER_FIELD_NAME = "classFinder";
+
+ private ClassFinder classFinder;
+
@Override
- public void execute() throws MojoFailureException {
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ PluginDescriptor pluginDescriptor = mojoExecution.getMojoDescriptor()
+ .getPluginDescriptor();
+ checkFlowCompatibility(pluginDescriptor);
+
+ Reflector reflector = getOrCreateReflector();
+ ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread()
+ .setContextClassLoader(reflector.getIsolatedClassLoader());
+ try {
+ org.apache.maven.plugin.Mojo task = reflector.createMojo(this);
+ findExecuteMethod(task.getClass()).invoke(task);
+ } catch (MojoExecutionException | MojoFailureException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new MojoFailureException(e.getMessage(), e);
+ } finally {
+ Thread.currentThread().setContextClassLoader(tccl);
+ }
+ }
+
+ public void executeInternal() throws MojoFailureException {
long start = System.nanoTime();
try {
@@ -243,7 +286,9 @@ public boolean compressBundle() {
* @param project
* a given MavenProject
* @return List of ClasspathElements
+ * @deprecated will be removed without replacement.
*/
+ @Deprecated(forRemoval = true)
public static List getClasspathElements(MavenProject project) {
try {
@@ -286,11 +331,13 @@ public File generatedTsFolder() {
@Override
public ClassFinder getClassFinder() {
-
- List classpathElements = getClasspathElements(project);
-
- return BuildFrontendUtil.getClassFinder(classpathElements);
-
+ if (classFinder == null) {
+ URLClassLoader classLoader = getOrCreateReflector()
+ .getIsolatedClassLoader();
+ classFinder = new ReflectionsClassFinder(classLoader,
+ classLoader.getURLs());
+ }
+ return classFinder;
}
@Override
@@ -483,4 +530,126 @@ public boolean checkRuntimeDependency(String groupId, String artifactId,
public boolean isNpmExcludeWebComponents() {
return npmExcludeWebComponents;
}
+
+ private static URLClassLoader createIsolatedClassLoader(
+ MavenProject project, MojoExecution mojoExecution) {
+ List urls = new ArrayList<>();
+ String outputDirectory = project.getBuild().getOutputDirectory();
+ if (outputDirectory != null) {
+ urls.add(FlowFileUtils.convertToUrl(new File(outputDirectory)));
+ }
+
+ Function keyMapper = artifact -> artifact.getGroupId()
+ + ":" + artifact.getArtifactId();
+
+ Map projectDependencies = new HashMap<>(project
+ .getArtifacts().stream()
+ .filter(artifact -> artifact.getFile() != null
+ && artifact.getArtifactHandler().isAddedToClasspath()
+ && (Artifact.SCOPE_COMPILE.equals(artifact.getScope())
+ || Artifact.SCOPE_RUNTIME
+ .equals(artifact.getScope())
+ || Artifact.SCOPE_SYSTEM
+ .equals(artifact.getScope())
+ || (Artifact.SCOPE_PROVIDED
+ .equals(artifact.getScope())
+ && artifact.getFile().getPath().matches(
+ INCLUDE_FROM_COMPILE_DEPS_REGEX))))
+ .collect(Collectors.toMap(keyMapper, Function.identity())));
+ if (mojoExecution != null) {
+ mojoExecution.getMojoDescriptor().getPluginDescriptor()
+ .getArtifacts().stream()
+ .filter(artifact -> !projectDependencies
+ .containsKey(keyMapper.apply(artifact)))
+ .forEach(artifact -> projectDependencies
+ .put(keyMapper.apply(artifact), artifact));
+ }
+
+ projectDependencies.values().stream()
+ .map(artifact -> FlowFileUtils.convertToUrl(artifact.getFile()))
+ .forEach(urls::add);
+ ClassLoader mavenApiClassLoader;
+ if (mojoExecution != null) {
+ ClassRealm pluginClassRealm = mojoExecution.getMojoDescriptor()
+ .getPluginDescriptor().getClassRealm();
+ try {
+ mavenApiClassLoader = pluginClassRealm.getWorld()
+ .getRealm("maven.api");
+ } catch (NoSuchRealmException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ mavenApiClassLoader = org.apache.maven.plugin.Mojo.class
+ .getClassLoader();
+ if (mavenApiClassLoader instanceof ClassRealm classRealm) {
+ try {
+ mavenApiClassLoader = classRealm.getWorld()
+ .getRealm("maven.api");
+ } catch (NoSuchRealmException e) {
+ // Should never happen. In case, ignore the error and use
+ // class loader from the Maven class
+ }
+ }
+ }
+ return new URLClassLoader(urls.toArray(URL[]::new),
+ mavenApiClassLoader);
+ }
+
+ private void checkFlowCompatibility(PluginDescriptor pluginDescriptor) {
+ Predicate isFlowServer = artifact -> "com.vaadin"
+ .equals(artifact.getGroupId())
+ && "flow-server".equals(artifact.getArtifactId());
+ String projectFlowVersion = project.getArtifacts().stream()
+ .filter(isFlowServer).map(Artifact::getVersion).findFirst()
+ .orElse(null);
+ String pluginFlowVersion = pluginDescriptor.getArtifacts().stream()
+ .filter(isFlowServer).map(Artifact::getVersion).findFirst()
+ .orElse(null);
+ if (!Objects.equals(projectFlowVersion, pluginFlowVersion)) {
+ getLog().warn(
+ "Vaadin Flow used in project does not match the version expected by the Vaadin plugin. "
+ + "Flow version for project is "
+ + projectFlowVersion
+ + ", Vaadin plugin is built for Flow version "
+ + pluginFlowVersion + ".");
+ }
+ }
+
+ private Reflector getOrCreateReflector() {
+ Map pluginContext = getPluginContext();
+ String pluginKey = mojoExecution.getPlugin().getKey();
+ String reflectorKey = Reflector.class.getName() + "-" + pluginKey + "-"
+ + mojoExecution.getLifecyclePhase();
+ if (pluginContext != null && pluginContext.containsKey(reflectorKey)) {
+ getLog().debug("Using cached Reflector for plugin " + pluginKey
+ + " and phase " + mojoExecution.getLifecyclePhase());
+ return Reflector.adapt(pluginContext.get(reflectorKey));
+ }
+ Reflector reflector = Reflector.of(project, mojoExecution);
+ if (pluginContext != null) {
+ pluginContext.put(reflectorKey, reflector);
+ getLog().debug("Cached Reflector for plugin " + pluginKey
+ + " and phase " + mojoExecution.getLifecyclePhase());
+ }
+ return reflector;
+ }
+
+ private Method findExecuteMethod(Class> taskClass)
+ throws NoSuchMethodException {
+
+ while (taskClass != null && taskClass != Object.class) {
+ try {
+ Method executeInternal = taskClass
+ .getDeclaredMethod("executeInternal");
+ executeInternal.setAccessible(true);
+ return executeInternal;
+ } catch (NoSuchMethodException e) {
+ // ignore
+ }
+ taskClass = taskClass.getSuperclass();
+ }
+ throw new NoSuchMethodException(
+ "Method executeInternal not found in " + getClass().getName());
+ }
+
}
diff --git a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java
new file mode 100644
index 00000000000..b130a235255
--- /dev/null
+++ b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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 com.vaadin.flow.plugin.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.Mojo;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+
+import com.vaadin.flow.internal.ReflectTools;
+import com.vaadin.flow.server.frontend.scanner.ClassFinder;
+import com.vaadin.flow.server.scanner.ReflectionsClassFinder;
+import com.vaadin.flow.utils.FlowFileUtils;
+
+/**
+ * Helper class to deal with classloading of Flow plugin mojos.
+ */
+public final class Reflector {
+
+ public static final String INCLUDE_FROM_COMPILE_DEPS_REGEX = ".*(/|\\\\)(portlet-api|javax\\.servlet-api)-.+jar$";
+ private static final Set DEPENDENCIES_GROUP_EXCLUSIONS = Set.of(
+ "org.apache.maven", "org.codehaus.plexus", "org.slf4j",
+ "org.eclipse.sisu");
+
+ private final URLClassLoader isolatedClassLoader;
+ private Object classFinder;
+
+ /**
+ * Creates a new reflector instance for the given classloader.
+ *
+ * @param isolatedClassLoader
+ * class loader to be used to create mojo instances.
+ */
+ public Reflector(URLClassLoader isolatedClassLoader) {
+ this.isolatedClassLoader = isolatedClassLoader;
+ }
+
+ private Reflector(URLClassLoader isolatedClassLoader, Object classFinder) {
+ this.isolatedClassLoader = isolatedClassLoader;
+ this.classFinder = classFinder;
+ }
+
+ /**
+ * Gets a {@link Reflector} instance usable with the caller class loader.
+ *
+ *
+ * Reflector instances are cached in Maven plugin context, but instances
+ * might be associated to the plugin class loader, thus not working with
+ * classes loaded by the isolated class loader. This method returns the
+ * input object if it is compatible with the class loader, otherwise it
+ * creates a copy referencing the same isolated class loader and
+ * {@link ClassFinder}.
+ *
+ * @param reflector
+ * the {@link Reflector} instance.
+ * @return a {@link Reflector} instance compatible with the current class
+ * loader.
+ * @throws IllegalArgumentException
+ * if the input object is not a {@link Reflector} instance or if
+ * it is not possible to make a copy for it due to class
+ * definition incompatibilities.
+ */
+ static Reflector adapt(Object reflector) {
+ if (reflector instanceof Reflector sameClassLoader) {
+ return sameClassLoader;
+ } else if (Reflector.class.getName()
+ .equals(reflector.getClass().getName())) {
+ Class> reflectorClass = reflector.getClass();
+ try {
+ URLClassLoader classLoader = (URLClassLoader) ReflectTools
+ .getJavaFieldValue(reflector,
+ findField(reflectorClass,
+ "isolatedClassLoader"),
+ URLClassLoader.class);
+ Object classFinder = ReflectTools.getJavaFieldValue(reflector,
+ findField(reflectorClass, "classFinder"));
+ return new Reflector(classLoader, classFinder);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Object of type " + reflector.getClass().getName()
+ + " is not a compatible Reflector",
+ e);
+ }
+ }
+ throw new IllegalArgumentException(
+ "Object of type " + reflector.getClass().getName()
+ + " is not a compatible Reflector");
+ }
+
+ /**
+ * Gets the isolated class loader.
+ *
+ * @return the isolated class loader.
+ */
+ public URLClassLoader getIsolatedClassLoader() {
+ return isolatedClassLoader;
+ }
+
+ /**
+ * Loads the class with the given name from the isolated classloader.
+ *
+ * @param className
+ * the name of the class to load.
+ * @return the class object.
+ * @throws ClassNotFoundException
+ * if the class was not found.
+ */
+ public Class> loadClass(String className) throws ClassNotFoundException {
+ return isolatedClassLoader.loadClass(className);
+ }
+
+ /**
+ * Get a resource from the classpath of the isolated class loader.
+ *
+ * @param name
+ * class literal
+ * @return the resource
+ */
+ public URL getResource(String name) {
+ return isolatedClassLoader.getResource(name);
+ }
+
+ /**
+ * Creates a copy of the given Flow mojo, loading classes the isolated
+ * classloader.
+ *
+ *
+ * Loads the given mojo class from the isolated class loader and then
+ * creates a new instance for it and fills all field copying values from the
+ * original mojo. The input mojo must have a public no-args constructor.
+ * Mojo fields must reference types that can be safely loaded be the
+ * isolated class loader, such as JDK or Maven core API. It also creates and
+ * injects a {@link ClassFinder}, based on the isolated class loader.
+ *
+ * @param sourceMojo
+ * The mojo for which to create the instance from the isolated
+ * class loader.
+ * @return an instance of the mojo loaded from the isolated class loader.
+ * @throws Exception
+ * if the mojo instance cannot be created.
+ */
+ public Mojo createMojo(BuildDevBundleMojo sourceMojo) throws Exception {
+ Class> targetMojoClass = loadClass(sourceMojo.getClass().getName());
+ Object targetMojo = targetMojoClass.getConstructor().newInstance();
+ copyFields(sourceMojo, targetMojo);
+ Field classFinderField = findField(targetMojoClass,
+ BuildDevBundleMojo.CLASSFINDER_FIELD_NAME);
+ ReflectTools.setJavaFieldValue(targetMojo, classFinderField,
+ getOrCreateClassFinder());
+ return (Mojo) targetMojo;
+ }
+
+ /**
+ * Gets a new {@link Reflector} instance for the current Mojo execution.
+ *
+ *
+ * An isolated class loader is created based on project and plugin
+ * dependencies, with the first ones having precedence over the seconds. The
+ * maven.api class realm is used as parent classloader, allowing usage of
+ * Maven core classes in the mojo.
+ *
+ * @param project
+ * the maven project.
+ * @param mojoExecution
+ * the current mojo execution.
+ * @return a Reflector instance for the current maven execution.
+ */
+ public static Reflector of(MavenProject project,
+ MojoExecution mojoExecution) {
+ URLClassLoader classLoader = createIsolatedClassLoader(project,
+ mojoExecution);
+ return new Reflector(classLoader);
+ }
+
+ private synchronized Object getOrCreateClassFinder() throws Exception {
+ if (classFinder == null) {
+ Class> classFinderImplClass = loadClass(
+ ReflectionsClassFinder.class.getName());
+ classFinder = classFinderImplClass
+ .getConstructor(ClassLoader.class, URL[].class).newInstance(
+ isolatedClassLoader, isolatedClassLoader.getURLs());
+ }
+ return classFinder;
+ }
+
+ private static URLClassLoader createIsolatedClassLoader(
+ MavenProject project, MojoExecution mojoExecution) {
+ List urls = new ArrayList<>();
+ String outputDirectory = project.getBuild().getOutputDirectory();
+ if (outputDirectory != null) {
+ urls.add(FlowFileUtils.convertToUrl(new File(outputDirectory)));
+ }
+
+ Function keyMapper = artifact -> artifact.getGroupId()
+ + ":" + artifact.getArtifactId();
+
+ Map projectDependencies = new HashMap<>(project
+ .getArtifacts().stream()
+ // Exclude all maven artifacts to prevent class loading clash
+ // with maven.api class realm
+ .filter(artifact -> !DEPENDENCIES_GROUP_EXCLUSIONS
+ .contains(artifact.getGroupId()))
+ .filter(artifact -> artifact.getFile() != null
+ && artifact.getArtifactHandler().isAddedToClasspath()
+ && (Artifact.SCOPE_COMPILE.equals(artifact.getScope())
+ || Artifact.SCOPE_RUNTIME
+ .equals(artifact.getScope())
+ || Artifact.SCOPE_SYSTEM
+ .equals(artifact.getScope())
+ || (Artifact.SCOPE_PROVIDED
+ .equals(artifact.getScope())
+ && artifact.getFile().getPath().matches(
+ INCLUDE_FROM_COMPILE_DEPS_REGEX))))
+ .collect(Collectors.toMap(keyMapper, Function.identity())));
+ if (mojoExecution != null) {
+ mojoExecution.getMojoDescriptor().getPluginDescriptor()
+ .getArtifacts().stream()
+ // Exclude all maven artifacts to prevent class loading
+ // clash with maven.api class realm
+ .filter(artifact -> !DEPENDENCIES_GROUP_EXCLUSIONS
+ .contains(artifact.getGroupId()))
+ .filter(artifact -> !projectDependencies
+ .containsKey(keyMapper.apply(artifact)))
+ .forEach(artifact -> projectDependencies
+ .put(keyMapper.apply(artifact), artifact));
+ }
+
+ projectDependencies.values().stream()
+ .map(artifact -> FlowFileUtils.convertToUrl(artifact.getFile()))
+ .forEach(urls::add);
+ ClassLoader mavenApiClassLoader;
+ if (mojoExecution != null) {
+ ClassRealm pluginClassRealm = mojoExecution.getMojoDescriptor()
+ .getPluginDescriptor().getClassRealm();
+ try {
+ mavenApiClassLoader = pluginClassRealm.getWorld()
+ .getRealm("maven.api");
+ } catch (NoSuchRealmException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ mavenApiClassLoader = Mojo.class.getClassLoader();
+ if (mavenApiClassLoader instanceof ClassRealm classRealm) {
+ try {
+ mavenApiClassLoader = classRealm.getWorld()
+ .getRealm("maven.api");
+ } catch (NoSuchRealmException e) {
+ // Should never happen. In case, ignore the error and use
+ // class loader from the Maven class
+ }
+ }
+ }
+ return new CombinedClassLoader(urls.toArray(new URL[0]),
+ mavenApiClassLoader);
+ }
+
+ // Tries to load class from the give class loader and fallbacks
+ // to Platform class loader in case of failure.
+ private static class CombinedClassLoader extends URLClassLoader {
+ private final ClassLoader delegate;
+
+ private CombinedClassLoader(URL[] urls, ClassLoader delegate) {
+ super(urls, null);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ try {
+ return super.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ // ignore and continue with delegate class loader
+ }
+ if (delegate != null) {
+ try {
+ return delegate.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ // ignore and continue with platform class loader
+ }
+ }
+ return ClassLoader.getPlatformClassLoader().loadClass(name);
+ }
+
+ @Override
+ public URL getResource(String name) {
+ URL url = super.getResource(name);
+ if (url == null && delegate != null) {
+ url = delegate.getResource(name);
+ }
+ if (url == null) {
+ url = ClassLoader.getPlatformClassLoader().getResource(name);
+ }
+ return url;
+ }
+
+ @Override
+ public Enumeration getResources(String name) throws IOException {
+ Enumeration resources = super.getResources(name);
+ if (!resources.hasMoreElements() && delegate != null) {
+ resources = delegate.getResources(name);
+ }
+ if (!resources.hasMoreElements()) {
+ resources = ClassLoader.getPlatformClassLoader()
+ .getResources(name);
+ }
+ return resources;
+ }
+ }
+
+ private void copyFields(BuildDevBundleMojo sourceMojo, Object targetMojo)
+ throws IllegalAccessException, NoSuchFieldException {
+ Class> sourceClass = sourceMojo.getClass();
+ Class> targetClass = targetMojo.getClass();
+ while (sourceClass != null && sourceClass != Object.class) {
+ for (Field sourceField : sourceClass.getDeclaredFields()) {
+ copyField(sourceMojo, targetMojo, sourceField, targetClass);
+ }
+ targetClass = targetClass.getSuperclass();
+ sourceClass = sourceClass.getSuperclass();
+ }
+ }
+
+ private static void copyField(BuildDevBundleMojo sourceMojo,
+ Object targetMojo, Field sourceField, Class> targetClass)
+ throws IllegalAccessException, NoSuchFieldException {
+ if (Modifier.isStatic(sourceField.getModifiers())) {
+ return;
+ }
+ sourceField.setAccessible(true);
+ Object value = sourceField.get(sourceMojo);
+ if (value == null) {
+ return;
+ }
+ Field targetField;
+ try {
+ targetField = targetClass.getDeclaredField(sourceField.getName());
+ } catch (NoSuchFieldException ex) {
+ // Should never happen, since the class definition should be
+ // the same
+ String message = "Field " + sourceField.getName() + " defined in "
+ + sourceField.getDeclaringClass().getName()
+ + " is missing in " + targetClass.getName();
+ sourceMojo.logError(message, ex);
+ throw ex;
+ }
+
+ Class> targetFieldType = targetField.getType();
+ if (!targetFieldType.isAssignableFrom(sourceField.getType())) {
+ String message = "Field " + targetFieldType.getName() + " in class "
+ + targetClass.getName() + " of type "
+ + targetFieldType.getName()
+ + " is loaded from different class loaders."
+ + " Source class loader: "
+ + sourceField.getType().getClassLoader()
+ + ", Target class loader: "
+ + targetFieldType.getClassLoader()
+ + ". This is likely a bug in the Vaadin Maven plugin."
+ + " Please, report the error on the issue tracker.";
+ sourceMojo.logError(message);
+ throw new NoSuchFieldException(message);
+ }
+ targetField.setAccessible(true);
+ targetField.set(targetMojo, value);
+ }
+
+ private static Field findField(Class> clazz, String fieldName)
+ throws NoSuchFieldException {
+ while (clazz != null && !clazz.equals(Object.class)) {
+ try {
+ return clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ clazz = clazz.getSuperclass();
+ }
+ }
+ throw new NoSuchFieldException(fieldName);
+ }
+
+}
\ No newline at end of file
diff --git a/flow-plugins/flow-gradle-plugin/README.md b/flow-plugins/flow-gradle-plugin/README.md
index d33612aca34..960e50704c4 100644
--- a/flow-plugins/flow-gradle-plugin/README.md
+++ b/flow-plugins/flow-gradle-plugin/README.md
@@ -209,7 +209,7 @@ Alternatively, you can build and publish the Flow Gradle plugin into the local M
1. Clone the Base Starter Gradle project.
2. Add `mavenLocal()` to `buildscript.repositories` as the first place to look up.
-3. Add `dependencies { classpath 'com.vaadin:flow-gradle-plugin:24.6-SNAPSHOT' }` to `buildscript.repositories`.
+3. Add `dependencies { classpath 'com.vaadin:flow-gradle-plugin:24.7-SNAPSHOT' }` to `buildscript.repositories`.
4. Run `./gradlew clean build publishToMavenLocal` in the `flow-plugins/flow-gradle-plugin` repo folder.
5. Run the previous command with `-x functionalTest` to skip functional tests.
6. If you now run `./gradlew vaadinPrepareFrontend` in the Starter project folder, Gradle will use the local version of the Flow plugin. You can verify that by adding `println()` statements into the `VaadinPrepareFrontendTask` class.
diff --git a/flow-plugins/flow-gradle-plugin/pom.xml b/flow-plugins/flow-gradle-plugin/pom.xml
index 9a808ca4104..9c6a9841a5c 100644
--- a/flow-plugins/flow-gradle-plugin/pom.xml
+++ b/flow-plugins/flow-gradle-plugin/pom.xml
@@ -3,7 +3,7 @@
com.vaadinflow-plugins
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-gradle-plugin
diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/AbstractGradleTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/AbstractGradleTest.kt
index 6262138a14c..df0f8a16d3e 100644
--- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/AbstractGradleTest.kt
+++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/AbstractGradleTest.kt
@@ -27,7 +27,7 @@ import java.io.File
*/
abstract class AbstractGradleTest {
- val flowVersion = System.getenv("vaadin.version").takeUnless { it.isNullOrEmpty() } ?: "24.6-SNAPSHOT"
+ val flowVersion = System.getenv("vaadin.version").takeUnless { it.isNullOrEmpty() } ?: "24.7-SNAPSHOT"
val slf4jVersion = "2.0.3"
/**
diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt
index 93380385efa..a5cc38b75e1 100644
--- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt
+++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt
@@ -290,7 +290,7 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val
public abstract val frontendExtraFileExtensions: ListProperty
/**
- * Whether to include web component npm packages in packages.json
+ * Whether to exclude Vaadin web component npm packages in packages.json
*/
public abstract val npmExcludeWebComponents: Property
diff --git a/flow-plugins/flow-maven-plugin/pom.xml b/flow-plugins/flow-maven-plugin/pom.xml
index ed1557f1ddd..dacfaefd4a5 100644
--- a/flow-plugins/flow-maven-plugin/pom.xml
+++ b/flow-plugins/flow-maven-plugin/pom.xml
@@ -1,11 +1,11 @@
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4.0.0com.vaadinflow-plugins
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-maven-pluginmaven-plugin
@@ -106,6 +106,35 @@
+
+
+ org.apache.maven.plugins
+ maven-invoker-plugin
+ 3.8.1
+
+ ${skipTests}
+ ${skipTests}
+ target/local-repo
+
+ com.vaadin:flow-client:${project.version}
+
+ true
+ src/it/settings.xml
+ verify
+ true
+
+
+
+ integration-test
+
+ install
+ integration-test
+ verify
+
+
+
+
+
diff --git a/flow-plugins/flow-maven-plugin/src/it/.gitignore b/flow-plugins/flow-maven-plugin/src/it/.gitignore
new file mode 100644
index 00000000000..bd1e4eb08d1
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/.gitignore
@@ -0,0 +1,7 @@
+**/src/main/bundles
+**/src/main/frontend/generated
+**/src/main/frontend/index.html
+**/package*.json
+**/tsconfig.json
+**/types.d.ts
+**/vite.*.ts
\ No newline at end of file
diff --git a/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/invoker.properties b/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/invoker.properties
new file mode 100644
index 00000000000..a6700ec7e57
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/invoker.properties
@@ -0,0 +1,18 @@
+#
+# Copyright 2000-2024 Vaadin Ltd.
+#
+# 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
+#
+# http://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.
+#
+invoker.goals=clean package
+invoker.profiles.1=prepare-frontend-after-compile
+invoker.profiles.2=build-frontend-full-dep-scan
\ No newline at end of file
diff --git a/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/pom.xml b/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/pom.xml
new file mode 100644
index 00000000000..3009df71bbf
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/pom.xml
@@ -0,0 +1,118 @@
+
+
+ 4.0.0
+
+ com.vaadin.test.maven
+ classfinder-lookup
+ 1.0
+ jar
+
+
+
+
+ UTF-8
+ 17
+ ${maven.compiler.release}
+ ${maven.compiler.release}
+ true
+
+ @project.version@
+
+
+
+
+ com.vaadin
+ flow-server
+ ${flow.version}
+
+
+ com.vaadin
+ flow-client
+ ${flow.version}
+
+
+ org.springframework.data
+ spring-data-jpa
+ 3.3.4
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ com.vaadin
+ flow-maven-plugin
+ ${flow.version}
+
+
+ org.springframework.data
+ spring-data-commons
+ 3.3.4
+
+
+
+
+
+
+
+
+ prepare-frontend-after-compile
+
+
+
+ com.vaadin
+ flow-maven-plugin
+
+
+ compile
+
+ prepare-frontend
+
+
+
+
+
+
+
+
+ build-frontend-full-dep-scan
+
+
+
+ com.vaadin
+ flow-maven-plugin
+
+ false
+
+
+
+
+ prepare-frontend
+ build-frontend
+
+
+
+
+
+
+
+
+
+
diff --git a/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/src/main/java/com/vaadin/test/AppConfig.java b/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/src/main/java/com/vaadin/test/AppConfig.java
new file mode 100644
index 00000000000..2c4d7d02ee0
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/src/main/java/com/vaadin/test/AppConfig.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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 com.vaadin.test;
+
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+import com.vaadin.flow.component.dependency.NpmPackage;
+import com.vaadin.flow.component.page.AppShellConfigurator;
+
+@NpmPackage(value = "react-error-boundary", version = "4.0.13")
+@EnableJpaRepositories
+public class AppConfig implements AppShellConfigurator {
+
+}
diff --git a/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/invoker.properties b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/invoker.properties
new file mode 100644
index 00000000000..44594528d31
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/invoker.properties
@@ -0,0 +1,17 @@
+#
+# Copyright 2000-2024 Vaadin Ltd.
+#
+# 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
+#
+# http://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.
+#
+
+invoker.goals=clean package
\ No newline at end of file
diff --git a/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/pom.xml b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/pom.xml
new file mode 100644
index 00000000000..d147b262e7b
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+ com.vaadin.test.maven
+ classfinder-lookup
+ 1.0
+ jar
+
+
+ Tests that there are no class loading issues for components loaded by Lookup backed by ClassFinder
+
+
+
+ UTF-8
+ 17
+ ${maven.compiler.release}
+ ${maven.compiler.release}
+ true
+
+ @project.version@
+
+
+
+
+ com.vaadin
+ flow-server
+ ${flow.version}
+
+
+ com.vaadin
+ flow-client
+ ${flow.version}
+
+
+ com.vaadin.test
+ flow-addon
+ 1.0.0
+ system
+ ${project.basedir}/../flow-addon/target/flow-addon-1.0.0.jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ com.vaadin
+ flow-maven-plugin
+ ${flow.version}
+
+
+
+ prepare-frontend
+ build-frontend
+
+
+
+
+
+
+
+
diff --git a/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/src/main/java/com/vaadin/test/ProjectFlowExtension.java b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/src/main/java/com/vaadin/test/ProjectFlowExtension.java
new file mode 100644
index 00000000000..fde6fd519aa
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/src/main/java/com/vaadin/test/ProjectFlowExtension.java
@@ -0,0 +1,21 @@
+package com.vaadin.test;
+
+import java.util.List;
+
+import com.vaadin.flow.server.frontend.Options;
+import com.vaadin.flow.server.frontend.TypeScriptBootstrapModifier;
+import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
+
+/**
+ * Hello world!
+ */
+public class ProjectFlowExtension implements TypeScriptBootstrapModifier {
+
+ @Override
+ public void modify(List bootstrapTypeScript, Options options,
+ FrontendDependenciesScanner frontendDependenciesScanner) {
+ bootstrapTypeScript.add("""
+ (window as any).testProject=1;
+ """);
+ }
+}
diff --git a/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/verify.bsh b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/verify.bsh
new file mode 100644
index 00000000000..530ee4be4b9
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/verify.bsh
@@ -0,0 +1,14 @@
+import java.nio.file.*;
+
+vaadinTs = basedir.toPath().resolve("src/main/frontend/generated/vaadin.ts");
+if ( !Files.exists(vaadinTs, new LinkOption[0]) )
+{
+ throw new RuntimeException("vaadin.ts file not generated");
+}
+lines = Files.readAllLines(vaadinTs);
+if (!lines.contains("(window as any).testProject=1;")) {
+ throw new RuntimeException("vaadin.ts does note contain lines added by project TypeScriptBootstrapModifier");
+}
+if (!lines.contains("(window as any).testAddOn=1;")) {
+ throw new RuntimeException("vaadin.ts does note contain lines added by project dependency TypeScriptBootstrapModifier");
+}
diff --git a/flow-plugins/flow-maven-plugin/src/it/flow-addon/invoker.properties b/flow-plugins/flow-maven-plugin/src/it/flow-addon/invoker.properties
new file mode 100644
index 00000000000..d7243b3a7fc
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/flow-addon/invoker.properties
@@ -0,0 +1,23 @@
+#
+# Copyright 2000-2024 Vaadin Ltd.
+#
+# 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
+#
+# http://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.
+#
+
+# High ordinal number to be executed first
+invoker.ordinal = 100
+# Not invoking clean to make sure JAR from both executions are preserved
+invoker.goals=package
+invoker.profiles.1=
+invoker.profiles.2=fake-flow-resources
+invoker.profiles.3=fake-flow-plugin-resources
diff --git a/flow-plugins/flow-maven-plugin/src/it/flow-addon/pom.xml b/flow-plugins/flow-maven-plugin/src/it/flow-addon/pom.xml
new file mode 100644
index 00000000000..163310e8b76
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/flow-addon/pom.xml
@@ -0,0 +1,64 @@
+
+
+ 4.0.0
+
+ com.vaadin.test
+ flow-addon
+ 1.0.0
+
+ flow-addon
+
+ Test project to build the JAR file for other tests.
+ Run 'mvn package' on this module and then copy the JAR
+ where needed.
+
+
+
+ @project.version@
+ UTF-8
+ 17
+ ${maven.compiler.release}
+ ${maven.compiler.release}
+ true
+
+
+
+
+ com.vaadin
+ flow-server
+ ${vaadin.version}
+
+
+ org.apache.commons
+ commons-lang3
+ 3.11
+
+
+
+
+
+ fake-flow-resources
+
+ fake-resources-${project.version}
+
+
+ ${project.basedir}/src/main/fake-resources
+
+
+
+
+
+ fake-flow-plugin-resources
+
+ fake-resources-plugin-${project.version}
+
+
+ ${project.basedir}/src/main/fake-plugin-resources
+
+
+
+
+
+
+
diff --git a/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-plugin-resources/com/vaadin/flow/server/frontend/Flow.tsx b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-plugin-resources/com/vaadin/flow/server/frontend/Flow.tsx
new file mode 100644
index 00000000000..a01c615012c
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-plugin-resources/com/vaadin/flow/server/frontend/Flow.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+// Resource loaded from plugin dependency
+export const serverSideRoutes = []
diff --git a/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx
new file mode 100644
index 00000000000..a843a54097a
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+// Resource loaded from project dependency
+export const serverSideRoutes = []
diff --git a/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/java/com/vaadin/test/Addon.java b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/java/com/vaadin/test/Addon.java
new file mode 100644
index 00000000000..9b6ea6fe637
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/java/com/vaadin/test/Addon.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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 com.vaadin.test;
+
+import java.util.List;
+
+import com.vaadin.flow.server.frontend.Options;
+import com.vaadin.flow.server.frontend.TypeScriptBootstrapModifier;
+import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
+
+public class Addon implements TypeScriptBootstrapModifier {
+
+ @Override
+ public void modify(List bootstrapTypeScript, Options options,
+ FrontendDependenciesScanner frontendDependenciesScanner) {
+ bootstrapTypeScript.add("""
+ (window as any).testAddOn=1;
+ """);
+ }
+}
diff --git a/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/invoker.properties b/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/invoker.properties
new file mode 100644
index 00000000000..5aa13827263
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/invoker.properties
@@ -0,0 +1,17 @@
+#
+# Copyright 2000-2024 Vaadin Ltd.
+#
+# 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
+#
+# http://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.
+#
+
+invoker.goals=clean package
diff --git a/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/pom.xml b/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/pom.xml
new file mode 100644
index 00000000000..56131acf0c8
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+ com.vaadin.test.maven
+ resources-from-project
+ 1.0
+ jar
+
+
+ Tests that plugin dependencies do not override resources from project artifacts.
+
+
+
+ UTF-8
+ 17
+ ${maven.compiler.release}
+ ${maven.compiler.release}
+ true
+
+ @project.version@
+ 3.9.9
+
+
+
+
+ com.vaadin
+ flow-server
+ ${flow.version}
+
+
+ com.vaadin
+ flow-client
+ ${flow.version}
+
+
+ org.apache.maven
+ maven-artifact
+ ${maven.version}
+
+
+ org.apache.maven
+ maven-core
+ ${maven.version}
+
+
+ org.apache.maven
+ maven-plugin-api
+ ${maven.version}
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.16
+
+
+ org.codehaus.plexus
+ plexus-build-api
+ 1.2.0
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ com.vaadin
+ flow-maven-plugin
+ ${flow.version}
+
+
+
+ prepare-frontend
+ build-frontend
+
+
+
+
+
+
+
+
diff --git a/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/src/main/java/com/vaadin/test/ProjectFlowExtension.java b/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/src/main/java/com/vaadin/test/ProjectFlowExtension.java
new file mode 100644
index 00000000000..fd5304d4b88
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/ignore-maven-deps-from-project/src/main/java/com/vaadin/test/ProjectFlowExtension.java
@@ -0,0 +1,20 @@
+package com.vaadin.test;
+
+import java.util.List;
+
+import com.vaadin.flow.server.frontend.Options;
+import com.vaadin.flow.server.frontend.TypeScriptBootstrapModifier;
+import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
+
+/**
+ * Hello world!
+ */
+public class ProjectFlowExtension implements TypeScriptBootstrapModifier {
+
+ @Override
+ public void modify(List bootstrapTypeScript, Options options,
+ FrontendDependenciesScanner frontendDependenciesScanner) {
+ System.out.println("ProjectFlowExtension");
+ bootstrapTypeScript.add("(window as any).testProject=1;");
+ }
+}
diff --git a/flow-plugins/flow-maven-plugin/src/it/resources-from-project/invoker.properties b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/invoker.properties
new file mode 100644
index 00000000000..44594528d31
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/invoker.properties
@@ -0,0 +1,17 @@
+#
+# Copyright 2000-2024 Vaadin Ltd.
+#
+# 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
+#
+# http://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.
+#
+
+invoker.goals=clean package
\ No newline at end of file
diff --git a/flow-plugins/flow-maven-plugin/src/it/resources-from-project/pom.xml b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/pom.xml
new file mode 100644
index 00000000000..489fc2c332b
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ com.vaadin.test.maven
+ resources-from-project
+ 1.0
+ jar
+
+
+ Tests that plugin dependencies do not override resources from project artifacts.
+
+
+
+ UTF-8
+ 17
+ ${maven.compiler.release}
+ ${maven.compiler.release}
+ true
+
+ @project.version@
+
+
+
+
+ com.vaadin
+ flow-server
+ ${flow.version}
+
+
+ com.vaadin
+ flow-client
+ ${flow.version}
+
+
+ com.vaadin.test
+ fake-flow-resources
+ 1.0.0
+ system
+ ${project.basedir}/../flow-addon/target/fake-resources-1.0.0.jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ com.vaadin
+ flow-maven-plugin
+ ${flow.version}
+
+
+
+ prepare-frontend
+ build-frontend
+
+
+
+
+
+ com.vaadin.test
+ fake-flow-resources
+ 1.0.0
+ system
+ ${project.basedir}/../flow-addon/target/fake-resources-plugin-1.0.0.jar
+
+
+
+
+
+
+
diff --git a/flow-plugins/flow-maven-plugin/src/it/resources-from-project/src/main/java/com/vaadin/test/ProjectFlowExtension.java b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/src/main/java/com/vaadin/test/ProjectFlowExtension.java
new file mode 100644
index 00000000000..fd5304d4b88
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/src/main/java/com/vaadin/test/ProjectFlowExtension.java
@@ -0,0 +1,20 @@
+package com.vaadin.test;
+
+import java.util.List;
+
+import com.vaadin.flow.server.frontend.Options;
+import com.vaadin.flow.server.frontend.TypeScriptBootstrapModifier;
+import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
+
+/**
+ * Hello world!
+ */
+public class ProjectFlowExtension implements TypeScriptBootstrapModifier {
+
+ @Override
+ public void modify(List bootstrapTypeScript, Options options,
+ FrontendDependenciesScanner frontendDependenciesScanner) {
+ System.out.println("ProjectFlowExtension");
+ bootstrapTypeScript.add("(window as any).testProject=1;");
+ }
+}
diff --git a/flow-plugins/flow-maven-plugin/src/it/resources-from-project/verify.bsh b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/verify.bsh
new file mode 100644
index 00000000000..ab033bdbc16
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/resources-from-project/verify.bsh
@@ -0,0 +1,12 @@
+import java.nio.file.*;
+
+flowTsx = basedir.toPath().resolve("src/main/frontend/generated/flow/Flow.tsx");
+if ( !Files.exists(flowTsx, new LinkOption[0]) )
+{
+ throw new RuntimeException("Flow.tsx file not generated");
+}
+
+lines = Files.readAllLines(flowTsx);
+if (lines.contains("// Resource loaded from plugin dependency")) {
+ throw new RuntimeException("Flow.tsx has been extracted from plugin classloader");
+}
diff --git a/flow-plugins/flow-maven-plugin/src/it/settings.xml b/flow-plugins/flow-maven-plugin/src/it/settings.xml
new file mode 100644
index 00000000000..21d21ecab70
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/it/settings.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ it-repo
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+
+ it-repo
+
+
\ No newline at end of file
diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java
index 6a3343c5f5c..53ef9e4162a 100644
--- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java
+++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java
@@ -133,7 +133,8 @@ public class BuildFrontendMojo extends FlowModeAbstractMojo
private boolean cleanFrontendFiles;
@Override
- public void execute() throws MojoExecutionException, MojoFailureException {
+ protected void executeInternal()
+ throws MojoExecutionException, MojoFailureException {
long start = System.nanoTime();
TaskCleanFrontendFiles cleanTask = new TaskCleanFrontendFiles(
diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/CleanFrontendMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/CleanFrontendMojo.java
index 80afe857b06..b8d7f09a307 100644
--- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/CleanFrontendMojo.java
+++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/CleanFrontendMojo.java
@@ -40,7 +40,7 @@
public class CleanFrontendMojo extends FlowModeAbstractMojo {
@Override
- public void execute() throws MojoFailureException {
+ protected void executeInternal() throws MojoFailureException {
try {
CleanFrontendUtil.runCleaning(this, new CleanOptions());
} catch (CleanFrontendException e) {
diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/ConvertPolymerMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/ConvertPolymerMojo.java
index 0ceaa86e078..1e45d7acff4 100644
--- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/ConvertPolymerMojo.java
+++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/ConvertPolymerMojo.java
@@ -50,7 +50,7 @@ public class ConvertPolymerMojo extends FlowModeAbstractMojo {
private boolean disableOptionalChaining;
@Override
- public void execute() throws MojoFailureException {
+ protected void executeInternal() throws MojoFailureException {
if (isHillaUsed(frontendDirectory())) {
getLog().warn(
"""
diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java
index c4dc8291a3e..f1367cf19d8 100644
--- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java
+++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java
@@ -15,24 +15,38 @@
*/
package com.vaadin.flow.plugin.maven;
+import javax.inject.Inject;
+
import java.io.File;
+import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
+import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.Mojo;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.build.BuildContext;
import com.vaadin.flow.internal.StringUtil;
import com.vaadin.flow.plugin.base.BuildFrontendUtil;
@@ -44,6 +58,7 @@
import com.vaadin.flow.server.frontend.installer.NodeInstaller;
import com.vaadin.flow.server.frontend.installer.Platform;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
+import com.vaadin.flow.server.scanner.ReflectionsClassFinder;
import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
import static com.vaadin.flow.server.Constants.VAADIN_WEBAPP_RESOURCES;
@@ -172,6 +187,9 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo
@Parameter(defaultValue = "${project}", readonly = true, required = true)
MavenProject project;
+ @Parameter(defaultValue = "${mojoExecution}")
+ MojoExecution mojoExecution;
+
/**
* The folder where `package.json` file is located. Default is project root
* dir.
@@ -239,7 +257,9 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo
private boolean npmExcludeWebComponents;
/**
- * Parameter for adding file extensions to handle during frontend tasks.
+ * Parameter for adding file extensions to handle when generating bundles.
+ * Hashes are calculated for these files as part of detecting if a new
+ * bundle should be generated.
*
* From the commandline use comma separated list
* {@code -Ddevmode.frontendExtraFileExtensions="svg,ico"}
@@ -262,8 +282,69 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo
@Parameter(property = InitParameters.APPLICATION_IDENTIFIER)
private String applicationIdentifier;
+ static final String CLASSFINDER_FIELD_NAME = "classFinder";
private ClassFinder classFinder;
+ private Consumer buildContextRefresher;
+
+ @Inject
+ void setBuildContext(BuildContext buildContext) {
+ buildContextRefresher = buildContext::refresh;
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ PluginDescriptor pluginDescriptor = mojoExecution.getMojoDescriptor()
+ .getPluginDescriptor();
+ checkFlowCompatibility(pluginDescriptor);
+
+ Reflector reflector = getOrCreateReflector();
+ ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread()
+ .setContextClassLoader(reflector.getIsolatedClassLoader());
+ try {
+ Mojo task = reflector.createMojo(this);
+ findExecuteMethod(task.getClass()).invoke(task);
+ } catch (MojoExecutionException | MojoFailureException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new MojoFailureException(e.getMessage(), e);
+ } finally {
+ Thread.currentThread().setContextClassLoader(tccl);
+ }
+ }
+
+ /**
+ * Perform whatever build-process behavior this Mojo
+ * implements.
+ * This is the main trigger for the Mojo inside the
+ * Maven system, and allows the Mojo to
+ * communicate errors.
+ *
+ * @throws MojoExecutionException
+ * if an unexpected problem occurs. Throwing this exception
+ * causes a "BUILD ERROR" message to be displayed.
+ * @throws MojoFailureException
+ * if an expected problem (such as a compilation failure)
+ * occurs. Throwing this exception causes a "BUILD FAILURE"
+ * message to be displayed.
+ */
+ protected abstract void executeInternal()
+ throws MojoExecutionException, MojoFailureException;
+
+ /**
+ * Indicates that the file or folder content has been modified during the
+ * build.
+ *
+ * @param file
+ * a {@link java.io.File} object.
+ */
+ protected void triggerRefresh(File file) {
+ if (buildContextRefresher != null) {
+ buildContextRefresher.accept(file);
+ }
+ }
+
/**
* Generates a List of ClasspathElements (Run and CompileTime) from a
* MavenProject.
@@ -271,7 +352,9 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo
* @param project
* a given MavenProject
* @return List of ClasspathElements
+ * @deprecated will be removed without replacement.
*/
+ @Deprecated(forRemoval = true)
public static List getClasspathElements(MavenProject project) {
try {
@@ -296,7 +379,7 @@ public static List getClasspathElements(MavenProject project) {
* @return true if Hilla is available, false otherwise
*/
public boolean isHillaAvailable() {
- return getClassFinder().getResource(
+ return getOrCreateReflector().getResource(
"com/vaadin/hilla/EndpointController.class") != null;
}
@@ -308,7 +391,7 @@ public boolean isHillaAvailable() {
* @return true if Hilla is available, false otherwise
*/
public static boolean isHillaAvailable(MavenProject mavenProject) {
- return createClassFinder(mavenProject).getResource(
+ return Reflector.of(mavenProject, null).getResource(
"com/vaadin/hilla/EndpointController.class") != null;
}
@@ -371,16 +454,14 @@ public File generatedTsFolder() {
@Override
public ClassFinder getClassFinder() {
if (classFinder == null) {
- classFinder = createClassFinder(project);
+ URLClassLoader classLoader = getOrCreateReflector()
+ .getIsolatedClassLoader();
+ classFinder = new ReflectionsClassFinder(classLoader,
+ classLoader.getURLs());
}
return classFinder;
}
- private static ClassFinder createClassFinder(MavenProject project) {
- List classpathElements = getClasspathElements(project);
- return BuildFrontendUtil.getClassFinder(classpathElements);
- }
-
@Override
public Set getJarFiles() {
@@ -604,4 +685,61 @@ public List frontendExtraFileExtensions() {
public boolean isNpmExcludeWebComponents() {
return npmExcludeWebComponents;
}
+
+ private void checkFlowCompatibility(PluginDescriptor pluginDescriptor) {
+ Predicate isFlowServer = artifact -> "com.vaadin"
+ .equals(artifact.getGroupId())
+ && "flow-server".equals(artifact.getArtifactId());
+ String projectFlowVersion = project.getArtifacts().stream()
+ .filter(isFlowServer).map(Artifact::getVersion).findFirst()
+ .orElse(null);
+ String pluginFlowVersion = pluginDescriptor.getArtifacts().stream()
+ .filter(isFlowServer).map(Artifact::getVersion).findFirst()
+ .orElse(null);
+ if (!Objects.equals(projectFlowVersion, pluginFlowVersion)) {
+ getLog().warn(
+ "Vaadin Flow used in project does not match the version expected by the Vaadin plugin. "
+ + "Flow version for project is "
+ + projectFlowVersion
+ + ", Vaadin plugin is built for Flow version "
+ + pluginFlowVersion + ".");
+ }
+ }
+
+ private Method findExecuteMethod(Class> taskClass)
+ throws NoSuchMethodException {
+
+ while (taskClass != null && taskClass != Object.class) {
+ try {
+ Method executeInternal = taskClass
+ .getDeclaredMethod("executeInternal");
+ executeInternal.setAccessible(true);
+ return executeInternal;
+ } catch (NoSuchMethodException e) {
+ // ignore
+ }
+ taskClass = taskClass.getSuperclass();
+ }
+ throw new NoSuchMethodException(
+ "Method executeInternal not found in " + getClass().getName());
+ }
+
+ private Reflector getOrCreateReflector() {
+ Map pluginContext = getPluginContext();
+ String pluginKey = mojoExecution.getPlugin().getKey();
+ String reflectorKey = Reflector.class.getName() + "-" + pluginKey + "-"
+ + mojoExecution.getLifecyclePhase();
+ if (pluginContext != null && pluginContext.containsKey(reflectorKey)) {
+ getLog().debug("Using cached Reflector for plugin " + pluginKey
+ + " and phase " + mojoExecution.getLifecyclePhase());
+ return Reflector.adapt(pluginContext.get(reflectorKey));
+ }
+ Reflector reflector = Reflector.of(project, mojoExecution);
+ if (pluginContext != null) {
+ pluginContext.put(reflectorKey, reflector);
+ getLog().debug("Cached Reflector for plugin " + pluginKey
+ + " and phase " + mojoExecution.getLifecyclePhase());
+ }
+ return reflector;
+ }
}
diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java
index 5926cc68f68..d5e6dc05904 100644
--- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java
+++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java
@@ -138,7 +138,8 @@ public class GenerateNpmBOMMojo extends FlowModeAbstractMojo {
private String specVersion;
@Override
- public void execute() throws MojoExecutionException, MojoFailureException {
+ protected void executeInternal()
+ throws MojoExecutionException, MojoFailureException {
InvocationRequestBuilder requestBuilder = new InvocationRequestBuilder();
InvocationRequest request = requestBuilder.groupId(GROUP)
.artifactId(ARTIFACT).version(VERSION).goal(GOAL)
diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojo.java
index 457f39a6b14..8e65eb821a7 100644
--- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojo.java
+++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojo.java
@@ -16,16 +16,12 @@
package com.vaadin.flow.plugin.maven;
import java.io.File;
-import java.io.IOException;
-import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.ResolutionScope;
-import org.codehaus.plexus.build.BuildContext;
import com.vaadin.flow.plugin.base.BuildFrontendUtil;
@@ -41,11 +37,9 @@
@Mojo(name = "prepare-frontend", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PROCESS_RESOURCES)
public class PrepareFrontendMojo extends FlowModeAbstractMojo {
- @Component
- private BuildContext buildContext; // m2eclipse integration
-
@Override
- public void execute() throws MojoExecutionException, MojoFailureException {
+ protected void executeInternal()
+ throws MojoExecutionException, MojoFailureException {
if (productionMode != null) {
logWarn("The " + productionMode
+ " Maven parameter no longer has any effect and can be removed. Production mode is automatically enabled when you run the build-frontend target.");
@@ -56,9 +50,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
// Inform m2eclipse that the directory containing the token file has
// been updated in order to trigger server re-deployment (#6103)
- if (buildContext != null) {
- buildContext.refresh(tokenFile.getParentFile());
- }
+ triggerRefresh(tokenFile.getParentFile());
try {
BuildFrontendUtil.prepareFrontend(this);
diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java
new file mode 100644
index 00000000000..b5ee45c4408
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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 com.vaadin.flow.plugin.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.Mojo;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;
+
+import com.vaadin.flow.internal.ReflectTools;
+import com.vaadin.flow.server.frontend.scanner.ClassFinder;
+import com.vaadin.flow.server.scanner.ReflectionsClassFinder;
+import com.vaadin.flow.utils.FlowFileUtils;
+
+/**
+ * Helper class to deal with classloading of Flow plugin mojos.
+ */
+public final class Reflector {
+
+ public static final String INCLUDE_FROM_COMPILE_DEPS_REGEX = ".*(/|\\\\)(portlet-api|javax\\.servlet-api)-.+jar$";
+ private static final Set DEPENDENCIES_GROUP_EXCLUSIONS = Set.of(
+ "org.apache.maven", "org.codehaus.plexus", "org.slf4j",
+ "org.eclipse.sisu");
+
+ private final URLClassLoader isolatedClassLoader;
+ private Object classFinder;
+
+ /**
+ * Creates a new reflector instance for the given classloader.
+ *
+ * @param isolatedClassLoader
+ * class loader to be used to create mojo instances.
+ */
+ public Reflector(URLClassLoader isolatedClassLoader) {
+ this.isolatedClassLoader = isolatedClassLoader;
+ }
+
+ private Reflector(URLClassLoader isolatedClassLoader, Object classFinder) {
+ this.isolatedClassLoader = isolatedClassLoader;
+ this.classFinder = classFinder;
+ }
+
+ /**
+ * Gets a {@link Reflector} instance usable with the caller class loader.
+ *
+ *
+ * Reflector instances are cached in Maven plugin context, but instances
+ * might be associated to the plugin class loader, thus not working with
+ * classes loaded by the isolated class loader. This method returns the
+ * input object if it is compatible with the class loader, otherwise it
+ * creates a copy referencing the same isolated class loader and
+ * {@link ClassFinder}.
+ *
+ * @param reflector
+ * the {@link Reflector} instance.
+ * @return a {@link Reflector} instance compatible with the current class
+ * loader.
+ * @throws IllegalArgumentException
+ * if the input object is not a {@link Reflector} instance or if
+ * it is not possible to make a copy for it due to class
+ * definition incompatibilities.
+ */
+ static Reflector adapt(Object reflector) {
+ if (reflector instanceof Reflector sameClassLoader) {
+ return sameClassLoader;
+ } else if (Reflector.class.getName()
+ .equals(reflector.getClass().getName())) {
+ Class> reflectorClass = reflector.getClass();
+ try {
+ URLClassLoader classLoader = (URLClassLoader) ReflectTools
+ .getJavaFieldValue(reflector,
+ findField(reflectorClass,
+ "isolatedClassLoader"),
+ URLClassLoader.class);
+ Object classFinder = ReflectTools.getJavaFieldValue(reflector,
+ findField(reflectorClass, "classFinder"));
+ return new Reflector(classLoader, classFinder);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Object of type " + reflector.getClass().getName()
+ + " is not a compatible Reflector",
+ e);
+ }
+ }
+ throw new IllegalArgumentException(
+ "Object of type " + reflector.getClass().getName()
+ + " is not a compatible Reflector");
+ }
+
+ /**
+ * Gets the isolated class loader.
+ *
+ * @return the isolated class loader.
+ */
+ public URLClassLoader getIsolatedClassLoader() {
+ return isolatedClassLoader;
+ }
+
+ /**
+ * Loads the class with the given name from the isolated classloader.
+ *
+ * @param className
+ * the name of the class to load.
+ * @return the class object.
+ * @throws ClassNotFoundException
+ * if the class was not found.
+ */
+ public Class> loadClass(String className) throws ClassNotFoundException {
+ return isolatedClassLoader.loadClass(className);
+ }
+
+ /**
+ * Get a resource from the classpath of the isolated class loader.
+ *
+ * @param name
+ * class literal
+ * @return the resource
+ */
+ public URL getResource(String name) {
+ return isolatedClassLoader.getResource(name);
+ }
+
+ /**
+ * Creates a copy of the given Flow mojo, loading classes the isolated
+ * classloader.
+ *
+ *
+ * Loads the given mojo class from the isolated class loader and then
+ * creates a new instance for it and fills all field copying values from the
+ * original mojo. The input mojo must have a public no-args constructor.
+ * Mojo fields must reference types that can be safely loaded be the
+ * isolated class loader, such as JDK or Maven core API. It also creates and
+ * injects a {@link ClassFinder}, based on the isolated class loader.
+ *
+ * @param sourceMojo
+ * The mojo for which to create the instance from the isolated
+ * class loader.
+ * @return an instance of the mojo loaded from the isolated class loader.
+ * @throws Exception
+ * if the mojo instance cannot be created.
+ */
+ public Mojo createMojo(FlowModeAbstractMojo sourceMojo) throws Exception {
+ Class> targetMojoClass = loadClass(sourceMojo.getClass().getName());
+ Object targetMojo = targetMojoClass.getConstructor().newInstance();
+ copyFields(sourceMojo, targetMojo);
+ Field classFinderField = findField(targetMojoClass,
+ FlowModeAbstractMojo.CLASSFINDER_FIELD_NAME);
+ ReflectTools.setJavaFieldValue(targetMojo, classFinderField,
+ getOrCreateClassFinder());
+ return (Mojo) targetMojo;
+ }
+
+ /**
+ * Gets a new {@link Reflector} instance for the current Mojo execution.
+ *
+ *
+ * An isolated class loader is created based on project and plugin
+ * dependencies, with the first ones having precedence over the seconds. The
+ * maven.api class realm is used as parent classloader, allowing usage of
+ * Maven core classes in the mojo.
+ *
+ * @param project
+ * the maven project.
+ * @param mojoExecution
+ * the current mojo execution.
+ * @return a Reflector instance for the current maven execution.
+ */
+ public static Reflector of(MavenProject project,
+ MojoExecution mojoExecution) {
+ URLClassLoader classLoader = createIsolatedClassLoader(project,
+ mojoExecution);
+ return new Reflector(classLoader);
+ }
+
+ private synchronized Object getOrCreateClassFinder() throws Exception {
+ if (classFinder == null) {
+ Class> classFinderImplClass = loadClass(
+ ReflectionsClassFinder.class.getName());
+ classFinder = classFinderImplClass
+ .getConstructor(ClassLoader.class, URL[].class).newInstance(
+ isolatedClassLoader, isolatedClassLoader.getURLs());
+ }
+ return classFinder;
+ }
+
+ private static URLClassLoader createIsolatedClassLoader(
+ MavenProject project, MojoExecution mojoExecution) {
+ List urls = new ArrayList<>();
+ String outputDirectory = project.getBuild().getOutputDirectory();
+ if (outputDirectory != null) {
+ urls.add(FlowFileUtils.convertToUrl(new File(outputDirectory)));
+ }
+
+ Function keyMapper = artifact -> artifact.getGroupId()
+ + ":" + artifact.getArtifactId();
+
+ Map projectDependencies = new HashMap<>(project
+ .getArtifacts().stream()
+ // Exclude all maven artifacts to prevent class loading clash
+ // with maven.api class realm
+ .filter(artifact -> !DEPENDENCIES_GROUP_EXCLUSIONS
+ .contains(artifact.getGroupId()))
+ .filter(artifact -> artifact.getFile() != null
+ && artifact.getArtifactHandler().isAddedToClasspath()
+ && (Artifact.SCOPE_COMPILE.equals(artifact.getScope())
+ || Artifact.SCOPE_RUNTIME
+ .equals(artifact.getScope())
+ || Artifact.SCOPE_SYSTEM
+ .equals(artifact.getScope())
+ || (Artifact.SCOPE_PROVIDED
+ .equals(artifact.getScope())
+ && artifact.getFile().getPath().matches(
+ INCLUDE_FROM_COMPILE_DEPS_REGEX))))
+ .collect(Collectors.toMap(keyMapper, Function.identity())));
+ if (mojoExecution != null) {
+ mojoExecution.getMojoDescriptor().getPluginDescriptor()
+ .getArtifacts().stream()
+ // Exclude all maven artifacts to prevent class loading
+ // clash with maven.api class realm
+ .filter(artifact -> !DEPENDENCIES_GROUP_EXCLUSIONS
+ .contains(artifact.getGroupId()))
+ .filter(artifact -> !projectDependencies
+ .containsKey(keyMapper.apply(artifact)))
+ .forEach(artifact -> projectDependencies
+ .put(keyMapper.apply(artifact), artifact));
+ }
+
+ projectDependencies.values().stream()
+ .map(artifact -> FlowFileUtils.convertToUrl(artifact.getFile()))
+ .forEach(urls::add);
+ ClassLoader mavenApiClassLoader;
+ if (mojoExecution != null) {
+ ClassRealm pluginClassRealm = mojoExecution.getMojoDescriptor()
+ .getPluginDescriptor().getClassRealm();
+ try {
+ mavenApiClassLoader = pluginClassRealm.getWorld()
+ .getRealm("maven.api");
+ } catch (NoSuchRealmException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ mavenApiClassLoader = Mojo.class.getClassLoader();
+ if (mavenApiClassLoader instanceof ClassRealm classRealm) {
+ try {
+ mavenApiClassLoader = classRealm.getWorld()
+ .getRealm("maven.api");
+ } catch (NoSuchRealmException e) {
+ // Should never happen. In case, ignore the error and use
+ // class loader from the Maven class
+ }
+ }
+ }
+ return new CombinedClassLoader(urls.toArray(new URL[0]),
+ mavenApiClassLoader);
+ }
+
+ // Tries to load class from the give class loader and fallbacks
+ // to Platform class loader in case of failure.
+ private static class CombinedClassLoader extends URLClassLoader {
+ private final ClassLoader delegate;
+
+ private CombinedClassLoader(URL[] urls, ClassLoader delegate) {
+ super(urls, null);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ try {
+ return super.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ // ignore and continue with delegate class loader
+ }
+ if (delegate != null) {
+ try {
+ return delegate.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ // ignore and continue with platform class loader
+ }
+ }
+ return ClassLoader.getPlatformClassLoader().loadClass(name);
+ }
+
+ @Override
+ public URL getResource(String name) {
+ URL url = super.getResource(name);
+ if (url == null && delegate != null) {
+ url = delegate.getResource(name);
+ }
+ if (url == null) {
+ url = ClassLoader.getPlatformClassLoader().getResource(name);
+ }
+ return url;
+ }
+
+ @Override
+ public Enumeration getResources(String name) throws IOException {
+ Enumeration resources = super.getResources(name);
+ if (!resources.hasMoreElements() && delegate != null) {
+ resources = delegate.getResources(name);
+ }
+ if (!resources.hasMoreElements()) {
+ resources = ClassLoader.getPlatformClassLoader()
+ .getResources(name);
+ }
+ return resources;
+ }
+ }
+
+ private void copyFields(FlowModeAbstractMojo sourceMojo, Object targetMojo)
+ throws IllegalAccessException, NoSuchFieldException {
+ Class> sourceClass = sourceMojo.getClass();
+ Class> targetClass = targetMojo.getClass();
+ while (sourceClass != null && sourceClass != Object.class) {
+ for (Field sourceField : sourceClass.getDeclaredFields()) {
+ copyField(sourceMojo, targetMojo, sourceField, targetClass);
+ }
+ targetClass = targetClass.getSuperclass();
+ sourceClass = sourceClass.getSuperclass();
+ }
+ }
+
+ private static void copyField(FlowModeAbstractMojo sourceMojo,
+ Object targetMojo, Field sourceField, Class> targetClass)
+ throws IllegalAccessException, NoSuchFieldException {
+ if (Modifier.isStatic(sourceField.getModifiers())) {
+ return;
+ }
+ sourceField.setAccessible(true);
+ Object value = sourceField.get(sourceMojo);
+ if (value == null) {
+ return;
+ }
+ Field targetField;
+ try {
+ targetField = targetClass.getDeclaredField(sourceField.getName());
+ } catch (NoSuchFieldException ex) {
+ // Should never happen, since the class definition should be
+ // the same
+ String message = "Field " + sourceField.getName() + " defined in "
+ + sourceField.getDeclaringClass().getName()
+ + " is missing in " + targetClass.getName();
+ sourceMojo.logError(message, ex);
+ throw ex;
+ }
+
+ Class> targetFieldType = targetField.getType();
+ if (!targetFieldType.isAssignableFrom(sourceField.getType())) {
+ String message = "Field " + targetFieldType.getName() + " in class "
+ + targetClass.getName() + " of type "
+ + targetFieldType.getName()
+ + " is loaded from different class loaders."
+ + " Source class loader: "
+ + sourceField.getType().getClassLoader()
+ + ", Target class loader: "
+ + targetFieldType.getClassLoader()
+ + ". This is likely a bug in the Vaadin Maven plugin."
+ + " Please, report the error on the issue tracker.";
+ sourceMojo.logError(message);
+ throw new NoSuchFieldException(message);
+ }
+ targetField.setAccessible(true);
+ targetField.set(targetMojo, value);
+ }
+
+ private static Field findField(Class> clazz, String fieldName)
+ throws NoSuchFieldException {
+ while (clazz != null && !clazz.equals(Object.class)) {
+ try {
+ return clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ clazz = clazz.getSuperclass();
+ }
+ }
+ throw new NoSuchFieldException(fieldName);
+ }
+
+}
\ No newline at end of file
diff --git a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java
index e3b3ac56455..d5f32136ea5 100644
--- a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java
+++ b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java
@@ -29,29 +29,26 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import com.vaadin.flow.di.Lookup;
-import com.vaadin.flow.internal.StringUtil;
-import com.vaadin.flow.plugin.TestUtils;
-import com.vaadin.flow.server.Constants;
-import com.vaadin.flow.server.InitParameters;
-import com.vaadin.flow.server.frontend.EndpointGeneratorTaskFactory;
-import com.vaadin.flow.server.frontend.FrontendTools;
-import com.vaadin.flow.server.frontend.FrontendUtils;
-import com.vaadin.flow.server.frontend.installer.NodeInstaller;
-import com.vaadin.flow.server.frontend.scanner.ClassFinder;
-import elemental.json.Json;
-import elemental.json.JsonObject;
-import elemental.json.impl.JsonUtil;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.DefaultArtifactHandler;
import org.apache.maven.model.Build;
+import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.ReflectionUtils;
import org.junit.After;
@@ -62,7 +59,20 @@
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
-import static java.io.File.pathSeparator;
+import com.vaadin.flow.di.Lookup;
+import com.vaadin.flow.internal.StringUtil;
+import com.vaadin.flow.plugin.TestUtils;
+import com.vaadin.flow.server.Constants;
+import com.vaadin.flow.server.InitParameters;
+import com.vaadin.flow.server.frontend.EndpointGeneratorTaskFactory;
+import com.vaadin.flow.server.frontend.FrontendTools;
+import com.vaadin.flow.server.frontend.FrontendUtils;
+import com.vaadin.flow.server.frontend.installer.NodeInstaller;
+import com.vaadin.flow.server.frontend.scanner.ClassFinder;
+
+import elemental.json.Json;
+import elemental.json.JsonObject;
+import elemental.json.impl.JsonUtil;
import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
import static com.vaadin.flow.server.Constants.TARGET;
@@ -79,8 +89,7 @@
import static com.vaadin.flow.server.frontend.FrontendUtils.TOKEN_FILE;
import static com.vaadin.flow.server.frontend.FrontendUtils.VITE_CONFIG;
import static com.vaadin.flow.server.frontend.FrontendUtils.VITE_GENERATED_CONFIG;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static java.io.File.pathSeparator;
public class BuildFrontendMojoTest {
public static final String TEST_PROJECT_RESOURCE_JS = "test_project_resource.js";
@@ -221,16 +230,48 @@ public void teardown() throws IOException {
static void setProject(AbstractMojo mojo, File baseFolder)
throws Exception {
- Build buildMock = mock(Build.class);
- when(buildMock.getFinalName()).thenReturn("finalName");
- MavenProject project = mock(MavenProject.class);
- Mockito.when(project.getGroupId()).thenReturn("com.vaadin.testing");
- Mockito.when(project.getArtifactId()).thenReturn("my-application");
- when(project.getBasedir()).thenReturn(baseFolder);
- when(project.getBuild()).thenReturn(buildMock);
- when(project.getRuntimeClasspathElements())
- .thenReturn(getClassPath(baseFolder.toPath()));
+ mojo.setPluginContext(new HashMap<>());
+
+ MavenProject project = new MavenProject();
+ project.setGroupId("com.vaadin.testing");
+ project.setArtifactId("my-application");
+ project.setFile(baseFolder.toPath().resolve("pom.xml").toFile());
+ project.setBuild(new Build());
+ project.getBuild().setFinalName("finalName");
+
+ List classPath = getClassPath(baseFolder.toPath()).stream()
+ // Exclude maven jars so classes will be loaded by them fake
+ // maven.api realm that will be the same for the test class
+ // and the mojo execution
+ .filter(path -> !path.matches(".*([\\\\/])maven-.*\\.jar"))
+ .toList();
+ AtomicInteger dependencyCounter = new AtomicInteger();
+ project.setArtifacts(classPath.stream().map(path -> {
+ DefaultArtifactHandler artifactHandler = new DefaultArtifactHandler();
+ artifactHandler.setAddedToClasspath(true);
+ DefaultArtifact artifact = new DefaultArtifact("com.vaadin.testing",
+ "dep-" + dependencyCounter.incrementAndGet(), "1.0",
+ "compile", "jar", null, artifactHandler);
+ artifact.setFile(new File(path));
+ return artifact;
+ }).collect(Collectors.toSet()));
ReflectionUtils.setVariableValueInObject(mojo, "project", project);
+
+ ClassWorld classWorld = new ClassWorld();
+ ClassRealm mavenApiRealm = classWorld.newRealm("maven.api", null);
+ mavenApiRealm.importFrom(MavenProject.class.getClassLoader(), "");
+ ClassRealm pluginClassRealm = classWorld.newRealm("flow-plugin", null);
+
+ PluginDescriptor pluginDescriptor = new PluginDescriptor();
+ pluginDescriptor.setArtifacts(List.of());
+ pluginDescriptor.setClassRealm(pluginClassRealm);
+ pluginDescriptor.setPlugin(new Plugin());
+ pluginDescriptor.setClassRealm(pluginClassRealm);
+ MojoDescriptor mojoDescriptor = new MojoDescriptor();
+ mojoDescriptor.setPluginDescriptor(pluginDescriptor);
+ MojoExecution mojoExecution = new MojoExecution(mojoDescriptor);
+ ReflectionUtils.setVariableValueInObject(mojo, "mojoExecution",
+ mojoExecution);
}
@Test
diff --git a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/CleanFrontendMojoTest.java b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/CleanFrontendMojoTest.java
index 47213c76d1e..4008091191b 100644
--- a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/CleanFrontendMojoTest.java
+++ b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/CleanFrontendMojoTest.java
@@ -22,6 +22,7 @@
import java.nio.file.Paths;
import java.util.Arrays;
+import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
@@ -112,7 +113,8 @@ public void mavenGoal_when_packageJsonMissing() throws Exception {
}
@Test
- public void should_removeNodeModulesFolder() throws MojoFailureException {
+ public void should_removeNodeModulesFolder()
+ throws MojoFailureException, MojoExecutionException {
final File nodeModules = new File(projectBase, NODE_MODULES);
Assert.assertTrue("Failed to create 'node_modules'",
nodeModules.mkdirs());
@@ -123,7 +125,7 @@ public void should_removeNodeModulesFolder() throws MojoFailureException {
@Test
public void should_notRemoveNodeModulesFolder_hilla()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
enableHilla();
final File nodeModules = new File(projectBase, NODE_MODULES);
Assert.assertTrue("Failed to create 'node_modules'",
@@ -135,7 +137,7 @@ public void should_notRemoveNodeModulesFolder_hilla()
@Test
public void should_removeCompressedDevBundle()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
final File devBundleDir = new File(projectBase,
Constants.BUNDLE_LOCATION);
final File devBundle = new File(projectBase,
@@ -150,7 +152,8 @@ public void should_removeCompressedDevBundle()
}
@Test
- public void should_removeOldDevBundle() throws MojoFailureException {
+ public void should_removeOldDevBundle()
+ throws MojoFailureException, MojoExecutionException {
final File devBundleDir = new File(projectBase, "src/main/dev-bundle/");
Assert.assertTrue("Failed to create 'dev-bundle' folder",
devBundleDir.mkdirs());
@@ -161,7 +164,7 @@ public void should_removeOldDevBundle() throws MojoFailureException {
@Test
public void should_removeFrontendGeneratedFolder()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
Assert.assertTrue("Failed to create 'frontend/generated'",
frontendGenerated.mkdirs());
FileUtils.fileWrite(new File(frontendGenerated, "my_theme.js"),
@@ -175,7 +178,8 @@ public void should_removeFrontendGeneratedFolder()
@Test
public void should_removeGeneratedFolderForCustomFrontendFolder()
- throws MojoFailureException, IOException, IllegalAccessException {
+ throws MojoFailureException, IOException, IllegalAccessException,
+ MojoExecutionException {
File customFrontendFolder = new File(projectBase, "src/main/frontend");
File customFrontendGenerated = new File(customFrontendFolder,
@@ -199,7 +203,7 @@ public void should_removeGeneratedFolderForCustomFrontendFolder()
@Test
public void should_removeNpmPackageLockFile()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
final File packageLock = new File(projectBase, "package-lock.json");
FileUtils.fileWrite(packageLock, "{ \"fake\": \"lock\"}");
@@ -210,7 +214,7 @@ public void should_removeNpmPackageLockFile()
@Test
public void should_notRemoveNpmPackageLockFile_hilla()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
enableHilla();
final File packageLock = new File(projectBase, "package-lock.json");
FileUtils.fileWrite(packageLock, "{ \"fake\": \"lock\"}");
@@ -222,7 +226,7 @@ public void should_notRemoveNpmPackageLockFile_hilla()
@Test
public void should_removePnpmFile()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
final File pnpmFile = new File(projectBase, ".pnpmfile.cjs");
FileUtils.fileWrite(pnpmFile, "{ \"fake\": \"pnpmfile\"}");
@@ -232,7 +236,7 @@ public void should_removePnpmFile()
@Test
public void should_removePnpmPackageLockFile()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
final File pnpmLock = new File(projectBase, "pnpm-lock.yaml");
FileUtils.fileWrite(pnpmLock, "lockVersion: -1");
mojo.execute();
@@ -241,7 +245,7 @@ public void should_removePnpmPackageLockFile()
@Test
public void should_cleanPackageJson_removeVaadinAndHashObjects()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
JsonObject json = createInitialPackageJson();
FileUtils.fileWrite(packageJson, json.toJson());
mojo.execute();
@@ -257,7 +261,7 @@ public void should_cleanPackageJson_removeVaadinAndHashObjects()
@Test
public void should_cleanPackageJson_removeVaadinDependenciesInOverrides()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
JsonObject json = createInitialPackageJson(true);
FileUtils.fileWrite(packageJson, json.toJson());
@@ -272,7 +276,7 @@ public void should_cleanPackageJson_removeVaadinDependenciesInOverrides()
@Test
public void should_keepUserDependencies_whenPackageJsonEdited()
- throws MojoFailureException, IOException {
+ throws MojoFailureException, IOException, MojoExecutionException {
JsonObject json = createInitialPackageJson();
json.put("dependencies", Json.createObject());
json.getObject("dependencies").put("foo", "bar");
diff --git a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojoTest.java b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojoTest.java
index 00ac4b79d8d..ef7622a154d 100644
--- a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojoTest.java
+++ b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojoTest.java
@@ -7,7 +7,6 @@
import java.util.Set;
import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.ReflectionUtils;
import org.junit.Assert;
@@ -23,6 +22,7 @@
import com.vaadin.flow.server.frontend.FrontendTools;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
+import static com.vaadin.flow.plugin.maven.BuildFrontendMojoTest.setProject;
import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR;
@@ -47,9 +47,7 @@ public class GenerateNpmBOMMojoTest {
public void setUp() throws Exception {
this.mojo = Mockito.spy(new GenerateNpmBOMMojo());
- MavenProject project = Mockito.mock(MavenProject.class);
File projectBase = temporaryFolder.getRoot();
- Mockito.when(project.getBasedir()).thenReturn(projectBase);
File frontendDirectory = new File(projectBase, DEFAULT_FRONTEND_DIR);
resourceOutputDirectory = new File(projectBase,
VAADIN_SERVLET_RESOURCES);
@@ -84,7 +82,6 @@ public void setUp() throws Exception {
ReflectionUtils.setVariableValueInObject(mojo, "packageManifest",
manifestFilePath);
ReflectionUtils.setVariableValueInObject(mojo, "specVersion", "1.4");
- ReflectionUtils.setVariableValueInObject(mojo, "project", project);
ReflectionUtils.setVariableValueInObject(mojo, "frontendDirectory",
frontendDirectory);
ReflectionUtils.setVariableValueInObject(mojo, "projectBasedir",
@@ -96,8 +93,9 @@ public void setUp() throws Exception {
ReflectionUtils.setVariableValueInObject(mojo, "npmFolder",
projectBase);
ReflectionUtils.setVariableValueInObject(mojo, "productionMode", false);
- Mockito.when(mojo.getJarFiles()).thenReturn(
- Set.of(jarResourcesSource.getParentFile().getParentFile()));
+ Mockito.doReturn(
+ Set.of(jarResourcesSource.getParentFile().getParentFile()))
+ .when(mojo).getJarFiles();
FileUtils.fileWrite(manifestFilePath, "UTF-8",
TestUtils.getInitialPackageJson().toJson());
@@ -109,6 +107,10 @@ public void setUp() throws Exception {
.lookup(ClassFinder.class);
return lookup;
}).when(mojo).createLookup(Mockito.any(ClassFinder.class));
+
+ setProject(mojo, projectBase);
+ // Prevent unwanted resources to be present on classpath
+ mojo.project.setArtifacts(Set.of());
}
@Test
diff --git a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojoTest.java b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojoTest.java
index e94e48d1915..98f75dc5d3f 100644
--- a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojoTest.java
+++ b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/PrepareFrontendMojoTest.java
@@ -74,7 +74,6 @@ public class PrepareFrontendMojoTest {
private File defaultJavaSource;
private File defaultJavaResource;
private File generatedTsFolder;
- private MavenProject project;
@Before
public void setup() throws Exception {
@@ -84,18 +83,6 @@ public void setup() throws Exception {
tokenFile = new File(temporaryFolder.getRoot(),
VAADIN_SERVLET_RESOURCES + TOKEN_FILE);
- project = Mockito.mock(MavenProject.class);
-
- List packages = Arrays
- .stream(System.getProperty("java.class.path")
- .split(File.pathSeparatorChar + ""))
- .collect(Collectors.toList());
- Mockito.when(project.getRuntimeClasspathElements())
- .thenReturn(packages);
- Mockito.when(project.getCompileClasspathElements())
- .thenReturn(Collections.emptyList());
- Mockito.when(project.getBasedir()).thenReturn(projectBase);
-
packageJson = new File(projectBase, PACKAGE_JSON).getAbsolutePath();
webpackOutputDirectory = new File(projectBase, VAADIN_WEBAPP_RESOURCES);
resourceOutputDirectory = new File(projectBase,
@@ -271,8 +258,8 @@ public void should_updateAndKeepDependencies_when_packageJsonExists()
public void jarPackaging_copyProjectFrontendResources()
throws MojoExecutionException, MojoFailureException,
IllegalAccessException {
- Mockito.when(project.getPackaging()).thenReturn("jar");
-
+ mojo.project.setPackaging("jar");
+ MavenProject project = Mockito.spy(mojo.project);
ReflectionUtils.setVariableValueInObject(mojo, "project", project);
mojo.execute();
diff --git a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/ReflectorTest.java b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/ReflectorTest.java
new file mode 100644
index 00000000000..8f6dc96b0ad
--- /dev/null
+++ b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/ReflectorTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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 com.vaadin.flow.plugin.maven;
+
+import javax.inject.Inject;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.DefaultArtifactHandler;
+import org.apache.maven.model.Build;
+import org.apache.maven.plugin.Mojo;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.flow.utils.FlowFileUtils;
+
+import static com.vaadin.flow.plugin.maven.BuildFrontendMojoTest.getClassPath;
+import static com.vaadin.flow.utils.FlowFileUtils.convertToUrl;
+
+public class ReflectorTest {
+
+ Reflector reflector;
+
+ @Before
+ public void setUp() {
+ ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
+ URLClassLoader urlClassLoader = new URLClassLoader(
+ getClassPath(Path.of(".")).stream().distinct().map(File::new)
+ .map(FlowFileUtils::convertToUrl).toArray(URL[]::new),
+ ClassLoader.getPlatformClassLoader()) {
+ @Override
+ protected Class> findClass(String name)
+ throws ClassNotFoundException {
+ // For test purposes, make maven API are loaded from shared
+ // class loader
+ if (!name.startsWith("com.vaadin.flow.plugin.maven.")) {
+ return systemClassLoader.loadClass(name);
+ }
+ return super.findClass(name);
+ }
+ };
+ reflector = new Reflector(urlClassLoader);
+ }
+
+ @Test
+ public void createMojo_createInstanceAndCopyFields() throws Exception {
+ MyMojo source = new MyMojo();
+ source.fillFields();
+ Mojo target = reflector.createMojo(source);
+ MatcherAssert.assertThat("foo field", target,
+ Matchers.hasProperty("foo", Matchers.equalTo(source.foo)));
+ MatcherAssert.assertThat("bar field", target,
+ Matchers.hasProperty("bar", Matchers.equalTo(source.bar)));
+ MatcherAssert.assertThat("notAnnotated field", target,
+ Matchers.hasProperty("notAnnotated",
+ Matchers.equalTo(source.notAnnotated)));
+ MatcherAssert.assertThat("mojoExecution field", target,
+ Matchers.hasProperty("mojoExecution",
+ Matchers.equalTo(source.mojoExecution)));
+ MatcherAssert.assertThat("maven project field", target, Matchers
+ .hasProperty("project", Matchers.equalTo(source.project)));
+ MatcherAssert.assertThat("classFinder field", target,
+ Matchers.hasProperty("classFinder", Matchers.notNullValue()));
+ }
+
+ @Test
+ public void createMojo_subclass_createInstanceAndCopyFields()
+ throws Exception {
+ SubClassMojo source = new SubClassMojo();
+ source.fillFields();
+ Mojo target = reflector.createMojo(source);
+ MatcherAssert.assertThat("foo field", target,
+ Matchers.hasProperty("foo", Matchers.equalTo(source.foo)));
+ MatcherAssert.assertThat("bar field", target,
+ Matchers.hasProperty("bar", Matchers.equalTo(source.bar)));
+ MatcherAssert.assertThat("childProperty field", target,
+ Matchers.hasProperty("childProperty",
+ Matchers.equalTo(source.childProperty)));
+ MatcherAssert.assertThat("notAnnotated field", target,
+ Matchers.hasProperty("notAnnotated",
+ Matchers.equalTo(source.notAnnotated)));
+ MatcherAssert.assertThat("mojoExecution field", target,
+ Matchers.hasProperty("mojoExecution",
+ Matchers.equalTo(source.mojoExecution)));
+ MatcherAssert.assertThat("maven project field", target, Matchers
+ .hasProperty("project", Matchers.equalTo(source.project)));
+ MatcherAssert.assertThat("classFinder field", target,
+ Matchers.hasProperty("classFinder", Matchers.notNullValue()));
+ }
+
+ @Test
+ public void createMojo_incompatibleFields_fails() {
+ IncompatibleFieldsMojo source = new IncompatibleFieldsMojo();
+ source.fillFields();
+ NoSuchFieldException exception = Assert.assertThrows(
+ NoSuchFieldException.class, () -> reflector.createMojo(source));
+ Assert.assertTrue(
+ "Expected exception to be thrown because of class loader mismatch",
+ exception.getMessage()
+ .contains("loaded from different class loaders"));
+ }
+
+ @Test
+ public void reflector_fromProject_getsIsolatedClassLoader()
+ throws Exception {
+ String outputDirectory = "/my/project/target";
+
+ MavenProject project = new MavenProject();
+ project.setGroupId("com.vaadin.test");
+ project.setArtifactId("reflector-tests");
+ project.setBuild(new Build());
+ project.getBuild().setOutputDirectory(outputDirectory);
+ project.setArtifacts(Set.of(
+ createArtifact("com.vaadin.test", "compile", "1.0", "compile",
+ true),
+ createArtifact("com.vaadin.test", "provided", "1.0", "provided",
+ true),
+ createArtifact("com.vaadin.test", "test", "1.0", "test", true),
+ createArtifact("com.vaadin.test", "system", "1.0", "system",
+ true),
+ createArtifact("com.vaadin.test", "not-classpath", "1.0",
+ "compile", false)));
+
+ MojoExecution mojoExecution = new MojoExecution(new MojoDescriptor());
+ PluginDescriptor pluginDescriptor = new PluginDescriptor();
+ mojoExecution.getMojoDescriptor().setPluginDescriptor(pluginDescriptor);
+ pluginDescriptor.setGroupId("com.vaadin.test");
+ pluginDescriptor.setArtifactId("test-plugin");
+ pluginDescriptor.setArtifacts(List.of(
+ createArtifact("com.vaadin.test", "plugin", "1.0", "compile",
+ true),
+ createArtifact("com.vaadin.test", "compile", "2.0", "compile",
+ true)));
+ ClassWorld classWorld = new ClassWorld("maven.api", null);
+ classWorld.getRealm("maven.api")
+ .addURL(Path
+ .of("src", "test", "resources",
+ "jar-without-frontend-resources.jar")
+ .toUri().toURL());
+ // .addURL(new URL("file:///some/flat/maven-repo/maven-api.jar"));
+ pluginDescriptor.setClassRealm(classWorld.newRealm("maven-plugin"));
+
+ Reflector execReflector = Reflector.of(project, mojoExecution);
+
+ URLClassLoader isolatedClassLoader = execReflector
+ .getIsolatedClassLoader();
+
+ Set urlSet = Set.of(isolatedClassLoader.getURLs());
+ Assert.assertEquals(4, urlSet.size());
+ Assert.assertTrue(
+ urlSet.contains(convertToUrl(new File(outputDirectory))));
+ Assert.assertTrue(urlSet.contains(convertToUrl(new File(
+ "/some/flat/maven-repo/com.vaadin.test-compile-1.0.jar"))));
+ Assert.assertTrue(urlSet.contains(convertToUrl(new File(
+ "/some/flat/maven-repo/com.vaadin.test-system-1.0.jar"))));
+ Assert.assertTrue(urlSet.contains(convertToUrl(new File(
+ "/some/flat/maven-repo/com.vaadin.test-plugin-1.0.jar"))));
+
+ // from platform class loader
+ Assert.assertNotNull(
+ isolatedClassLoader.loadClass("java.net.http.HttpClient"));
+ // from maven.api class loader
+ Assert.assertNotNull(
+ isolatedClassLoader.getResource("org/json/CookieList.class"));
+ Assert.assertNotNull(
+ isolatedClassLoader.loadClass("org.json.CookieList"));
+ }
+
+ private Artifact createArtifact(String groupId, String artifactId,
+ String version, String scope, boolean addedToClasspath) {
+ DefaultArtifactHandler artifactHandler = new DefaultArtifactHandler();
+ artifactHandler.setAddedToClasspath(addedToClasspath);
+ DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId,
+ version, scope, "jar", null, artifactHandler);
+ artifact.setFile(
+ new File(String.format("/some/flat/maven-repo/%s-%s-%s.jar",
+ groupId, artifactId, version)));
+ return artifact;
+ }
+
+ public static class MyMojo extends FlowModeAbstractMojo {
+
+ @Parameter
+ String foo;
+
+ @Parameter
+ Boolean bar;
+
+ String notAnnotated = "NOT ANNOTATED";
+
+ public MyMojo() {
+ project = new MavenProject();
+ project.setGroupId("com.vaadin.test");
+ project.setArtifactId("reflector-tests");
+ }
+
+ void fillFields() {
+ mojoExecution = new MojoExecution(new MojoDescriptor());
+ project = new MavenProject();
+ foo = "foo";
+ bar = true;
+ }
+
+ protected void executeInternal() {
+
+ }
+
+ public String getFoo() {
+ return foo;
+ }
+
+ public Boolean getBar() {
+ return bar;
+ }
+
+ public String getNotAnnotated() {
+ return notAnnotated;
+ }
+
+ public MojoExecution getMojoExecution() {
+ return mojoExecution;
+ }
+
+ public MavenProject getProject() {
+ return project;
+ }
+
+ }
+
+ public static class SubClassMojo extends MyMojo {
+
+ @Parameter
+ private String childProperty;
+
+ @Override
+ void fillFields() {
+ super.fillFields();
+ childProperty = "CHILD";
+ }
+
+ public String getChildProperty() {
+ return childProperty;
+ }
+ }
+
+ public static class FakeMavenComponent {
+ }
+
+ public static class IncompatibleFieldsMojo extends MyMojo {
+
+ @Inject
+ private FakeMavenComponent buildContext;
+
+ @Override
+ void fillFields() {
+ super.fillFields();
+ buildContext = new FakeMavenComponent();
+ }
+
+ public FakeMavenComponent getBuildContext() {
+ return buildContext;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/flow-plugins/flow-plugin-base/pom.xml b/flow-plugins/flow-plugin-base/pom.xml
index 06467c40bfc..c423e286db8 100644
--- a/flow-plugins/flow-plugin-base/pom.xml
+++ b/flow-plugins/flow-plugin-base/pom.xml
@@ -3,7 +3,7 @@
com.vaadinflow-plugins
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-plugin-base
diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java
index e496e70fda9..eaca7600e15 100644
--- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java
+++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java
@@ -358,7 +358,9 @@ public static void runNodeUpdater(PluginAdapterBuild adapter)
.withForceProductionBuild(adapter.forceProductionBuild())
.withReact(adapter.isReactEnabled())
.withNpmExcludeWebComponents(
- adapter.isNpmExcludeWebComponents());
+ adapter.isNpmExcludeWebComponents())
+ .withFrontendExtraFileExtensions(
+ adapter.frontendExtraFileExtensions());
new NodeTasks(options).execute();
} catch (ExecutionFailedException exception) {
throw exception;
diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java
index d084c6ca631..3e795ebd948 100644
--- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java
+++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java
@@ -349,9 +349,9 @@ default Lookup createLookup(ClassFinder classFinder) {
List frontendExtraFileExtensions();
/**
- * Whether to include web component npm packages in packages.json.
+ * Whether to exclude Vaadin web component npm packages in packages.json.
*
- * @return {@code true} to include web component npm packages.
+ * @return {@code true} to exclude Vaadin web component npm packages.
*/
boolean isNpmExcludeWebComponents();
}
diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java
index 75cbb65a381..35aff4716a4 100644
--- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java
+++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java
@@ -56,8 +56,12 @@ public class ReflectionsClassFinder implements ClassFinder {
* the list of urls for finding classes.
*/
public ReflectionsClassFinder(URL... urls) {
- classLoader = new URLClassLoader(urls,
- Thread.currentThread().getContextClassLoader());
+ this(new URLClassLoader(urls,
+ Thread.currentThread().getContextClassLoader()), urls);
+ }
+
+ public ReflectionsClassFinder(ClassLoader classLoader, URL... urls) {
+ this.classLoader = classLoader;
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
.addClassLoaders(classLoader).setExpandSuperTypes(false)
.addUrls(urls);
diff --git a/flow-plugins/pom.xml b/flow-plugins/pom.xml
index 65042d8aeec..a68a5d18409 100644
--- a/flow-plugins/pom.xml
+++ b/flow-plugins/pom.xml
@@ -5,7 +5,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-pluginspom
diff --git a/flow-polymer-template/pom.xml b/flow-polymer-template/pom.xml
index 8338465924d..356484f8904 100644
--- a/flow-polymer-template/pom.xml
+++ b/flow-polymer-template/pom.xml
@@ -6,7 +6,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-polymer-templateFlow Polymer Templates Support
diff --git a/flow-polymer2lit/README.md b/flow-polymer2lit/README.md
index 1becc8e266c..afa063cf894 100644
--- a/flow-polymer2lit/README.md
+++ b/flow-polymer2lit/README.md
@@ -42,7 +42,7 @@ mvn vaadin:convert-polymer
To convert a project that is based on Vaadin < 24, use the full Maven goal:
```bash
-mvn com.vaadin:vaadin-maven-plugin:24.6-SNAPSHOT:convert-polymer
+mvn com.vaadin:vaadin-maven-plugin:24.7-SNAPSHOT:convert-polymer
```
Or, in the case of using Gradle, add the following to `build.gradle`:
@@ -50,7 +50,7 @@ Or, in the case of using Gradle, add the following to `build.gradle`:
```gradle
buildscript {
repositories {
- classpath 'com.vaadin:flow-gradle-plugin:24.6-SNAPSHOT'
+ classpath 'com.vaadin:flow-gradle-plugin:24.7-SNAPSHOT'
}
}
```
diff --git a/flow-polymer2lit/pom.xml b/flow-polymer2lit/pom.xml
index 627d9cc42a9..8483076eefc 100644
--- a/flow-polymer2lit/pom.xml
+++ b/flow-polymer2lit/pom.xml
@@ -6,7 +6,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-polymer2litPolymer to Lit converter
diff --git a/flow-push/pom.xml b/flow-push/pom.xml
index 1d4c5101d8e..c78c2737958 100644
--- a/flow-push/pom.xml
+++ b/flow-push/pom.xml
@@ -5,7 +5,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-pushFlow Push
diff --git a/flow-react/pom.xml b/flow-react/pom.xml
index bff702aaba9..04e4de5fecf 100644
--- a/flow-react/pom.xml
+++ b/flow-react/pom.xml
@@ -6,7 +6,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-react
diff --git a/flow-server-production-mode/pom.xml b/flow-server-production-mode/pom.xml
index 7949d2685aa..0d1e56a563b 100644
--- a/flow-server-production-mode/pom.xml
+++ b/flow-server-production-mode/pom.xml
@@ -5,7 +5,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-server-production-modeFlow Server Production Mode
diff --git a/flow-server/pom.xml b/flow-server/pom.xml
index f84d973fdfc..102dccace2e 100644
--- a/flow-server/pom.xml
+++ b/flow-server/pom.xml
@@ -5,7 +5,7 @@
com.vaadinflow-project
- 24.6-SNAPSHOT
+ 24.7-SNAPSHOTflow-serverFlow Server
diff --git a/flow-server/src/main/java/com/vaadin/experimental/FeatureFlags.java b/flow-server/src/main/java/com/vaadin/experimental/FeatureFlags.java
index a2431265070..f87af5ef39f 100644
--- a/flow-server/src/main/java/com/vaadin/experimental/FeatureFlags.java
+++ b/flow-server/src/main/java/com/vaadin/experimental/FeatureFlags.java
@@ -67,11 +67,6 @@ public class FeatureFlags implements Serializable {
"collaborationEngineBackend",
"https://github.com/vaadin/platform/issues/1988", true, null);
- public static final Feature WEB_PUSH = new Feature(
- "Server side WebPush API", "webPush",
- "https://vaadin.com/docs/latest/configuration/setting-up-webpush",
- true, "com.vaadin.flow.server.webpush.WebPush");
-
public static final Feature FORM_FILLER_ADDON = new Feature(
"Form Filler Add-on", "formFillerAddon",
"https://github.com/vaadin/form-filler-addon", true,
@@ -90,6 +85,11 @@ public class FeatureFlags implements Serializable {
"Hilla Full-stack Signals", "fullstackSignals",
"https://github.com/vaadin/hilla/discussions/1902", true, null);
+ public static final Feature DASHBOARD_COMPONENT = new Feature(
+ "Dashboard component (Pro)", "dashboardComponent",
+ "https://github.com/vaadin/platform/issues/6626", true,
+ "com.vaadin.flow.component.dashboard.Dashboard");
+
private List features = new ArrayList<>();
File propertiesFolder = null;
@@ -112,11 +112,11 @@ public FeatureFlags(Lookup lookup) {
this.lookup = lookup;
features.add(new Feature(EXAMPLE));
features.add(new Feature(COLLABORATION_ENGINE_BACKEND));
- features.add(new Feature(WEB_PUSH));
features.add(new Feature(FORM_FILLER_ADDON));
features.add(new Feature(HILLA_I18N));
features.add(new Feature(HILLA_FULLSTACK_SIGNALS));
features.add(new Feature(COPILOT_EXPERIMENTAL));
+ features.add(new Feature(DASHBOARD_COMPONENT));
loadProperties();
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentTracker.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentTracker.java
index 611424b5d94..5e0ec629f85 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentTracker.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentTracker.java
@@ -294,20 +294,47 @@ public static void trackAttach(Component component) {
*/
public static void refreshLocation(Location location, int offset) {
refreshLocation(createLocation, location, offset);
+ refreshLocations(createLocations, location, offset);
refreshLocation(attachLocation, location, offset);
+ refreshLocations(attachLocations, location, offset);
+ }
+
+ private static boolean needsUpdate(Location l, Location referenceLocation) {
+ return Objects.equals(l.className, referenceLocation.className)
+ && l.lineNumber > referenceLocation.lineNumber;
+ }
+
+ private static Location updateLocation(Location l, int offset) {
+ return new Location(l.className, l.filename, l.methodName,
+ l.lineNumber + offset);
}
private static void refreshLocation(Map targetRef,
- Location location, int offset) {
+ Location referenceLocation, int offset) {
Map updatedLocations = new HashMap<>();
- targetRef.entrySet().stream().filter(
- e -> Objects.equals(e.getValue().className, location.className))
- .filter(e -> e.getValue().lineNumber > location.lineNumber)
- .forEach(e -> {
- Location l = e.getValue();
- updatedLocations.put(e.getKey(), new Location(l.className,
- l.filename, l.methodName, l.lineNumber + offset));
- });
+ for (Component c : targetRef.keySet()) {
+ Location l = targetRef.get(c);
+ if (needsUpdate(l, referenceLocation)) {
+ updatedLocations.put(c, updateLocation(l, offset));
+ }
+ }
+
+ targetRef.putAll(updatedLocations);
+ }
+
+ private static void refreshLocations(Map targetRef,
+ Location referenceLocation, int offset) {
+ Map updatedLocations = new HashMap<>();
+ for (Component c : targetRef.keySet()) {
+ Location[] locations = targetRef.get(c);
+
+ for (int i = 0; i < locations.length; i++) {
+ if (needsUpdate(locations[i], referenceLocation)) {
+ locations[i] = updateLocation(locations[i], offset);
+ }
+ }
+ }
+
targetRef.putAll(updatedLocations);
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java b/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java
index cfd7c2e7885..63944c2a540 100644
--- a/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java
+++ b/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java
@@ -25,6 +25,7 @@
import com.vaadin.flow.router.NavigationStateBuilder;
import com.vaadin.flow.router.NotFoundException;
import com.vaadin.flow.router.RouteResolver;
+import com.vaadin.flow.router.RouterLayout;
import com.vaadin.flow.server.RouteRegistry;
import com.vaadin.flow.internal.menu.MenuRegistry;
import com.vaadin.flow.server.menu.AvailableViewInfo;
@@ -58,15 +59,16 @@ public NavigationState resolve(ResolveRequest request) {
: "/" + clientPath);
if (viewInfo != null && viewInfo.flowLayout()) {
- Class extends Component> layout = (Class extends Component>) registry
+ Class extends RouterLayout> layout = registry
.getLayout(path);
if (layout == null) {
throw new NotFoundException(
"No layout for client path '%s'"
.formatted(path));
}
- RouteTarget target = new RouteTarget(layout,
- Collections.emptyList());
+ RouteTarget target = new RouteTarget(
+ (Class extends Component>) layout, RouteUtil
+ .getParentLayoutsForNonRouteTarget(layout));
navigationResult = new NavigationRouteTarget(
navigationResult.getPath(), target,
Collections.emptyMap());
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/Constants.java b/flow-server/src/main/java/com/vaadin/flow/server/Constants.java
index 25e15f8b9e2..edc8e74af5c 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/Constants.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/Constants.java
@@ -185,6 +185,11 @@ public final class Constants implements Serializable {
*/
public static final String VAADIN_WEBAPP = "webapp/";
+ /**
+ * The generated PWA icons folder.
+ */
+ public static final String VAADIN_PWA_ICONS = "pwa-icons/";
+
/**
* The path to meta-inf/VAADIN/ where static resources are put on the
* servlet.
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java b/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java
index 6b946bde642..9d877e52bdf 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java
@@ -16,12 +16,14 @@
package com.vaadin.flow.server;
import javax.imageio.ImageIO;
+
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
@@ -99,6 +101,15 @@ public enum Domain {
setRelativeName();
}
+ protected PwaIcon(PwaIcon icon) {
+ this.width = icon.width;
+ this.height = icon.height;
+ this.baseName = icon.baseName;
+ this.domain = icon.domain;
+ this.shouldBeCached = icon.shouldBeCached;
+ this.attributes.putAll(icon.attributes);
+ }
+
/**
* Gets an {@link Element} presentation of the icon.
*
@@ -236,6 +247,25 @@ public void setImage(BufferedImage image) {
}
}
+ void setImage(InputStream image) throws IOException {
+ if (image != null) {
+ data = image.readAllBytes();
+ fileHash = Arrays.hashCode(data);
+ setRelativeName();
+ }
+ }
+
+ /**
+ * Gets if the icon can be written on a stream or not.
+ *
+ * @return {@literal true} if the icon can be written, otherwise
+ * {@literal false}.
+ * @see #write(OutputStream)
+ */
+ boolean isAvailable() {
+ return data != null || registry.getBaseImage() != null;
+ }
+
/**
* Writes the icon image to output stream.
*
@@ -246,7 +276,7 @@ public void write(OutputStream outputStream) {
if (data == null) {
// New image with wanted size
// Store byte array and hashcode of image (GeneratedImage)
- setImage(drawIconImage(registry.getBaseImage()));
+ setImage(drawIconImage(getBaseImage()));
}
try {
outputStream.write(data);
@@ -257,6 +287,11 @@ public void write(OutputStream outputStream) {
}
}
+ // visible for test
+ protected BufferedImage getBaseImage() {
+ return registry.getBaseImage();
+ }
+
private BufferedImage drawIconImage(BufferedImage baseImage) {
// Pick top-left pixel as fill color if needed for image
// resizing
@@ -296,4 +331,5 @@ private BufferedImage drawIconImage(BufferedImage baseImage) {
graphics.dispose();
return bimage;
}
+
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java
index 6446891c917..e759c5ff22d 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java
@@ -15,12 +15,10 @@
*/
package com.vaadin.flow.server;
-import javax.imageio.ImageIO;
import jakarta.servlet.ServletContext;
-import java.awt.Color;
-import java.awt.Graphics2D;
-import java.awt.Image;
+import javax.imageio.ImageIO;
+
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
@@ -29,21 +27,21 @@
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
-import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.vaadin.flow.di.Lookup;
+import com.vaadin.flow.di.ResourceProvider;
import com.vaadin.flow.server.communication.PwaHandler;
import com.vaadin.flow.server.startup.ApplicationConfiguration;
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;
@@ -85,6 +83,7 @@ public class PwaRegistry implements Serializable {
private List icons = new ArrayList<>();
private final PwaConfiguration pwaConfiguration;
+ private URL baseImageUrl;
private BufferedImage baseImage;
/**
@@ -113,7 +112,21 @@ public PwaRegistry(PWA pwa, ServletContext servletContext)
initializeResources(servletContext);
}
+ // Lazy load base image to prevent using AWT api unless icon
+ // generation is required at runtime.
+ // baseImageUrl is computed during registry initialization and used on to
+ // load the image.
BufferedImage getBaseImage() {
+ if (baseImage == null && baseImageUrl != null) {
+ try {
+ baseImage = getBaseImage(baseImageUrl);
+ } catch (IOException ex) {
+ getLogger().error("Image is not found or can't be loaded: {}",
+ baseImageUrl);
+ } finally {
+ baseImageUrl = null;
+ }
+ }
return baseImage;
}
@@ -124,24 +137,19 @@ private void initializeResources(ServletContext servletContext)
}
long start = System.currentTimeMillis();
+ // Load base logo from servlet context if available
+ // fall back to local image if unavailable
URL logo = getResourceUrl(servletContext,
pwaConfiguration.relIconPath());
+ baseImageUrl = logo != null ? logo
+ : BootstrapHandler.class.getResource("default-logo.png");
URL offlinePage = pwaConfiguration.isOfflinePathEnabled()
? getResourceUrl(servletContext,
pwaConfiguration.relOfflinePath())
: null;
- // Load base logo from servlet context if available
- // fall back to local image if unavailable
- baseImage = getBaseImage(logo);
-
- if (baseImage == null) {
- getLogger().error("Image is not found or can't be loaded: " + logo);
- } else {
- // initialize icons
- icons = initializeIcons();
- }
+ icons = initializeIcons(servletContext);
// Load offline page as string, from servlet context if
// available, fall back to default page
@@ -175,14 +183,43 @@ private URL getResourceUrl(ServletContext context, String path)
return resourceUrl;
}
- private List initializeIcons() {
+ private List initializeIcons(ServletContext servletContext) {
+ Optional optionalResourceProvider = Optional
+ .ofNullable(new VaadinServletContext(servletContext)
+ .getAttribute(Lookup.class))
+ .map(lookup -> lookup.lookup(ResourceProvider.class));
for (PwaIcon icon : getIconTemplates(pwaConfiguration.getIconPath())) {
icon.setRegistry(this);
- icons.add(icon);
+ // Try to find a pre-generated image
+ String iconPath = Constants.VAADIN_WEBAPP_RESOURCES
+ + Constants.VAADIN_PWA_ICONS
+ + icon.getRelHref().substring(1);
+ optionalResourceProvider.ifPresent(
+ provider -> tryLoadGeneratedIcon(provider, icon, iconPath));
+ if (icon.isAvailable()) {
+ icons.add(icon);
+ }
}
return icons;
}
+ private static void tryLoadGeneratedIcon(ResourceProvider resourceProvider,
+ PwaIcon icon, String iconPath) {
+ URL iconResource = resourceProvider.getApplicationResource(iconPath);
+ if (iconResource != null) {
+ try (InputStream data = iconResource.openStream()) {
+ icon.setImage(data);
+ getLogger().trace("Loading generated PWA image from {}",
+ iconPath);
+ } catch (IOException ex) {
+ // Ignore, icon will be generated at runtime
+ getLogger().debug(
+ "Cannot load generated PWA image from {}. Icon will be regenerated at runtime.",
+ iconPath, ex);
+ }
+ }
+ }
+
/**
* Creates manifest.webmanifest json object.
*
@@ -443,7 +480,14 @@ public PwaConfiguration getPwaConfiguration() {
return pwaConfiguration;
}
- static List getIconTemplates(String baseName) {
+ /**
+ * Gets all PWA icon variants for the give base icon.
+ *
+ * @param baseName
+ * path of the base icon.
+ * @return list of PWA icons variants.
+ */
+ public static List getIconTemplates(String baseName) {
List icons = new ArrayList<>();
// Basic manifest icons for android support
icons.add(
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/SynchronizedRequestHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/SynchronizedRequestHandler.java
index d271b8a189f..a5432b00587 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/SynchronizedRequestHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/SynchronizedRequestHandler.java
@@ -15,7 +15,11 @@
*/
package com.vaadin.flow.server;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.Reader;
+import java.io.Serializable;
+import java.util.Optional;
/**
* RequestHandler which takes care of locking and unlocking of the VaadinSession
@@ -28,6 +32,21 @@
*/
public abstract class SynchronizedRequestHandler implements RequestHandler {
+ public static final int MAX_BUFFER_SIZE = 64 * 1024;
+
+ /**
+ * ResponseWriter is optionally returned by request handlers which implement
+ * {@link SynchronizedRequestHandler#synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse, String)}
+ *
+ * The ResponseWriter will be executed by
+ * {@link #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)}
+ * without holding Vaadin session lock.
+ */
+ @FunctionalInterface
+ public interface ResponseWriter extends Serializable {
+ void writeResponse() throws IOException;
+ }
+
@Override
public boolean handleRequest(VaadinSession session, VaadinRequest request,
VaadinResponse response) throws IOException {
@@ -35,11 +54,27 @@ public boolean handleRequest(VaadinSession session, VaadinRequest request,
return false;
}
- session.lock();
try {
- return synchronizedHandleRequest(session, request, response);
+ if (isReadAndWriteOutsideSessionLock()) {
+ BufferedReader reader = request.getReader();
+ String requestBody = reader == null ? null
+ : getRequestBody(reader);
+ session.lock();
+ Optional responseWriter = synchronizedHandleRequest(
+ session, request, response, requestBody);
+ session.unlock();
+ if (responseWriter.isPresent()) {
+ responseWriter.get().writeResponse();
+ }
+ return responseWriter.isPresent();
+ } else {
+ session.lock();
+ return synchronizedHandleRequest(session, request, response);
+ }
} finally {
- session.unlock();
+ if (session.hasLock()) {
+ session.unlock();
+ }
}
}
@@ -65,6 +100,51 @@ public boolean handleRequest(VaadinSession session, VaadinRequest request,
public abstract boolean synchronizedHandleRequest(VaadinSession session,
VaadinRequest request, VaadinResponse response) throws IOException;
+ /**
+ * Gets if request body should be read and the response written without
+ * holding {@link VaadinSession} lock
+ *
+ * @return {@literal true} if
+ * {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse, String)}
+ * should be called. Returns {@literal false} if
+ * {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse)}
+ * should be called.
+ */
+ public boolean isReadAndWriteOutsideSessionLock() {
+ return false;
+ }
+
+ /**
+ * Identical to
+ * {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse)}
+ * except the {@link VaadinSession} is locked before this is called and the
+ * response requestBody has been read before locking the session and is
+ * provided as a separate parameter.
+ *
+ * @param session
+ * The session for the request
+ * @param request
+ * The request to handle
+ * @param response
+ * The response object to which a response can be written.
+ * @param requestBody
+ * Request body pre-read from the request object
+ * @return a ResponseWriter wrapped into an Optional, if this handler will
+ * write the response and no further request handlers should be
+ * called, otherwise an empty Optional. The ResponseWriter will be
+ * executed after the VaadinSession is unlocked.
+ *
+ * @throws IOException
+ * If an IO error occurred
+ * @see #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)
+ */
+ public Optional synchronizedHandleRequest(
+ VaadinSession session, VaadinRequest request,
+ VaadinResponse response, String requestBody)
+ throws IOException, UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
/**
* Check whether a request may be handled by this handler. This can be used
* as an optimization to avoid locking the session just to investigate some
@@ -85,4 +165,18 @@ protected boolean canHandleRequest(VaadinRequest request) {
return true;
}
+ public static String getRequestBody(Reader reader) throws IOException {
+ StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
+ char[] buffer = new char[MAX_BUFFER_SIZE];
+
+ while (true) {
+ int read = reader.read(buffer);
+ if (read == -1) {
+ break;
+ }
+ sb.append(buffer, 0, read);
+ }
+
+ return sb.toString();
+ }
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/PushHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/PushHandler.java
index 9af8e65f458..06b0965b91f 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/communication/PushHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/PushHandler.java
@@ -42,6 +42,7 @@
import com.vaadin.flow.server.ErrorEvent;
import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.server.SessionExpiredException;
+import com.vaadin.flow.server.SynchronizedRequestHandler;
import com.vaadin.flow.server.SystemMessages;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.VaadinRequest;
@@ -56,7 +57,6 @@
import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.flow.shared.JsonConstants;
import com.vaadin.flow.shared.communication.PushMode;
-
import elemental.json.JsonException;
/**
@@ -164,7 +164,9 @@ interface PushEventCallback {
assert vaadinRequest != null;
try {
- new ServerRpcHandler().handleRpc(ui, reader, vaadinRequest);
+ new ServerRpcHandler().handleRpc(ui,
+ SynchronizedRequestHandler.getRequestBody(reader),
+ vaadinRequest);
connection.push(false);
} catch (JsonException e) {
getLogger().error("Error writing JSON to response", e);
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/ServerRpcHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/ServerRpcHandler.java
index ff8b1ad47d7..291d887d4c3 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/communication/ServerRpcHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/ServerRpcHandler.java
@@ -39,6 +39,7 @@
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.router.PreserveOnRefresh;
import com.vaadin.flow.server.ErrorEvent;
+import com.vaadin.flow.server.SynchronizedRequestHandler;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.communication.rpc.AttachExistingElementRpcHandler;
@@ -94,6 +95,11 @@ public static class RpcRequest implements Serializable {
* the request through which the JSON was received
*/
public RpcRequest(String jsonString, VaadinRequest request) {
+ this(jsonString, request.getService().getDeploymentConfiguration()
+ .isSyncIdCheckEnabled());
+ }
+
+ public RpcRequest(String jsonString, boolean isSyncIdCheckEnabled) {
json = JsonUtil.parse(jsonString);
JsonValue token = json.get(ApplicationConstants.CSRF_TOKEN);
@@ -107,8 +113,7 @@ public RpcRequest(String jsonString, VaadinRequest request) {
this.csrfToken = csrfToken;
}
- if (request.getService().getDeploymentConfiguration()
- .isSyncIdCheckEnabled()) {
+ if (isSyncIdCheckEnabled) {
syncId = (int) json
.getNumber(ApplicationConstants.SERVER_SYNC_ID);
} else {
@@ -199,8 +204,6 @@ private boolean isUnloadBeaconRequest() {
}
- private static final int MAX_BUFFER_SIZE = 64 * 1024;
-
/**
* Exception thrown then the security key sent by the client does not match
* the expected one.
@@ -251,16 +254,35 @@ public ResynchronizationRequiredException() {
*/
public void handleRpc(UI ui, Reader reader, VaadinRequest request)
throws IOException, InvalidUIDLSecurityKeyException {
- ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());
+ handleRpc(ui, SynchronizedRequestHandler.getRequestBody(reader),
+ request);
+ }
- String changeMessage = getMessage(reader);
+ /**
+ * Reads JSON containing zero or more serialized RPC calls (including legacy
+ * variable changes) and executes the calls.
+ *
+ * @param ui
+ * The {@link UI} receiving the calls. Cannot be null.
+ * @param message
+ * The JSON message from the request.
+ * @param request
+ * The request through which the RPC was received
+ * @throws InvalidUIDLSecurityKeyException
+ * If the received security key does not match the one stored in
+ * the session.
+ */
+ public void handleRpc(UI ui, String message, VaadinRequest request)
+ throws InvalidUIDLSecurityKeyException {
+ ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());
- if (changeMessage == null || changeMessage.equals("")) {
+ if (message == null || message.isEmpty()) {
// The client sometimes sends empty messages, this is probably a bug
return;
}
- RpcRequest rpcRequest = new RpcRequest(changeMessage, request);
+ RpcRequest rpcRequest = new RpcRequest(message, request.getService()
+ .getDeploymentConfiguration().isSyncIdCheckEnabled());
// Security: double cookie submission pattern unless disabled by
// property
@@ -268,9 +290,9 @@ public void handleRpc(UI ui, Reader reader, VaadinRequest request)
throw new InvalidUIDLSecurityKeyException();
}
- String hashMessage = changeMessage;
+ String hashMessage = message;
if (hashMessage.length() > 64 * 1024) {
- hashMessage = changeMessage.substring(0, 64 * 1024);
+ hashMessage = message.substring(0, 64 * 1024);
}
byte[] messageHash = MessageDigestUtil.sha256(hashMessage);
@@ -374,7 +396,6 @@ public void handleRpc(UI ui, Reader reader, VaadinRequest request)
getLogger().debug("UI closed with a beacon request");
}
}
-
}
private void enforceIfNeeded(VaadinRequest request, RpcRequest rpcRequest) {
@@ -550,8 +571,9 @@ private static void callErrorHandler(UI ui, JsonObject invocationJson,
protected String getMessage(Reader reader) throws IOException {
- StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
- char[] buffer = new char[MAX_BUFFER_SIZE];
+ StringBuilder sb = new StringBuilder(
+ SynchronizedRequestHandler.MAX_BUFFER_SIZE);
+ char[] buffer = new char[SynchronizedRequestHandler.MAX_BUFFER_SIZE];
while (true) {
int read = reader.read(buffer);
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/StreamReceiverHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/StreamReceiverHandler.java
index f9c3256714b..12e355ae2b2 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/communication/StreamReceiverHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/StreamReceiverHandler.java
@@ -38,6 +38,8 @@
import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadSizeException;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
+import org.apache.commons.fileupload2.core.FileItemInputIterator;
+import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -630,11 +632,23 @@ protected Collection getParts(VaadinRequest request)
protected FileItemInputIterator getItemIterator(VaadinRequest request)
throws FileUploadException, IOException {
+ JakartaServletFileUpload upload = createServletFileUpload(request);
+ return upload.getItemIterator((HttpServletRequest) request);
+ }
+
+ // protected for testing purposes only
+ protected JakartaServletFileUpload createServletFileUpload(
+ VaadinRequest request) {
JakartaServletFileUpload upload = new JakartaServletFileUpload();
upload.setSizeMax(requestSizeMax);
upload.setFileSizeMax(fileSizeMax);
upload.setFileCountMax(fileCountMax);
- return upload.getItemIterator((HttpServletRequest) request);
+ if (request.getCharacterEncoding() == null) {
+ // Request body's file upload headers are expected to be encoded in
+ // UTF-8 if not explicitly set otherwise in the request.
+ upload.setHeaderCharset(StandardCharsets.UTF_8);
+ }
+ return upload;
}
public void setRequestSizeMax(long requestSizeMax) {
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/UidlRequestHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/UidlRequestHandler.java
index 4a1b75f2f1f..8a90334aac1 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/communication/UidlRequestHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/UidlRequestHandler.java
@@ -20,6 +20,7 @@
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -102,39 +103,55 @@ protected ServerRpcHandler createRpcHandler() {
@Override
public boolean synchronizedHandleRequest(VaadinSession session,
VaadinRequest request, VaadinResponse response) throws IOException {
+ String requestBody = SynchronizedRequestHandler
+ .getRequestBody(request.getReader());
+ Optional responseWriter = synchronizedHandleRequest(
+ session, request, response, requestBody);
+ if (responseWriter.isPresent()) {
+ responseWriter.get().writeResponse();
+ }
+ return responseWriter.isPresent();
+ }
+
+ @Override
+ public boolean isReadAndWriteOutsideSessionLock() {
+ return true;
+ }
+
+ @Override
+ public Optional synchronizedHandleRequest(
+ VaadinSession session, VaadinRequest request,
+ VaadinResponse response, String requestBody)
+ throws IOException, UnsupportedOperationException {
UI uI = session.getService().findUI(request);
if (uI == null) {
// This should not happen but it will if the UI has been closed. We
// really don't want to see it in the server logs though
- commitJsonResponse(response,
- VaadinService.createUINotFoundJSON(false));
- return true;
+ return Optional.of(() -> commitJsonResponse(response,
+ VaadinService.createUINotFoundJSON(false)));
}
StringWriter stringWriter = new StringWriter();
try {
- getRpcHandler(session).handleRpc(uI, request.getReader(), request);
+ getRpcHandler(session).handleRpc(uI, requestBody, request);
writeUidl(uI, stringWriter, false);
} catch (JsonException e) {
getLogger().error("Error writing JSON to response", e);
// Refresh on client side
- writeRefresh(response);
- return true;
+ return Optional.of(() -> writeRefresh(response));
} catch (InvalidUIDLSecurityKeyException e) {
getLogger().warn("Invalid security key received from {}",
request.getRemoteHost());
// Refresh on client side
- writeRefresh(response);
- return true;
+ return Optional.of(() -> writeRefresh(response));
} catch (DauEnforcementException e) {
getLogger().warn(
"Daily Active User limit reached. Blocking new user request");
response.setHeader(DAUUtils.STATUS_CODE_KEY, String
.valueOf(HttpStatusCode.SERVICE_UNAVAILABLE.getCode()));
String json = DAUUtils.jsonEnforcementResponse(request, e);
- commitJsonResponse(response, json);
- return true;
+ return Optional.of(() -> commitJsonResponse(response, json));
} catch (ResynchronizationRequiredException e) { // NOSONAR
// Resync on the client side
writeUidl(uI, stringWriter, true);
@@ -142,8 +159,8 @@ public boolean synchronizedHandleRequest(VaadinSession session,
stringWriter.close();
}
- commitJsonResponse(response, stringWriter.toString());
- return true;
+ return Optional.of(
+ () -> commitJsonResponse(response, stringWriter.toString()));
}
private void writeRefresh(VaadinResponse response) throws IOException {
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java
index 5b0114ce888..3169fa8d667 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java
@@ -35,7 +35,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.vaadin.experimental.FeatureFlags;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.server.Constants;
@@ -80,6 +79,7 @@ public class NodeTasks implements FallibleCommand {
TaskGenerateEndpoint.class,
TaskCopyFrontendFiles.class,
TaskCopyLocalFrontendFiles.class,
+ TaskGeneratePWAIcons.class,
TaskUpdateSettingsFile.class,
TaskUpdateVite.class,
TaskUpdateImports.class,
@@ -259,6 +259,9 @@ public NodeTasks(Options options) {
} else {
pwa = new PwaConfiguration();
}
+ if (options.isProductionMode() && pwa.isEnabled()) {
+ commands.add(new TaskGeneratePWAIcons(options, pwa));
+ }
commands.add(new TaskUpdateSettingsFile(options, themeName, pwa));
if (options.isFrontendHotdeploy() || options.isBundleBuild()) {
commands.add(new TaskUpdateVite(options, webComponentTags));
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java
new file mode 100644
index 00000000000..52a0cbbead2
--- /dev/null
+++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGeneratePWAIcons.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2000-2024 Vaadin Ltd.
+ *
+ * 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
+ *
+ * http://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 com.vaadin.flow.server.frontend;
+
+import javax.imageio.ImageIO;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.vaadin.flow.server.BootstrapHandler;
+import com.vaadin.flow.server.Constants;
+import com.vaadin.flow.server.ExecutionFailedException;
+import com.vaadin.flow.server.PwaConfiguration;
+import com.vaadin.flow.server.PwaIcon;
+import com.vaadin.flow.server.PwaRegistry;
+import com.vaadin.flow.server.frontend.scanner.ClassFinder;
+
+/**
+ * Generates necessary PWA icons.
+ *