blob: 38191e691b7d65e391a6f52fd82fa6fee08eabc5 [file] [log] [blame]
/**
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.karaf.tooling.features;
import static org.apache.karaf.tooling.features.ManifestUtils.getExports;
import static org.apache.karaf.tooling.features.ManifestUtils.getMandatoryImports;
import static org.apache.karaf.tooling.features.ManifestUtils.matches;
import java.io.*;
import java.net.URI;
import java.net.URL;
import java.util.*;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.apache.karaf.features.BundleInfo;
import org.apache.karaf.features.Feature;
import org.apache.karaf.features.Repository;
import org.apache.karaf.features.internal.FeatureValidationUtil;
import org.apache.karaf.features.internal.RepositoryImpl;
import org.apache.felix.utils.manifest.Clause;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.DefaultArtifactRepository;
import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.shared.dependency.tree.DependencyNode;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor;
/**
* Validates a features XML file
*
* @version $Revision$
* @goal validate
* @execute phase="process-resources"
* @requiresDependencyResolution runtime
* @inheritByDefault true
* @description Validates the features XML file
*/
@SuppressWarnings("unchecked")
public class ValidateFeaturesMojo extends MojoSupport {
private static final String MVN_URI_PREFIX = "mvn:";
private static final String MVN_REPO_SEPARATOR = "!";
private static final String KARAF_CORE_STANDARD_FEATURE_URL = "mvn:org.apache.karaf.assemblies.features/standard/%s/xml/features";
private static final String KARAF_CORE_ENTERPRISE_FEATURE_URL = "mvn:org.apache.karaf.assemblies.features/enterprise/%s/xml/features";
/**
* The dependency tree builder to use.
*
* @component
* @required
* @readonly
*/
private DependencyTreeBuilder dependencyTreeBuilder;
/**
* The ArtifactCollector provided by Maven at runtime
*
* @component
* @required
* @readonly
*/
private ArtifactCollector collector;
/**
* The file to generate
*
* @parameter default-value="${project.build.directory}/classes/features.xml"
*/
private File file;
/**
* Karaf config.properties
*
* @parameter default-value="config.properties"
*/
private String karafConfig;
/**
* which JRE version to parse from config.properties to get the JRE exported packages
*
* @parameter default-value="jre-1.5"
*/
private String jreVersion;
/**
* which Karaf version used for Karaf core features resolution
*
* @parameter
*/
private String karafVersion;
/**
* The repositories which are included from the plugin config
*
* @parameter
*/
private List<String> repositories;
/**
* skip non maven protocols or not skip
*
* @parameter default-value="false"
*/
private boolean skipNonMavenProtocols = false;
/*
* A map to cache the mvn: uris and the artifacts that correspond with them if it's mvn protocol
* or just uris itself if it's non mvn protocol
*/
private Map<String, Object> bundles = new HashMap<String, Object>();
/*
* A map to cache manifests that have been extracted from the bundles
*/
private Map<Object, Manifest> manifests = new HashMap<Object, Manifest>();
/*
* The list of features, includes both the features to be validated and the features from included <repository>s
*/
private Features features = new Features();
/*
* The packages exported by the features themselves -- useful when features depend on other features
*/
private Map<String, Set<Clause>> featureExports = new HashMap<String, Set<Clause>>();
/*
* The set of packages exported by the system bundle and by Karaf itself
*/
private Set<String> systemExports = new HashSet<String>();
/**
* The Mojo's main method
*/
public void execute() throws MojoExecutionException, MojoFailureException {
try {
prepare();
URI uri = file.toURI();
Repository repository = new RepositoryImpl(uri);
schemaCheck(repository, uri);
analyze(repository);
validate(repository);
} catch (Exception e) {
throw new MojoExecutionException(String.format("Unable to validate %s: %s", file.getAbsolutePath(), e.getMessage()), e);
}
}
/**
* Checks feature repository with XML Schema.
*
* @param repository Repository object.
* @param uri Display URI.
*/
private void schemaCheck(Repository repository, URI uri) {
try {
info(" - validation of %s", uri);
FeatureValidationUtil.validate(repository.getURI());
} catch (Exception e) {
error("Failed to validate repository %s. Schema validation fails. Fix errors to continue validation",
e, uri);
}
}
/*
* Prepare for validation by determing system and Karaf exports
*/
private void prepare() throws Exception {
info("== Preparing for validation ==");
URL.setURLStreamHandlerFactory(new CustomBundleURLStreamHandlerFactory());
info(" - getting list of system bundle exports");
readSystemPackages();
info(" - getting list of provided bundle exports");
readProvidedBundles();
info(" - populating repositories with Karaf core features descriptors");
appendKarafCoreFeaturesDescriptors();
}
/**
* Add Karaf core features descriptors in the default repositories set.
*/
private void appendKarafCoreFeaturesDescriptors() {
if (repositories == null) {
repositories = new ArrayList<String>();
}
if (karafVersion == null) {
Package p = Package.getPackage("org.apache.karaf.tooling.features");
karafVersion = p.getImplementationVersion();
}
String karafCoreStandardFeaturesUrl = String.format(KARAF_CORE_STANDARD_FEATURE_URL, karafVersion);
String karafCoreEnterpriseFeaturesUrl = String.format(KARAF_CORE_ENTERPRISE_FEATURE_URL, karafVersion);
try {
resolve(karafCoreStandardFeaturesUrl);
repositories.add(karafCoreStandardFeaturesUrl);
} catch (Exception e) {
warn("Can't add " + karafCoreStandardFeaturesUrl + " in the default repositories set");
}
try {
resolve(karafCoreEnterpriseFeaturesUrl);
repositories.add(karafCoreEnterpriseFeaturesUrl);
} catch (Exception e) {
warn("Can't add " + karafCoreStandardFeaturesUrl + " in the default repositories set");
}
}
/*
* Analyse the descriptor and any <repository>s that might be part of it
*/
private void analyze(Repository repository) throws Exception {
info("== Analyzing feature descriptor ==");
info(" - read %s", file.getAbsolutePath());
features.add(repository.getFeatures());
// add the repositories from the plugin configuration
if (repositories != null) {
for (String uri : repositories) {
getLog().info(String.format(" - adding repository from %s", uri));
Repository dependency = new RepositoryImpl(URI.create(translateFromMaven(uri)));
schemaCheck(dependency, URI.create(uri));
features.add(dependency.getFeatures());
validateBundlesAvailable(dependency);
analyzeExports(dependency);
}
}
for (URI uri : repository.getRepositories()) {
Artifact artifact = (Artifact) resolve(uri.toString());
Repository dependency = new RepositoryImpl(new File(localRepo.getBasedir(), localRepo.pathOf(artifact)).toURI());
schemaCheck(dependency, uri);
getLog().info(String.format(" - adding %d known features from %s", dependency.getFeatures().length, uri));
features.add(dependency.getFeatures());
// we need to do this to get all the information ready for further processing
validateBundlesAvailable(dependency);
analyzeExports(dependency);
}
}
/*
* Perform the actual validation
*/
private void validate(Repository repository) throws Exception {
info("== Validating feature descriptor ==");
info(" - validating %d features", repository.getFeatures().length);
info(" - step 1: Checking if all artifacts exist");
validateBundlesAvailable(repository);
info(" OK: all %d OSGi bundles have been found", bundles.size());
info(" - step 2: Checking if all imports for bundles can be resolved");
validateImportsExports(repository);
info("== Done! ==========================");
}
/*
* Determine list of exports by bundles that have been marked provided in the pom
* //TODO: we probably want to figure this out somewhere from the Karaf build itself instead of putting the burden on the user
*/
private void readProvidedBundles() throws Exception {
DependencyNode tree = dependencyTreeBuilder.buildDependencyTree(project, localRepo, factory, artifactMetadataSource, new ArtifactFilter() {
public boolean include(Artifact artifact) {
return true;
}
}, collector);
tree.accept(new DependencyNodeVisitor() {
public boolean endVisit(DependencyNode node) {
// we want the next sibling too
return true;
}
public boolean visit(DependencyNode node) {
if (node.getState() != DependencyNode.OMITTED_FOR_CONFLICT) {
Artifact artifact = node.getArtifact();
info(" scanning %s for exports", artifact);
if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) && !artifact.getType().equals("pom")) {
try {
for (Clause clause : ManifestUtils.getExports(getManifest("", artifact))) {
getLog().debug(" adding " + clause.getName() + " to list of available packages");
systemExports.add(clause.getName());
}
} catch (ArtifactResolutionException e) {
error("Unable to find bundle exports for %s: %s", e, artifact, e.getMessage());
} catch (ArtifactNotFoundException e) {
error("Unable to find bundle exports for %s: %s", e, artifact, e.getMessage());
} catch (IOException e) {
error("Unable to find bundle exports for %s: %s", e, artifact, e.getMessage());
}
}
}
// we want the children too
return true;
}
});
}
/*
* Read system packages from a properties file
* //TODO: we should probably grab this file from the Karaf distro itself instead of duplicating it in the plugin
*/
private void readSystemPackages() throws IOException {
Properties properties = new Properties();
if (karafConfig.equals("config.properties")) {
properties.load(getClass().getClassLoader().getResourceAsStream("config.properties"));
} else {
properties.load(new FileInputStream(new File(karafConfig)));
}
String packages = (String) properties.get(jreVersion);
for (String pkg : packages.split(";")) {
systemExports.add(pkg.trim());
}
for (String pkg : packages.split(",")) {
systemExports.add(pkg.trim());
}
}
/*
* Analyze exports in all features in the repository without validating the features
* (e.g. used for <repository> elements found in a descriptor)
*/
private void analyzeExports(Repository repository) throws Exception {
for (Feature feature : repository.getFeatures()) {
Set<Clause> exports = new HashSet<Clause>();
for (String bundle : getBundleLocations(feature)) {
exports.addAll(getExports(getManifest(bundle, bundles.get(bundle))));
}
info(" scanning feature %s for exports", feature.getName());
featureExports.put(feature.getName(), exports);
}
}
/*
* Check if all the bundles can be downloaded and are actually OSGi bundles and not plain JARs
*/
private void validateBundlesAvailable(Repository repository) throws Exception {
for (Feature feature : repository.getFeatures()) {
for (String bundle : getBundleLocations(feature)) {
if (!isMavenProtocol(bundle) && skipNonMavenProtocols) {
continue;
}
// this will throw an exception if the artifact can not be resolved
final Object artifact = resolve(bundle);
bundles.put(bundle, artifact);
if (isBundle(bundle, artifact)) {
manifests.put(artifact, getManifest(bundle, artifact));
} else {
throw new Exception(String.format("%s is not an OSGi bundle", bundle));
}
}
}
}
/*
* Get a list of bundle locations in a feature
*/
private List<String> getBundleLocations(Feature feature) {
List<String> result = new LinkedList<String>();
if (feature != null && feature.getBundles() != null) {
for (BundleInfo bundle : feature.getBundles()) {
result.add(bundle.getLocation());
}
}
return result;
}
/*
* Validate if all features in a repository have bundles which can be resolved
*/
private void validateImportsExports(Repository repository) throws ArtifactResolutionException, ArtifactNotFoundException, Exception {
for (Feature feature : repository.getFeatures()) {
// make sure the feature hasn't been validated before as a dependency
if (!featureExports.containsKey(feature.getName())) {
validateImportsExports(feature);
}
}
}
/*
* Validate if all imports for a feature are being matched with exports
*/
private void validateImportsExports(Feature feature) throws Exception {
Map<Clause, String> imports = new HashMap<Clause, String>();
Set<Clause> exports = new HashSet<Clause>();
for (Feature dependency : feature.getDependencies()) {
if (featureExports.containsKey(dependency.getName())) {
exports.addAll(featureExports.get(dependency.getName()));
} else {
validateImportsExports(features.get(dependency.getName(), dependency.getVersion()));
}
}
for (String bundle : getBundleLocations(feature)) {
Manifest meta = manifests.get(bundles.get(bundle));
exports.addAll(getExports(meta));
for (Clause clause : getMandatoryImports(meta)) {
imports.put(clause, bundle);
}
}
// setting up the set of required imports
Set<Clause> requirements = new HashSet<Clause>();
requirements.addAll(imports.keySet());
// now, let's remove requirements whenever we find a matching export for them
for (Clause element : imports.keySet()) {
if (systemExports.contains(element.getName())) {
debug("%s is resolved by a system bundle export or provided bundle", element);
requirements.remove(element);
continue;
}
for (Clause export : exports) {
if (matches(element, export)) {
debug("%s is resolved by export %s", element, export);
requirements.remove(element);
continue;
}
debug("%s is not resolved by export %s", element, export);
}
}
// if there are any more requirements left here, there's a problem with the feature
if (!requirements.isEmpty()) {
warn("Failed to validate feature %s", feature.getName());
for (Clause entry : requirements) {
warn("No export found to match %s (imported by %s)",
entry, imports.get(entry));
}
throw new Exception(String.format("%d unresolved imports in feature %s",
requirements.size(), feature.getName()));
}
info(" OK: imports resolved for %s", feature.getName());
featureExports.put(feature.getName(), exports);
}
/*
* Check if the artifact is an OSGi bundle
*/
private boolean isBundle(String bundle, Object artifact) {
if (artifact instanceof Artifact && "bundle".equals(((Artifact) artifact).getArtifactHandler().getPackaging())) {
return true;
} else {
try {
return ManifestUtils.isBundle(getManifest(bundle, artifact));
} catch (ZipException e) {
getLog().debug("Unable to determine if " + artifact + " is a bundle; defaulting to false", e);
} catch (IOException e) {
getLog().debug("Unable to determine if " + artifact + " is a bundle; defaulting to false", e);
} catch (Exception e) {
getLog().debug("Unable to determine if " + artifact + " is a bundle; defaulting to false", e);
}
}
return false;
}
/*
* Extract the META-INF/MANIFEST.MF file from an artifact
*/
private Manifest getManifest(String bundle, Object artifact) throws ArtifactResolutionException, ArtifactNotFoundException,
ZipException, IOException {
ZipFile file = null;
if (!(artifact instanceof Artifact)) {
//not resolved as mvn artifact, so it's non-mvn protocol, just use the CustomBundleURLStreamHandlerFactory
// to open stream
InputStream is = null;
try {
is = new BufferedInputStream(new URL(bundle).openStream());
} catch (Exception e) {
getLog().warn("Error while opening artifact", e);
}
try {
is.mark(256 * 1024);
JarInputStream jar = new JarInputStream(is);
Manifest m = jar.getManifest();
if (m == null) {
throw new IOException("Manifest not present in the first entry of the zip");
}
return m;
} finally {
if (is != null) { // just in case when we did not open bundle
is.close();
}
}
} else {
Artifact mvnArtifact = (Artifact) artifact;
File localFile = new File(localRepo.pathOf(mvnArtifact));
if (localFile.exists()) {
// avoid going over to the repository if the file is already on
// the disk
file = new ZipFile(localFile);
} else {
resolver.resolve(mvnArtifact, remoteRepos, localRepo);
file = new ZipFile(mvnArtifact.getFile());
}
// let's replace syserr for now to hide warnings being issues by the Manifest reading process
PrintStream original = System.err;
try {
System.setErr(new PrintStream(new ByteArrayOutputStream()));
Manifest manifest = new Manifest(file.getInputStream(file.getEntry("META-INF/MANIFEST.MF")));
return manifest;
} finally {
System.setErr(original);
}
}
}
/*
* Resolve an artifact, downloading it from remote repositories when necessary
*/
private Object resolve(String bundle) throws Exception, ArtifactNotFoundException {
if (!isMavenProtocol(bundle)) {
return bundle;
}
Artifact artifact = getArtifact(bundle);
if (bundle.indexOf(MVN_REPO_SEPARATOR) >= 0) {
if (bundle.startsWith(MVN_URI_PREFIX)) {
bundle = bundle.substring(MVN_URI_PREFIX.length());
}
String repo = bundle.substring(0, bundle.indexOf(MVN_REPO_SEPARATOR));
ArtifactRepository repository = new DefaultArtifactRepository(artifact.getArtifactId() + "-repo", repo,
new DefaultRepositoryLayout());
List<ArtifactRepository> repos = new LinkedList<ArtifactRepository>();
repos.add(repository);
resolver.resolve(artifact, repos, localRepo);
} else {
resolver.resolve(artifact, remoteRepos, localRepo);
}
if (artifact == null) {
throw new Exception("Unable to resolve artifact for uri " + bundle);
} else {
return artifact;
}
}
/*
* Create an artifact for a given mvn: uri
*/
private Artifact getArtifact(String uri) {
// remove the mvn: prefix when necessary
if (uri.startsWith(MVN_URI_PREFIX)) {
uri = uri.substring(MVN_URI_PREFIX.length());
}
// remove the repository url when specified
if (uri.contains(MVN_REPO_SEPARATOR)) {
uri = uri.split(MVN_REPO_SEPARATOR)[1];
}
String[] elements = uri.split("/");
switch (elements.length) {
case 5:
return factory.createArtifactWithClassifier(elements[0], elements[1], elements[2], elements[3], elements[4]);
case 3:
return factory.createArtifact(elements[0], elements[1], elements[2], Artifact.SCOPE_PROVIDED, "jar");
default:
return null;
}
}
/*
* see if bundle url is start with mvn protocol
*/
private boolean isMavenProtocol(String bundle) {
return bundle.startsWith(MVN_URI_PREFIX);
}
/*
* Helper method for debug logging
*/
private void debug(String message, Object... parms) {
if (getLog().isDebugEnabled()) {
getLog().debug(String.format(message, parms));
}
}
/*
* Helper method for info logging
*/
private void info(String message, Object... parms) {
getLog().info(String.format(message, parms));
}
/*
* Helper method for warn logging
*/
private void warn(String message, Object... parms) {
getLog().warn(String.format(message, parms));
}
/*
* Helper method for error logging
*/
private void error(String message, Exception error, Object... parms) {
getLog().error(String.format(message, parms), error);
}
/*
* Convenience collection for holding features
*/
private class Features {
private List<Feature> features = new LinkedList<Feature>();
public void add(Feature feature) {
features.add(feature);
}
public Feature get(String name, String version) throws Exception {
for (Feature feature : features) {
if (name.equals(feature.getName()) && version.equals(feature.getVersion())) {
return feature;
}
}
throw new Exception(String.format("Unable to find definition for feature %s (version %s)",
name, version));
}
public void add(Feature[] array) {
for (Feature feature : array) {
add(feature);
}
}
}
}