/**
*
* 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 java.io.*;
import java.util.*;
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.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.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.Mojo;
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.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.filtering.MavenResourcesFiltering;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.xml.sax.SAXException;
import static org.apache.karaf.deployer.kar.KarArtifactInstaller.FEATURE_CLASSIFIER;
/**
* Generates the features XML file
* NB this requires a recent maven-install-plugin such as 2.3.1
*
* @goal features-generate-descriptor
* @phase compile
* @requiresDependencyResolution runtime
* @inheritByDefault true
* @description Generates the features XML file starting with an optional source feature.xml and adding
* project dependencies as bundles and feature/car dependencies
*/
public class GenerateDescriptorMojo extends AbstractLogEnabled implements Mojo {
/**
* An (optional) input feature file to extend. This is highly recommended as it is the only way to add <code><feature/></code>
* 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.
* <p/>
* 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 default-value="${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 default-value="${project.build.directory}/feature/filteredInputFeature.xml"
*/
private File filteredInputFile;
/**
* (wrapper) The file to generate. This file is attached as a project output artifact.
*
* @parameter default-value="${project.build.directory}/feature/feature.xml"
*/
private File outputFile;
/**
* (wrapper) Exclude some artifacts from the generated feature.
*
* @parameter
*/
private List<String> excludedArtifactIds = new ArrayList<String>();
/**
* The resolver to use for the feature. Normally null or "OBR" or "(OBR)"
*
* @parameter default-value="${resolver}"
*/
private String resolver;
/**
* (wrapper) The artifact type for attaching the generated file to the project
*
* @parameter default-value="xml"
*/
private String attachmentArtifactType = "xml";
/**
* (wrapper) The artifact classifier for attaching the generated file to the project
*
* @parameter default-value="features"
*/
private String attachmentArtifactClassifier = "features";
/**
* Specifies whether features dependencies of this project will be included inline of the the
* final output (<code>true</code>) or simply referenced as output artifact dependencies (<code>false</code>).
* If <code>true</code>, feature dependencies xml descriptors are read and their contents added to the features descriptor under assembly.
* If <code>false</code>, feature dependencies are added to the assembled feature as dependencies.
* Setting this value to <code>true</code> is especially helpful in multiproject builds where subprojects build their own features
* using <code>aggregateFeatures = false</code>, then combined with <code>aggregateFeatures = true</code> in an
* aggregation project with explicit dependencies to the child projects.
*
* @parameter default-value="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 <code>inputFile</code> parameter.
*
* @parameter
*/
private Integer startLevel;
/**
* Installation mode. If present, generate "feature.install" attribute:
* <p/>
* <a href="http://karaf.apache.org/xmlns/features/v1.1.0">Installation mode</a>
* <p/>
* 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 (<code>true</code>) or not (<code>false</code>).
* <p/>
* N.B. Note the default value of this is true, but is suboptimal in cases where specific <code><feature/></code> dependencies are
* provided by the <code>inputFile</code> parameter.
*
* @parameter default-value="true"
*/
private boolean includeTransitiveDependency;
/**
* The standard behavior is to add dependencies as <code><bundle></code> elements to a <code><feature></code>
* with the same name as the artifactId of the project. This flag disables that behavior.
*
* @parameter default-value="true"
*/
private boolean addBundlesToPrimaryFeature;
/**
* The standard behavior is to add any dependencies other than those in the <code>runtime</code> scope to the feature bundle.
* Setting this flag to "true" disables adding any dependencies (transient or otherwise) that are in
* <code><scope>provided</scope></code>.
*
* @parameter default-value="false"
*/
private boolean ignoreScopeProvided;
/**
* Flag indicating whether the main project artifact should be included (<code>true</code>) or not (<code>false</code>).
* <p/>
* Assumes the main project artifact is a bundle and the feature will be attached alongside using <code>attachmentArtifactClassifier</code>.
*
* @parameter default-value="false"
*/
private boolean includeProjectArtifact;
// *************************************************
// READ-ONLY MAVEN PLUGIN PARAMETERS
// *************************************************
/**
* (wrapper) The maven project.
*
* @parameter default-value="${project}"
* @required
* @readonly
*/
protected MavenProject project;
/**
* The maven project's helper.
*
* @component
* @required
* @readonly
*/
protected MavenProjectHelper projectHelper;
/**
* 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
* @required
* @readonly
*/
private PlexusContainer container;
/**
* @component role="org.apache.maven.shared.filtering.MavenResourcesFiltering" role-hint="default"
* @required
* @readonly
*/
protected MavenResourcesFiltering mavenResourcesFiltering;
/**
* @parameter default-value="${session}"
* @required
* @readonly
*/
protected MavenSession session;
/**
* @plexus.requirement role-hint="default"
* @component
* @required
* @readonly
*/
protected MavenFileFilter mavenFileFilter;
// dependencies we are interested in
protected Map<?, String> 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;
public void execute() throws MojoExecutionException, MojoFailureException {
try {
this.dependencyHelper = DependencyHelperFactory.createDependencyHelper(this.container, this.project, this.session, getLog());
this.dependencyHelper.getDependencies(project, includeTransitiveDependency);
this.localDependencies = dependencyHelper.getLocalDependencies();
this.treeListing = dependencyHelper.getTreeListing();
File dir = outputFile.getParentFile();
if (dir.isDirectory() || dir.mkdirs()) {
PrintStream out = new PrintStream(new FileOutputStream(outputFile));
try {
writeFeatures(out);
} finally {
out.close();
}
// 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) {
getLogger().error(e.getMessage());
throw new MojoExecutionException("Unable to create features.xml file: " + e, e);
}
}
/*
* Write all project dependencies as feature
*/
private void writeFeatures(PrintStream out) throws ArtifactResolutionException, ArtifactNotFoundException,
IOException, JAXBException, SAXException, ParserConfigurationException, XMLStreamException, MojoExecutionException {
getLogger().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(project.getArtifactId())) {
feature = test;
}
}
if (feature == null) {
feature = objectFactory.createFeature();
feature.setName(project.getArtifactId());
}
if (!feature.hasVersion()) {
feature.setVersion(project.getArtifact().getBaseVersion());
}
if (feature.getDescription() == null) {
feature.setDescription(project.getName());
}
if (resolver != null) {
feature.setResolver(resolver);
}
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()));
if (startLevel != null) {
bundle.setStartLevel(startLevel);
}
feature.getBundle().add(bundle);
}
for (Map.Entry<?, String> entry : localDependencies.entrySet()) {
Object artifact = entry.getKey();
if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) {
continue;
}
if (this.dependencyHelper.isArtifactAFeature(artifact)) {
if (aggregateFeatures && 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 = readFeaturesFile(featuresFile);
//TODO check for duplicates?
features.getFeature().addAll(includedFeatures.getFeature());
}
} else if (addBundlesToPrimaryFeature) {
String bundleName = this.dependencyHelper.artifactToMvn(artifact);
File bundleFile = this.dependencyHelper.resolve(artifact, getLog());
Manifest manifest = getManifest(bundleFile);
if (manifest == null || !ManifestUtils.isBundle(getManifest(bundleFile))) {
bundleName = "wrap:" + bundleName;
}
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);
if (!"provided".equals(entry.getValue()) || !ignoreScopeProvided) {
feature.getBundle().add(bundle);
}
}
if ("runtime".equals(entry.getValue())) {
bundle.setDependency(true);
}
if (startLevel != null && bundle.getStartLevel() == 0) {
bundle.setStartLevel(startLevel);
}
}
}
if ((!feature.getBundle().isEmpty() || !feature.getFeature().isEmpty()) && !features.getFeature().contains(feature)) {
features.getFeature().add(feature);
}
JaxbUtil.marshal(features, out);
try {
checkChanges(features, objectFactory);
} catch (Exception e) {
throw new MojoExecutionException("Features contents have changed", e);
}
getLogger().info("...done!");
}
/**
* Extract the MANIFEST from the give file.
*/
private Manifest getManifest(File file) throws IOException {
InputStream is = null;
try {
is = new BufferedInputStream(new FileInputStream(file));
} catch (Exception e) {
getLogger().warn("Error while opening artifact", e);
return null;
}
try {
is.mark(256 * 1024);
JarInputStream jar = new JarInputStream(is);
Manifest m = jar.getManifest();
if (m == null) {
getLogger().warn("Manifest not present in the first entry of the zip - " + file.getName());
}
jar.close();
return m;
} finally {
if (is != null) { // just in case when we did not open bundle
is.close();
}
}
}
private Features readFeaturesFile(File featuresFile) throws XMLStreamException, JAXBException, IOException {
return JaxbUtil.unmarshal(featuresFile.toURI().toASCIIString(), false);
}
public void setLog(Log log) {
this.log = log;
}
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 <code>true</code> and the file referenced by
* <code>dependencyCache</code> 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,
* <code>failOnDependencyChange = true</code> will cause the build to fail.
*
* @parameter default-value="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
* <code>checkDependencyChange</code> is set to <code>true</code>.
*
* @parameter default-value="${basedir}/src/main/history/dependencies.xml"
*/
private File dependencyCache;
/**
* Location of filtered dependency file.
*
* @parameter default-value="${basedir}/target/history/dependencies.xml"
* @readonly
*/
private File filteredDependencyCache;
/**
* Whether to fail on changed dependencies (default, <code>true</code>) or warn (<code>false</code>). This is parameter is ignored unless
* <code>checkDependencyChange</code> is set to <code>true</code> and <code>dependencyCache</code> exists to compare
* against.
*
* @parameter default-value="true"
*/
private boolean failOnDependencyChange;
/**
* Copies the contents of dependency change logs that are generated to stdout. This is parameter is ignored unless
* <code>checkDependencyChange</code> is set to <code>true</code> and <code>dependencyCache</code> exists to compare
* against.
*
* @parameter default-value="false"
*/
private boolean logDependencyChanges;
/**
* Whether to overwrite the file referenced by <code>dependencyCache</code> if it has changed. This is parameter is
* ignored unless <code>checkDependencyChange</code> is set to <code>true</code>, <code>failOnDependencyChange</code>
* is set to <code>false</code> and <code>dependencyCache</code> exists to compare against.
*
* @parameter default-value="false"
*/
private boolean overwriteChangedDependencies;
//filtering support
/**
* The character encoding scheme to be applied when filtering resources.
*
* @parameter default-value="${project.build.sourceEncoding}"
*/
protected String encoding;
/**
* Expression preceded with the String won't be interpolated
* \${foo} will be replaced with ${foo}
*
* @parameter default-value="${maven.resources.escapeString}"
*/
protected String escapeString = "\\";
/**
* System properties.
*
* @parameter
*/
protected Map<String, String> systemProperties;
private void checkChanges(Features newFeatures, ObjectFactory objectFactory) throws Exception, IOException, JAXBException, XMLStreamException {
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);
}
}
Collections.sort(feature.getBundle(), new Comparator<Bundle>() {
public int compare(Bundle bundle, Bundle bundle1) {
return bundle.getLocation().compareTo(bundle1.getLocation());
}
});
Collections.sort(feature.getFeature(), new Comparator<Dependency>() {
public int compare(Dependency dependency, Dependency dependency1) {
return dependency.getName().compareTo(dependency1.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<Bundle> addedBundles = new ArrayList<Bundle>(feature.getBundle());
List<Bundle> removedBundles = new ArrayList<Bundle>();
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) {
getLogger().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
}
if (t1 == (s1 == s2)) {
getLogger().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2);
}
if (!t2) {
removedBundles.add(test);
}
}
List<Dependency> addedDependencys = new ArrayList<Dependency>(feature.getFeature());
List<Dependency> removedDependencys = new ArrayList<Dependency>();
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) {
getLogger().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
}
if (t1 == (s1 == s2)) {
getLogger().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<Bundle> addedBundles, Collection<Bundle> removedBundles, Collection<Dependency> addedDependencys, Collection<Dependency> 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 Features toFeatures(Collection<Bundle> addedBundles, Collection<Dependency> 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 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());
}
FileOutputStream out = new FileOutputStream(file);
try {
JaxbUtil.marshal(features, out);
} finally {
out.close();
}
}
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();
@SuppressWarnings("rawtypes")
List filters = mavenFileFilter.getDefaultFilterWrappers(project, null, true, session, null);
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");
OutputStream os = new FileOutputStream(treeListFile);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os));
try {
writer.write(treeListing);
} finally {
writer.close();
}
return "\tTree listing is saved here: " + treeListFile.getAbsolutePath() + "\n";
}
}