blob: 8de7ae559721bb45d181d1284bb2abf53b49cf88 [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.sling.feature.cpconverter.handlers;
import org.apache.felix.utils.manifest.Clause;
import org.apache.felix.utils.manifest.Parser;
import org.apache.jackrabbit.vault.fs.io.Archive;
import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
import org.apache.sling.feature.cpconverter.artifacts.InputStreamArtifactWriter;
import org.codehaus.plexus.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Objects.requireNonNull;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_VERSION;
import static org.osgi.framework.Version.parseVersion;
public final class BundleEntryHandler extends AbstractRegexEntryHandler {
private static final String NAME_GROUP_ID = "groupId";
private static final String NAME_ARTIFACT_ID = "artifactId";
private static final String NAME_CLASSIFIER = "classifier";
private static final String BUNDLE_SYMBOLIC_NAME = "Bundle-SymbolicName";
private static final String BUNDLE_VERSION = "Bundle-Version";
private static final String JAR_TYPE = "jar";
private final Pattern pomPropertiesPattern = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.properties");
private final Pattern pomXmlPropertiesPattern = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.xml");
private boolean enforceBundlesBelowInstallFolder;
public BundleEntryHandler() {
super("/jcr_root/(?:apps|libs)/.+/(?<foldername>install|config)(?:\\.(?<runmode>[^/]+))?/(?:(?<startlevel>[0-9]+)/)?.+\\.jar");
}
void setEnforceBundlesBelowInstallFolder(boolean enforceBundlesBelowInstallFolder) {
this.enforceBundlesBelowInstallFolder = enforceBundlesBelowInstallFolder;
}
@Override
public void handle(@NotNull String path, @NotNull Archive archive, @NotNull Entry entry, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception {
logger.info("Processing bundle {}...", entry.getName());
String groupId;
String artifactId = null;
String version;
String classifier = null;
Manifest manifest;
Matcher matcher = getPattern().matcher(path);
String runMode = null;
Integer startLevel = null;
// we are pretty sure it matches, here
if (!matcher.matches()) {
throw new IllegalStateException("Something went terribly wrong: pattern '"
+ getPattern().pattern()
+ "' should have matched already with path '"
+ path
+ "' but it does not, currently");
}
if (enforceBundlesBelowInstallFolder && !"install".equals(matcher.group("foldername"))) {
throw new IllegalStateException("OSGi bundles are only considered if placed below a folder called 'install', but the bundle at '"+ path + "' is placed outside!");
}
if (StringUtils.isNotBlank(matcher.group("runmode"))) {
// there is a specified RunMode
runMode = matcher.group("runmode");
logger.debug("Runmode {} was extracted from path {}", runMode, path);
}
if (StringUtils.isNotBlank(matcher.group("startlevel"))) {
// there is a specified Start Level
startLevel = Integer.parseInt(matcher.group("startlevel")); // NumberFormatException impossible due to RegEx
logger.debug("Start level {} was extracted from path {}", startLevel, path);
}
try (JarInputStream jarInput = new JarInputStream(Objects.requireNonNull(archive.openInputStream(entry)))) {
Properties properties = readGav(entry.getName(), jarInput);
manifest = jarInput.getManifest();
if (!properties.isEmpty()) {
groupId = getCheckedProperty(properties, NAME_GROUP_ID);
artifactId = getCheckedProperty(properties, NAME_ARTIFACT_ID);
version = getCheckedProperty(properties, NAME_VERSION);
classifier = properties.getProperty(NAME_CLASSIFIER);
} else { // maybe the included jar is just an OSGi bundle but not a valid Maven artifact
groupId = getCheckedProperty(manifest, BUNDLE_SYMBOLIC_NAME);
// Make sure there are not spaces in the name to adhere to the Maven Group Id specification
groupId = groupId.replace(' ', '_').replace(':', '_').replace('/', '_').replace('\\', '_');
if (groupId.indexOf('.') != -1) {
artifactId = groupId.substring(groupId.lastIndexOf('.') + 1);
groupId = groupId.substring(0, groupId.lastIndexOf('.'));
}
if (artifactId == null || artifactId.isEmpty()) {
artifactId = groupId;
}
Version osgiVersion = Version.parseVersion(getCheckedProperty(manifest, BUNDLE_VERSION));
version = osgiVersion.getMajor() + "." + osgiVersion.getMinor() + "." + osgiVersion.getMicro() + (osgiVersion.getQualifier().isEmpty() ? "" : "-" + osgiVersion.getQualifier());
}
}
try (InputStream input = archive.openInputStream(entry)) {
if (input != null) {
ArtifactId id = new ArtifactId(groupId, artifactId, version, classifier, JAR_TYPE);
Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(input), id);
Objects.requireNonNull(converter.getFeaturesManager()).addArtifact(runMode, id, startLevel);
String epHeader = manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
if (epHeader != null) {
for (Clause clause : Parser.parseHeader(epHeader)) {
converter.getFeaturesManager().addAPIRegionExport(runMode, clause.getName());
}
}
}
}
}
// method visibility set to 'protected' fot testing purposes
protected @NotNull Properties readGav(@NotNull String entryName, @NotNull JarInputStream jarInput) throws IOException {
Properties pomProperties = new Properties();
Properties pomXmlProperties = new Properties();
String bundleName = entryName;
// Remove the leading path
int idx = bundleName.lastIndexOf('/');
if (idx >= 0) {
bundleName = bundleName.substring(idx + 1);
}
// Remove the extension
int edx = bundleName.lastIndexOf('.');
if (edx > 0) {
bundleName = bundleName.substring(0, edx);
}
JarEntry jarEntry;
while ((jarEntry = jarInput.getNextJarEntry()) != null) {
String nextEntryName = jarEntry.getName();
String artifactId = null;
String version = null;
String groupId = null;
Properties properties = new Properties();
boolean pomXml = false;
if (pomPropertiesPattern.matcher(nextEntryName).matches()) {
logger.info("Reading '{}' bundle GAV from {}...", bundleName, nextEntryName);
properties = pomProperties;
properties.load(jarInput);
groupId = properties.getProperty(NAME_GROUP_ID);
artifactId = properties.getProperty(NAME_ARTIFACT_ID);
version = properties.getProperty(NAME_VERSION);
} else if (pomXmlPropertiesPattern.matcher(nextEntryName).matches()) {
pomXml = true;
logger.info("Reading '{}' bundle GAV from {}...", bundleName, nextEntryName);
properties = pomXmlProperties;
String path = nextEntryName.substring(0, nextEntryName.length() - "/pom.xml".length());
groupId = path.substring("META-INF/maven/".length(), path.lastIndexOf('/'));
artifactId = path.substring(path.lastIndexOf('/') + 1);
if (artifactId.indexOf('-') != -1) {
version = artifactId.substring(artifactId.indexOf('-'));
artifactId = artifactId.substring(0, artifactId.indexOf('-'));
properties.put(NAME_VERSION, version);
} else if (bundleName.indexOf('-') != -1){
try {
String versionString = bundleName.substring(bundleName.indexOf('-') + 1);
if (!parseVersion(versionString).equals(Version.emptyVersion)) {
version = versionString;
properties.put(NAME_VERSION, version);
}
} catch (IllegalArgumentException ex) {
// Not a version
}
}
properties.put(NAME_GROUP_ID, groupId);
properties.put(NAME_ARTIFACT_ID, artifactId);
}
if (groupId != null && artifactId != null && version != null) {
// bundleName is now the bare name without extension
String synthesized = artifactId + "-" + version;
// it was the pom.properties we were looking for
if (bundleName.startsWith(synthesized) || bundleName.equals(artifactId)) {
// check the artifact has a classifier in the bundle file name
if (synthesized.length() < bundleName.length()) {
String suffix = bundleName.substring(synthesized.length());
if (suffix.length() > 1 && suffix.startsWith("-")) {
String classifier = suffix.substring(1);
logger.info("Inferred classifier of '"
+ artifactId
+ ":"
+ version
+ "' to be '"
+ classifier
+ "'");
properties.setProperty(NAME_CLASSIFIER, classifier);
}
}
if (!pomXml) {
pomProperties = properties;
break;
} else {
pomXmlProperties = properties;
}
}
}
}
return pomProperties.isEmpty() ? pomXmlProperties : pomProperties;
}
private static @NotNull String getCheckedProperty(@NotNull Manifest manifest, @NotNull String name) {
String property = manifest.getMainAttributes().getValue(name);
if (property != null) {
property = property.trim();
}
return requireNonNull(property, "Jar file can not be defined as a valid OSGi bundle without specifying a valid '"
+ name
+ "' property.");
}
private static String getCheckedProperty(@NotNull Properties properties, @NotNull String name) {
String property = properties.getProperty(name).trim();
if (property != null) {
property = property.trim();
}
return requireNonNull(property, "Jar file can not be defined as a valid Maven artifact without specifying a valid '"
+ name
+ "' property.");
}
}