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);