All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.karaf.tooling.features.GenerateDescriptorMojo Maven / Gradle / Ivy

There is a newer version: 4.4.6
Show newest version
/**
 *
 * 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.karaf.tooling.features;

import static java.lang.String.format;
import static org.apache.karaf.deployer.kar.KarArtifactInstaller.FEATURE_CLASSIFIER;

import java.io.*;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;

import org.apache.karaf.features.internal.model.Bundle;
import org.apache.karaf.features.internal.model.ConfigFile;
import org.apache.karaf.features.internal.model.Dependency;
import org.apache.karaf.features.internal.model.Feature;
import org.apache.karaf.features.internal.model.Features;
import org.apache.karaf.features.internal.model.JaxbUtil;
import org.apache.karaf.features.internal.model.ObjectFactory;
import org.apache.karaf.tooling.utils.DependencyHelper;
import org.apache.karaf.tooling.utils.DependencyHelperFactory;
import org.apache.karaf.tooling.utils.LocalDependency;
import org.apache.karaf.tooling.utils.ManifestUtils;
import org.apache.karaf.tooling.utils.MavenUtil;
import org.apache.karaf.tooling.utils.MojoSupport;
import org.apache.karaf.tooling.utils.SimpleLRUCache;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
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.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
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.repository.RepositorySystem;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.filtering.MavenResourcesExecution;
import org.apache.maven.shared.filtering.MavenResourcesFiltering;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.xml.sax.SAXException;

/**
 * Generates the features XML file starting with an optional source feature.xml and adding
 * project dependencies as bundles and feature/car dependencies.
 *
 * NB this requires a recent maven-install-plugin such as 2.3.1
 */
@Mojo(name = "features-generate-descriptor", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true)
public class GenerateDescriptorMojo extends MojoSupport {

    /**
     * An (optional) input feature file to extend. The plugin reads this file, and uses it as a template
     * to create the output.
     * This is highly recommended as it is the only way to add <feature/>
     * elements to the individual features that are generated.  Note that this file is filtered using standard Maven
     * resource interpolation, allowing attributes of the input file to be set with information such as ${project.version}
     * from the current build.
     * 

* When dependencies are processed, if they are duplicated in this file, the dependency here provides the baseline * information and is supplemented by additional information from the dependency. */ @Parameter(defaultValue = "${project.basedir}/src/main/feature/feature.xml") private File inputFile; /** * (wrapper) The filtered input file. This file holds the result of Maven resource interpolation and is generally * not necessary to change, although it may be helpful for debugging. */ @Parameter(defaultValue = "${project.build.directory}/feature/filteredInputFeature.xml") private File filteredInputFile; /** * The file to generate. This file is attached as a project output artifact with the classifier specified by * attachmentArtifactClassifier. */ @Parameter(defaultValue = "${project.build.directory}/feature/feature.xml") private File outputFile; /** * Exclude some artifacts from the generated feature. * See addBundlesToPrimaryFeature for more details. * */ @Parameter private List excludedArtifactIds = new ArrayList<>(); /** * The resolver to use for the feature. Normally null or "OBR" or "(OBR)" */ @Parameter(defaultValue = "${resolver}") private String resolver; /** * The artifact type for attaching the generated file to the project */ @Parameter(defaultValue = "xml") private String attachmentArtifactType = "xml"; /** * (wrapper) The artifact classifier for attaching the generated file to the project */ @Parameter(defaultValue = "features") private String attachmentArtifactClassifier = "features"; /** * Specifies whether features dependencies of this project will be included inline in the * final output (true), or simply referenced as output artifact dependencies (false). * If true, feature dependencies' xml descriptors are read and their contents added to the features descriptor under assembly. * If false, feature dependencies are added to the assembled feature as dependencies. * Setting this value to true is especially helpful in multiproject builds where subprojects build their own features * using aggregateFeatures = false, then combined with aggregateFeatures = true in an * aggregation project with explicit dependencies to the child projects. */ @Parameter(defaultValue = "false") private boolean aggregateFeatures = false; /** * If present, the bundles added to the feature constructed from the dependencies will be marked with this default * startlevel. If this parameter is not present, no startlevel attribute will be created. Finer resolution for specific * dependencies can be obtained by specifying the dependency in the file referenced by the inputFile parameter. */ @Parameter private Integer startLevel; /** * Installation mode. If present, generate "feature.install" attribute: *

* Installation mode *

* Can be either manual or auto. Specifies whether the feature should be automatically installed when * dropped inside the deploy folder. Note: this attribute doesn't affect feature descriptors that are installed * from the feature:install command or as part of the etc/org.apache.karaf.features.cfg file. */ @Parameter private String installMode; /** * Flag indicating whether transitive dependencies should be included (true) or not (false). *

* N.B. Note the default value of this is true, but is suboptimal in cases where specific <feature/> dependencies are * provided by the inputFile parameter. */ @Parameter(defaultValue = "true") private boolean includeTransitiveDependency; /** * Flag indicating whether the plugin should mark transitive dependencies' <bundle> elements as a dependency. * This flag has only an effect when {@link #includeTransitiveDependency} is true. */ @Parameter(defaultValue = "false") private boolean markTransitiveAsDependency; /** * Flag indicating whether the plugin should mark dependencies' in the runtime scope <bundle> elements as a dependency. * This flag has only an effect when {@link #includeTransitiveDependency} is true. */ @Parameter(defaultValue = "true") private boolean markRuntimeScopeAsDependency; /** * The standard behavior is to add dependencies as <bundle> elements to a <feature> * with the same name as the artifactId of the project. This flag disables that behavior. * If this parameter is true, then two other parameters refine the list of bundles added to the primary feature: * excludedArtifactIds and ignoreScopeProvided. Each of these specifies dependent artifacts * that should not be added to the primary feature. *

* Note that you may tune the bundle elements by including them in the inputFile. * If the inputFile has a feature element for the primary feature, the plugin will * respect it, so that you can, for example, set the startLevel or start attribute. *

* */ @Parameter(defaultValue = "true") private boolean addBundlesToPrimaryFeature; /** * The standard behavior is to add any dependencies other than those in the runtime scope to the feature bundle. * Setting this flag to "true" disables adding any dependencies (transient or otherwise) that are in * <scope>provided</scope>. See addBundlesToPrimaryFeature for more details. */ @Parameter(defaultValue = "false") private boolean ignoreScopeProvided; /** * Flag indicating whether the main project artifact should be included (true) or not (false). * This parameter is useful when you add an execution of this plugin to a project with some packaging that is not * feature. If you don't set this, then you will get a feature that contains the dependencies but * not the primary artifact itself. *

* Assumes the main project artifact is a bundle and the feature will be attached alongside using attachmentArtifactClassifier. */ @Parameter(defaultValue = "false") private boolean includeProjectArtifact; /** * The name of the primary feature. This is the feature that will be created or modified to include the * main project artifact and/or the bundles. * @see #addBundlesToPrimaryFeature * @see #includeProjectArtifact */ @Parameter(defaultValue = "${project.artifactId}") private String primaryFeatureName; /** * Flag indicating whether bundles should use the version range declared in the POM. If false, * the actual version of the resolved artifacts will be used. */ @Parameter(defaultValue = "false") private boolean useVersionRange; /** * Flag indicating whether the plugin should determine whether transitive dependencies are declared with * a version range. If this flag is set to true and a transitive dependency has been found * which had been declared with a version range, that version range will be used to build the appropriate * bundle element instead of the newest version. This flag has only an effect when {@link #useVersionRange} * is true */ @Parameter(defaultValue = "false") private boolean includeTransitiveVersionRanges; @Parameter private Boolean enableGeneration; /** * Flag indicating whether the plugin should simplify bundle dependencies. If the flag is set to {@code true} * and a bundle dependency is determined to be included in a feature dependency, the bundle dependency is * dropped. */ @Parameter(defaultValue = "false") private boolean simplifyBundleDependencies; /** * Maximum size of the artifact LRU cache. This cache is used to prevent repeated artifact-to-file resolution. */ @Parameter(defaultValue = "1024") private int artifactCacheSize; /** * Maximum size of the Features LRU cache. This cache is used to prevent repeated deserialization of features * XML files. */ @Parameter(defaultValue = "256") private int featuresCacheSize; /** * Name of features which are prerequisites (they still need to be defined separately). */ @Parameter private List prerequisiteFeatures = new ArrayList<>(); /** * Name of features which are dependencies (they still need to be defined separately). */ @Parameter private List dependencyFeatures = new ArrayList<>(); // ************************************************* // READ-ONLY MAVEN PLUGIN PARAMETERS // ************************************************* /** * We can't autowire strongly typed RepositorySystem from Aether because it may be Sonatype (Maven 3.0.x) * or Eclipse (Maven 3.1.x/3.2.x) implementation, so we switch to service locator. */ @Component private PlexusContainer container; @Component private RepositorySystem repoSystem; @Component protected MavenResourcesFiltering mavenResourcesFiltering; @Component protected MavenFileFilter mavenFileFilter; @Component private ProjectBuilder mavenProjectBuilder; // dependencies we are interested in protected Collection localDependencies; // log of what happened during search protected String treeListing; // an access layer for available Aether implementation protected DependencyHelper dependencyHelper; // maven log private Log log; // If useVersionRange is true, this map will be used to cache // resolved MavenProjects private final Map resolvedProjects = new HashMap<>(); @Override public void execute() throws MojoExecutionException, MojoFailureException { try { if (enableGeneration == null) { String packaging = this.project.getPackaging(); enableGeneration = !"feature".equals(packaging); } if (!enableGeneration) { if (inputFile.exists()) { File dir = outputFile.getParentFile(); if (!dir.isDirectory() && !dir.mkdirs()) { throw new MojoExecutionException("Could not create directory for features file: " + dir); } filter(inputFile, outputFile); getLog().info("Generation not enabled"); getLog().info("Attaching artifact"); //projectHelper.attachArtifact(project, attachmentArtifactType, attachmentArtifactClassifier, outputFile); Artifact artifact = factory.createArtifactWithClassifier(project.getGroupId(), project.getArtifactId(), project.getVersion(), attachmentArtifactType, attachmentArtifactClassifier); artifact.setFile(outputFile); project.setArtifact(artifact); return; } } this.dependencyHelper = DependencyHelperFactory.createDependencyHelper(this.container, this.project, this.mavenSession, this.artifactCacheSize, getLog()); this.dependencyHelper.getDependencies(project, includeTransitiveDependency); this.localDependencies = dependencyHelper.getLocalDependencies(); this.treeListing = dependencyHelper.getTreeListing(); File dir = outputFile.getParentFile(); if (dir.isDirectory() || dir.mkdirs()) { try (PrintStream out = new PrintStream(new FileOutputStream(outputFile))) { writeFeatures(out); } getLog().info("Attaching features XML"); // now lets attach it projectHelper.attachArtifact(project, attachmentArtifactType, attachmentArtifactClassifier, outputFile); } else { throw new MojoExecutionException("Could not create directory for features file: " + dir); } } catch (Exception e) { getLog().error(e.getMessage()); throw new MojoExecutionException("Unable to create features.xml file: " + e, e); } } private MavenProject resolveProject(final Object artifact) throws MojoExecutionException { MavenProject resolvedProject = project; if (includeTransitiveVersionRanges) { resolvedProject = resolvedProjects.get(artifact); if (resolvedProject == null) { final ProjectBuildingRequest request = new DefaultProjectBuildingRequest(); // Fixes KARAF-4626; if the system properties are not transferred to the request, // test-feature-use-version-range-transfer-properties will fail request.setSystemProperties(System.getProperties()); request.setResolveDependencies(true); request.setRemoteRepositories(project.getPluginArtifactRepositories()); request.setLocalRepository(localRepo); request.setProfiles(new ArrayList<>(mavenSession.getRequest().getProfiles())); request.setActiveProfileIds(new ArrayList<>(mavenSession.getRequest().getActiveProfiles())); dependencyHelper.setRepositorySession(request); final Artifact pomArtifact = repoSystem.createArtifact(dependencyHelper.getGroupId(artifact), dependencyHelper.getArtifactId(artifact), dependencyHelper.getBaseVersion(artifact), "pom"); try { resolvedProject = mavenProjectBuilder.build(pomArtifact, request).getProject(); resolvedProjects.put(pomArtifact, resolvedProject); } catch (final ProjectBuildingException e) { throw new MojoExecutionException( format("Maven-project could not be built for artifact %s", pomArtifact), e); } } } return resolvedProject; } private String getVersionOrRange(final Object parent, final Object artifact) throws MojoExecutionException { String versionOrRange = dependencyHelper.getBaseVersion(artifact); if (useVersionRange) { for (final org.apache.maven.model.Dependency dependency : resolveProject(parent).getDependencies()) { if (dependency.getGroupId().equals(dependencyHelper.getGroupId(artifact)) && dependency.getArtifactId().equals(dependencyHelper.getArtifactId(artifact))) { versionOrRange = dependency.getVersion(); break; } } } return versionOrRange; } /* * Write all project dependencies as feature */ private void writeFeatures(PrintStream out) throws ArtifactResolutionException, ArtifactNotFoundException, IOException, JAXBException, SAXException, ParserConfigurationException, XMLStreamException, MojoExecutionException { getLog().info("Generating feature descriptor file " + outputFile.getAbsolutePath()); //read in an existing feature.xml ObjectFactory objectFactory = new ObjectFactory(); Features features; if (inputFile.exists()) { filter(inputFile, filteredInputFile); features = readFeaturesFile(filteredInputFile); } else { features = objectFactory.createFeaturesRoot(); } if (features.getName() == null) { features.setName(project.getArtifactId()); } Feature feature = null; for (Feature test : features.getFeature()) { if (test.getName().equals(primaryFeatureName)) { feature = test; } } if (feature == null) { feature = objectFactory.createFeature(); feature.setName(primaryFeatureName); } if (!feature.hasVersion()) { feature.setVersion(project.getArtifact().getBaseVersion()); } if (feature.getDescription() == null) { feature.setDescription(project.getName()); } if (installMode != null) { feature.setInstall(installMode); } if (project.getDescription() != null && feature.getDetails() == null) { feature.setDetails(project.getDescription()); } if (includeProjectArtifact) { Bundle bundle = objectFactory.createBundle(); bundle.setLocation(this.dependencyHelper.artifactToMvn(project.getArtifact(), project.getVersion())); if (startLevel != null) { bundle.setStartLevel(startLevel); } feature.getBundle().add(bundle); } boolean needWrap = false; // First pass to look for features // Track other features we depend on and their repositories (we track repositories instead of building them from // the feature's Maven artifact to allow for multi-feature repositories) // TODO Initialise the repositories from the existing feature file if any Map otherFeatures = new HashMap<>(); Map featureRepositories = new HashMap<>(); FeaturesCache cache = new FeaturesCache(featuresCacheSize, artifactCacheSize); for (final LocalDependency entry : localDependencies) { Object artifact = entry.getArtifact(); if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) { continue; } processFeatureArtifact(features, feature, otherFeatures, featureRepositories, cache, artifact, entry.getParent(), true); } // Do not retain cache beyond this point cache = null; // Second pass to look for bundles if (addBundlesToPrimaryFeature) { localDependency: for (final LocalDependency entry : localDependencies) { Object artifact = entry.getArtifact(); if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) { continue; } if (!this.dependencyHelper.isArtifactAFeature(artifact)) { String bundleName = this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(entry.getParent(), artifact)); for (ConfigFile cf : feature.getConfigfile()) { if (bundleName.equals(cf.getLocation().replace('\n', ' ').trim())) { // The bundle matches a configfile, ignore it continue localDependency; } } File bundleFile = this.dependencyHelper.resolve(artifact, getLog()); Manifest manifest = getManifest(bundleFile); if (manifest == null || !ManifestUtils.isBundle(manifest)) { bundleName = "wrap:" + bundleName; needWrap = true; } Bundle bundle = null; for (Bundle b : feature.getBundle()) { if (bundleName.equals(b.getLocation())) { bundle = b; break; } } if (bundle == null) { bundle = objectFactory.createBundle(); bundle.setLocation(bundleName); // Check the features this feature depends on don't already contain the dependency // TODO Perhaps only for transitive dependencies? boolean includedTransitively = simplifyBundleDependencies && isBundleIncludedTransitively(feature, otherFeatures, bundle); if (!includedTransitively && (!"provided".equals(entry.getScope()) || !ignoreScopeProvided)) { feature.getBundle().add(bundle); } } if ( (markRuntimeScopeAsDependency && "runtime".equals( entry.getScope() )) || (markTransitiveAsDependency && entry.isTransitive()) ) { bundle.setDependency(true); } if (startLevel != null && bundle.getStartLevel() == 0) { bundle.setStartLevel(startLevel); } } } } if (needWrap) { Dependency wrapDependency = new Dependency(); wrapDependency.setName("wrap"); wrapDependency.setDependency(false); wrapDependency.setPrerequisite(true); feature.getFeature().add(wrapDependency); } if ((!feature.getBundle().isEmpty() || !feature.getFeature().isEmpty()) && !features.getFeature().contains(feature)) { features.getFeature().add(feature); } // Add any missing repositories for the included features for (Feature includedFeature : features.getFeature()) { for (Dependency dependency : includedFeature.getFeature()) { Feature dependedFeature = otherFeatures.get(dependency); if (dependedFeature != null && !features.getFeature().contains(dependedFeature)) { String repository = featureRepositories.get(dependedFeature); if (repository != null && !features.getRepository().contains(repository)) { features.getRepository().add(repository); } } } } JaxbUtil.marshal(features, out); try { checkChanges(features, objectFactory); } catch (Exception e) { throw new MojoExecutionException("Features contents have changed", e); } getLog().info("...done!"); } private void processFeatureArtifact(Features features, Feature feature, Map otherFeatures, Map featureRepositories, FeaturesCache cache, Object artifact, Object parent, boolean add) throws MojoExecutionException, XMLStreamException, JAXBException, IOException { if (this.dependencyHelper.isArtifactAFeature(artifact) && FEATURE_CLASSIFIER.equals( this.dependencyHelper.getClassifier(artifact))) { File featuresFile = this.dependencyHelper.resolve(artifact, getLog()); if (featuresFile == null || !featuresFile.exists()) { throw new MojoExecutionException( "Cannot locate file for feature: " + artifact + " at " + featuresFile); } Features includedFeatures = cache.getFeature(featuresFile); for (String repository : includedFeatures.getRepository()) { processFeatureArtifact(features, feature, otherFeatures, featureRepositories, cache, cache.getArtifact(repository), parent, false); } for (Feature includedFeature : includedFeatures.getFeature()) { Dependency dependency = new Dependency(includedFeature.getName(), includedFeature.getVersion()); dependency.setPrerequisite(prerequisiteFeatures.contains(dependency.getName())); dependency.setDependency(dependencyFeatures.contains(dependency.getName())); // Determine what dependency we're actually going to use Dependency matchingDependency = findMatchingDependency(feature.getFeature(), dependency); if (matchingDependency != null) { // The feature already has a matching dependency, merge mergeDependencies(matchingDependency, dependency); dependency = matchingDependency; } // We mustn't de-duplicate here, we may have seen a feature in !add mode otherFeatures.put(dependency, includedFeature); if (add) { if (!feature.getFeature().contains(dependency)) { feature.getFeature().add(dependency); } if (aggregateFeatures) { features.getFeature().add(includedFeature); } } if (!featureRepositories.containsKey(includedFeature)) { featureRepositories.put(includedFeature, this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(parent, artifact))); } } } } private static Dependency findMatchingDependency(Collection dependencies, Dependency reference) { String referenceName = reference.getName(); for (Dependency dependency : dependencies) { if (referenceName.equals(dependency.getName())) { return dependency; } } return null; } private static void mergeDependencies(Dependency target, Dependency source) { if (target.getVersion() == null || Feature.DEFAULT_VERSION.equals(target.getVersion())) { target.setVersion(source.getVersion()); } if (source.isDependency()) { target.setDependency(true); } if (source.isPrerequisite()) { target.setPrerequisite(true); } } private boolean isBundleIncludedTransitively(Feature feature, Map otherFeatures, Bundle bundle) { for (Dependency dependency : feature.getFeature()) { // Match dependencies “generously” (we might be matching single-version dependencies with version ranges) Dependency otherDependency = findMatchingDependency(otherFeatures.keySet(), dependency); Feature otherFeature = otherDependency != null ? otherFeatures.get(otherDependency) : null; if (otherFeature != null) { if (otherFeature.getBundle().contains(bundle) || isBundleIncludedTransitively(otherFeature, otherFeatures, bundle)) { return true; } } } return false; } /** * Extract the MANIFEST from the give file. */ private Manifest getManifest(File file) { // In case of a maven build below the 'package' phase, references to the 'target/classes' // directories are passed in instead of jar-file references. if(file.isDirectory()) { File manifestFile = new File(file, "META-INF/MANIFEST.MF"); if(manifestFile.exists() && manifestFile.isFile()) { try { InputStream manifestInputStream = new FileInputStream(manifestFile); return new Manifest(manifestInputStream); } catch (IOException e) { getLog().warn("Error while reading artifact from directory", e); return null; } } getLog().warn("Manifest not present in the module directory " + file.getAbsolutePath()); return null; } else { final InputStream is; try { is = Files.newInputStream(file.toPath()); } catch (Exception e) { getLog().warn("Error while opening artifact", e); return null; } try (BufferedInputStream bis = new BufferedInputStream(is)) { bis.mark(256 * 1024); try (JarInputStream jar = new JarInputStream(bis)) { Manifest m = jar.getManifest(); if (m == null) { getLog().warn("Manifest not present in the first entry of the zip - " + file.getName()); } return m; } } catch (IOException e) { getLog().warn("Error while reading artifact", e); return null; } } } static Features readFeaturesFile(File featuresFile) throws XMLStreamException, JAXBException, IOException { return JaxbUtil.unmarshal(featuresFile.toURI().toASCIIString(), false); } @Override public void setLog(Log log) { this.log = log; } @Override public Log getLog() { if (log == null) { setLog(new SystemStreamLog()); } return log; } //------------------------------------------------------------------------// // dependency change detection /** * Master switch to look for and log changed dependencies. If this is set to true and the file referenced by * dependencyCache does not exist, it will be unconditionally generated. If the file does exist, it is * used to detect changes from previous builds and generate logs of those changes. In that case, * failOnDependencyChange = true will cause the build to fail. */ @Parameter(defaultValue = "false") private boolean checkDependencyChange; /** * (wrapper) Location of dependency cache. This file is generated to contain known dependencies and is generally * located in SCM so that it may be used across separate developer builds. This is parameter is ignored unless * checkDependencyChange is set to true. */ @Parameter(defaultValue = "${basedir}/src/main/history/dependencies.xml") private File dependencyCache; /** * Location of filtered dependency file. */ @Parameter(defaultValue = "${basedir}/target/history/dependencies.xml", readonly = true) private File filteredDependencyCache; /** * Whether to fail on changed dependencies (default, true) or warn (false). This is parameter is ignored unless * checkDependencyChange is set to true and dependencyCache exists to compare * against. */ @Parameter(defaultValue = "true") private boolean failOnDependencyChange; /** * Copies the contents of dependency change logs that are generated to stdout. This is parameter is ignored unless * checkDependencyChange is set to true and dependencyCache exists to compare * against. */ @Parameter(defaultValue = "false") private boolean logDependencyChanges; /** * Whether to overwrite the file referenced by dependencyCache if it has changed. This is parameter is * ignored unless checkDependencyChange is set to true, failOnDependencyChange * is set to false and dependencyCache exists to compare against. */ @Parameter(defaultValue = "false") private boolean overwriteChangedDependencies; //filtering support /** * The character encoding scheme to be applied when filtering resources. */ @Parameter(defaultValue = "${project.build.sourceEncoding}") protected String encoding; /** * Expression preceded with the String won't be interpolated * \${foo} will be replaced with ${foo} */ @Parameter(defaultValue = "${maven.resources.escapeString}") protected String escapeString = "\\"; /** * System properties. */ @Parameter protected Map systemProperties; private void checkChanges(Features newFeatures, ObjectFactory objectFactory) throws Exception { if (checkDependencyChange) { //combine all the dependencies to one feature and strip out versions Features features = objectFactory.createFeaturesRoot(); features.setName(newFeatures.getName()); Feature feature = objectFactory.createFeature(); features.getFeature().add(feature); for (Feature f : newFeatures.getFeature()) { for (Bundle b : f.getBundle()) { Bundle bundle = objectFactory.createBundle(); bundle.setLocation(b.getLocation()); feature.getBundle().add(bundle); } for (Dependency d : f.getFeature()) { Dependency dependency = objectFactory.createDependency(); dependency.setName(d.getName()); feature.getFeature().add(dependency); } } feature.getBundle().sort(Comparator.comparing(Bundle::getLocation)); feature.getFeature().sort(Comparator.comparing(Dependency::getName)); if (dependencyCache.exists()) { //filter dependencies file filter(dependencyCache, filteredDependencyCache); //read dependency types, convert to dependencies, compare. Features oldfeatures = readFeaturesFile(filteredDependencyCache); Feature oldFeature = oldfeatures.getFeature().get(0); List addedBundles = new ArrayList<>(feature.getBundle()); List removedBundles = new ArrayList<>(); for (Bundle test : oldFeature.getBundle()) { boolean t1 = addedBundles.contains(test); int s1 = addedBundles.size(); boolean t2 = addedBundles.remove(test); int s2 = addedBundles.size(); if (t1 != t2) { getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2); } if (t1 == (s1 == s2)) { getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2); } if (!t2) { removedBundles.add(test); } } List addedDependencys = new ArrayList<>(feature.getFeature()); List removedDependencys = new ArrayList<>(); for (Dependency test : oldFeature.getFeature()) { boolean t1 = addedDependencys.contains(test); int s1 = addedDependencys.size(); boolean t2 = addedDependencys.remove(test); int s2 = addedDependencys.size(); if (t1 != t2) { getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2); } if (t1 == (s1 == s2)) { getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2); } if (!t2) { removedDependencys.add(test); } } if (!addedBundles.isEmpty() || !removedBundles.isEmpty() || !addedDependencys.isEmpty() || !removedDependencys.isEmpty()) { saveDependencyChanges(addedBundles, removedBundles, addedDependencys, removedDependencys, objectFactory); if (overwriteChangedDependencies) { writeDependencies(features, dependencyCache); } } else { getLog().info(saveTreeListing()); } } else { writeDependencies(features, dependencyCache); } } } protected void saveDependencyChanges(Collection addedBundles, Collection removedBundles, Collection addedDependencys, Collection removedDependencys, ObjectFactory objectFactory) throws Exception { File addedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.added.xml"); Features added = toFeatures(addedBundles, addedDependencys, objectFactory); writeDependencies(added, addedFile); File removedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.removed.xml"); Features removed = toFeatures(removedBundles, removedDependencys, objectFactory); writeDependencies(removed, removedFile); StringWriter out = new StringWriter(); out.write(saveTreeListing()); out.write("Dependencies have changed:\n"); if (!addedBundles.isEmpty() || !addedDependencys.isEmpty()) { out.write("\tAdded dependencies are saved here: " + addedFile.getAbsolutePath() + "\n"); if (logDependencyChanges) { JaxbUtil.marshal(added, out); } } if (!removedBundles.isEmpty() || !removedDependencys.isEmpty()) { out.write("\tRemoved dependencies are saved here: " + removedFile.getAbsolutePath() + "\n"); if (logDependencyChanges) { JaxbUtil.marshal(removed, out); } } out.write("Delete " + dependencyCache.getAbsolutePath() + " if you are happy with the dependency changes."); if (failOnDependencyChange) { throw new MojoFailureException(out.toString()); } else { getLog().warn(out.toString()); } } private static Features toFeatures(Collection addedBundles, Collection addedDependencys, ObjectFactory objectFactory) { Features features = objectFactory.createFeaturesRoot(); Feature feature = objectFactory.createFeature(); feature.getBundle().addAll(addedBundles); feature.getFeature().addAll(addedDependencys); features.getFeature().add(feature); return features; } private static void writeDependencies(Features features, File file) throws JAXBException, IOException { file.getParentFile().mkdirs(); if (!file.getParentFile().exists() || !file.getParentFile().isDirectory()) { throw new IOException("Cannot create directory at " + file.getParent()); } try (OutputStream out = new FileOutputStream(file)) { JaxbUtil.marshal(features, out); } } protected void filter(File sourceFile, File targetFile) throws MojoExecutionException { try { if (StringUtils.isEmpty(encoding)) { getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING + ", i.e. build is platform dependent!"); } targetFile.getParentFile().mkdirs(); final MavenResourcesExecution mre = new MavenResourcesExecution(); mre.setMavenProject(project); mre.setMavenSession(mavenSession); mre.setFilters(null); mre.setEscapedBackslashesInFilePath(true); final LinkedHashSet delimiters = new LinkedHashSet<>(); delimiters.add("${*}"); mre.setDelimiters(delimiters); @SuppressWarnings("rawtypes") List filters = mavenFileFilter.getDefaultFilterWrappers(mre); mavenFileFilter.copyFile(sourceFile, targetFile, true, filters, encoding, true); } catch (MavenFilteringException e) { throw new MojoExecutionException(e.getMessage(), e); } } protected String saveTreeListing() throws IOException { File treeListFile = new File(filteredDependencyCache.getParentFile(), "treeListing.txt"); try (OutputStream os = new FileOutputStream(treeListFile); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os))) { writer.write(treeListing); } return "\tTree listing is saved here: " + treeListFile.getAbsolutePath() + "\n"; } private static final class FeaturesCache { // Maven-to-Aether Artifact cache, as parsing strings is expensive private final SimpleLRUCache artifactCache; private final SimpleLRUCache featuresCache; FeaturesCache(int featuresCacheSize, int artifactCacheSize) { featuresCache = new SimpleLRUCache<>(featuresCacheSize); artifactCache = new SimpleLRUCache<>(artifactCacheSize); } DefaultArtifact getArtifact(String mavenName) { return artifactCache.computeIfAbsent(mavenName, MavenUtil::mvnToArtifact); } Features getFeature(final File featuresFile) throws XMLStreamException, JAXBException, IOException { final Features existing = featuresCache.get(featuresFile); if (existing != null) { return existing; } final Features computed = readFeaturesFile(featuresFile); featuresCache.put(featuresFile, computed); return computed; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy