Add .tox/ to .gitignore
[odlparent.git] / karaf / karaf-maven-plugin / src / main / java / org / apache / karaf / tooling / features / GenerateDescriptorMojo.java
1 /**
2  *
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
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
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.
17  */
18 package org.apache.karaf.tooling.features;
19
20 import static java.lang.String.format;
21 import static org.apache.karaf.deployer.kar.KarArtifactInstaller.FEATURE_CLASSIFIER;
22
23 import java.io.BufferedInputStream;
24 import java.io.BufferedWriter;
25 import java.io.File;
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;
41 import java.util.Map;
42 import java.util.jar.JarInputStream;
43 import java.util.jar.Manifest;
44
45 import javax.xml.bind.JAXBException;
46 import javax.xml.parsers.ParserConfigurationException;
47 import javax.xml.stream.XMLStreamException;
48
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;
88
89 /**
90  * Generates the features XML file starting with an optional source feature.xml and adding
91  * project dependencies as bundles and feature/car dependencies.
92  * 
93  * NB this requires a recent maven-install-plugin such as 2.3.1
94  */
95 @Mojo(name = "features-generate-descriptor", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true)
96 public class GenerateDescriptorMojo extends MojoSupport {
97
98     /**
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>&lt;feature/&gt;</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.
105      * <p/>
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.
108      */
109     @Parameter(defaultValue = "${project.basedir}/src/main/feature/feature.xml")
110     private File inputFile;
111
112     /**
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.
115      */
116     @Parameter(defaultValue = "${project.build.directory}/feature/filteredInputFeature.xml")
117     private File filteredInputFile;
118
119     /**
120      * The file to generate.  This file is attached as a project output artifact with the classifier specified by
121      * <code>attachmentArtifactClassifier</code>.
122      */
123     @Parameter(defaultValue = "${project.build.directory}/feature/feature.xml")
124     private File outputFile;
125
126     /**
127      * Exclude some artifacts from the generated feature.
128      * See addBundlesToPrimaryFeature for more details.
129      *
130      */
131     @Parameter
132     private List<String> excludedArtifactIds = new ArrayList<String>();
133
134     /**
135      * The resolver to use for the feature.  Normally null or "OBR" or "(OBR)"
136      */
137     @Parameter(defaultValue = "${resolver}")
138     private String resolver;
139
140     /**
141      * The artifact type for attaching the generated file to the project
142      */
143     @Parameter(defaultValue = "xml")
144     private String attachmentArtifactType = "xml";
145
146     /**
147      * (wrapper) The artifact classifier for attaching the generated file to the project
148      */
149     @Parameter(defaultValue = "features")
150     private String attachmentArtifactClassifier = "features";
151
152     /**
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.
160      */
161     @Parameter(defaultValue = "false")
162     private boolean aggregateFeatures = false;
163
164     /**
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.
168      */
169     @Parameter
170     private Integer startLevel;
171
172     /**
173      * Installation mode. If present, generate "feature.install" attribute:
174      * <p/>
175      * <a href="http://karaf.apache.org/xmlns/features/v1.1.0">Installation mode</a>
176      * <p/>
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.
180      */
181     @Parameter
182     private String installMode;
183
184     /**
185      * Flag indicating whether transitive dependencies should be included (<code>true</code>) or not (<code>false</code>).
186      * <p/>
187      * N.B. Note the default value of this is true, but is suboptimal in cases where specific <code>&lt;feature/&gt;</code> dependencies are
188      * provided by the <code>inputFile</code> parameter.
189      */
190     @Parameter(defaultValue = "true")
191     private boolean includeTransitiveDependency;
192
193     /**
194      * The standard behavior is to add dependencies as <code>&lt;bundle&gt;</code> elements to a <code>&lt;feature&gt;</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.
199      * <p>
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.
203      * </p>
204      *
205      */
206     @Parameter(defaultValue = "true")
207     private boolean addBundlesToPrimaryFeature;
208
209     /**
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>&lt;scope&gt;provided&lt;/scope&gt;</code>. See <code>addBundlesToPrimaryFeature</code> for more details.
213      */
214     @Parameter(defaultValue = "false")
215     private boolean ignoreScopeProvided;
216
217     /**
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.
222      * <p/>
223      * Assumes the main project artifact is a bundle and the feature will be attached alongside using <code>attachmentArtifactClassifier</code>.
224      */
225     @Parameter(defaultValue = "false")
226     private boolean includeProjectArtifact;
227
228     /**
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
233      */
234     @Parameter(defaultValue = "${project.artifactId}")
235     private String primaryFeatureName;
236     
237     /**
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.
240      */
241     @Parameter(defaultValue = "false")
242     private boolean useVersionRange;
243     
244     /**
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>
250      */
251     @Parameter(defaultValue = "false")
252     private boolean includeTransitiveVersionRanges;
253
254     /**
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
257      * dropped.
258      */
259     @Parameter(defaultValue = "false")
260     private boolean simplifyBundleDependencies;
261
262     /**
263      * Names of features which are prerequisites (they still need to be defined separately).
264      */
265     @Parameter
266     private List<String> prerequisiteFeatures = new ArrayList<>();
267
268     /**
269      * Names of features which are dependencies (they still need to be defined separately).
270      */
271     @Parameter
272     private List<String> dependencyFeatures = new ArrayList<>();
273
274     // *************************************************
275     // READ-ONLY MAVEN PLUGIN PARAMETERS
276     // *************************************************
277
278     /**
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.
281      */
282     @Component
283     private PlexusContainer container;
284     
285     @Component
286     private RepositorySystem repoSystem;
287
288     @Component
289     protected MavenResourcesFiltering mavenResourcesFiltering;
290
291     @Component
292     protected MavenFileFilter mavenFileFilter;
293     
294         @Component
295         private ProjectBuilder mavenProjectBuilder;
296
297     // dependencies we are interested in
298     protected Collection<LocalDependency> localDependencies;
299
300     // log of what happened during search
301     protected String treeListing;
302
303     // an access layer for available Aether implementation
304     protected DependencyHelper dependencyHelper;
305
306     // maven log
307     private Log log;
308     
309     // If useVersionRange is true, this map will be used to cache
310     // resolved MavenProjects
311     private final Map<Artifact, MavenProject> resolvedProjects = new HashMap<>();
312
313     public void execute() throws MojoExecutionException, MojoFailureException {
314         try {
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));
322                 try {
323                     writeFeatures(out);
324                 } finally {
325                     out.close();
326                 }
327                 // now lets attach it
328                 projectHelper.attachArtifact(project, attachmentArtifactType, attachmentArtifactClassifier, outputFile);
329
330             } else {
331                 throw new MojoExecutionException("Could not create directory for features file: " + dir);
332             }
333         } catch (Exception e) {
334             getLog().error(e.getMessage());
335             throw new MojoExecutionException("Unable to create features.xml file: " + e, e);
336         }
337     }
338
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();
345                                 
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());
349                                 
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");
358                                 try {
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);
364                                 }
365                         }
366                 }
367                 return resolvedProject;
368         }
369
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()) {
374
375                                 if (dependency.getGroupId().equals(dependencyHelper.getGroupId(artifact))
376                                                 && dependency.getArtifactId().equals(dependencyHelper.getArtifactId(artifact))) {
377                                         versionOrRange = dependency.getVersion();
378                                         break;
379                                 }
380                         }
381                 }
382                 return versionOrRange;
383         }
384     
385     /*
386      * Write all project dependencies as feature
387      */
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();
393         Features features;
394         if (inputFile.exists()) {
395             filter(inputFile, filteredInputFile);
396             features = readFeaturesFile(filteredInputFile);
397         } else {
398             features = objectFactory.createFeaturesRoot();
399         }
400         if (features.getName() == null) {
401             features.setName(project.getArtifactId());
402         }
403
404         Feature feature = null;
405         for (Feature test : features.getFeature()) {
406             if (test.getName().equals(primaryFeatureName)) {
407                 feature = test;
408             }
409         }
410         if (feature == null) {
411             feature = objectFactory.createFeature();
412             feature.setName(primaryFeatureName);
413         }
414         if (!feature.hasVersion()) {
415             feature.setVersion(project.getArtifact().getBaseVersion());
416         }
417         if (feature.getDescription() == null) {
418             feature.setDescription(project.getName());
419         }
420         if (installMode != null) {
421             feature.setInstall(installMode);
422         }
423         if (project.getDescription() != null && feature.getDetails() == null) {
424             feature.setDetails(project.getDescription());
425         }
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);
431             }
432             feature.getBundle().add(bundle);
433         }
434         boolean needWrap = false;
435
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();
444
445             if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) {
446                 continue;
447             }
448
449             processFeatureArtifact(features, feature, otherFeatures, featureRepositories, artifact, entry.getParent(),
450                     true);
451         }
452
453         // Second pass to look for bundles
454         if (addBundlesToPrimaryFeature) {
455             for (final LocalDependency entry : localDependencies) {
456                 Object artifact = entry.getArtifact();
457
458                 if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) {
459                     continue;
460                 }
461
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);
466
467                     if (manifest == null || !ManifestUtils.isBundle(getManifest(bundleFile))) {
468                         bundleName = "wrap:" + bundleName;
469                         needWrap = true;
470                     }
471
472                     Bundle bundle = null;
473                     for (Bundle b : feature.getBundle()) {
474                         if (bundleName.equals(b.getLocation())) {
475                             bundle = b;
476                             break;
477                         }
478                     }
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);
488                         }
489                     }
490                     if ("runtime".equals(entry.getScope())) {
491                         bundle.setDependency(true);
492                     }
493                     if (startLevel != null && bundle.getStartLevel() == 0) {
494                         bundle.setStartLevel(startLevel);
495                     }
496                 }
497             }
498         }
499
500         if (needWrap) {
501             Dependency wrapDependency = new Dependency();
502             wrapDependency.setName("wrap");
503             wrapDependency.setDependency(false);
504             wrapDependency.setPrerequisite(true);
505             feature.getFeature().add(wrapDependency);
506         }
507         
508         if ((!feature.getBundle().isEmpty() || !feature.getFeature().isEmpty()) && !features.getFeature().contains(feature)) {
509             features.getFeature().add(feature);
510         }
511
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);
520                     }
521                 }
522             }
523         }
524
525         JaxbUtil.marshal(features, out);
526         try {
527             checkChanges(features, objectFactory);
528         } catch (Exception e) {
529             throw new MojoExecutionException("Features contents have changed", e);
530         }
531         getLog().info("...done!");
532     }
533
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);
544             }
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);
549             }
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;
560                 }
561                 // We mustn't de-duplicate here, we may have seen a feature in !add mode
562                 otherFeatures.put(dependency, includedFeature);
563                 if (add) {
564                     if (!feature.getFeature().contains(dependency)) {
565                         feature.getFeature().add(dependency);
566                     }
567                     if (aggregateFeatures) {
568                         features.getFeature().add(includedFeature);
569                     }
570                 }
571                 if (!featureRepositories.containsKey(includedFeature)) {
572                     featureRepositories.put(includedFeature,
573                             this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(parent, artifact)));
574                 }
575             }
576         }
577     }
578
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())) {
583                 return dependency;
584             }
585         }
586         return null;
587     }
588
589     private void mergeDependencies(Dependency target, Dependency source) {
590         if (target.getVersion() == null || Feature.DEFAULT_VERSION.equals(target.getVersion())) {
591             target.setVersion(source.getVersion());
592         }
593         if (source.isDependency()) {
594             target.setDependency(true);
595         }
596         if (source.isPrerequisite()) {
597             target.setPrerequisite(true);
598         }
599     }
600
601     private boolean isBundleIncludedTransitively(Feature feature, Map<Dependency, Feature> otherFeatures,
602                                                  Bundle bundle) {
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)) {
608                     return true;
609                 }
610             }
611         }
612         return false;
613     }
614
615     /**
616      * Extract the MANIFEST from the give file.
617      */
618
619     private Manifest getManifest(File file) throws IOException {
620         InputStream is;
621         try {
622             is = new BufferedInputStream(new FileInputStream(file));
623         } catch (Exception e) {
624             getLog().warn("Error while opening artifact", e);
625             return null;
626         }
627
628         try {
629             is.mark(256 * 1024);
630             JarInputStream jar = new JarInputStream(is);
631             Manifest m = jar.getManifest();
632             if (m == null) {
633                 getLog().warn("Manifest not present in the first entry of the zip - " + file.getName());
634             }
635             jar.close();
636             return m;
637         } finally {
638             // just in case when we did not open bundle
639             is.close();
640         }
641     }
642
643     private Features readFeaturesFile(File featuresFile) throws XMLStreamException, JAXBException, IOException {
644         return JaxbUtil.unmarshal(featuresFile.toURI().toASCIIString(), false);
645     }
646
647     public void setLog(Log log) {
648         this.log = log;
649     }
650
651     public Log getLog() {
652         if (log == null) {
653             setLog(new SystemStreamLog());
654         }
655         return log;
656     }
657
658     //------------------------------------------------------------------------//
659     // dependency change detection
660
661     /**
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.
666      */
667     @Parameter(defaultValue = "false")
668     private boolean checkDependencyChange;
669
670     /**
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>.
674      */
675     @Parameter(defaultValue = "${basedir}/src/main/history/dependencies.xml")
676     private File dependencyCache;
677
678     /**
679      * Location of filtered dependency file.
680      */
681     @Parameter(defaultValue = "${basedir}/target/history/dependencies.xml", readonly = true)
682     private File filteredDependencyCache;
683
684     /**
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
687      * against.
688      */
689     @Parameter(defaultValue = "true")
690     private boolean failOnDependencyChange;
691
692     /**
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
695      * against.
696      */
697     @Parameter(defaultValue = "false")
698     private boolean logDependencyChanges;
699
700     /**
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.
704      */
705     @Parameter(defaultValue = "false")
706     private boolean overwriteChangedDependencies;
707
708     //filtering support
709     /**
710      * The character encoding scheme to be applied when filtering resources.
711      */
712     @Parameter(defaultValue = "${project.build.sourceEncoding}")
713     protected String encoding;
714
715     /**
716      * Expression preceded with the String won't be interpolated
717      * \${foo} will be replaced with ${foo}
718      */
719     @Parameter(defaultValue = "${maven.resources.escapeString}")
720     protected String escapeString = "\\";
721
722     /**
723      * System properties.
724      */
725     @Parameter
726     protected Map<String, String> systemProperties;
727
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);
740                 }
741                 for (Dependency d : f.getFeature()) {
742                     Dependency dependency = objectFactory.createDependency();
743                     dependency.setName(d.getName());
744                     feature.getFeature().add(dependency);
745                 }
746             }
747
748             Collections.sort(feature.getBundle(), new Comparator<Bundle>() {
749
750                 public int compare(Bundle bundle, Bundle bundle1) {
751                     return bundle.getLocation().compareTo(bundle1.getLocation());
752                 }
753             });
754             Collections.sort(feature.getFeature(), new Comparator<Dependency>() {
755                 public int compare(Dependency dependency, Dependency dependency1) {
756                     return dependency.getName().compareTo(dependency1.getName());
757                 }
758             });
759
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);
766
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();
774                     if (t1 != t2) {
775                         getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
776                     }
777                     if (t1 == (s1 == s2)) {
778                         getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2);
779                     }
780                     if (!t2) {
781                         removedBundles.add(test);
782                     }
783                 }
784
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();
792                     if (t1 != t2) {
793                         getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
794                     }
795                     if (t1 == (s1 == s2)) {
796                         getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2);
797                     }
798                     if (!t2) {
799                         removedDependencys.add(test);
800                     }
801                 }
802                 if (!addedBundles.isEmpty() || !removedBundles.isEmpty() || !addedDependencys.isEmpty() || !removedDependencys.isEmpty()) {
803                     saveDependencyChanges(addedBundles, removedBundles, addedDependencys, removedDependencys, objectFactory);
804                     if (overwriteChangedDependencies) {
805                         writeDependencies(features, dependencyCache);
806                     }
807                 } else {
808                     getLog().info(saveTreeListing());
809                 }
810
811             } else {
812                 writeDependencies(features, dependencyCache);
813             }
814         }
815     }
816
817     protected void saveDependencyChanges(Collection<Bundle> addedBundles, Collection<Bundle> removedBundles, Collection<Dependency> addedDependencys, Collection<Dependency> removedDependencys, ObjectFactory objectFactory)
818             throws Exception {
819         File addedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.added.xml");
820         Features added = toFeatures(addedBundles, addedDependencys, objectFactory);
821         writeDependencies(added, addedFile);
822
823         File removedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.removed.xml");
824         Features removed = toFeatures(removedBundles, removedDependencys, objectFactory);
825         writeDependencies(removed, removedFile);
826
827         StringWriter out = new StringWriter();
828         out.write(saveTreeListing());
829
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);
835             }
836         }
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);
841             }
842         }
843         out.write("Delete " + dependencyCache.getAbsolutePath()
844                 + " if you are happy with the dependency changes.");
845
846         if (failOnDependencyChange) {
847             throw new MojoFailureException(out.toString());
848         } else {
849             getLog().warn(out.toString());
850         }
851     }
852
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);
859         return features;
860     }
861
862
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());
867         }
868         FileOutputStream out = new FileOutputStream(file);
869         try {
870             JaxbUtil.marshal(features, out);
871         } finally {
872             out.close();
873         }
874     }
875
876     protected void filter(File sourceFile, File targetFile)
877             throws MojoExecutionException {
878         try {
879
880             if (StringUtils.isEmpty(encoding)) {
881                 getLog().warn(
882                         "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
883                                 + ", i.e. build is platform dependent!");
884             }
885             targetFile.getParentFile().mkdirs();
886
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);
895
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);
901         }
902     }
903
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));
908         try {
909             writer.write(treeListing);
910         } finally {
911             writer.close();
912         }
913         return "\tTree listing is saved here: " + treeListFile.getAbsolutePath() + "\n";
914     }
915
916 }