blob: c4731ec3a59de41188fe857044470c69f2af4fb3 [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;
import java.io.*;
import java.nio.file.Files;
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<>();
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 = Files.createTempFile("maven-metadata-local.xml", ".tmp").toFile();
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");
}
}