/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.nifi.extension.definition.extraction; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DefaultArtifact; import org.apache.maven.artifact.handler.ArtifactHandler; import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.project.MavenProject; import org.apache.maven.project.ProjectBuilder; import org.apache.maven.project.ProjectBuildingException; import org.apache.maven.project.ProjectBuildingRequest; import org.apache.maven.project.ProjectBuildingResult; import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder; import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException; import org.apache.maven.shared.dependency.graph.DependencyNode; import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor; import org.eclipse.aether.RepositorySystemSession; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; public class ExtensionClassLoaderFactory { private final Log log; private final MavenProject project; private final RepositorySystemSession repoSession; private final ProjectBuilder projectBuilder; private final ArtifactRepository localRepo; private final DependencyGraphBuilder dependencyGraphBuilder; private final ArtifactResolver artifactResolver; private final ArtifactHandlerManager artifactHandlerManager; private ExtensionClassLoaderFactory(final Builder builder) { this.log = builder.log; this.project = builder.project; this.repoSession = builder.repositorySession; this.projectBuilder = builder.projectBuilder; this.localRepo = builder.localRepo; this.dependencyGraphBuilder = builder.dependencyGraphBuilder; this.artifactResolver = builder.artifactResolver; this.artifactHandlerManager = builder.artifactHandlerManager; } private Log getLog() { return log; } public ExtensionClassLoader createExtensionClassLoader() throws MojoExecutionException, ProjectBuildingException { final Artifact narArtifact = project.getArtifact(); final Set<Artifact> narArtifacts = getNarDependencies(narArtifact); final ArtifactsHolder artifactsHolder = new ArtifactsHolder(); artifactsHolder.addArtifacts(narArtifacts); getLog().debug("Project artifacts: "); narArtifacts.forEach(artifact -> getLog().debug(artifact.getArtifactId())); final ExtensionClassLoader parentClassLoader = createClassLoader(narArtifacts, artifactsHolder); final ExtensionClassLoader classLoader = createClassLoader(narArtifacts, parentClassLoader, narArtifact); if (getLog().isDebugEnabled()) { getLog().debug("Full ClassLoader is:\n" + classLoader.toTree()); } return classLoader; } private ExtensionClassLoader createClassLoader(final Set<Artifact> artifacts, final ArtifactsHolder artifactsHolder) throws MojoExecutionException, ProjectBuildingException { final Artifact nar = removeNarArtifact(artifacts); if (nar == null) { final ExtensionClassLoader providedEntityClassLoader = createProvidedEntitiesClassLoader(artifactsHolder); return createClassLoader(artifacts, providedEntityClassLoader, null); } final Set<Artifact> narDependencies = getNarDependencies(nar); artifactsHolder.addArtifacts(narDependencies); return createClassLoader(narDependencies, createClassLoader(narDependencies, artifactsHolder), nar); } private Artifact removeNarArtifact(final Set<Artifact> artifacts) { final Iterator<Artifact> itr = artifacts.iterator(); while (itr.hasNext()) { final Artifact artifact = itr.next(); if (artifact.equals(project.getArtifact())) { continue; } if ("nar".equalsIgnoreCase(artifact.getType())) { getLog().info("Found NAR dependency of " + artifact); itr.remove(); return artifact; } } return null; } private Set<Artifact> getNarDependencies(final Artifact narArtifact) throws MojoExecutionException, ProjectBuildingException { final ProjectBuildingRequest narRequest = new DefaultProjectBuildingRequest(); narRequest.setRepositorySession(repoSession); narRequest.setSystemProperties(System.getProperties()); narRequest.setLocalRepository(localRepo); final ProjectBuildingResult narResult = projectBuilder.build(narArtifact, narRequest); final Set<Artifact> narDependencies = new TreeSet<>(); gatherArtifacts(narResult.getProject(), narDependencies); narDependencies.remove(narArtifact); narDependencies.remove(project.getArtifact()); getLog().debug("Found NAR dependency of " + narArtifact + ", which resolved to the following artifacts: " + narDependencies); return narDependencies; } private String determineProvidedEntityVersion(final Set<Artifact> artifacts, final String groupId, final String artifactId) throws ProjectBuildingException, MojoExecutionException { getLog().debug("Determining provided entities for " + groupId + ":" + artifactId); for (final Artifact artifact : artifacts) { if (artifact.getGroupId().equals(groupId) && artifact.getArtifactId().equals(artifactId)) { return artifact.getVersion(); } } return findProvidedDependencyVersion(artifacts, groupId, artifactId); } private String findProvidedDependencyVersion(final Set<Artifact> artifacts, final String groupId, final String artifactId) { final ProjectBuildingRequest projectRequest = new DefaultProjectBuildingRequest(); projectRequest.setRepositorySession(repoSession); projectRequest.setSystemProperties(System.getProperties()); projectRequest.setLocalRepository(localRepo); for (final Artifact artifact : artifacts) { final Set<Artifact> artifactDependencies = new HashSet<>(); try { final ProjectBuildingResult projectResult = projectBuilder.build(artifact, projectRequest); gatherArtifacts(projectResult.getProject(), artifactDependencies); getLog().debug("For Artifact " + artifact + ", found the following dependencies:"); artifactDependencies.forEach(dep -> getLog().debug(dep.toString())); for (final Artifact dependency : artifactDependencies) { if (dependency.getGroupId().equals(groupId) && dependency.getArtifactId().equals(artifactId)) { getLog().debug("Found version of " + groupId + ":" + artifactId + " to be " + artifact.getVersion()); return artifact.getVersion(); } } } catch (final Exception e) { getLog().warn("Unable to construct Maven Project for " + artifact + " when attempting to determine the expected version of NiFi API"); getLog().debug("Unable to construct Maven Project for " + artifact + " when attempting to determine the expected version of NiFi API", e); } } return null; } private Artifact getProvidedArtifact(final String groupId, final String artifactId, final String version) throws MojoExecutionException { final ArtifactHandler handler = artifactHandlerManager.getArtifactHandler("jar"); final VersionRange versionRange; try { versionRange = VersionRange.createFromVersionSpec(version); } catch (final Exception e) { throw new MojoExecutionException("Could not determine appropriate version for Provided Artifact " + groupId + ":" + artifactId, e); } final Artifact artifact = new DefaultArtifact(groupId, artifactId, versionRange, null, "jar", null, handler); final ArtifactResolutionRequest request = new ArtifactResolutionRequest(); request.setLocalRepository(localRepo); request.setArtifact(artifact); final ArtifactResolutionResult result = artifactResolver.resolve(request); if (!result.isSuccess()) { final List<Exception> exceptions = result.getExceptions(); final MojoExecutionException exception = new MojoExecutionException("Could not resolve local dependency " + artifact); if (exceptions != null) { for (final Exception e : exceptions) { exception.addSuppressed(e); } } throw exception; } final Set<Artifact> artifacts = result.getArtifacts(); if (artifacts.isEmpty()) { throw new MojoExecutionException("Could not resolve any artifacts for dependency " + artifact); } final List<Artifact> sorted = new ArrayList<>(artifacts); Collections.sort(sorted); return sorted.get(0); } private ExtensionClassLoader createProvidedEntitiesClassLoader(final ArtifactsHolder artifactsHolder) throws MojoExecutionException, ProjectBuildingException { final String nifiApiVersion = determineProvidedEntityVersion(artifactsHolder.getAllArtifacts(), "org.apache.nifi", "nifi-api"); if (nifiApiVersion == null) { throw new MojoExecutionException("Could not find any dependency, provided or otherwise, on [org.apache.nifi:nifi-api]"); } else { getLog().info("Found a dependency on version " + nifiApiVersion + " of NiFi API"); } final String slf4jApiVersion = determineProvidedEntityVersion(artifactsHolder.getAllArtifacts(),"org.slf4j", "slf4j-api"); final Artifact nifiApiArtifact = getProvidedArtifact("org.apache.nifi", "nifi-api", nifiApiVersion); final Artifact nifiFrameworkApiArtifact = getProvidedArtifact("org.apache.nifi", "nifi-framework-api", nifiApiArtifact.getVersion()); final Artifact slf4jArtifact = getProvidedArtifact("org.slf4j", "slf4j-api", slf4jApiVersion); final Set<Artifact> providedArtifacts = new HashSet<>(); providedArtifacts.add(nifiApiArtifact); providedArtifacts.add(nifiFrameworkApiArtifact); providedArtifacts.add(slf4jArtifact); getLog().debug("Creating Provided Entities Class Loader with artifacts: " + providedArtifacts); return createClassLoader(providedArtifacts, null, null); } private ExtensionClassLoader createClassLoader(final Set<Artifact> artifacts, final ExtensionClassLoader parent, final Artifact narArtifact) throws MojoExecutionException { final Set<URL> urls = new HashSet<>(); for (final Artifact artifact : artifacts) { final Set<URL> artifactUrls = toURLs(artifact); urls.addAll(artifactUrls); } getLog().debug("Creating class loader with following dependencies: " + urls); final URL[] urlArray = urls.toArray(new URL[0]); if (parent == null) { return new ExtensionClassLoader(urlArray, narArtifact, artifacts); } else { return new ExtensionClassLoader(urlArray, parent, narArtifact, artifacts); } } private void gatherArtifacts(final MavenProject mavenProject, final Set<Artifact> artifacts) throws MojoExecutionException { final DependencyNodeVisitor nodeVisitor = new DependencyNodeVisitor() { @Override public boolean visit(final DependencyNode dependencyNode) { final Artifact artifact = dependencyNode.getArtifact(); artifacts.add(artifact); return true; } @Override public boolean endVisit(final DependencyNode dependencyNode) { return true; } }; try { final ProjectBuildingRequest projectRequest = new DefaultProjectBuildingRequest(); projectRequest.setRepositorySession(repoSession); projectRequest.setSystemProperties(System.getProperties()); projectRequest.setLocalRepository(localRepo); projectRequest.setProject(mavenProject); final DependencyNode depNode = dependencyGraphBuilder.buildDependencyGraph(projectRequest, null); depNode.accept(nodeVisitor); } catch (DependencyGraphBuilderException e) { throw new MojoExecutionException("Failed to build dependency tree", e); } } private Set<URL> toURLs(final Artifact artifact) throws MojoExecutionException { final Set<URL> urls = new HashSet<>(); final File artifactFile = artifact.getFile(); if (artifactFile == null) { getLog().debug("Attempting to resolve Artifact " + artifact + " because it has no File associated with it"); final ArtifactResolutionRequest request = new ArtifactResolutionRequest(); request.setLocalRepository(localRepo); request.setArtifact(artifact); final ArtifactResolutionResult result = artifactResolver.resolve(request); if (!result.isSuccess()) { throw new MojoExecutionException("Could not resolve local dependency " + artifact); } getLog().debug("Resolved Artifact " + artifact + " to " + result.getArtifacts()); for (final Artifact resolved : result.getArtifacts()) { urls.addAll(toURLs(resolved)); } } else { try { final URL url = artifact.getFile().toURI().toURL(); getLog().debug("Adding URL " + url + " to ClassLoader"); urls.add(url); } catch (final MalformedURLException mue) { throw new MojoExecutionException("Failed to convert File " + artifact.getFile() + " into URL", mue); } } return urls; } public static class Builder { private Log log; private MavenProject project; private ArtifactRepository localRepo; private DependencyGraphBuilder dependencyGraphBuilder; private ArtifactResolver artifactResolver; private ProjectBuilder projectBuilder; private RepositorySystemSession repositorySession; private ArtifactHandlerManager artifactHandlerManager; public Builder log(final Log log) { this.log = log; return this; } public Builder projectBuilder(final ProjectBuilder projectBuilder) { this.projectBuilder = projectBuilder; return this; } public Builder project(final MavenProject project) { this.project = project; return this; } public Builder localRepository(final ArtifactRepository localRepo) { this.localRepo = localRepo; return this; } public Builder dependencyGraphBuilder(final DependencyGraphBuilder dependencyGraphBuilder) { this.dependencyGraphBuilder = dependencyGraphBuilder; return this; } public Builder artifactResolver(final ArtifactResolver resolver) { this.artifactResolver = resolver; return this; } public Builder repositorySession(final RepositorySystemSession repositorySession) { this.repositorySession = repositorySession; return this; } public Builder artifactHandlerManager(final ArtifactHandlerManager artifactHandlerManager) { this.artifactHandlerManager = artifactHandlerManager; return this; } public ExtensionClassLoaderFactory build() { return new ExtensionClassLoaderFactory(this); } } private static class ArtifactsHolder { private Set<Artifact> allArtifacts = new TreeSet<>(); public void addArtifacts(final Set<Artifact> artifacts) { if (artifacts != null) { allArtifacts.addAll(artifacts); } } public Set<Artifact> getAllArtifacts() { return allArtifacts; } } }