Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix class and resource loading in maven plugin (#20465) (CP: 24.5) #20518

Merged
merged 1 commit into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,39 @@

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.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;
Expand All @@ -52,7 +65,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;
Expand Down Expand Up @@ -130,6 +145,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.
Expand Down Expand Up @@ -171,8 +189,33 @@ public class BuildDevBundleMojo extends AbstractMojo
@Parameter(defaultValue = "${project.basedir}/src/main/" + FRONTEND)
private File frontendDirectory;

static final String CLASSFINDER_FIELD_NAME = "classFinder";

private ClassFinder classFinder;

@Override
public void execute() throws MojoFailureException {
public void execute() throws MojoExecutionException, MojoFailureException {
PluginDescriptor pluginDescriptor = mojoExecution.getMojoDescriptor()
.getPluginDescriptor();
checkFlowCompatibility(pluginDescriptor);

Reflector reflector = getOrCreateReflector();
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Thread.currentThread()
.setContextClassLoader(reflector.getIsolatedClassLoader());
try {
org.apache.maven.plugin.Mojo task = reflector.createMojo(this);
findExecuteMethod(task.getClass()).invoke(task);
} catch (MojoExecutionException | MojoFailureException e) {
throw e;
} catch (Exception e) {
throw new MojoFailureException(e.getMessage(), e);
} finally {
Thread.currentThread().setContextClassLoader(tccl);
}
}

public void executeInternal() throws MojoFailureException {
long start = System.nanoTime();

try {
Expand Down Expand Up @@ -239,7 +282,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<String> getClasspathElements(MavenProject project) {

try {
Expand Down Expand Up @@ -282,11 +327,13 @@ public File generatedTsFolder() {

@Override
public ClassFinder getClassFinder() {

List<String> classpathElements = getClasspathElements(project);

return BuildFrontendUtil.getClassFinder(classpathElements);

if (classFinder == null) {
URLClassLoader classLoader = getOrCreateReflector()
.getIsolatedClassLoader();
classFinder = new ReflectionsClassFinder(classLoader,
classLoader.getURLs());
}
return classFinder;
}

@Override
Expand Down Expand Up @@ -469,4 +516,126 @@ public boolean checkRuntimeDependency(String groupId, String artifactId,
Consumer<String> missingDependencyMessageConsumer) {
return false;
}

private static URLClassLoader createIsolatedClassLoader(
MavenProject project, MojoExecution mojoExecution) {
List<URL> urls = new ArrayList<>();
String outputDirectory = project.getBuild().getOutputDirectory();
if (outputDirectory != null) {
urls.add(FlowFileUtils.convertToUrl(new File(outputDirectory)));
}

Function<Artifact, String> keyMapper = artifact -> artifact.getGroupId()
+ ":" + artifact.getArtifactId();

Map<String, Artifact> 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<Artifact> 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<String, Object> pluginContext = getPluginContext();
String pluginKey = mojoExecution.getPlugin().getKey();
String reflectorKey = Reflector.class.getName() + "-" + pluginKey + "-"
+ mojoExecution.getLifecyclePhase();
if (pluginContext != null && pluginContext.containsKey(reflectorKey)) {
getLog().debug("Using cached Reflector for plugin " + pluginKey
+ " and phase " + mojoExecution.getLifecyclePhase());
return Reflector.adapt(pluginContext.get(reflectorKey));
}
Reflector reflector = Reflector.of(project, mojoExecution);
if (pluginContext != null) {
pluginContext.put(reflectorKey, reflector);
getLog().debug("Cached Reflector for plugin " + pluginKey
+ " and phase " + mojoExecution.getLifecyclePhase());
}
return reflector;
}

private Method findExecuteMethod(Class<?> taskClass)
throws NoSuchMethodException {

while (taskClass != null && taskClass != Object.class) {
try {
Method executeInternal = taskClass
.getDeclaredMethod("executeInternal");
executeInternal.setAccessible(true);
return executeInternal;
} catch (NoSuchMethodException e) {
// ignore
}
taskClass = taskClass.getSuperclass();
}
throw new NoSuchMethodException(
"Method executeInternal not found in " + getClass().getName());
}

}
Loading