blob: d3f99cd221c31ce688eb706fa3fec1141f61bc77 [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.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
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.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 Logger logger = LoggerFactory.getLogger(ContentPackageScanner.class);
private final byte[] buffer = new byte[65536];
private enum FileType {
BUNDLE,
CONFIG,
PACKAGE
}
/**
* 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<ContentPackageDescriptor> scan(final Artifact artifact, final URL url) throws IOException {
final Set<ContentPackageDescriptor> contentPackages = new HashSet<>();
if (url != null) {
final int lastDot = url.getPath().lastIndexOf(".");
final ContentPackageDescriptor cp = new ContentPackageDescriptor(url.getPath().substring(url.getPath().lastIndexOf("/") + 1, lastDot), artifact, url);
extractContentPackage(cp, contentPackages, url);
contentPackages.add(cp);
cp.lock();
}
return contentPackages;
}
private void extractContentPackage(final ContentPackageDescriptor cp,
final Set<ContentPackageDescriptor> infos,
final URL archive)
throws IOException {
logger.debug("Analyzing Content Package {}", archive);
final File tempDir = Files.createTempDirectory(null).toFile();
try {
final File toDir = new File(tempDir, archive.getPath().substring(archive.getPath().lastIndexOf("/") + 1));
toDir.mkdirs();
final List<File> toProcess = new ArrayList<>();
try (final JarFile zipFile = IOUtils.getJarFileFromURL(archive, true, null)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
final String entryName = entry.getName();
logger.debug("Content package entry {}", entryName);
if ( !entryName.endsWith("/") && entryName.startsWith("jcr_root/") ) {
final String contentPath = entryName.substring(8);
cp.paths.add(contentPath);
FileType fileType = null;
if (entryName.endsWith(".zip")) {
// embedded content package
fileType = FileType.PACKAGE;
// check for libs or apps
} else 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(".xml") || contentPath.endsWith(".config") || contentPath.endsWith(".cfg.json")) {
fileType = FileType.CONFIG;
}
}
}
if (fileType != null) {
logger.debug("- extracting : {}", entryName);
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(tempDir, newFile));
bundle.setStartOrder(startLevel);
final BundleDescriptor info = new BundleDescriptorImpl(bundle, newFile.toURI().toURL(),
startLevel);
bundle.getMetadata().put(ContentPackageDescriptor.METADATA_PACKAGE,
cp.getArtifact().getId().toMvnId());
bundle.getMetadata().put(ContentPackageDescriptor.METADATA_PATH, contentPath);
cp.bundles.add(info);
} else if (fileType == FileType.CONFIG) {
final Configuration configEntry = this.process(newFile, cp.getArtifact(), contentPath);
if (configEntry != null) {
cp.configs.add(configEntry);
}
} else if (fileType == FileType.PACKAGE) {
toProcess.add(newFile);
}
}
}
}
for (final File f : toProcess) {
extractContentPackage(cp, infos, f.toURI().toURL());
final int lastDot = f.getName().lastIndexOf(".");
final ContentPackageDescriptor i = new ContentPackageDescriptor(f.getName().substring(0, lastDot), null, f.toURI().toURL());
i.setContentPackageInfo(cp.getArtifact(), f.getName());
infos.add(i);
i.lock();
}
}
} 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);
}
}
}
}
private ArtifactId extractArtifactId(final File tempDir, final File bundleFile)
throws IOException {
logger.debug("Extracting Bundle {}", bundleFile.getName());
final File toDir = new File(tempDir, bundleFile.getName());
toDir.mkdirs();
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 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);
}
}
}
}
}
// check for maven
final File metaInfDir = new File(toDir, "META-INF");
if ( metaInfDir.exists() ) {
final File mavenDir = new File(metaInfDir, "maven");
if ( mavenDir.exists() ) {
File groupDir = null;
for(final File d : mavenDir.listFiles()) {
if ( d.isDirectory() && !d.getName().startsWith(".") ) {
groupDir = d;
break;
}
}
if ( groupDir != null ) {
File artifactDir = null;
for(final File d : groupDir.listFiles()) {
if ( d.isDirectory() && !d.getName().startsWith(".") ) {
artifactDir = d;
break;
}
}
if ( artifactDir != null ) {
final File propsFile = new File(artifactDir, "pom.properties");
if ( propsFile.exists() ) {
final Properties props = new Properties();
try ( final Reader r = new FileReader(propsFile) ) {
props.load(r);
}
String groupId = props.getProperty("groupId");
String artifactId = props.getProperty("artifactId");
String version = props.getProperty("version");
String classifier = null;
// Capture classifier
final int pos = bundleFile.getName().indexOf(version) + version.length();
if ( bundleFile.getName().charAt(pos) == '-') {
classifier = bundleFile.getName().substring(pos + 1, bundleFile.getName().lastIndexOf('.'));
}
final String parts[] = version.split("\\.");
if ( parts.length == 4 ) {
final int lastDot = version.lastIndexOf('.');
version = version.substring(0, lastDot) + '-' + version.substring(lastDot + 1);
}
if ( groupId != null && artifactId != null && version != null ) {
return new ArtifactId(groupId,
artifactId,
version, classifier, null);
}
}
}
}
}
}
throw new IOException(bundleFile.getName() + " has no maven coordinates!");
}
private Configuration process(final File configFile,
final Artifact packageArtifact,
final String contentPath)
throws IOException {
boolean isConfig = true;
if ( configFile.getName().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 ( ".content.xml".equals(configFile.getName()) ) {
final int lastSlash = contentPath.lastIndexOf('/');
final int previousSlash = contentPath.lastIndexOf('/', lastSlash - 1);
id = contentPath.substring(previousSlash + 1, lastSlash);
} else {
final int lastDot = configFile.getName().lastIndexOf('.');
id = configFile.getName().substring(0, lastDot);
}
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 + ContentPackageDescriptor.METADATA_PATH, contentPath);
cfg.getProperties().put(Configuration.PROP_PREFIX + ContentPackageDescriptor.METADATA_PACKAGE,
packageArtifact.getId().toMvnId());
return cfg;
}
return null;
}
}