blob: 44383d29a63d364dcedfe8cb0ed7c2bebd11b1cd [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.scanner.impl;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.apache.sling.feature.Artifact;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.Configuration;
import org.apache.sling.feature.io.IOUtils;
import org.apache.sling.feature.scanner.BundleDescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Scan the contents of a content package.
*/
public class ContentPackageScanner {
private static final String FILE_PACKAGE_PROPS = "META-INF/vault/properties.xml";
private static final String FILE_MANIFEST = "META-INF/MANIFEST.MF";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final byte[] buffer = new byte[65536];
enum FileType {
BUNDLE,
CONFIG,
PACKAGE,
}
private static final List<String> CFG_EXTENSIONS = Arrays.asList(".config", ".cfg", ".cfg.json", ".xml");
/**
* Scan the content package for embedded artifacts
* @param artifact The content package
* @param url The url to the binary
* @return A set of artifacts
* @throws IOException If processing fails
*/
public Set<ContentPackageDescriptorImpl> scan(final Artifact artifact, final URL url) throws IOException {
final Set<ContentPackageDescriptorImpl> contentPackages = new HashSet<>();
if (url != null) {
final String path = url.getPath();
final int lastDotInUrl = path.lastIndexOf(".");
final String name = path.substring(path.lastIndexOf("/") + 1, lastDotInUrl);
extractContentPackage(null, null, artifact, name, url, contentPackages);
}
return contentPackages;
}
/**
* Detect the file type, bundle, configuration or embedded package from the content path
* @param contentPath The content path
* @return The detected file type or {@code null}
*/
FileType detectContentFileType(final String contentPath) {
FileType fileType = null;
// check for install folders in libs or apps
if (contentPath.startsWith("/libs/") || contentPath.startsWith("/apps/")) {
// check if this is an install folder (I)
// install folders are either named:
// "install" or
// "install.{runmode}"
boolean isInstall = contentPath.indexOf("/install/") != -1;
if (!isInstall) {
final int pos = contentPath.indexOf("/install.");
if (pos != -1) {
final int endSlashPos = contentPath.indexOf('/', pos + 1);
if (endSlashPos != -1) {
isInstall = true;
}
}
}
if (!isInstall) {
// check if this is an install folder (II)
// config folders are either named:
// "config" or
// "config.{runmode}"
isInstall = contentPath.indexOf("/config/") != -1;
if (!isInstall) {
final int pos = contentPath.indexOf("/config.");
if (pos != -1) {
final int endSlashPos = contentPath.indexOf('/', pos + 1);
if (endSlashPos != -1) {
isInstall = true;
}
}
}
}
if (isInstall) {
if (contentPath.endsWith(".jar")) {
fileType = FileType.BUNDLE;
} else if (contentPath.endsWith(".zip")) {
fileType = FileType.PACKAGE;
} else {
for(final String ext : CFG_EXTENSIONS) {
if ( contentPath.endsWith(ext) ) {
fileType = FileType.CONFIG;
break;
}
}
}
}
} else if ( contentPath.startsWith("/etc/packages/") && contentPath.endsWith(".zip")) {
// embedded content package
fileType = FileType.PACKAGE;
}
return fileType;
}
private ContentPackageDescriptorImpl extractContentPackage(final ContentPackageDescriptorImpl parentPackage,
final String parentContentPath,
final Artifact packageArtifact,
final String name,
final URL archiveUrl,
final Set<ContentPackageDescriptorImpl> infos)
throws IOException {
logger.debug("Analyzing Content Package {}", archiveUrl);
final File tempDir = Files.createTempDirectory(null).toFile();
try {
final File toDir = new File(tempDir, archiveUrl.getPath().substring(archiveUrl.getPath().lastIndexOf("/") + 1));
toDir.mkdirs();
Manifest manifest = null;
final List<File> toProcess = new ArrayList<>();
final List<String> contentPaths = new ArrayList<>();
final List<BundleDescriptor> bundles = new ArrayList<>();
final List<Configuration> configs = new ArrayList<>();
final Properties packageProps = new Properties();
try (final JarFile zipFile = IOUtils.getJarFileFromURL(archiveUrl, true, null)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
final String entryName = entry.getName();
// skip dirs
if ( entryName.endsWith("/") ) {
continue;
}
logger.debug("Content package entry {}", entryName);
if ( entryName.startsWith("jcr_root/") ) {
final String contentPath = entryName.substring(8);
contentPaths.add(contentPath);
final FileType fileType = detectContentFileType(contentPath);
if (fileType != null) {
logger.debug("- extracting : {}", contentPath);
final File newFile = new File(toDir, entryName.replace('/', File.separatorChar));
newFile.getParentFile().mkdirs();
try (
final FileOutputStream fos = new FileOutputStream(newFile);
final InputStream zis = zipFile.getInputStream(entry);
) {
int len;
while ((len = zis.read(buffer)) > -1) {
fos.write(buffer, 0, len);
}
}
if (fileType == FileType.BUNDLE) {
int startLevel = 20;
final int lastSlash = contentPath.lastIndexOf('/');
final int nextSlash = contentPath.lastIndexOf('/', lastSlash - 1);
final String part = contentPath.substring(nextSlash + 1, lastSlash);
try {
startLevel = Integer.valueOf(part);
} catch (final NumberFormatException ignore) {
// ignore
}
final Artifact bundle = new Artifact(extractArtifactId(packageArtifact.getId(), newFile));
bundle.setStartOrder(startLevel);
final BundleDescriptor info = new BundleDescriptorImpl(bundle, newFile.toURI().toURL());
bundle.getMetadata().put(ContentPackageDescriptorImpl.METADATA_PACKAGE,
packageArtifact.getId().toMvnId());
bundle.getMetadata().put(ContentPackageDescriptorImpl.METADATA_PATH, contentPath);
bundles.add(info);
} else if (fileType == FileType.CONFIG) {
final Configuration configEntry = this.processConfiguration(newFile, packageArtifact.getId(), contentPath);
if (configEntry != null) {
configs.add(configEntry);
}
} else if (fileType == FileType.PACKAGE) {
toProcess.add(newFile);
}
}
} else if ( FILE_MANIFEST.equals(entry.getName()) ) {
try ( final InputStream zis = zipFile.getInputStream(entry)) {
manifest = new Manifest(zis);
} catch ( final IOException ignore ) {
logger.warn("Failure reading manifest from {} : {}", packageArtifact.getId(), ignore.getMessage());
}
} else if ( FILE_PACKAGE_PROPS.equals(entry.getName()) ) {
try ( final InputStream zis = zipFile.getInputStream(entry)) {
packageProps.loadFromXML(zis);
}
}
}
final ContentPackageDescriptorImpl desc = new ContentPackageDescriptorImpl(name, packageArtifact, archiveUrl, manifest,
bundles, contentPaths, configs, packageProps);
if ( parentPackage != null ) {
desc.setParentContentPackageInfo(parentPackage, parentContentPath);
}
for (final File f : toProcess) {
final int lastDot = f.getName().lastIndexOf(".");
final String subName = f.getName().substring(0, lastDot);
final String contentPath = f.getAbsolutePath().substring(toDir.getAbsolutePath().length()).replace(File.separatorChar, '/');
// create synthetic artifact with a synthetic id containing the file name
final Artifact subArtifact = new Artifact(packageArtifact.getId().changeClassifier(subName));
extractContentPackage(desc, contentPath, subArtifact, subName, f.toURI().toURL(), infos);
}
infos.add(desc);
desc.lock();
return desc;
}
} finally {
deleteOnExitRecursive(tempDir);
}
}
private void deleteOnExitRecursive(File file) {
file.deleteOnExit();
if (file.isDirectory()) {
File[] childs = file.listFiles();
if (childs != null) {
for (File child : childs) {
deleteOnExitRecursive(child);
}
}
}
}
final List<Properties> getInitialCandidates(final File bundleFile) throws IOException{
logger.debug("Extracting Bundle {}", bundleFile.getName());
final List<Properties> candidates = new ArrayList<>();
try (final JarFile zipFile = new JarFile(bundleFile)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while ( entries.hasMoreElements() ) {
final ZipEntry entry = entries.nextElement();
final String entryName = entry.getName();
if ( !entryName.endsWith("/") && entryName.startsWith("META-INF/maven/") && entryName.endsWith("/pom.properties")) {
logger.debug("- extracting : {}", entryName);
final Properties props = new Properties();
try (final InputStream zis = zipFile.getInputStream(entry)) {
props.load(zis);
}
candidates.add(props);
}
}
}
return candidates;
}
private String adjustVersion(final String version) {
final String parts[] = version.split("\\.");
if ( parts.length == 4 ) {
final int lastDot = version.lastIndexOf('.');
return version.substring(0, lastDot) + '-' + version.substring(lastDot + 1);
}
return version;
}
private ArtifactId adjustClassifier(ArtifactId id, final String bundleFileName) {
// check for classifier
final int versionStart = bundleFileName.indexOf(id.getVersion());
if ( versionStart != -1 ) {
// capture classifier
final int versionEnd = versionStart + id.getVersion().length();
if ( bundleFileName.length() > versionEnd && bundleFileName.charAt(versionEnd) == '-') {
id = id.changeClassifier(bundleFileName.substring(versionEnd + 1, bundleFileName.lastIndexOf('.')));
}
}
return id;
}
private ArtifactId extractArtifactId(final ArtifactId packageArtifactId, final File bundleFile)
throws IOException {
final List<Properties> candidates = this.getInitialCandidates(bundleFile);
return extractArtifactId(candidates, bundleFile.getName(), packageArtifactId);
}
private List<ArtifactId> getArtifactIds(final List<Properties> candidates) {
final List<ArtifactId> idCandidates = new ArrayList<>();
for(final Properties props : candidates) {
final String version = props.getProperty("version");
final String groupId = props.getProperty("groupId");
final String artifactId = props.getProperty("artifactId");
if ( version != null && groupId != null && artifactId != null ) {
idCandidates.add(new ArtifactId(groupId, artifactId, adjustVersion(version), null, null));
}
}
return idCandidates;
}
private List<ArtifactId> filterCandidatesByVersion(final List<ArtifactId> ids, final String bundleFileName) {
final List<ArtifactId> idCandidates = new ArrayList<>();
for(final ArtifactId id : ids) {
final int versionStart = bundleFileName.indexOf(id.getVersion());
if ( versionStart != -1 ) {
idCandidates.add(id);
}
}
return idCandidates;
}
ArtifactId extractArtifactId(final List<Properties> candidates, final String bundleFileName, final ArtifactId packageArtifactId)
throws IOException {
logger.debug("Properties candidates for {} : {}", bundleFileName, candidates);
final List<ArtifactId> idCandidates = getArtifactIds(candidates);
logger.debug("Artifact candidates for {} : {}", bundleFileName, candidates);
// single candidate? return
if ( idCandidates.size() == 1 ) {
final ArtifactId result = adjustClassifier(idCandidates.get(0), bundleFileName);
logger.debug("Found single candidate : {}", result);
return result;
}
// more than one candidate, find matching version
final List<ArtifactId> versionIds = filterCandidatesByVersion(idCandidates, bundleFileName);
if ( versionIds.size() == 1 ) {
final ArtifactId result = adjustClassifier(versionIds.get(0), bundleFileName);
logger.debug("Found single candidate matching version : {}", result);
return result;
}
// check parent group id
for(final ArtifactId id : versionIds.isEmpty() ? idCandidates : versionIds) {
if ( id.getGroupId().equals(packageArtifactId.getGroupId()) ) {
final ArtifactId result = adjustClassifier(id, bundleFileName);
logger.debug("Found candidate with parent group id {} : {}",packageArtifactId.getGroupId(), result);
return result;
}
}
// randomly pick one
if ( idCandidates.size() > 0 ) {
final ArtifactId result = adjustClassifier(idCandidates.get(0), bundleFileName);
logger.debug("Picking random candidate : {}", result);
return result;
}
throw new IOException(bundleFileName + " has no maven coordinates!");
}
Configuration processConfiguration(final File configFile,
final ArtifactId packageArtifactId,
final String contentPath)
throws IOException {
boolean isConfig = true;
if ( contentPath.endsWith(".xml") ) {
final String contents = Files.readAllLines(configFile.toPath()).toString();
if ( contents.indexOf("jcr:primaryType=\"sling:OsgiConfig\"") == -1 ) {
isConfig = false;
}
}
if ( isConfig ) {
final String id;
if ( contentPath.endsWith("/.content.xml") ) {
final int lastSlash = contentPath.lastIndexOf('/');
final int previousSlash = contentPath.lastIndexOf('/', lastSlash - 1);
id = contentPath.substring(previousSlash + 1, lastSlash);
} else {
String name = contentPath;
final int lastSlash = contentPath.lastIndexOf('/');
for(final String ext : CFG_EXTENSIONS) {
if ( name.endsWith(ext) ) {
name = name.substring(lastSlash + 1, name.length() - ext.length());
}
}
id = name;
}
final String pid;
final int slashPos = id.indexOf('-');
if ( slashPos == -1 ) {
pid = id;
} else {
pid = id.substring(0, slashPos) + '~' + id.substring(slashPos + 1);
}
final Configuration cfg = new Configuration(pid);
cfg.getProperties().put(Configuration.PROP_PREFIX + ContentPackageDescriptorImpl.METADATA_PATH, contentPath);
cfg.getProperties().put(Configuration.PROP_PREFIX + ContentPackageDescriptorImpl.METADATA_PACKAGE,
packageArtifactId.toMvnId());
return cfg;
}
return null;
}
}