From 4304c035b4c3fcaba99731e9ab894d2bb1e3fe87 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Wed, 13 Nov 2024 14:59:09 +0100 Subject: [PATCH] fix: fix class and resource loading in maven plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run Flow mojos using an isolated class loader that includes both project and plugin dependencies, with project dependencies taking precedence. This ensures that classes are always loaded from the same class loader at runtime, preventing errors where a class might be loaded by the plugin's class loader while one of its parent classes is only available in the project’s class loader (see #19616). Additionally, this approach prevents the retrieval of resources from plugin dependencies when the same artifact is defined within the project (see #19009). This refactoring also introduces caching for ClassFinder instances per execution phase, allowing multiple goals configured for the same phase to reuse the same ClassFinder. It also removes the need to instantiate a ClassFinder solely for Hilla class checks, reducing the number of scans performed during the build. Fixes #19616 Fixes #19009 Fixes #20385 --- .../flow/plugin/maven/BuildDevBundleMojo.java | 181 ++++++++++- .../vaadin/flow/plugin/maven/Reflector.java | 282 +++++++++++++++++ flow-plugins/flow-maven-plugin/pom.xml | 33 +- .../flow-maven-plugin/src/it/.gitignore | 7 + .../invoker.properties | 18 ++ .../pom.xml | 118 +++++++ .../main/java/com/vaadin/test/AppConfig.java | 28 ++ .../it/classfinder-lookup/invoker.properties | 17 + .../src/it/classfinder-lookup/pom.xml | 68 ++++ .../com/vaadin/test/ProjectFlowExtension.java | 21 ++ .../src/it/classfinder-lookup/verify.bsh | 14 + .../src/it/flow-addon/invoker.properties | 23 ++ .../src/it/flow-addon/pom.xml | 64 ++++ .../com/vaadin/flow/server/frontend/Flow.tsx | 18 ++ .../com/vaadin/flow/server/frontend/Flow.tsx | 18 ++ .../src/main/java/com/vaadin/test/Addon.java | 34 ++ .../resources-from-project/invoker.properties | 17 + .../src/it/resources-from-project/pom.xml | 77 +++++ .../com/vaadin/test/ProjectFlowExtension.java | 20 ++ .../src/it/resources-from-project/verify.bsh | 12 + .../flow-maven-plugin/src/it/settings.xml | 51 +++ .../flow/plugin/maven/BuildFrontendMojo.java | 3 +- .../flow/plugin/maven/CleanFrontendMojo.java | 2 +- .../flow/plugin/maven/ConvertPolymerMojo.java | 2 +- .../plugin/maven/FlowModeAbstractMojo.java | 153 ++++++++- .../flow/plugin/maven/GenerateNpmBOMMojo.java | 3 +- .../plugin/maven/PrepareFrontendMojo.java | 14 +- .../vaadin/flow/plugin/maven/Reflector.java | 282 +++++++++++++++++ .../plugin/maven/BuildFrontendMojoTest.java | 85 +++-- .../plugin/maven/CleanFrontendMojoTest.java | 30 +- .../plugin/maven/GenerateNpmBOMMojoTest.java | 14 +- .../plugin/maven/PrepareFrontendMojoTest.java | 17 +- .../flow/plugin/maven/ReflectorTest.java | 292 ++++++++++++++++++ .../scanner/ReflectionsClassFinder.java | 8 +- 34 files changed, 1934 insertions(+), 92 deletions(-) create mode 100644 flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java create mode 100644 flow-plugins/flow-maven-plugin/src/it/.gitignore create mode 100644 flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/invoker.properties create mode 100644 flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/pom.xml create mode 100644 flow-plugins/flow-maven-plugin/src/it/appshellconfiguration-external-annotations/src/main/java/com/vaadin/test/AppConfig.java create mode 100644 flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/invoker.properties create mode 100644 flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/pom.xml create mode 100644 flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/src/main/java/com/vaadin/test/ProjectFlowExtension.java create mode 100644 flow-plugins/flow-maven-plugin/src/it/classfinder-lookup/verify.bsh create mode 100644 flow-plugins/flow-maven-plugin/src/it/flow-addon/invoker.properties create mode 100644 flow-plugins/flow-maven-plugin/src/it/flow-addon/pom.xml create mode 100644 flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-plugin-resources/com/vaadin/flow/server/frontend/Flow.tsx create mode 100644 flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/fake-resources/com/vaadin/flow/server/frontend/Flow.tsx create mode 100644 flow-plugins/flow-maven-plugin/src/it/flow-addon/src/main/java/com/vaadin/test/Addon.java create mode 100644 flow-plugins/flow-maven-plugin/src/it/resources-from-project/invoker.properties create mode 100644 flow-plugins/flow-maven-plugin/src/it/resources-from-project/pom.xml create mode 100644 flow-plugins/flow-maven-plugin/src/it/resources-from-project/src/main/java/com/vaadin/test/ProjectFlowExtension.java create mode 100644 flow-plugins/flow-maven-plugin/src/it/resources-from-project/verify.bsh create mode 100644 flow-plugins/flow-maven-plugin/src/it/settings.xml create mode 100644 flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java create mode 100644 flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/ReflectorTest.java 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..87a3456447c 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,31 @@ public class BuildDevBundleMojo extends AbstractMojo @Parameter(property = InitParameters.NPM_EXCLUDE_WEB_COMPONENTS, defaultValue = "false") private boolean npmExcludeWebComponents; + 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 +284,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 +329,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 +528,128 @@ 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 + .get(reflectorKey) instanceof Reflector cachedReflector) { + + getLog().debug("Using cached Reflector for plugin " + pluginKey + + " and phase " + mojoExecution.getLifecyclePhase()); + return cachedReflector; + } + 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..202fdf7f968 --- /dev/null +++ b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java @@ -0,0 +1,282 @@ +/* + * 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.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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 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; + } + + /** + * 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 the 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, "classFinder"); + 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() + .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 = 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 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()) { + if (Modifier.isStatic(sourceField.getModifiers())) { + continue; + } + sourceField.setAccessible(true); + Object value = sourceField.get(sourceMojo); + if (value == null) { + sourceMojo.logDebug( + "Skipping null field " + sourceField.getName()); + continue; + } + 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 " + sourceClass.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." + + " 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); + } + targetClass = targetClass.getSuperclass(); + sourceClass = sourceClass.getSuperclass(); + } + } + + 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/pom.xml b/flow-plugins/flow-maven-plugin/pom.xml index ed1557f1ddd..52b2975453f 100644 --- a/flow-plugins/flow-maven-plugin/pom.xml +++ b/flow-plugins/flow-maven-plugin/pom.xml @@ -1,6 +1,6 @@ + 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.0 com.vaadin @@ -106,6 +106,35 @@ + + + org.apache.maven.plugins + maven-invoker-plugin + 3.6.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/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..f2e4770f8c5 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. @@ -264,6 +282,66 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo 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 +349,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 +376,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 +388,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 +451,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 +682,63 @@ 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 + .get(reflectorKey) instanceof Reflector cachedReflector) { + + getLog().debug("Using cached Reflector for plugin " + pluginKey + + " and phase " + mojoExecution.getLifecyclePhase()); + return cachedReflector; + } + 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..b5e9c41b7c2 --- /dev/null +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java @@ -0,0 +1,282 @@ +/* + * 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.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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 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; + } + + /** + * 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 the 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, "classFinder"); + 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() + .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 = 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 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()) { + if (Modifier.isStatic(sourceField.getModifiers())) { + continue; + } + sourceField.setAccessible(true); + Object value = sourceField.get(sourceMojo); + if (value == null) { + sourceMojo.logDebug( + "Skipping null field " + sourceField.getName()); + continue; + } + 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 " + sourceClass.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." + + " 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); + } + targetClass = targetClass.getSuperclass(); + sourceClass = sourceClass.getSuperclass(); + } + } + + 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..0feed876f3a 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 @@ -31,27 +31,23 @@ import java.util.Collections; 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 +58,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 +88,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 +229,43 @@ 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())); + 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()); + 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(); + classWorld.newRealm("maven.api"); + ClassRealm pluginClassRealm = classWorld.newRealm("flow-plugin"); + + PluginDescriptor pluginDescriptor = new PluginDescriptor(); + pluginDescriptor.setArtifacts(List.of()); + pluginDescriptor.setClassRealm( + new ClassRealm(new ClassWorld("test-world", null), "test-realm", + new URLClassLoader(new URL[] {}, + ClassLoader.getPlatformClassLoader()))); + 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..3b3ef8774ea --- /dev/null +++ b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/ReflectorTest.java @@ -0,0 +1,292 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +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.codehaus.plexus.classworlds.realm.ClassRealm; +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; + +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(new URL("file:///some/flat/maven-repo/maven-api.jar")); + pluginDescriptor.setClassRealm(classWorld.newRealm("maven-plugin")); + + Reflector execReflector = Reflector.of(project, mojoExecution); + + URLClassLoader taskClassLoader = execReflector.getIsolatedClassLoader(); + + Set urlSet = Arrays.stream(taskClassLoader.getURLs()) + .map(URL::getFile).collect(Collectors.toSet()); + Assert.assertEquals(4, urlSet.size()); + Assert.assertTrue(urlSet.contains(outputDirectory)); + Assert.assertTrue(urlSet.contains( + "/some/flat/maven-repo/com.vaadin.test-compile-1.0.jar")); + Assert.assertTrue(urlSet.contains( + "/some/flat/maven-repo/com.vaadin.test-system-1.0.jar")); + Assert.assertTrue(urlSet.contains( + "/some/flat/maven-repo/com.vaadin.test-plugin-1.0.jar")); + + ClassLoader parentClassloader = taskClassLoader.getParent(); + Assert.assertTrue(parentClassloader instanceof ClassRealm); + ClassRealm mavenApi = (ClassRealm) parentClassloader; + Assert.assertEquals("maven.api", mavenApi.getId()); + Assert.assertEquals(1, mavenApi.getURLs().length); + Assert.assertEquals("/some/flat/maven-repo/maven-api.jar", + mavenApi.getURLs()[0].getFile()); + + } + + 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/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);