blob: 71c7613760acdf61636723d07e9a927fa4fdb720 [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.vltpkg;
import static org.apache.jackrabbit.vault.util.Constants.CONFIG_XML;
import static org.apache.jackrabbit.vault.util.Constants.FILTER_XML;
import static org.apache.jackrabbit.vault.util.Constants.META_DIR;
import static org.apache.jackrabbit.vault.util.Constants.PACKAGE_DEFINITION_XML;
import static org.apache.jackrabbit.vault.util.Constants.PROPERTIES_XML;
import static org.apache.jackrabbit.vault.util.Constants.ROOT_DIR;
import static org.apache.jackrabbit.vault.util.Constants.SETTINGS_XML;
import static org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.PACKAGE_CLASSIFIER;
import static org.apache.sling.feature.cpconverter.vltpkg.VaultPackageUtils.getDependencies;
import static org.apache.sling.feature.cpconverter.vltpkg.VaultPackageUtils.setDependencies;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
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.Dependency;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.apache.jackrabbit.vault.packaging.VaultPackage;
import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
import org.apache.sling.feature.cpconverter.handlers.EntryHandler;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;
import org.codehaus.plexus.archiver.zip.ZipArchiver;
public class VaultPackageAssembler implements EntryHandler, FileFilter {
private static final String NAME_PATH = "path";
private static final String JCR_ROOT_DIR = "jcr_root";
private static final String[] INCLUDE_RESOURCES = { PACKAGE_DEFINITION_XML, CONFIG_XML, SETTINGS_XML };
private static final File TMP_DIR = new File(System.getProperty("java.io.tmpdir"), "syntethic-content-packages");
private static final Pattern OSGI_BUNDLE_PATTERN = Pattern.compile("(jcr_root)?/apps/[^/]+/install(\\.([^/]+))?/.+\\.jar");
public static VaultPackageAssembler create(VaultPackage vaultPackage) {
return create(vaultPackage, vaultPackage.getMetaInf().getFilter());
}
public static File createSynthetic(VaultPackage vaultPackage) throws Exception {
DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter();
PathFilterSet filterSet = new PathFilterSet();
SyntheticPathFilter pathFilter = new SyntheticPathFilter();
filterSet.addExclude(pathFilter);
filterSet.setImportMode(ImportMode.MERGE);
filter.add(filterSet);
return create(vaultPackage, filter).createPackage();
}
private static VaultPackageAssembler create(VaultPackage vaultPackage, WorkspaceFilter filter) {
PackageId packageId = vaultPackage.getId();
String fileName = packageId.toString().replaceAll("/", "-").replaceAll(":", "-") + "-" + vaultPackage.getFile().getName();
File storingDirectory = new File(TMP_DIR, fileName + "-deflated");
if(storingDirectory.exists()) {
try {
FileUtils.deleteDirectory(storingDirectory);
} catch(IOException e) {
throw new FolderDeletionException("Unable to delete existing deflated folder: '" + storingDirectory + "'", e);
}
}
// avoid any possible Stream is not a content package. Missing 'jcr_root' error
File jcrRootDirectory = new File(storingDirectory, ROOT_DIR);
jcrRootDirectory.mkdirs();
PackageProperties packageProperties = vaultPackage.getProperties();
Properties properties = new Properties();
properties.setProperty(PackageProperties.NAME_VERSION,
packageProperties.getProperty(PackageProperties.NAME_VERSION)
+ '-'
+ PACKAGE_CLASSIFIER);
for (String key : new String[] {
PackageProperties.NAME_GROUP,
PackageProperties.NAME_NAME,
PackageProperties.NAME_CREATED_BY,
PackageProperties.NAME_CREATED,
PackageProperties.NAME_REQUIRES_ROOT,
PackageProperties.NAME_PACKAGE_TYPE,
PackageProperties.NAME_AC_HANDLING,
NAME_PATH
}) {
String value = packageProperties.getProperty(key);
if (value != null && !value.isEmpty()) {
properties.setProperty(key, value);
}
}
Set<Dependency> dependencies = getDependencies(vaultPackage);
VaultPackageAssembler assembler = new VaultPackageAssembler(storingDirectory, properties, dependencies);
assembler.mergeFilters(filter);
return assembler;
}
private final DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter();
private final Set<Dependency> dependencies;
private final File storingDirectory;
private final Properties properties;
@Override
public boolean matches(String path) {
return true;
}
@Override
public void handle(String path, Archive archive, Entry entry, ContentPackage2FeatureModelConverter converter)
throws Exception {
addEntry(path, archive, entry);
}
/**
* This class can not be instantiated from outside
*/
private VaultPackageAssembler(File storingDirectory, Properties properties, Set<Dependency> dependencies) {
this.storingDirectory = storingDirectory;
this.properties = properties;
this.dependencies = dependencies;
}
public void mergeFilters(WorkspaceFilter filter) {
for (PathFilterSet pathFilterSet : filter.getFilterSets()) {
if (!OSGI_BUNDLE_PATTERN.matcher(pathFilterSet.getRoot()).matches()) {
this.filter.add(pathFilterSet);
}
}
}
public void addEntry(String path, Archive archive, Entry entry) throws IOException {
try (InputStream input = archive.openInputStream(entry)) {
addEntry(path, input);
}
}
public void addEntry(String path, File file) throws IOException {
try (InputStream input = new FileInputStream(file)) {
addEntry(path, input);
}
}
public void addEntry(String path, InputStream input) throws IOException {
try (OutputStream output = createEntry(path)) {
IOUtils.copy(input, output);
}
}
public OutputStream createEntry(String path) throws IOException {
File target = new File(storingDirectory, path);
target.getParentFile().mkdirs();
return new FileOutputStream(target);
}
public File getEntry(String path) {
if (!path.startsWith(ROOT_DIR)) {
path = ROOT_DIR + path;
}
return new File(storingDirectory, path);
}
public void updateDependencies(Map<PackageId, Set<Dependency>> mutableContentsIds) {
for (Dependency dependency : dependencies) {
for (java.util.Map.Entry<PackageId, Set<Dependency>> mutableContentId : mutableContentsIds.entrySet()) {
if (dependency.matches(mutableContentId.getKey())) {
dependencies.remove(dependency);
dependencies.addAll(mutableContentId.getValue());
}
}
}
}
public File createPackage() throws IOException {
return createPackage(TMP_DIR);
}
public File createPackage(File outputDirectory) throws IOException {
// generate the Vault properties XML file
File metaDir = new File(storingDirectory, META_DIR);
if (!metaDir.exists()) {
metaDir.mkdirs();
}
setDependencies(dependencies, properties);
File xmlProperties = new File(metaDir, PROPERTIES_XML);
try (FileOutputStream fos = new FileOutputStream(xmlProperties)) {
properties.storeToXML(fos, null);
}
// generate the Vault filter XML file
computeFilters(outputDirectory);
File xmlFilter = new File(metaDir, FILTER_XML);
try (InputStream input = filter.getSource();
FileOutputStream output = new FileOutputStream(xmlFilter)) {
IOUtils.copy(input, output);
}
// copy the required resources
for (String resource : INCLUDE_RESOURCES) {
try (InputStream input = getClass().getResourceAsStream(resource)) {
addEntry(ROOT_DIR + '/' + resource, input);
}
}
// create the target archiver
Archiver archiver = new ZipArchiver();
archiver.setIncludeEmptyDirs(true);
String destFileName = storingDirectory.getName().substring(0, storingDirectory.getName().lastIndexOf('-'));
File destFile = new File(TMP_DIR, destFileName);
archiver.setDestFile(destFile);
archiver.addFileSet(new DefaultFileSet(storingDirectory));
archiver.createArchive();
return destFile;
}
private void computeFilters(File outputDirectory) {
File jcrRootDir = new File(outputDirectory, JCR_ROOT_DIR);
if (jcrRootDir.exists() && jcrRootDir.isDirectory()) {
for (File child : jcrRootDir.listFiles(this)) {
File lowestCommonAncestor = lowestCommonAncestor(new TreeNode(child)).val;
if (lowestCommonAncestor != null) {
String root = outputDirectory.toURI().relativize(lowestCommonAncestor.toURI()).getPath();
filter.add(new PathFilterSet(root));
}
}
}
}
@Override
public boolean accept(File pathname) {
return pathname.isDirectory();
}
private TreeNode lowestCommonAncestor(TreeNode root) {
int currMaxDepth = 0;//curr tree's deepest leaf depth
int countMaxDepth = 0;//num of deepest leaves
TreeNode node = null;
for (File child : root.val.listFiles(this)) {
TreeNode temp = lowestCommonAncestor(new TreeNode(child));
if (temp == null) {
continue;
} else if (temp.maxDepth > currMaxDepth) {//if deeper leaf found,update everything to that deeper leaf
currMaxDepth = temp.maxDepth;
node = temp;//update the maxDepth leaf/LCA
countMaxDepth = 1;//reset count of maxDepth leaves
} else if (temp.maxDepth == currMaxDepth) {
countMaxDepth++;//more deepest leaves of curr (sub)tree found
}
}
if (countMaxDepth > 1) {
//if there're several leaves at the deepest level of curr tree,curr root is the LCA of them
//OR if there're several LCA of several deepest leaves in curr tree,curr root is also the LCA of them
root.maxDepth = node.maxDepth + 1;//update root's maxDepth and return it
return root;
} else if (countMaxDepth == 1) {
//if there's only 1 deepest leaf or only 1 LCA of curr tree,return that leaf/LCA
node.maxDepth++;//update node's maxDepth and return it
return node;
} else if (countMaxDepth == 0) {
//if curr root's children have no children(all leaves,so all return null to temp),set root's maxDepth to 2,return
root.maxDepth = 2;//update node's maxDepth to 2 cuz its children are leaves
return root;
}
return null;
}
private static final class TreeNode {
File val;
int maxDepth;//this means the maxDepth of curr treenode-rooted (sub)tree
TreeNode(File x) {
val = x;
maxDepth = 0;
}
}
public static class FolderDeletionException extends RuntimeException {
public FolderDeletionException(String message) {
super(message);
}
public FolderDeletionException(String message, Throwable cause) {
super(message, cause);
}
}
}