/* * 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; import java.io.*; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.karaf.deployer.kar.KarArtifactInstaller; import org.apache.karaf.features.BundleInfo; import org.apache.karaf.features.ConfigFileInfo; 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.tooling.utils.MavenUtil; import org.apache.karaf.tooling.utils.MojoSupport; import org.apache.maven.archiver.MavenArchiveConfiguration; import org.apache.maven.archiver.MavenArchiver; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; import org.apache.maven.artifact.repository.metadata.Metadata; import org.apache.maven.artifact.repository.metadata.Snapshot; import org.apache.maven.artifact.repository.metadata.SnapshotVersion; import org.apache.maven.artifact.repository.metadata.Versioning; import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; 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.codehaus.plexus.archiver.Archiver; import org.codehaus.plexus.archiver.jar.JarArchiver; /** * Assemble a kar archive from a features.xml file */ @Mojo(name = "kar", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true) public class KarMojo extends MojoSupport { /** * The maven archive configuration to use. * <p/> * See <a href="http://maven.apache.org/ref/current/maven-archiver/apidocs/org/apache/maven/archiver/MavenArchiveConfiguration.html">the Javadocs for MavenArchiveConfiguration</a>. */ @Parameter private MavenArchiveConfiguration archive = new MavenArchiveConfiguration(); /** * The Jar archiver. */ @Component(role = Archiver.class, hint="jar") private JarArchiver jarArchiver = null; /** * Directory containing the generated archive. */ @Parameter(defaultValue = "${project.build.directory}") private File outputDirectory = null; /** * Name of the generated archive. */ @Parameter(defaultValue = "${project.build.finalName}") private String finalName = null; /** * Ignore the dependency flag on the bundles in the features XML */ @Parameter(defaultValue = "false") private boolean ignoreDependencyFlag; /** * Classifier to add to the artifact generated. If given, the artifact will be attached. * If it's not given, it will merely be written to the output directory according to the finalName. */ @Parameter protected String classifier; /** * Location of resources directory for additional content to include in the kar. * Note that it includes everything under classes so as to include maven-remote-resources */ @Parameter(defaultValue = "${project.build.directory}/classes") private File resourcesDir; /** * The features file to use as instructions */ @Parameter(defaultValue = "${project.build.directory}/feature/feature.xml") private String featuresFile; /** * The wrapper repository in the kar. */ @Parameter(defaultValue = "${repositoryPath}") private String repositoryPath = "repository/"; private static final Pattern mvnPattern = Pattern.compile("mvn:([^/ ]+)/([^/ ]+)/([^/ ]*)(/([^/ ]+)(/([^/ ]+))?)?"); // // Mojo // public void execute() throws MojoExecutionException, MojoFailureException { File featuresFileResolved = resolveFile(featuresFile); String groupId = project.getGroupId(); String artifactId = project.getArtifactId(); String version = project.getVersion(); if (isMavenUrl(featuresFile)) { Artifact artifactTemp = resourceToArtifact(featuresFile, false); if (artifactTemp.getGroupId() != null) groupId = artifactTemp.getGroupId(); if (artifactTemp.getArtifactHandler() != null) artifactId = artifactTemp.getArtifactId(); if (artifactTemp.getVersion() != null) version = artifactTemp.getVersion(); } List<Artifact> resources = readResources(featuresFileResolved); // Build the archive File archive = createArchive(resources, featuresFileResolved, groupId, artifactId, version); // if no classifier is specified and packaging is not kar, display a warning // and attach artifact if (classifier == null && !this.getProject().getPackaging().equals("kar")) { this.getLog().warn("Your project should use the \"kar\" packaging or configure a \"classifier\" for kar attachment"); projectHelper.attachArtifact(getProject(), "kar", null, archive); return; } // Attach the generated archive for install/deploy if (classifier != null) { projectHelper.attachArtifact(getProject(), "kar", classifier, archive); } else { getProject().getArtifact().setFile(archive); } } private File resolveFile(String file) { File fileResolved = null; if (isMavenUrl(file)) { fileResolved = new File(fromMaven(file)); try { Artifact artifactTemp = resourceToArtifact(file, false); if (!fileResolved.exists()) { try { artifactResolver.resolve(artifactTemp, remoteRepos, localRepo); fileResolved = artifactTemp.getFile(); } catch (ArtifactResolutionException e) { getLog().error("Artifact was not resolved", e); } catch (ArtifactNotFoundException e) { getLog().error("Artifact was not found", e); } } } catch (MojoExecutionException e) { getLog().error(e); } } else { fileResolved = new File(file); } return fileResolved; } /** * Read bundles and configuration files in the features file. * * @return * @throws MojoExecutionException */ private List<Artifact> readResources(File featuresFile) throws MojoExecutionException { List<Artifact> resources = new ArrayList<Artifact>(); try { Features features = JaxbUtil.unmarshal(featuresFile.toURI().toASCIIString(), false); for (Feature feature : features.getFeature()) { for (BundleInfo bundle : feature.getBundles()) { if (ignoreDependencyFlag || (!ignoreDependencyFlag && !bundle.isDependency())) { resources.add(resourceToArtifact(bundle.getLocation(), false)); } } for (ConfigFileInfo configFile : feature.getConfigurationFiles()) { resources.add(resourceToArtifact(configFile.getLocation(), false)); } } return resources; } catch (MojoExecutionException e) { throw e; } catch (Exception e) { throw new MojoExecutionException("Could not interpret features.xml", e); } } /** * Generates the configuration archive. * * @param bundles */ @SuppressWarnings("deprecation") private File createArchive(List<Artifact> bundles, File featuresFile, String groupId, String artifactId, String version) throws MojoExecutionException { ArtifactRepositoryLayout layout = new DefaultRepositoryLayout(); File archiveFile = getArchiveFile(outputDirectory, finalName, classifier); MavenArchiver archiver = new MavenArchiver(); MavenArchiveConfiguration configuration = new MavenArchiveConfiguration(); configuration.addManifestEntries(archive.getManifestEntries()); archiver.setArchiver(jarArchiver); archiver.setOutputFile(archiveFile); try { //TODO should .kar be a bundle? // archive.addManifestEntry(Constants.BUNDLE_NAME, project.getName()); // archive.addManifestEntry(Constants.BUNDLE_VENDOR, project.getOrganization().getName()); // ArtifactVersion version = project.getArtifact().getSelectedVersion(); // String versionString = "" + version.getMajorVersion() + "." + version.getMinorVersion() + "." + version.getIncrementalVersion(); // if (version.getQualifier() != null) { // versionString += "." + version.getQualifier(); // } // archive.addManifestEntry(Constants.BUNDLE_VERSION, versionString); // archive.addManifestEntry(Constants.BUNDLE_MANIFESTVERSION, "2"); // archive.addManifestEntry(Constants.BUNDLE_DESCRIPTION, project.getDescription()); // // NB, no constant for this one // archive.addManifestEntry("Bundle-License", ((License) project.getLicenses().get(0)).getUrl()); // archive.addManifestEntry(Constants.BUNDLE_DOCURL, project.getUrl()); // //TODO this might need some help // archive.addManifestEntry(Constants.BUNDLE_SYMBOLICNAME, project.getArtifactId()); //include the feature.xml Artifact featureArtifact = factory.createArtifactWithClassifier(groupId, artifactId, version, "xml", KarArtifactInstaller.FEATURE_CLASSIFIER); jarArchiver.addFile(featuresFile, repositoryPath + layout.pathOf(featureArtifact)); if (featureArtifact.isSnapshot()) { // the artifact is a snapshot, create the maven-metadata-local.xml getLog().debug("Feature artifact is a SNAPSHOT, handling the maven-metadata-local.xml"); File metadataTarget = new File(featuresFile.getParentFile(), "maven-metadata-local.xml"); getLog().debug("Looking for " + metadataTarget.getAbsolutePath()); if (!metadataTarget.exists()) { // the maven-metadata-local.xml doesn't exist, create it getLog().debug(metadataTarget.getAbsolutePath() + " doesn't exist, create it"); Metadata metadata = new Metadata(); metadata.setGroupId(featureArtifact.getGroupId()); metadata.setArtifactId(featureArtifact.getArtifactId()); metadata.setVersion(featureArtifact.getVersion()); metadata.setModelVersion("1.1.0"); Versioning versioning = new Versioning(); versioning.setLastUpdatedTimestamp(new Date(System.currentTimeMillis())); Snapshot snapshot = new Snapshot(); snapshot.setLocalCopy(true); versioning.setSnapshot(snapshot); SnapshotVersion snapshotVersion = new SnapshotVersion(); snapshotVersion.setClassifier(featureArtifact.getClassifier()); snapshotVersion.setVersion(featureArtifact.getVersion()); snapshotVersion.setExtension(featureArtifact.getType()); snapshotVersion.setUpdated(versioning.getLastUpdated()); versioning.addSnapshotVersion(snapshotVersion); metadata.setVersioning(versioning); MetadataXpp3Writer metadataWriter = new MetadataXpp3Writer(); try { Writer writer = new FileWriter(metadataTarget); metadataWriter.write(writer, metadata); } catch (Exception e) { getLog().warn("Could not create maven-metadata-local.xml", e); getLog().warn("It means that this SNAPSHOT could be overwritten by an older one present on remote repositories"); } } getLog().debug("Adding file " + metadataTarget.getAbsolutePath() + " in the jar path " + repositoryPath + layout.pathOf(featureArtifact).substring(0, layout.pathOf(featureArtifact).lastIndexOf('/')) + "/maven-metadata-local.xml"); jarArchiver.addFile(metadataTarget, repositoryPath + layout.pathOf(featureArtifact).substring(0, layout.pathOf(featureArtifact).lastIndexOf('/')) + "/maven-metadata-local.xml"); } for (Artifact artifact : bundles) { artifactResolver.resolve(artifact, remoteRepos, localRepo); //TODO this may not be reasonable, but... resolved snapshot artifacts have timestamped versions //which do not work in startup.properties. artifact.setVersion(artifact.getBaseVersion()); if (artifact.isSnapshot()) { // the artifact is a snapshot, create the maven-metadata-local.xml final File metadataTmp = File.createTempFile("maven-metadata-local.xml", ".tmp"); try { MavenUtil.generateMavenMetadata(artifact, metadataTmp); } catch (Exception e) { getLog().warn("Could not create maven-metadata-local.xml", e); getLog().warn("It means that this SNAPSHOT could be overwritten by an older one present on remote repositories"); } jarArchiver.addFile(metadataTmp, repositoryPath + layout.pathOf(artifact).substring(0, layout.pathOf(artifact).lastIndexOf('/')) + "/maven-metadata-local.xml"); try { metadataTmp.delete(); } catch (final Exception ex) { getLog().warn("Cannot delete temporary created file.", ex); } } String targetFileName = repositoryPath + layout.pathOf(artifact); jarArchiver.addFile(artifact.getFile(), targetFileName); } if (resourcesDir.isDirectory()) { archiver.getArchiver().addDirectory(resourcesDir); } archiver.createArchive(project, archive); return archiveFile; } catch (Exception e) { throw new MojoExecutionException("Failed to create archive", e); } } protected static boolean isMavenUrl(String name) { Matcher m = mvnPattern.matcher(name); return m.matches(); } /** * Return a path for an artifact: * - if the input is already a path (doesn't contain ':'), the same path is returned. * - if the input is a Maven URL, the input is converted to a default repository location path, type and classifier * are optional. * * @param name artifact data * @return path as supplied or a default Maven repository path */ private static String fromMaven(String name) { Matcher m = mvnPattern.matcher(name); if (!m.matches()) { return name; } StringBuilder b = new StringBuilder(); b.append(m.group(1)); for (int i = 0; i < b.length(); i++) { if (b.charAt(i) == '.') { b.setCharAt(i, '/'); } } b.append("/"); // groupId String artifactId = m.group(2); String version = m.group(3); String extension = m.group(5); String classifier = m.group(7); b.append(artifactId).append("/"); // artifactId b.append(version).append("/"); // version b.append(artifactId).append("-").append(version); if (present(classifier)) { b.append("-").append(classifier); } if (present(classifier)) { b.append(".").append(extension); } else { b.append(".jar"); } return b.toString(); } private static boolean present(String part) { return part != null && !part.isEmpty(); } protected static File getArchiveFile(final File basedir, final String finalName, String classifier) { if (classifier == null) { classifier = ""; } else if (classifier.trim().length() > 0 && !classifier.startsWith("-")) { classifier = "-" + classifier; } return new File(basedir, finalName + classifier + ".kar"); } }