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

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

/**
 *
 * 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.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

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

import org.apache.karaf.features.internal.model.Bundle;
import org.apache.karaf.features.internal.model.Dependency;
import org.apache.karaf.features.internal.model.Feature;
import org.apache.karaf.features.internal.model.Features;
import org.apache.karaf.features.internal.model.JaxbUtil;
import org.apache.karaf.features.internal.model.ObjectFactory;
import org.apache.karaf.tooling.utils.MojoSupport;
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.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.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.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 starting with an optional source feature.xml and adding
 * project dependencies as bundles and feature/car dependencies.
 * 
 * NB this requires a recent maven-install-plugin such as 2.3.1
 */
@Mojo(name = "features-generate-descriptor", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.RUNTIME)
public class GenerateDescriptorMojo extends MojoSupport {

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

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

* Installation mode *

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

* N.B. Note the default value of this is true, but is suboptimal in cases where specific <feature/> dependencies are * provided by the inputFile parameter. */ @Parameter(defaultValue = "true") private boolean includeTransitiveDependency; /** * The standard behavior is to add dependencies as <bundle> elements to a <feature> * with the same name as the artifactId of the project. This flag disables that behavior. */ @Parameter(defaultValue = "true") private boolean addBundlesToPrimaryFeature; /** * The standard behavior is to add any dependencies other than those in the runtime scope to the feature bundle. * Setting this flag to "true" disables adding any dependencies (transient or otherwise) that are in * <scope>provided</scope>. */ @Parameter(defaultValue = "false") private boolean ignoreScopeProvided; /** * Flag indicating whether the main project artifact should be included (true) or not (false). *

* Assumes the main project artifact is a bundle and the feature will be attached alongside using attachmentArtifactClassifier. */ @Parameter(defaultValue = "false") private boolean includeProjectArtifact; // ************************************************* // READ-ONLY MAVEN PLUGIN PARAMETERS // ************************************************* /** * We can't autowire strongly typed RepositorySystem from Aether because it may be Sonatype (Maven 3.0.x) * or Eclipse (Maven 3.1.x/3.2.x) implementation, so we switch to service locator. */ @Component private PlexusContainer container; @Component protected MavenResourcesFiltering mavenResourcesFiltering; @Component protected MavenFileFilter mavenFileFilter; // dependencies we are interested in protected Map 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.mavenSession, 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) { getLog().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 { getLog().info("Generating feature descriptor file " + outputFile.getAbsolutePath()); //read in an existing feature.xml ObjectFactory objectFactory = new ObjectFactory(); Features features; if (inputFile.exists()) { filter(inputFile, filteredInputFile); features = readFeaturesFile(filteredInputFile); } else { features = objectFactory.createFeaturesRoot(); } if (features.getName() == null) { features.setName(project.getArtifactId()); } Feature feature = null; for (Feature test : features.getFeature()) { if (test.getName().equals(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 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); } getLog().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) { getLog().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) { getLog().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 true and the file referenced by * dependencyCache does not exist, it will be unconditionally generated. If the file does exist, it is * used to detect changes from previous builds and generate logs of those changes. In that case, * failOnDependencyChange = true will cause the build to fail. */ @Parameter(defaultValue = "false") private boolean checkDependencyChange; /** * (wrapper) Location of dependency cache. This file is generated to contain known dependencies and is generally * located in SCM so that it may be used across separate developer builds. This is parameter is ignored unless * checkDependencyChange is set to true. */ @Parameter(defaultValue = "${basedir}/src/main/history/dependencies.xml") private File dependencyCache; /** * Location of filtered dependency file. */ @Parameter(defaultValue = "${basedir}/target/history/dependencies.xml", readonly = true) private File filteredDependencyCache; /** * Whether to fail on changed dependencies (default, true) or warn (false). This is parameter is ignored unless * checkDependencyChange is set to true and dependencyCache exists to compare * against. */ @Parameter(defaultValue = "true") private boolean failOnDependencyChange; /** * Copies the contents of dependency change logs that are generated to stdout. This is parameter is ignored unless * checkDependencyChange is set to true and dependencyCache exists to compare * against. */ @Parameter(defaultValue = "false") private boolean logDependencyChanges; /** * Whether to overwrite the file referenced by dependencyCache if it has changed. This is parameter is * ignored unless checkDependencyChange is set to true, failOnDependencyChange * is set to false and dependencyCache exists to compare against. */ @Parameter(defaultValue = "false") private boolean overwriteChangedDependencies; //filtering support /** * The character encoding scheme to be applied when filtering resources. */ @Parameter(defaultValue = "${project.build.sourceEncoding}") protected String encoding; /** * Expression preceded with the String won't be interpolated * \${foo} will be replaced with ${foo} */ @Parameter(defaultValue = "${maven.resources.escapeString}") protected String escapeString = "\\"; /** * System properties. */ @Parameter protected Map systemProperties; private void checkChanges(Features newFeatures, ObjectFactory objectFactory) throws Exception, 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() { public int compare(Bundle bundle, Bundle bundle1) { return bundle.getLocation().compareTo(bundle1.getLocation()); } }); Collections.sort(feature.getFeature(), new Comparator() { 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 addedBundles = new ArrayList(feature.getBundle()); List removedBundles = new ArrayList(); for (Bundle test : oldFeature.getBundle()) { boolean t1 = addedBundles.contains(test); int s1 = addedBundles.size(); boolean t2 = addedBundles.remove(test); int s2 = addedBundles.size(); if (t1 != t2) { getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2); } if (t1 == (s1 == s2)) { getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2); } if (!t2) { removedBundles.add(test); } } List addedDependencys = new ArrayList(feature.getFeature()); List removedDependencys = new ArrayList(); for (Dependency test : oldFeature.getFeature()) { boolean t1 = addedDependencys.contains(test); int s1 = addedDependencys.size(); boolean t2 = addedDependencys.remove(test); int s2 = addedDependencys.size(); if (t1 != t2) { getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2); } if (t1 == (s1 == s2)) { getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2); } if (!t2) { removedDependencys.add(test); } } if (!addedBundles.isEmpty() || !removedBundles.isEmpty() || !addedDependencys.isEmpty() || !removedDependencys.isEmpty()) { saveDependencyChanges(addedBundles, removedBundles, addedDependencys, removedDependencys, objectFactory); if (overwriteChangedDependencies) { writeDependencies(features, dependencyCache); } } else { getLog().info(saveTreeListing()); } } else { writeDependencies(features, dependencyCache); } } } protected void saveDependencyChanges(Collection addedBundles, Collection removedBundles, Collection addedDependencys, Collection removedDependencys, ObjectFactory objectFactory) throws Exception { File addedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.added.xml"); Features added = toFeatures(addedBundles, addedDependencys, objectFactory); writeDependencies(added, addedFile); File removedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.removed.xml"); Features removed = toFeatures(removedBundles, removedDependencys, objectFactory); writeDependencies(removed, removedFile); StringWriter out = new StringWriter(); out.write(saveTreeListing()); out.write("Dependencies have changed:\n"); if (!addedBundles.isEmpty() || !addedDependencys.isEmpty()) { out.write("\tAdded dependencies are saved here: " + addedFile.getAbsolutePath() + "\n"); if (logDependencyChanges) { JaxbUtil.marshal(added, out); } } if (!removedBundles.isEmpty() || !removedDependencys.isEmpty()) { out.write("\tRemoved dependencies are saved here: " + removedFile.getAbsolutePath() + "\n"); if (logDependencyChanges) { JaxbUtil.marshal(removed, out); } } out.write("Delete " + dependencyCache.getAbsolutePath() + " if you are happy with the dependency changes."); if (failOnDependencyChange) { throw new MojoFailureException(out.toString()); } else { getLog().warn(out.toString()); } } private Features toFeatures(Collection addedBundles, Collection addedDependencys, ObjectFactory objectFactory) { Features features = objectFactory.createFeaturesRoot(); Feature feature = objectFactory.createFeature(); feature.getBundle().addAll(addedBundles); feature.getFeature().addAll(addedDependencys); features.getFeature().add(feature); return features; } private 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, mavenSession, 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"; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy