| /** |
| * |
| * 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.servicemix.tooling.features; |
| |
| import java.io.BufferedInputStream; |
| 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.PrintStream; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipException; |
| import java.util.zip.ZipFile; |
| |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.artifact.InvalidArtifactRTException; |
| import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException; |
| import org.apache.maven.artifact.metadata.ResolutionGroup; |
| import org.apache.maven.artifact.resolver.ArtifactNotFoundException; |
| import org.apache.maven.artifact.resolver.ArtifactResolutionException; |
| import org.apache.maven.artifact.versioning.ArtifactVersion; |
| import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; |
| import org.apache.maven.artifact.versioning.OverConstrainedVersionException; |
| import org.apache.maven.artifact.versioning.VersionRange; |
| import org.apache.maven.model.Dependency; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| import org.osgi.impl.bundle.obr.resource.Manifest; |
| |
| /** |
| * Generates the features XML file |
| * |
| * @version $Revision: 1.1 $ |
| * @goal generate-features-file |
| * @phase compile |
| * @execute phase="compile" |
| * @requiresDependencyResolution runtime |
| * @inheritByDefault true |
| * @description Generates the features XML file |
| */ |
| @SuppressWarnings("unchecked") |
| public class GenerateFeaturesFileMojo extends MojoSupport { |
| protected static final String SEPARATOR = "/"; |
| |
| /** |
| * The file to generate |
| * |
| * @parameter default-value="${project.build.directory}/classes/feature.xml" |
| */ |
| private File outputFile; |
| |
| /** |
| * The name of the feature, which defaults to the artifact ID if its not |
| * specified |
| * |
| * @parameter default-value="${project.artifactId}" |
| */ |
| private String featureName; |
| |
| /** |
| * The artifact type for attaching the generated file to the project |
| * |
| * @parameter default-value="xml" |
| */ |
| private String attachmentArtifactType = "xml"; |
| |
| /** |
| * The artifact classifier for attaching the generated file to the project |
| * |
| * @parameter default-value="features" |
| */ |
| private String attachmentArtifactClassifier = "features"; |
| |
| /** |
| * Should we generate a <feature> for the current project? |
| * |
| * @parameter default-value="false" |
| */ |
| private boolean includeProject = false; |
| |
| /** |
| * Should we generate a <feature> for the current project's <dependency>s? |
| * |
| * @parameter default-value="true" |
| */ |
| private boolean includeDependencies = true; |
| |
| /** |
| * The kernel version for which to generate the bundle |
| * |
| * @parameter |
| */ |
| private String kernelVersion; |
| |
| /** |
| * A properties file containing bundle translations |
| * |
| * @parameter |
| */ |
| private File translation; |
| |
| /* |
| * The translations |
| */ |
| private Map<String, Map<VersionRange, String>> translations = new HashMap<String, Map<VersionRange,String>>() { |
| @Override |
| public Map<VersionRange, String> get(Object key) { |
| if (super.get(key) == null) { |
| super.put(key.toString(), new HashMap<VersionRange, String>()); |
| } |
| return super.get(key); |
| } |
| }; |
| |
| /* |
| * These bundles are the features that will be built |
| */ |
| private Set<Artifact> features = new HashSet<Artifact>(); |
| |
| /* |
| * These bundles are provided by SMX4 and will be excluded from <feature/> |
| * generation |
| */ |
| private Set<Artifact> provided = new HashSet<Artifact>(); |
| |
| /* |
| * List of bundles included in the current feature |
| */ |
| private Set<Artifact> currentFeature = new HashSet<Artifact>(); |
| |
| /* |
| * List of missing bundles |
| */ |
| private Set<Artifact> missingBundles = new TreeSet<Artifact>(); |
| |
| public void execute() throws MojoExecutionException, MojoFailureException { |
| OutputStream out = null; |
| try { |
| prepare(); |
| getLog().info(String.format("-- Start generating %s --", outputFile.getAbsolutePath())); |
| outputFile.getParentFile().mkdirs(); |
| out = new FileOutputStream(outputFile); |
| |
| PrintStream printer = new PrintStream(out); |
| populateProperties(printer); |
| getLog().info(String.format("-- Done generating %s --", outputFile.getAbsolutePath())); |
| |
| // now lets attach it |
| projectHelper.attachArtifact(project, attachmentArtifactType, attachmentArtifactClassifier, outputFile); |
| } catch (Exception e) { |
| throw new MojoExecutionException("Unable to create dependencies file: " + e, e); |
| } finally { |
| if (out != null) { |
| try { |
| out.close(); |
| } catch (IOException e) { |
| getLog().info("Failed to close: " + outputFile + ". Reason: " + e, e); |
| } |
| } |
| } |
| } |
| |
| protected void populateProperties(PrintStream out) throws ArtifactResolutionException, ArtifactNotFoundException, IOException { |
| out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); |
| out.println("<features>"); |
| if (includeProject) { |
| writeCurrentProjectFeature(out); |
| } |
| if (includeDependencies) { |
| writeProjectDependencyFeatures(out); |
| } |
| out.println("</features>"); |
| } |
| |
| private void prepare() throws ArtifactResolutionException, ArtifactNotFoundException, IOException, InvalidVersionSpecificationException { |
| if (translation != null) { |
| InputStream stream = null; |
| try { |
| stream = new BufferedInputStream(new FileInputStream(translation)); |
| Properties file = new Properties(); |
| file.load(stream); |
| ArrayList<String> stringNames = getStringNames(file); |
| for (String key : stringNames) { |
| String[] elements = key.split("/"); |
| translations.get(String.format("%s/%s", elements[0], elements[1])) |
| .put(VersionRange.createFromVersionSpec(elements[2]), file.getProperty(key)); |
| } |
| getLog().info("Loaded " + translations.size() + " bundle name translation rules from " + translation.getAbsolutePath()); |
| } finally { |
| if (stream != null) { |
| stream.close(); |
| } |
| } |
| } |
| |
| Artifact kernel = factory.createArtifact("org.apache.servicemix.kernel", |
| "apache-servicemix-kernel", |
| kernelVersion, Artifact.SCOPE_PROVIDED, "pom"); |
| resolver.resolve(kernel, remoteRepos, localRepo); |
| getLog().info("-- List of bundles provided by ServiceMix Kernel " + kernelVersion + " --"); |
| for (Artifact artifact : getDependencies(kernel)) { |
| getLog().info(" " + artifact); |
| provided.add(artifact); |
| } |
| getLog().info("-- <end of list> --"); |
| } |
| |
| private ArrayList<String> getStringNames(Properties file) { |
| // this method simulate the Properties.stringPropertyNames() of JDK6 in order to make this class |
| // compile with jdk5 |
| ArrayList<String> ret = new ArrayList<String>(); |
| Enumeration<?> name = file.propertyNames(); |
| while (name.hasMoreElements()) { |
| Object ele = name.nextElement(); |
| if (ele instanceof String && file.get(ele) instanceof String) { |
| ret.add((String)ele); |
| } |
| } |
| return ret; |
| } |
| |
| private void writeProjectDependencyFeatures(PrintStream out) { |
| Set<Artifact> dependencies = (Set<Artifact>)project.getDependencyArtifacts(); |
| dependencies.removeAll(provided); |
| for (Artifact artifact : dependencies) { |
| getLog().info(" Generating feature " + artifact.getArtifactId() + " from " + artifact); |
| out.println(" <feature name='" + artifact.getArtifactId() + "'>"); |
| currentFeature.clear(); |
| writeBundle(out, artifact); |
| features.add(artifact); |
| out.println(" </feature>"); |
| } |
| if (missingBundles.size() > 0) { |
| getLog().info("-- Some bundles were missing --"); |
| for (Artifact artifact : missingBundles) { |
| getLog().info(String.format(" %s", artifact)); |
| } |
| } |
| } |
| |
| private void writeBundle(PrintStream out, Artifact artifact) { |
| Artifact replacement = getReplacement(artifact); |
| if (replacement != null) { |
| writeBundle(out, replacement); |
| return; |
| } |
| if (isProvided(artifact)) { |
| getLog().debug(String.format("Skipping '%s' -- bundle will be provided at runtime", artifact)); |
| return; |
| } |
| if (features.contains(artifact)) { |
| // if we already created a feature for this one, just add that instead of the bundle |
| out.println(String.format(" <feature>%s</feature>", artifact.getArtifactId())); |
| return; |
| } |
| // first write the dependencies |
| for (Artifact dependency : getDependencies(artifact)) { |
| if (dependency.isOptional() || Artifact.SCOPE_TEST.equals(dependency.getScope())) { |
| // omit optional dependencies |
| getLog().debug(String.format("Omitting optional and/or test scoped dependency '%s' for '%s'", |
| dependency, artifact)); |
| continue; |
| } |
| getLog().debug(String.format("Adding '%s' as a dependency for '%s'", dependency, artifact)); |
| writeBundle(out, dependency); |
| } |
| // skip the bundle if it was already added to this feature previously |
| if (!currentFeature.add(artifact)) { |
| getLog().debug(String.format("Artifact '%s' was already added to the current feature", artifact)); |
| return; |
| } |
| // and then write the bundle itself |
| if (isBundle(artifact)) { |
| getLog().info(String.format(" adding bundle %s", artifact)); |
| writeBundle(out, artifact.getGroupId(), artifact.getArtifactId(), artifact.getBaseVersion()); |
| } else { |
| Artifact wrapper = findServicemixBundle(artifact); |
| if (wrapper != null) { |
| getLog().info(String.format(" adding bundle %s (for %s)", wrapper, artifact)); |
| writeBundle(out, wrapper.getGroupId(), wrapper.getArtifactId(), wrapper.getBaseVersion()); |
| } else { |
| getLog().error(String.format(" unable to find suitable bundle for artifact '%s'", artifact)); |
| missingBundles.add(artifact); |
| } |
| } |
| } |
| |
| private Artifact getReplacement(Artifact artifact) { |
| String key = String.format("%s/%s", artifact.getGroupId(), artifact.getArtifactId()); |
| String bundle = null; |
| for (VersionRange range : translations.get(key).keySet()) { |
| try { |
| if (range.containsVersion(artifact.getSelectedVersion())) { |
| bundle = translations.get(key).get(range); |
| break; |
| } |
| } catch (OverConstrainedVersionException e) { |
| bundle = null; |
| } |
| } |
| if (bundle != null) { |
| String[] split = bundle.split("/"); |
| return factory.createArtifact(split[0], split[1], split[2], Artifact.SCOPE_PROVIDED, artifact.getArtifactHandler().getPackaging()); |
| } else { |
| return null; |
| } |
| } |
| |
| private Artifact findServicemixBundle(Artifact artifact) { |
| Artifact noVersionWrapper = factory.createArtifact("org.apache.servicemix.bundles", |
| "org.apache.servicemix.bundles." + artifact.getArtifactId(), |
| "", |
| artifact.getScope(), artifact.getType()); |
| try { |
| List versions = artifactMetadataSource.retrieveAvailableVersions(noVersionWrapper, localRepo, remoteRepos); |
| Artifact wrapper = factory.createArtifact("org.apache.servicemix.bundles", |
| "org.apache.servicemix.bundles." + artifact.getArtifactId(), |
| getBestVersionForArtifact(artifact, versions), |
| artifact.getScope(), artifact.getType()); |
| // let's check if the servicemix bundle for this artifact exists |
| resolver.resolve(wrapper, remoteRepos, localRepo); |
| for (Artifact dependency : getDependencies(wrapper)) { |
| //some of these wrapper bundles provide for multiple JAR files, no need to include any of them after adding the wrapper |
| getLog().debug(String.format("'%s' also provides '%s'", wrapper, dependency)); |
| currentFeature.add(dependency); |
| } |
| return wrapper; |
| } catch (ArtifactResolutionException e) { |
| getLog().debug("Couldn't find a ServiceMix bundle for " + artifact, e); |
| } catch (ArtifactNotFoundException e) { |
| getLog().debug("Couldn't find a ServiceMix bundle for " + artifact, e); |
| } catch (ArtifactMetadataRetrievalException e) { |
| getLog().debug("Couldn't find a ServiceMix bundle for " + artifact, e); |
| } |
| if (artifact.getArtifactId().contains("-")) { |
| //let's try to see if we can't find a bundle wrapping multiple artifacts (e.g. mina -> mina-core, mina-codec, ...) |
| return findServicemixBundle(factory.createArtifact(artifact.getGroupId(), artifact.getArtifactId().split("-")[0], |
| artifact.getVersion(), artifact.getScope(), artifact.getType())); |
| } else { |
| return null; |
| } |
| } |
| |
| protected String getBestVersionForArtifact(Artifact artifact, List<ArtifactVersion> versions) throws ArtifactMetadataRetrievalException { |
| if (versions.size() == 0) { |
| throw new ArtifactMetadataRetrievalException("No wrapper bundle available for " + artifact); |
| } |
| Collections.sort(versions, Collections.reverseOrder()); |
| //check for same version |
| for (ArtifactVersion version : versions) { |
| if (version.toString().startsWith(artifact.getVersion())) { |
| return version.toString(); |
| } |
| } |
| //check for same major/minor version |
| for (ArtifactVersion version : versions) { |
| String[] elements = version.toString().split("\\."); |
| if (elements.length >= 2 && artifact.getVersion().startsWith(elements[0] + "." + elements[1])) { |
| return version.toString(); |
| } |
| } |
| throw new ArtifactMetadataRetrievalException("No suitable version found for " + artifact + " wrapper bundle"); |
| } |
| |
| private boolean isProvided(Artifact bundle) { |
| for (Artifact artifact : provided) { |
| if (bundle.getArtifactId().equals(artifact.getArtifactId()) |
| && bundle.getGroupId().equals(artifact.getGroupId())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isBundle(Artifact artifact) { |
| if (artifact.getArtifactHandler().getPackaging().equals("bundle")) { |
| return true; |
| } else { |
| try { |
| resolver.resolve(artifact, remoteRepos, localRepo); |
| ZipFile file = new ZipFile(artifact.getFile()); |
| ZipEntry entry = file.getEntry("META-INF/MANIFEST.MF"); |
| Manifest manifest = new Manifest(file.getInputStream(entry)); |
| if (manifest.getBsn() != null) { |
| getLog().debug(String.format("MANIFEST.MF for '%s' contains Bundle-Name '%s'", |
| artifact, manifest.getBsn().getName())); |
| return true; |
| } |
| } catch (ZipException e) { |
| getLog().warn("Unable to determine if " + artifact + " is a bundle; defaulting to false", e); |
| } catch (IOException e) { |
| getLog().warn("Unable to determine if " + artifact + " is a bundle; defaulting to false", e); |
| } catch (Exception e) { |
| getLog().warn("Unable to determine if " + artifact + " is a bundle; defaulting to false", e); |
| } |
| } |
| return false; |
| } |
| |
| private List<Artifact> getDependencies(Artifact artifact) { |
| List<Artifact> list = new ArrayList<Artifact>(); |
| try { |
| ResolutionGroup pom = artifactMetadataSource.retrieve(artifact, localRepo, remoteRepos); |
| if (pom != null) { |
| list.addAll(pom.getArtifacts()); |
| } |
| } catch (ArtifactMetadataRetrievalException e) { |
| getLog().warn("Unable to retrieve metadata for " + artifact + ", not including dependencies for it"); |
| } catch (InvalidArtifactRTException e) { |
| getLog().warn("Unable to retrieve metadata for " + artifact + ", not including dependencies for it"); |
| } |
| return list; |
| } |
| |
| |
| private void writeCurrentProjectFeature(PrintStream out) { |
| out.println(" <feature name='" + featureName + "'>"); |
| |
| writeBundle(out, project.getGroupId(), project.getArtifactId(), project.getVersion()); |
| out.println(); |
| |
| Iterator iterator = project.getDependencies().iterator(); |
| while (iterator.hasNext()) { |
| Dependency dependency = (Dependency)iterator.next(); |
| |
| if (isValidDependency(dependency)) { |
| out.print(" "); |
| writeBundle(out, dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()); |
| } |
| } |
| |
| out.println(" </feature>"); |
| } |
| |
| protected boolean isValidDependency(Dependency dependency) { |
| // TODO filter out only compile time dependencies which are OSGi |
| // bundles? |
| return true; |
| } |
| |
| protected void writeBundle(PrintStream out, String groupId, String artifactId, String version) { |
| out.print(" <bundle>mvn:"); |
| out.print(groupId); |
| out.print("/"); |
| out.print(artifactId); |
| out.print("/"); |
| out.print(version); |
| out.print("</bundle>"); |
| out.println(); |
| } |
| } |