| /** |
| * |
| * 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); |
| } |
| } |
| } |
| } |