3 * Licensed to the Apache Software Foundation (ASF) under one or more
4 * contributor license agreements. See the NOTICE file distributed with
5 * this work for additional information regarding copyright ownership.
6 * The ASF licenses this file to You under the Apache License, Version 2.0
7 * (the "License"); you may not use this file except in compliance with
8 * the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 package org.apache.karaf.tooling.features;
20 import static java.lang.String.format;
21 import static org.apache.karaf.deployer.kar.KarArtifactInstaller.FEATURE_CLASSIFIER;
23 import java.io.BufferedInputStream;
24 import java.io.BufferedWriter;
26 import java.io.FileInputStream;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.OutputStream;
31 import java.io.OutputStreamWriter;
32 import java.io.PrintStream;
33 import java.io.StringWriter;
34 import java.util.ArrayList;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.LinkedHashSet;
40 import java.util.List;
42 import java.util.jar.JarInputStream;
43 import java.util.jar.Manifest;
45 import javax.xml.bind.JAXBException;
46 import javax.xml.parsers.ParserConfigurationException;
47 import javax.xml.stream.XMLStreamException;
49 import org.apache.karaf.features.internal.model.Bundle;
50 import org.apache.karaf.features.internal.model.Dependency;
51 import org.apache.karaf.features.internal.model.Feature;
52 import org.apache.karaf.features.internal.model.Features;
53 import org.apache.karaf.features.internal.model.JaxbUtil;
54 import org.apache.karaf.features.internal.model.ObjectFactory;
55 import org.apache.karaf.tooling.utils.DependencyHelper;
56 import org.apache.karaf.tooling.utils.DependencyHelperFactory;
57 import org.apache.karaf.tooling.utils.LocalDependency;
58 import org.apache.karaf.tooling.utils.ManifestUtils;
59 import org.apache.karaf.tooling.utils.MavenUtil;
60 import org.apache.karaf.tooling.utils.MojoSupport;
61 import org.apache.maven.artifact.Artifact;
62 import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
63 import org.apache.maven.artifact.resolver.ArtifactResolutionException;
64 import org.apache.maven.plugin.MojoExecutionException;
65 import org.apache.maven.plugin.MojoFailureException;
66 import org.apache.maven.plugin.logging.Log;
67 import org.apache.maven.plugin.logging.SystemStreamLog;
68 import org.apache.maven.plugins.annotations.Component;
69 import org.apache.maven.plugins.annotations.LifecyclePhase;
70 import org.apache.maven.plugins.annotations.Mojo;
71 import org.apache.maven.plugins.annotations.Parameter;
72 import org.apache.maven.plugins.annotations.ResolutionScope;
73 import org.apache.maven.project.DefaultProjectBuildingRequest;
74 import org.apache.maven.project.MavenProject;
75 import org.apache.maven.project.ProjectBuilder;
76 import org.apache.maven.project.ProjectBuildingException;
77 import org.apache.maven.project.ProjectBuildingRequest;
78 import org.apache.maven.repository.RepositorySystem;
79 import org.apache.maven.shared.filtering.MavenFileFilter;
80 import org.apache.maven.shared.filtering.MavenFilteringException;
81 import org.apache.maven.shared.filtering.MavenResourcesExecution;
82 import org.apache.maven.shared.filtering.MavenResourcesFiltering;
83 import org.codehaus.plexus.PlexusContainer;
84 import org.codehaus.plexus.util.ReaderFactory;
85 import org.codehaus.plexus.util.StringUtils;
86 import org.eclipse.aether.artifact.DefaultArtifact;
87 import org.xml.sax.SAXException;
90 * Generates the features XML file starting with an optional source feature.xml and adding
91 * project dependencies as bundles and feature/car dependencies.
93 * NB this requires a recent maven-install-plugin such as 2.3.1
95 @Mojo(name = "features-generate-descriptor", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true)
96 public class GenerateDescriptorMojo extends MojoSupport {
99 * An (optional) input feature file to extend. The plugin reads this file, and uses it as a template
100 * to create the output.
101 * This is highly recommended as it is the only way to add <code><feature/></code>
102 * elements to the individual features that are generated. Note that this file is filtered using standard Maven
103 * resource interpolation, allowing attributes of the input file to be set with information such as ${project.version}
104 * from the current build.
106 * When dependencies are processed, if they are duplicated in this file, the dependency here provides the baseline
107 * information and is supplemented by additional information from the dependency.
109 @Parameter(defaultValue = "${project.basedir}/src/main/feature/feature.xml")
110 private File inputFile;
113 * (wrapper) The filtered input file. This file holds the result of Maven resource interpolation and is generally
114 * not necessary to change, although it may be helpful for debugging.
116 @Parameter(defaultValue = "${project.build.directory}/feature/filteredInputFeature.xml")
117 private File filteredInputFile;
120 * The file to generate. This file is attached as a project output artifact with the classifier specified by
121 * <code>attachmentArtifactClassifier</code>.
123 @Parameter(defaultValue = "${project.build.directory}/feature/feature.xml")
124 private File outputFile;
127 * Exclude some artifacts from the generated feature.
128 * See addBundlesToPrimaryFeature for more details.
132 private List<String> excludedArtifactIds = new ArrayList<String>();
135 * The resolver to use for the feature. Normally null or "OBR" or "(OBR)"
137 @Parameter(defaultValue = "${resolver}")
138 private String resolver;
141 * The artifact type for attaching the generated file to the project
143 @Parameter(defaultValue = "xml")
144 private String attachmentArtifactType = "xml";
147 * (wrapper) The artifact classifier for attaching the generated file to the project
149 @Parameter(defaultValue = "features")
150 private String attachmentArtifactClassifier = "features";
153 * Specifies whether features dependencies of this project will be included inline in the
154 * final output (<code>true</code>), or simply referenced as output artifact dependencies (<code>false</code>).
155 * If <code>true</code>, feature dependencies' xml descriptors are read and their contents added to the features descriptor under assembly.
156 * If <code>false</code>, feature dependencies are added to the assembled feature as dependencies.
157 * Setting this value to <code>true</code> is especially helpful in multiproject builds where subprojects build their own features
158 * using <code>aggregateFeatures = false</code>, then combined with <code>aggregateFeatures = true</code> in an
159 * aggregation project with explicit dependencies to the child projects.
161 @Parameter(defaultValue = "false")
162 private boolean aggregateFeatures = false;
165 * If present, the bundles added to the feature constructed from the dependencies will be marked with this default
166 * startlevel. If this parameter is not present, no startlevel attribute will be created. Finer resolution for specific
167 * dependencies can be obtained by specifying the dependency in the file referenced by the <code>inputFile</code> parameter.
170 private Integer startLevel;
173 * Installation mode. If present, generate "feature.install" attribute:
175 * <a href="http://karaf.apache.org/xmlns/features/v1.1.0">Installation mode</a>
177 * Can be either manual or auto. Specifies whether the feature should be automatically installed when
178 * dropped inside the deploy folder. Note: this attribute doesn't affect feature descriptors that are installed
179 * from the feature:install command or as part of the etc/org.apache.karaf.features.cfg file.
182 private String installMode;
185 * Flag indicating whether transitive dependencies should be included (<code>true</code>) or not (<code>false</code>).
187 * N.B. Note the default value of this is true, but is suboptimal in cases where specific <code><feature/></code> dependencies are
188 * provided by the <code>inputFile</code> parameter.
190 @Parameter(defaultValue = "true")
191 private boolean includeTransitiveDependency;
194 * The standard behavior is to add dependencies as <code><bundle></code> elements to a <code><feature></code>
195 * with the same name as the artifactId of the project. This flag disables that behavior.
196 * If this parameter is <code>true</code>, then two other parameters refine the list of bundles added to the primary feature:
197 * <code>excludedArtifactIds</code> and <code>ignoreScopeProvided</code>. Each of these specifies dependent artifacts
198 * that should <strong>not</strong> be added to the primary feature.
200 * Note that you may tune the <code>bundle</code> elements by including them in the <code>inputFile</code>.
201 * If the <code>inputFile</code> has a <code>feature</code> element for the primary feature, the plugin will
202 * respect it, so that you can, for example, set the <code>startLevel</code> or <code>start</code> attribute.
206 @Parameter(defaultValue = "true")
207 private boolean addBundlesToPrimaryFeature;
210 * The standard behavior is to add any dependencies other than those in the <code>runtime</code> scope to the feature bundle.
211 * Setting this flag to "true" disables adding any dependencies (transient or otherwise) that are in
212 * <code><scope>provided</scope></code>. See <code>addBundlesToPrimaryFeature</code> for more details.
214 @Parameter(defaultValue = "false")
215 private boolean ignoreScopeProvided;
218 * Flag indicating whether the main project artifact should be included (<code>true</code>) or not (<code>false</code>).
219 * This parameter is useful when you add an execution of this plugin to a project with some packaging that is <strong>not</strong>
220 * <code>feature</code>. If you don't set this, then you will get a feature that contains the dependencies but
221 * not the primary artifact itself.
223 * Assumes the main project artifact is a bundle and the feature will be attached alongside using <code>attachmentArtifactClassifier</code>.
225 @Parameter(defaultValue = "false")
226 private boolean includeProjectArtifact;
229 * The name of the primary feature. This is the feature that will be created or modified to include the
230 * main project artifact and/or the bundles.
231 * @see #addBundlesToPrimaryFeature
232 * @see #includeProjectArtifact
234 @Parameter(defaultValue = "${project.artifactId}")
235 private String primaryFeatureName;
238 * Flag indicating whether bundles should use the version range declared in the POM. If <code>false</code>,
239 * the actual version of the resolved artifacts will be used.
241 @Parameter(defaultValue = "false")
242 private boolean useVersionRange;
245 * Flag indicating whether the plugin should determine whether transitive dependencies are declared with
246 * a version range. If this flag is set to <code>true</code> and a transitive dependency has been found
247 * which had been declared with a version range, that version range will be used to build the appropriate
248 * bundle element instead of the newest version. This flag has only an effect when {@link #useVersionRange}
249 * is <code>true</code>
251 @Parameter(defaultValue = "false")
252 private boolean includeTransitiveVersionRanges;
255 * Flag indicating whether the plugin should simplify bundle dependencies. If the flag is set to {@code true}
256 * and a bundle dependency is determined to be included in a feature dependency, the bundle dependency is
259 @Parameter(defaultValue = "false")
260 private boolean simplifyBundleDependencies;
263 * Names of features which are prerequisites (they still need to be defined separately).
266 private List<String> prerequisiteFeatures = new ArrayList<>();
269 * Names of features which are dependencies (they still need to be defined separately).
272 private List<String> dependencyFeatures = new ArrayList<>();
274 // *************************************************
275 // READ-ONLY MAVEN PLUGIN PARAMETERS
276 // *************************************************
279 * We can't autowire strongly typed RepositorySystem from Aether because it may be Sonatype (Maven 3.0.x)
280 * or Eclipse (Maven 3.1.x/3.2.x) implementation, so we switch to service locator.
283 private PlexusContainer container;
286 private RepositorySystem repoSystem;
289 protected MavenResourcesFiltering mavenResourcesFiltering;
292 protected MavenFileFilter mavenFileFilter;
295 private ProjectBuilder mavenProjectBuilder;
297 // dependencies we are interested in
298 protected Collection<LocalDependency> localDependencies;
300 // log of what happened during search
301 protected String treeListing;
303 // an access layer for available Aether implementation
304 protected DependencyHelper dependencyHelper;
309 // If useVersionRange is true, this map will be used to cache
310 // resolved MavenProjects
311 private final Map<Artifact, MavenProject> resolvedProjects = new HashMap<>();
313 public void execute() throws MojoExecutionException, MojoFailureException {
315 this.dependencyHelper = DependencyHelperFactory.createDependencyHelper(this.container, this.project, this.mavenSession, getLog());
316 this.dependencyHelper.getDependencies(project, includeTransitiveDependency);
317 this.localDependencies = dependencyHelper.getLocalDependencies();
318 this.treeListing = dependencyHelper.getTreeListing();
319 File dir = outputFile.getParentFile();
320 if (dir.isDirectory() || dir.mkdirs()) {
321 PrintStream out = new PrintStream(new FileOutputStream(outputFile));
327 // now lets attach it
328 projectHelper.attachArtifact(project, attachmentArtifactType, attachmentArtifactClassifier, outputFile);
331 throw new MojoExecutionException("Could not create directory for features file: " + dir);
333 } catch (Exception e) {
334 getLog().error(e.getMessage());
335 throw new MojoExecutionException("Unable to create features.xml file: " + e, e);
339 private MavenProject resolveProject(final Object artifact) throws MojoExecutionException {
340 MavenProject resolvedProject = project;
341 if (includeTransitiveVersionRanges) {
342 resolvedProject = resolvedProjects.get(artifact);
343 if (resolvedProject == null) {
344 final ProjectBuildingRequest request = new DefaultProjectBuildingRequest();
346 // Fixes KARAF-4626; if the system properties are not transferred to the request,
347 // test-feature-use-version-range-transfer-properties will fail
348 request.setSystemProperties(System.getProperties());
350 request.setResolveDependencies(true);
351 request.setRemoteRepositories(project.getPluginArtifactRepositories());
352 request.setLocalRepository(localRepo);
353 request.setProfiles(new ArrayList<>(mavenSession.getRequest().getProfiles()));
354 request.setActiveProfileIds(new ArrayList<>(mavenSession.getRequest().getActiveProfiles()));
355 dependencyHelper.setRepositorySession(request);
356 final Artifact pomArtifact = repoSystem.createArtifact(dependencyHelper.getGroupId(artifact),
357 dependencyHelper.getArtifactId(artifact), dependencyHelper.getBaseVersion(artifact), "pom");
359 resolvedProject = mavenProjectBuilder.build(pomArtifact, request).getProject();
360 resolvedProjects.put(pomArtifact, resolvedProject);
361 } catch (final ProjectBuildingException e) {
362 throw new MojoExecutionException(
363 format("Maven-project could not be built for artifact %s", pomArtifact), e);
367 return resolvedProject;
370 private String getVersionOrRange(final Object parent, final Object artifact) throws MojoExecutionException {
371 String versionOrRange = dependencyHelper.getBaseVersion(artifact);
372 if (useVersionRange) {
373 for (final org.apache.maven.model.Dependency dependency : resolveProject(parent).getDependencies()) {
375 if (dependency.getGroupId().equals(dependencyHelper.getGroupId(artifact))
376 && dependency.getArtifactId().equals(dependencyHelper.getArtifactId(artifact))) {
377 versionOrRange = dependency.getVersion();
382 return versionOrRange;
386 * Write all project dependencies as feature
388 private void writeFeatures(PrintStream out) throws ArtifactResolutionException, ArtifactNotFoundException,
389 IOException, JAXBException, SAXException, ParserConfigurationException, XMLStreamException, MojoExecutionException {
390 getLog().info("Generating feature descriptor file " + outputFile.getAbsolutePath());
391 //read in an existing feature.xml
392 ObjectFactory objectFactory = new ObjectFactory();
394 if (inputFile.exists()) {
395 filter(inputFile, filteredInputFile);
396 features = readFeaturesFile(filteredInputFile);
398 features = objectFactory.createFeaturesRoot();
400 if (features.getName() == null) {
401 features.setName(project.getArtifactId());
404 Feature feature = null;
405 for (Feature test : features.getFeature()) {
406 if (test.getName().equals(primaryFeatureName)) {
410 if (feature == null) {
411 feature = objectFactory.createFeature();
412 feature.setName(primaryFeatureName);
414 if (!feature.hasVersion()) {
415 feature.setVersion(project.getArtifact().getBaseVersion());
417 if (feature.getDescription() == null) {
418 feature.setDescription(project.getName());
420 if (installMode != null) {
421 feature.setInstall(installMode);
423 if (project.getDescription() != null && feature.getDetails() == null) {
424 feature.setDetails(project.getDescription());
426 if (includeProjectArtifact) {
427 Bundle bundle = objectFactory.createBundle();
428 bundle.setLocation(this.dependencyHelper.artifactToMvn(project.getArtifact(), project.getVersion()));
429 if (startLevel != null) {
430 bundle.setStartLevel(startLevel);
432 feature.getBundle().add(bundle);
434 boolean needWrap = false;
436 // First pass to look for features
437 // Track other features we depend on and their repositories (we track repositories instead of building them from
438 // the feature's Maven artifact to allow for multi-feature repositories)
439 // TODO Initialise the repositories from the existing feature file if any
440 Map<Dependency, Feature> otherFeatures = new HashMap<>();
441 Map<Feature, String> featureRepositories = new HashMap<Feature, String>();
442 for (final LocalDependency entry : localDependencies) {
443 Object artifact = entry.getArtifact();
445 if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) {
449 processFeatureArtifact(features, feature, otherFeatures, featureRepositories, artifact, entry.getParent(),
453 // Second pass to look for bundles
454 if (addBundlesToPrimaryFeature) {
455 for (final LocalDependency entry : localDependencies) {
456 Object artifact = entry.getArtifact();
458 if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) {
462 if (!this.dependencyHelper.isArtifactAFeature(artifact)) {
463 String bundleName = this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(entry.getParent(), artifact));
464 File bundleFile = this.dependencyHelper.resolve(artifact, getLog());
465 Manifest manifest = getManifest(bundleFile);
467 if (manifest == null || !ManifestUtils.isBundle(getManifest(bundleFile))) {
468 bundleName = "wrap:" + bundleName;
472 Bundle bundle = null;
473 for (Bundle b : feature.getBundle()) {
474 if (bundleName.equals(b.getLocation())) {
479 if (bundle == null) {
480 bundle = objectFactory.createBundle();
481 bundle.setLocation(bundleName);
482 // Check the features this feature depends on don't already contain the dependency
483 // TODO Perhaps only for transitive dependencies?
484 boolean includedTransitively =
485 simplifyBundleDependencies && isBundleIncludedTransitively(feature, otherFeatures, bundle);
486 if (!includedTransitively && (!"provided".equals(entry.getScope()) || !ignoreScopeProvided)) {
487 feature.getBundle().add(bundle);
490 if ("runtime".equals(entry.getScope())) {
491 bundle.setDependency(true);
493 if (startLevel != null && bundle.getStartLevel() == 0) {
494 bundle.setStartLevel(startLevel);
501 Dependency wrapDependency = new Dependency();
502 wrapDependency.setName("wrap");
503 wrapDependency.setDependency(false);
504 wrapDependency.setPrerequisite(true);
505 feature.getFeature().add(wrapDependency);
508 if ((!feature.getBundle().isEmpty() || !feature.getFeature().isEmpty()) && !features.getFeature().contains(feature)) {
509 features.getFeature().add(feature);
512 // Add any missing repositories for the included features
513 for (Feature includedFeature : features.getFeature()) {
514 for (Dependency dependency : includedFeature.getFeature()) {
515 Feature dependedFeature = otherFeatures.get(dependency);
516 if (dependedFeature != null && !features.getFeature().contains(dependedFeature)) {
517 String repository = featureRepositories.get(dependedFeature);
518 if (repository != null && !features.getRepository().contains(repository)) {
519 features.getRepository().add(repository);
525 JaxbUtil.marshal(features, out);
527 checkChanges(features, objectFactory);
528 } catch (Exception e) {
529 throw new MojoExecutionException("Features contents have changed", e);
531 getLog().info("...done!");
534 private void processFeatureArtifact(Features features, Feature feature, Map<Dependency, Feature> otherFeatures,
535 Map<Feature, String> featureRepositories,
536 Object artifact, Object parent, boolean add)
537 throws MojoExecutionException, XMLStreamException, JAXBException, IOException {
538 if (this.dependencyHelper.isArtifactAFeature(artifact) && FEATURE_CLASSIFIER.equals(
539 this.dependencyHelper.getClassifier(artifact))) {
540 File featuresFile = this.dependencyHelper.resolve(artifact, getLog());
541 if (featuresFile == null || !featuresFile.exists()) {
542 throw new MojoExecutionException(
543 "Cannot locate file for feature: " + artifact + " at " + featuresFile);
545 Features includedFeatures = readFeaturesFile(featuresFile);
546 for (String repository : includedFeatures.getRepository()) {
547 processFeatureArtifact(features, feature, otherFeatures, featureRepositories,
548 new DefaultArtifact(MavenUtil.mvnToAether(repository)), parent, false);
550 for (Feature includedFeature : includedFeatures.getFeature()) {
551 Dependency dependency = new Dependency(includedFeature.getName(), includedFeature.getVersion());
552 dependency.setPrerequisite(prerequisiteFeatures.contains(dependency.getName()));
553 dependency.setDependency(dependencyFeatures.contains(dependency.getName()));
554 // Determine what dependency we're actually going to use
555 Dependency matchingDependency = findMatchingDependency(feature.getFeature(), dependency);
556 if (matchingDependency != null) {
557 // The feature already has a matching dependency, merge them
558 mergeDependencies(matchingDependency, dependency);
559 dependency = matchingDependency;
561 // We mustn't de-duplicate here, we may have seen a feature in !add mode
562 otherFeatures.put(dependency, includedFeature);
564 if (!feature.getFeature().contains(dependency)) {
565 feature.getFeature().add(dependency);
567 if (aggregateFeatures) {
568 features.getFeature().add(includedFeature);
571 if (!featureRepositories.containsKey(includedFeature)) {
572 featureRepositories.put(includedFeature,
573 this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(parent, artifact)));
579 private Dependency findMatchingDependency(List<Dependency> dependencies, Dependency reference) {
580 String referenceName = reference.getName();
581 for (Dependency dependency : dependencies) {
582 if (referenceName.equals(dependency.getName())) {
589 private void mergeDependencies(Dependency target, Dependency source) {
590 if (target.getVersion() == null || Feature.DEFAULT_VERSION.equals(target.getVersion())) {
591 target.setVersion(source.getVersion());
593 if (source.isDependency()) {
594 target.setDependency(true);
596 if (source.isPrerequisite()) {
597 target.setPrerequisite(true);
601 private boolean isBundleIncludedTransitively(Feature feature, Map<Dependency, Feature> otherFeatures,
603 for (Dependency dependency : feature.getFeature()) {
604 Feature otherFeature = otherFeatures.get(dependency);
605 if (otherFeature != null) {
606 if (otherFeature.getBundle().contains(bundle) || isBundleIncludedTransitively(otherFeature,
607 otherFeatures, bundle)) {
616 * Extract the MANIFEST from the give file.
619 private Manifest getManifest(File file) throws IOException {
622 is = new BufferedInputStream(new FileInputStream(file));
623 } catch (Exception e) {
624 getLog().warn("Error while opening artifact", e);
630 JarInputStream jar = new JarInputStream(is);
631 Manifest m = jar.getManifest();
633 getLog().warn("Manifest not present in the first entry of the zip - " + file.getName());
638 // just in case when we did not open bundle
643 private Features readFeaturesFile(File featuresFile) throws XMLStreamException, JAXBException, IOException {
644 return JaxbUtil.unmarshal(featuresFile.toURI().toASCIIString(), false);
647 public void setLog(Log log) {
651 public Log getLog() {
653 setLog(new SystemStreamLog());
658 //------------------------------------------------------------------------//
659 // dependency change detection
662 * Master switch to look for and log changed dependencies. If this is set to <code>true</code> and the file referenced by
663 * <code>dependencyCache</code> does not exist, it will be unconditionally generated. If the file does exist, it is
664 * used to detect changes from previous builds and generate logs of those changes. In that case,
665 * <code>failOnDependencyChange = true</code> will cause the build to fail.
667 @Parameter(defaultValue = "false")
668 private boolean checkDependencyChange;
671 * (wrapper) Location of dependency cache. This file is generated to contain known dependencies and is generally
672 * located in SCM so that it may be used across separate developer builds. This is parameter is ignored unless
673 * <code>checkDependencyChange</code> is set to <code>true</code>.
675 @Parameter(defaultValue = "${basedir}/src/main/history/dependencies.xml")
676 private File dependencyCache;
679 * Location of filtered dependency file.
681 @Parameter(defaultValue = "${basedir}/target/history/dependencies.xml", readonly = true)
682 private File filteredDependencyCache;
685 * Whether to fail on changed dependencies (default, <code>true</code>) or warn (<code>false</code>). This is parameter is ignored unless
686 * <code>checkDependencyChange</code> is set to <code>true</code> and <code>dependencyCache</code> exists to compare
689 @Parameter(defaultValue = "true")
690 private boolean failOnDependencyChange;
693 * Copies the contents of dependency change logs that are generated to stdout. This is parameter is ignored unless
694 * <code>checkDependencyChange</code> is set to <code>true</code> and <code>dependencyCache</code> exists to compare
697 @Parameter(defaultValue = "false")
698 private boolean logDependencyChanges;
701 * Whether to overwrite the file referenced by <code>dependencyCache</code> if it has changed. This is parameter is
702 * ignored unless <code>checkDependencyChange</code> is set to <code>true</code>, <code>failOnDependencyChange</code>
703 * is set to <code>false</code> and <code>dependencyCache</code> exists to compare against.
705 @Parameter(defaultValue = "false")
706 private boolean overwriteChangedDependencies;
710 * The character encoding scheme to be applied when filtering resources.
712 @Parameter(defaultValue = "${project.build.sourceEncoding}")
713 protected String encoding;
716 * Expression preceded with the String won't be interpolated
717 * \${foo} will be replaced with ${foo}
719 @Parameter(defaultValue = "${maven.resources.escapeString}")
720 protected String escapeString = "\\";
726 protected Map<String, String> systemProperties;
728 private void checkChanges(Features newFeatures, ObjectFactory objectFactory) throws Exception, IOException, JAXBException, XMLStreamException {
729 if (checkDependencyChange) {
730 //combine all the dependencies to one feature and strip out versions
731 Features features = objectFactory.createFeaturesRoot();
732 features.setName(newFeatures.getName());
733 Feature feature = objectFactory.createFeature();
734 features.getFeature().add(feature);
735 for (Feature f : newFeatures.getFeature()) {
736 for (Bundle b : f.getBundle()) {
737 Bundle bundle = objectFactory.createBundle();
738 bundle.setLocation(b.getLocation());
739 feature.getBundle().add(bundle);
741 for (Dependency d : f.getFeature()) {
742 Dependency dependency = objectFactory.createDependency();
743 dependency.setName(d.getName());
744 feature.getFeature().add(dependency);
748 Collections.sort(feature.getBundle(), new Comparator<Bundle>() {
750 public int compare(Bundle bundle, Bundle bundle1) {
751 return bundle.getLocation().compareTo(bundle1.getLocation());
754 Collections.sort(feature.getFeature(), new Comparator<Dependency>() {
755 public int compare(Dependency dependency, Dependency dependency1) {
756 return dependency.getName().compareTo(dependency1.getName());
760 if (dependencyCache.exists()) {
761 //filter dependencies file
762 filter(dependencyCache, filteredDependencyCache);
763 //read dependency types, convert to dependencies, compare.
764 Features oldfeatures = readFeaturesFile(filteredDependencyCache);
765 Feature oldFeature = oldfeatures.getFeature().get(0);
767 List<Bundle> addedBundles = new ArrayList<Bundle>(feature.getBundle());
768 List<Bundle> removedBundles = new ArrayList<Bundle>();
769 for (Bundle test : oldFeature.getBundle()) {
770 boolean t1 = addedBundles.contains(test);
771 int s1 = addedBundles.size();
772 boolean t2 = addedBundles.remove(test);
773 int s2 = addedBundles.size();
775 getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
777 if (t1 == (s1 == s2)) {
778 getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2);
781 removedBundles.add(test);
785 List<Dependency> addedDependencys = new ArrayList<Dependency>(feature.getFeature());
786 List<Dependency> removedDependencys = new ArrayList<Dependency>();
787 for (Dependency test : oldFeature.getFeature()) {
788 boolean t1 = addedDependencys.contains(test);
789 int s1 = addedDependencys.size();
790 boolean t2 = addedDependencys.remove(test);
791 int s2 = addedDependencys.size();
793 getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
795 if (t1 == (s1 == s2)) {
796 getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2);
799 removedDependencys.add(test);
802 if (!addedBundles.isEmpty() || !removedBundles.isEmpty() || !addedDependencys.isEmpty() || !removedDependencys.isEmpty()) {
803 saveDependencyChanges(addedBundles, removedBundles, addedDependencys, removedDependencys, objectFactory);
804 if (overwriteChangedDependencies) {
805 writeDependencies(features, dependencyCache);
808 getLog().info(saveTreeListing());
812 writeDependencies(features, dependencyCache);
817 protected void saveDependencyChanges(Collection<Bundle> addedBundles, Collection<Bundle> removedBundles, Collection<Dependency> addedDependencys, Collection<Dependency> removedDependencys, ObjectFactory objectFactory)
819 File addedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.added.xml");
820 Features added = toFeatures(addedBundles, addedDependencys, objectFactory);
821 writeDependencies(added, addedFile);
823 File removedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.removed.xml");
824 Features removed = toFeatures(removedBundles, removedDependencys, objectFactory);
825 writeDependencies(removed, removedFile);
827 StringWriter out = new StringWriter();
828 out.write(saveTreeListing());
830 out.write("Dependencies have changed:\n");
831 if (!addedBundles.isEmpty() || !addedDependencys.isEmpty()) {
832 out.write("\tAdded dependencies are saved here: " + addedFile.getAbsolutePath() + "\n");
833 if (logDependencyChanges) {
834 JaxbUtil.marshal(added, out);
837 if (!removedBundles.isEmpty() || !removedDependencys.isEmpty()) {
838 out.write("\tRemoved dependencies are saved here: " + removedFile.getAbsolutePath() + "\n");
839 if (logDependencyChanges) {
840 JaxbUtil.marshal(removed, out);
843 out.write("Delete " + dependencyCache.getAbsolutePath()
844 + " if you are happy with the dependency changes.");
846 if (failOnDependencyChange) {
847 throw new MojoFailureException(out.toString());
849 getLog().warn(out.toString());
853 private Features toFeatures(Collection<Bundle> addedBundles, Collection<Dependency> addedDependencys, ObjectFactory objectFactory) {
854 Features features = objectFactory.createFeaturesRoot();
855 Feature feature = objectFactory.createFeature();
856 feature.getBundle().addAll(addedBundles);
857 feature.getFeature().addAll(addedDependencys);
858 features.getFeature().add(feature);
863 private void writeDependencies(Features features, File file) throws JAXBException, IOException {
864 file.getParentFile().mkdirs();
865 if (!file.getParentFile().exists() || !file.getParentFile().isDirectory()) {
866 throw new IOException("Cannot create directory at " + file.getParent());
868 FileOutputStream out = new FileOutputStream(file);
870 JaxbUtil.marshal(features, out);
876 protected void filter(File sourceFile, File targetFile)
877 throws MojoExecutionException {
880 if (StringUtils.isEmpty(encoding)) {
882 "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
883 + ", i.e. build is platform dependent!");
885 targetFile.getParentFile().mkdirs();
887 final MavenResourcesExecution mre = new MavenResourcesExecution();
888 mre.setMavenProject(project);
889 mre.setMavenSession(mavenSession);
890 mre.setFilters(null);
891 mre.setEscapedBackslashesInFilePath(true);
892 final LinkedHashSet<String> delimiters = new LinkedHashSet<>();
893 delimiters.add("${*}");
894 mre.setDelimiters(delimiters);
896 @SuppressWarnings("rawtypes")
897 List filters = mavenFileFilter.getDefaultFilterWrappers(mre);
898 mavenFileFilter.copyFile(sourceFile, targetFile, true, filters, encoding, true);
899 } catch (MavenFilteringException e) {
900 throw new MojoExecutionException(e.getMessage(), e);
904 protected String saveTreeListing() throws IOException {
905 File treeListFile = new File(filteredDependencyCache.getParentFile(), "treeListing.txt");
906 OutputStream os = new FileOutputStream(treeListFile);
907 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os));
909 writer.write(treeListing);
913 return "\tTree listing is saved here: " + treeListFile.getAbsolutePath() + "\n";