blob: f0601b7b57309ad60745f2786382bcbe6629b861 [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 static java.util.Objects.requireNonNull;
import static org.osgi.framework.Version.parseVersion;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jcr.RepositoryException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.felix.utils.manifest.Clause;
import org.apache.felix.utils.manifest.Parser;
import org.apache.jackrabbit.commons.cnd.ParseException;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
import org.apache.jackrabbit.vault.fs.io.Archive;
import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.apache.jackrabbit.vault.packaging.PackageType;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.apache.sling.commons.osgi.ManifestHeader;
import org.apache.sling.contentparser.api.ContentParser;
import org.apache.sling.contentparser.api.ParserOptions;
import org.apache.sling.contentparser.json.JSONParserOptions;
import org.apache.sling.contentparser.json.internal.JSONContentParser;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.SlingInitialContentPolicy;
import org.apache.sling.feature.cpconverter.artifacts.InputStreamArtifactWriter;
import org.apache.sling.feature.cpconverter.vltpkg.DocViewSerializerContentHandler;
import org.apache.sling.feature.cpconverter.vltpkg.DocViewSerializerContentHandlerException;
import org.apache.sling.feature.cpconverter.vltpkg.JcrNamespaceRegistry;
import org.apache.sling.feature.cpconverter.vltpkg.SingleFileArchive;
import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageUtils;
import org.apache.sling.jcr.contentloader.PathEntry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
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 JAR_TYPE = "jar";
public static final String NODETYPES_BUNDLE_HEADER = "Sling-Nodetypes";
public static final String NAMESPACES_BUNDLE_HEADER = "Sling-Namespaces";
private static final Pattern POM_PROPERTIES_PATTERN = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.properties");
private static final Pattern POM_XML_PATTERN = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.xml");
private boolean enforceBundlesBelowInstallFolder;
private SlingInitialContentPolicy slingInitialContentPolicy;
public BundleEntryHandler() {
super("/jcr_root/(?:apps|libs)/.+/(?<foldername>install|config)(?:\\.(?<runmode>[^/]+))?/(?:(?<startlevel>[0-9]+)/)?.+\\.jar");
}
void setEnforceBundlesBelowInstallFolder(boolean enforceBundlesBelowInstallFolder) {
this.enforceBundlesBelowInstallFolder = enforceBundlesBelowInstallFolder;
}
void setSlingInitialContentPolicy(SlingInitialContentPolicy slingInitialContentPolicy) {
this.slingInitialContentPolicy = slingInitialContentPolicy;
}
@Override
public void handle(@NotNull String path, @NotNull Archive archive, @NotNull Entry entry, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception {
logger.info("Processing bundle {}...", entry.getName());
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!");
}
runMode = matcher.group("runmode");
if (runMode != null) {
// there is a specified RunMode
logger.debug("Runmode {} was extracted from path {}", runMode, path);
}
final String value = matcher.group("startlevel");
if (value != null) {
// there is a specified Start Level
startLevel = Integer.parseInt(value); // NumberFormatException impossible due to RegEx
logger.debug("Start level {} was extracted from path {}", startLevel, path);
}
String bundleName = entry.getName();
// 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);
}
// create a temporary JAR file (extracted from archive)
Path tmpBundleJar = Files.createTempFile(converter.getTempDirectory().toPath(), "extracted", bundleName + ".jar");
try (OutputStream output = Files.newOutputStream(tmpBundleJar);
InputStream input = Objects.requireNonNull(archive.openInputStream(entry))) {
IOUtils.copy(input, output);
}
try {
processBundleInputStream(tmpBundleJar, bundleName, runMode, startLevel, converter);
} finally {
Files.delete(tmpBundleJar);
}
}
void processBundleInputStream(@NotNull Path originalBundleFile, @NotNull String bundleName, @Nullable String runMode, @Nullable Integer startLevel, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception {
try (JarFile jarFile = new JarFile(originalBundleFile.toFile())) {
// first extract bundle metadata from JAR input stream
ArtifactId id = extractArtifactId(bundleName, jarFile);
try (InputStream strippedBundleInput = extractSlingInitialContent(id, jarFile, converter, runMode)) {
if (strippedBundleInput != null && slingInitialContentPolicy == SlingInitialContentPolicy.EXTRACT_AND_REMOVE) {
id = id.changeVersion(id.getVersion() + "-" + ContentPackage2FeatureModelConverter.PACKAGE_CLASSIFIER);
Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(strippedBundleInput), id);
} else {
try (InputStream originalBundleInput = Files.newInputStream(originalBundleFile)) {
Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(originalBundleInput), id);
}
}
}
Objects.requireNonNull(converter.getFeaturesManager()).addArtifact(runMode, id, startLevel);
String exportHeader = Objects.requireNonNull(jarFile.getManifest()).getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
if (exportHeader != null) {
for (Clause clause : Parser.parseHeader(exportHeader)) {
converter.getFeaturesManager().addAPIRegionExport(runMode, clause.getName());
}
}
}
}
static Version getModifiedOsgiVersion(Version originalVersion) {
return new Version(originalVersion.getMajor(), originalVersion.getMinor(), originalVersion.getMicro(), originalVersion.getQualifier() + "_" + ContentPackage2FeatureModelConverter.PACKAGE_CLASSIFIER);
}
@Nullable InputStream extractSlingInitialContent(@NotNull ArtifactId bundleArtifactId, @NotNull JarFile jarFile, @NotNull ContentPackage2FeatureModelConverter converter, @Nullable String runMode) throws Exception {
if (slingInitialContentPolicy == SlingInitialContentPolicy.KEEP) {
return null;
}
// parse "Sling-Initial-Content" header
Manifest manifest = Objects.requireNonNull(jarFile.getManifest());
Iterator<PathEntry> pathEntries = PathEntry.getContentPaths(manifest, -1);
if (pathEntries == null) {
return null;
}
logger.info("Extracting Sling-Initial-Content from '{}'", bundleArtifactId);
Collection<PathEntry> pathEntryList = new ArrayList<>();
pathEntries.forEachRemaining(pathEntryList::add);
// remove header
manifest.getMainAttributes().remove(new Attributes.Name(PathEntry.CONTENT_HEADER));
// change version to have suffix
Version originalVersion = new Version(Objects.requireNonNull(manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION)));
manifest.getMainAttributes().putValue(Constants.BUNDLE_VERSION, getModifiedOsgiVersion(originalVersion).toString());
Path newBundleFile = Files.createTempFile(converter.getTempDirectory().toPath(), "newBundle", ".jar");
// create JAR file to prevent extracting it twice and for random access
JcrNamespaceRegistry namespaceRegistry = createNamespaceRegistry(manifest, jarFile, converter.getFeaturesManager().getNamespaceUriByPrefix());
Map<PackageType, VaultPackageAssembler> packageAssemblers = new EnumMap<>(PackageType.class);
try (OutputStream fileOutput = Files.newOutputStream(newBundleFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
JarOutputStream bundleOutput = new JarOutputStream(fileOutput, manifest)) {
for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
JarEntry jarEntry = e.nextElement();
if (!jarEntry.isDirectory()) {
try (InputStream input = jarFile.getInputStream(jarEntry)) {
if (!extractSlingInitialContent(jarEntry, input, bundleArtifactId, pathEntryList, packageAssemblers, namespaceRegistry, converter)) {
// skip manifest, as already written in the constructor (as first entry)
if (jarEntry.getName().equals(JarFile.MANIFEST_NAME)) {
continue;
}
// copy entry as is to the stripped bundle
bundleOutput.putNextEntry(jarEntry);
IOUtils.copy(input, bundleOutput);
bundleOutput.closeEntry();
}
}
}
}
}
// add additional content packages to feature model
finalizePackageAssembly(packageAssemblers, converter, runMode);
// return stripped bundle's inputstream which must be deleted on close
return Files.newInputStream(newBundleFile, StandardOpenOption.READ, StandardOpenOption.DELETE_ON_CLOSE);
}
/**
*
* @param jarEntry
* @param bundleFileInputStream
* @param pathEntriesStream
* @param packageAssemblers
* @param converter
* @return {@code true} in case the given entry was part of the initial content otherwise {@code false}
* @throws Exception
*/
boolean extractSlingInitialContent(@NotNull JarEntry jarEntry, @NotNull InputStream bundleFileInputStream, @NotNull ArtifactId bundleArtifactId, @NotNull Collection<PathEntry> pathEntries, @NotNull Map<PackageType, VaultPackageAssembler> packageAssemblers, @NotNull JcrNamespaceRegistry nsRegistry, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception {
final String entryName = jarEntry.getName();
// check if current JAR entry is initial content
Optional<PathEntry> pathEntry = pathEntries.stream().filter(p -> entryName.startsWith(p.getPath())).findFirst();
if (!pathEntry.isPresent()) {
return false;
}
Map.Entry<ContentParser, ParserOptions> contentParserAndOptions = getContentParserForEntry(jarEntry, pathEntry.get());
// https://sling.apache.org/documentation/bundles/content-loading-jcr-contentloader.html#file-name-escaping
String repositoryPath = (pathEntry.get().getTarget() != null ? pathEntry.get().getTarget() : "/") + URLDecoder.decode(entryName.substring(pathEntry.get().getPath().length()), "UTF-8");
// all entry paths used by entry handlers start with "/"
String contentPackageEntryPath = "/" + org.apache.jackrabbit.vault.util.Constants.ROOT_DIR + PlatformNameFormat.getPlatformPath(repositoryPath);
Path tmpDocViewInputFile = null;
try {
if (contentParserAndOptions != null) {
// convert to docview xml
tmpDocViewInputFile = Files.createTempFile(converter.getTempDirectory().toPath(), "docview", ".xml");
try (OutputStream docViewOutput = Files.newOutputStream(tmpDocViewInputFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
DocViewSerializerContentHandler contentHandler = new DocViewSerializerContentHandler(docViewOutput, nsRegistry)) {
contentParserAndOptions.getKey().parse(contentHandler, bundleFileInputStream, contentParserAndOptions.getValue());
contentPackageEntryPath = FilenameUtils.removeExtension(contentPackageEntryPath) + ".xml";
} catch (IOException e) {
throw new IOException("Can not parse " + jarEntry, e);
} catch (DocViewSerializerContentHandlerException e) {
throw new IOException("Can not convert " + jarEntry + " to enhanced DocView format", e);
}
}
// remap CND files to make sure they are picked up by NodeTypesEntryHandler
if (nsRegistry.getRegisteredCndSystemIds().contains(jarEntry.getName())) {
contentPackageEntryPath = "/META-INF/vault/" + Text.getName(jarEntry.getName()) + ".cnd";
}
try (Archive virtualArchive = SingleFileArchive.fromPathOrInputStream(tmpDocViewInputFile, bundleFileInputStream,
() -> Files.createTempFile(converter.getTempDirectory().toPath(), "initial-content", Text.getName(jarEntry.getName())), contentPackageEntryPath)) {
// does entry in initial content need to be extracted into feature model (e.g. for OSGi configurations)?
if (!converter.process(contentPackageEntryPath, virtualArchive, null, false)) {
// ... otherwise add it to the content package
// in which content package should this end up?
VaultPackageAssembler packageAssembler = initPackageAssemblerForPath(bundleArtifactId, repositoryPath, pathEntry.get(), packageAssemblers, converter);
if (tmpDocViewInputFile != null) {
packageAssembler.addEntry(contentPackageEntryPath, tmpDocViewInputFile.toFile());
} else {
packageAssembler.addEntry(contentPackageEntryPath, bundleFileInputStream);
}
}
}
} finally {
if (tmpDocViewInputFile != null) {
Files.delete(tmpDocViewInputFile);
}
}
return true;
}
JcrNamespaceRegistry createNamespaceRegistry(@NotNull Manifest manifest, @NotNull JarFile jarFile, @NotNull Map<String, String> predefinedNamespaceUriByPrefix) throws RepositoryException, IOException, ParseException {
JcrNamespaceRegistry registry = new JcrNamespaceRegistry();
for (Map.Entry<String, String> entry : predefinedNamespaceUriByPrefix.entrySet()) {
registry.registerNamespace(entry.getKey(), entry.getValue());
}
// parse Sling-Namespaces header (https://github.com/apache/sling-org-apache-sling-jcr-base/blob/66be360910c265473799635fcac0e23895898913/src/main/java/org/apache/sling/jcr/base/internal/loader/Loader.java#L192)
final String namespacesDefinitionHeader = manifest.getMainAttributes().getValue(NAMESPACES_BUNDLE_HEADER);
if (namespacesDefinitionHeader != null) {
final StringTokenizer st = new StringTokenizer(namespacesDefinitionHeader, ",");
while ( st.hasMoreTokens() ) {
final String token = st.nextToken().trim();
int pos = token.indexOf('=');
if ( pos == -1 ) {
logger.warn("createNamespaceRegistry: Bundle {} has an invalid namespace manifest header entry: {}",
manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME), token);
} else {
final String prefix = token.substring(0, pos).trim();
final String namespace = token.substring(pos+1).trim();
registry.registerNamespace(prefix, namespace);
}
}
}
// parse Sling-Nodetypes header
final String typesHeader = manifest.getMainAttributes().getValue(NODETYPES_BUNDLE_HEADER);
if (typesHeader != null) {
for (ManifestHeader.Entry entry : ManifestHeader.parse(typesHeader).getEntries()) {
JarEntry jarEntry = jarFile.getJarEntry(entry.getValue());
if (jarEntry == null) {
logger.warn("createNamespaceRegistry: Bundle {} has referenced a non existing node type definition: {}",
manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME), entry.getValue());
} else {
try (InputStream inputStream = jarFile.getInputStream(jarEntry);
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
registry.registerCnd(reader, entry.getValue());
}
}
}
}
return registry;
}
/**
* Lazily initializes the cache with the necessary VaultPackageAssemblers
* @param bundleArtifactId
* @param repositoryPath
* @param cache
* @param converter
* @return the VaultPackageAssembler from the cache to use for the given repository path
*/
public VaultPackageAssembler initPackageAssemblerForPath(@NotNull ArtifactId bundleArtifactId, @NotNull String repositoryPath, @NotNull PathEntry pathEntry, @NotNull Map<PackageType, VaultPackageAssembler> cache, @NotNull ContentPackage2FeatureModelConverter converter) {
PackageType packageType = VaultPackageUtils.detectPackageType(repositoryPath);
VaultPackageAssembler assembler = cache.get(packageType);
if (assembler == null) {
final String packageNameSuffix;
switch (packageType) {
case APPLICATION:
packageNameSuffix = "-apps";
break;
case CONTENT:
packageNameSuffix = "-content";
break;
default:
throw new IllegalStateException("Unexpected package type " + packageType + " detected for path " + repositoryPath);
}
final PackageId packageId = new PackageId(bundleArtifactId.getGroupId(), bundleArtifactId.getArtifactId()+packageNameSuffix, bundleArtifactId.getVersion());
assembler = VaultPackageAssembler.create(converter.getTempDirectory(), packageId, "Generated out of Sling Initial Content from bundle " + bundleArtifactId + " by cp2fm");
cache.put(packageType, assembler);
logger.info("Created package {} out of Sling-Initial-Content from '{}'", packageId, bundleArtifactId);
}
DefaultWorkspaceFilter filter = assembler.getFilter();
if (!filter.covers(repositoryPath)) {
PathFilterSet pathFilterSet = new PathFilterSet(pathEntry.getTarget() != null ? pathEntry.getTarget() : "/");
ImportMode importMode;
if (pathEntry.isOverwrite()) {
importMode = ImportMode.REPLACE;
} else {
importMode = ImportMode.MERGE;
}
// TODO: add handling for merge, mergeProperties and overwriteProperties (https://issues.apache.org/jira/browse/SLING-10318)
pathFilterSet.setImportMode(importMode);
filter.add(pathFilterSet);
}
return assembler;
}
void finalizePackageAssembly(@NotNull Map<PackageType, VaultPackageAssembler> packageAssemblers, @NotNull ContentPackage2FeatureModelConverter converter, @Nullable String runMode) throws Exception {
for (java.util.Map.Entry<PackageType, VaultPackageAssembler> entry : packageAssemblers.entrySet()) {
File packageFile = entry.getValue().createPackage(false);
converter.processContentPackageArchive(packageFile, entry.getValue(), runMode);
}
}
Map.Entry<ContentParser, ParserOptions> getContentParserForEntry(JarEntry entry, PathEntry pathEntry) {
if (entry.getName().endsWith(".json") && !pathEntry.isIgnoredImportProvider("json")) {
return new AbstractMap.SimpleEntry<>(new JSONContentParser(), new JSONParserOptions().withFeatures(JSONParserOptions.DEFAULT_JSON_PARSER_FEATURES));
} else {
return null;
}
}
protected @NotNull ArtifactId extractArtifactId(@NotNull String bundleName, @NotNull JarFile jarFile) throws IOException {
String artifactId = null;
String version = null;
String groupId = null;
String classifier = null;
for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
JarEntry jarEntry = e.nextElement();
String nextEntryName = jarEntry.getName();
if (POM_PROPERTIES_PATTERN.matcher(nextEntryName).matches()) {
logger.info("Reading '{}' bundle GAV from {}...", bundleName, nextEntryName);
Properties properties = new Properties();
try (InputStream input = jarFile.getInputStream(jarEntry)) {
properties.load(input);
}
groupId = properties.getProperty(NAME_GROUP_ID);
artifactId = properties.getProperty(NAME_ARTIFACT_ID);
version = properties.getProperty(PackageProperties.NAME_VERSION);
} else if (POM_XML_PATTERN.matcher(nextEntryName).matches()) {
logger.info("Reading '{}' bundle GAV from {}...", bundleName, nextEntryName);
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('-'));
} else if (bundleName.indexOf('-') != -1){
try {
String versionString = bundleName.substring(bundleName.indexOf('-') + 1);
if (!parseVersion(versionString).equals(Version.emptyVersion)) {
version = versionString;
}
} catch (IllegalArgumentException ex) {
// Not a version
}
}
}
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("-")) {
classifier = suffix.substring(1);
logger.info("Inferred classifier of '{}:{}:{}' to be '{}'", groupId, artifactId, version, classifier);
}
}
// no need to iterate further
break;
}
}
}
if (groupId == null) {
// maybe the included jar is just an OSGi bundle but not a valid Maven artifact
groupId = getCheckedProperty(jarFile.getManifest(), Constants.BUNDLE_SYMBOLICNAME);
// 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(jarFile.getManifest(), Constants.BUNDLE_VERSION));
version = osgiVersion.getMajor() + "." + osgiVersion.getMinor() + "." + osgiVersion.getMicro() + (osgiVersion.getQualifier().isEmpty() ? "" : "-" + osgiVersion.getQualifier());
}
return new ArtifactId(groupId, artifactId, version, classifier, JAR_TYPE);
}
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.");
}
}