| /* |
| * 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.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Enumeration; |
| import java.util.Objects; |
| import java.util.Properties; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.jar.Manifest; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.commons.lang3.StringUtils; |
| 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.jackrabbit.vault.packaging.PackageProperties; |
| import org.apache.sling.feature.Artifact; |
| import org.apache.sling.feature.ArtifactId; |
| import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter; |
| import org.apache.sling.feature.cpconverter.ConverterException; |
| import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.SlingInitialContentPolicy; |
| import org.apache.sling.feature.cpconverter.artifacts.InputStreamArtifactWriter; |
| import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractor; |
| import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractContext; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.Version; |
| |
| public 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"; |
| |
| 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; |
| |
| protected SlingInitialContentPolicy slingInitialContentPolicy; |
| private BundleSlingInitialContentExtractor bundleSlingInitialContentExtractor = new BundleSlingInitialContentExtractor(); |
| |
| public BundleEntryHandler() { |
| super("/jcr_root/(?:apps|libs)/.+/(?<foldername>install|config)(?:\\.(?<runmode>[^/]+))?/(?:(?<startlevel>[0-9]+)/)?.+\\.jar"); |
| } |
| |
| void setEnforceBundlesBelowInstallFolder(boolean enforceBundlesBelowInstallFolder) { |
| this.enforceBundlesBelowInstallFolder = enforceBundlesBelowInstallFolder; |
| } |
| |
| public void setSlingInitialContentPolicy(@NotNull SlingInitialContentPolicy slingInitialContentPolicy) { |
| this.slingInitialContentPolicy = slingInitialContentPolicy; |
| } |
| |
| @Override |
| public void handle(@NotNull String path, |
| @NotNull Archive archive, |
| @NotNull Entry entry, |
| @NotNull ContentPackage2FeatureModelConverter converter) throws IOException, ConverterException { |
| 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 ConverterException("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 { |
| try (OutputStream output = Files.newOutputStream(tmpBundleJar); |
| InputStream input = Objects.requireNonNull(archive.openInputStream(entry))) { |
| IOUtils.copy(input, output); |
| } |
| processBundleInputStream(path, tmpBundleJar, bundleName, runMode, startLevel, converter); |
| } finally { |
| Files.delete(tmpBundleJar); |
| } |
| } |
| |
| void processBundleInputStream(@NotNull String path, @NotNull Path originalBundleFile, @NotNull String bundleName, @Nullable String runMode, @Nullable Integer startLevel, @NotNull ContentPackage2FeatureModelConverter converter) |
| throws ConverterException, IOException { |
| try (JarFile jarFile = new JarFile(originalBundleFile.toFile())) { |
| // first extract bundle metadata from JAR input stream |
| Artifact artifact = extractFeatureArtifact(bundleName, jarFile); |
| ArtifactId id = artifact.getId(); |
| |
| BundleSlingInitialContentExtractContext context = new BundleSlingInitialContentExtractContext(slingInitialContentPolicy, path, id, jarFile, converter, runMode); |
| try (InputStream strippedBundleInput = bundleSlingInitialContentExtractor.extract(context)) { |
| if (strippedBundleInput != null && slingInitialContentPolicy == ContentPackage2FeatureModelConverter.SlingInitialContentPolicy.EXTRACT_AND_REMOVE) { |
| id = id.changeVersion(id.getVersion() + "-" + ContentPackage2FeatureModelConverter.PACKAGE_CLASSIFIER); |
| Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(strippedBundleInput), runMode, id); |
| } else { |
| try (InputStream originalBundleInput = Files.newInputStream(originalBundleFile)) { |
| Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(originalBundleInput), runMode, id); |
| } |
| } |
| } |
| artifact = artifact.copy(id); |
| Objects.requireNonNull(converter.getFeaturesManager()).addArtifact(runMode, artifact, 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()); |
| } |
| } |
| } |
| } |
| |
| private @Nullable ArtifactId extractArtifactIdFromPomProperties(@NotNull String bundleName, @NotNull final JarFile jarFile, @NotNull final JarEntry jarEntry) throws IOException { |
| logger.info("Reading '{}' bundle GAV from {}...", bundleName, jarEntry.getName()); |
| final Properties properties = new Properties(); |
| try (final InputStream input = jarFile.getInputStream(jarEntry)) { |
| properties.load(input); |
| } |
| final String groupId = properties.getProperty(NAME_GROUP_ID); |
| final String artifactId = properties.getProperty(NAME_ARTIFACT_ID); |
| final String version = properties.getProperty(PackageProperties.NAME_VERSION); |
| if ( groupId != null && artifactId != null && version != null ) { |
| return new ArtifactId(groupId, artifactId, version, null, null); |
| } |
| return null; |
| } |
| |
| private @Nullable ArtifactId extractArtifactIdFromPom(@NotNull String bundleName, @NotNull final JarFile jarFile, @NotNull final JarEntry jarEntry) { |
| logger.info("Reading '{}' bundle GAV from {}...", bundleName, jarEntry.getName()); |
| String path = jarEntry.getName().substring(0, jarEntry.getName().length() - "/pom.xml".length()); |
| final String groupId = path.substring("META-INF/maven/".length(), path.lastIndexOf('/')); |
| String artifactId = path.substring(path.lastIndexOf('/') + 1); |
| String version = null; |
| 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 ) { |
| return new ArtifactId(groupId, artifactId, version, null, null); |
| } |
| return null; |
| } |
| |
| private @NotNull ArtifactId extractArtifactIdFromSymbolicName(@NotNull final JarFile jarFile) throws IOException { |
| // maybe the included jar is just an OSGi bundle but not a valid Maven artifact |
| String groupId = StringUtils.substringBefore(getCheckedProperty(jarFile.getManifest(), Constants.BUNDLE_SYMBOLICNAME), ";"); |
| String artifactId = null; |
| |
| // 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; |
| } |
| final Version osgiVersion = Version.parseVersion(getCheckedProperty(jarFile.getManifest(), Constants.BUNDLE_VERSION)); |
| final String version = osgiVersion.getMajor() + "." + osgiVersion.getMinor() + "." + osgiVersion.getMicro() + (osgiVersion.getQualifier().isEmpty() ? "" : "-" + osgiVersion.getQualifier()); |
| |
| return new ArtifactId(groupId, artifactId, version, null, null); |
| } |
| |
| protected @NotNull Artifact extractFeatureArtifact(@NotNull String bundleName, @NotNull JarFile jarFile) throws IOException { |
| ArtifactId resultId = null; |
| for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) { |
| final JarEntry jarEntry = e.nextElement(); |
| |
| if (POM_PROPERTIES_PATTERN.matcher(jarEntry.getName()).matches()) { |
| resultId = extractArtifactIdFromPomProperties(bundleName, jarFile, jarEntry); |
| |
| } else if (POM_XML_PATTERN.matcher(jarEntry.getName()).matches()) { |
| resultId = extractArtifactIdFromPom(bundleName, jarFile, jarEntry); |
| |
| } |
| |
| if (resultId != null) { |
| // bundleName is now the bare name without extension |
| final String synthesized = resultId.getArtifactId().concat("-").concat(resultId.getVersion()); |
| |
| // it was the pom.properties we were looking for |
| if (bundleName.startsWith(synthesized) || bundleName.equals(resultId.getArtifactId())) { |
| |
| // 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("-")) { |
| resultId = resultId.changeClassifier(suffix.substring(1)); |
| logger.info("Inferred classifier of '{}'", resultId.toMvnId()); |
| } |
| } |
| // no need to iterate further |
| break; |
| } |
| } |
| // resultId should be reset here, however, this will make a lot of tests fail |
| // resultId = null; |
| } |
| |
| if (resultId == null) { |
| resultId = extractArtifactIdFromSymbolicName(jarFile); |
| } |
| |
| // create artifact and store symbolic name and version in metadata |
| final Artifact result = new Artifact(resultId.changeType(JAR_TYPE)); |
| setMetadataFromManifest(jarFile.getManifest(), Constants.BUNDLE_VERSION, result, false); |
| setMetadataFromManifest(jarFile.getManifest(), Constants.BUNDLE_SYMBOLICNAME, result, true); |
| |
| return result; |
| } |
| |
| private static void setMetadataFromManifest(@NotNull Manifest manifest, @NotNull String name, @NotNull Artifact artifact, boolean strip) { |
| String value = manifest.getMainAttributes().getValue(name); |
| if (strip) { |
| value = StringUtils.substringBefore(value, ";"); |
| } |
| if (value != null) { |
| artifact.getMetadata().put(name, value); |
| } |
| } |
| |
| 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."); |
| } |
| |
| public void setBundleSlingInitialContentExtractor(BundleSlingInitialContentExtractor bundleSlingInitialContentExtractor) { |
| this.bundleSlingInitialContentExtractor = bundleSlingInitialContentExtractor; |
| } |
| } |