blob: 39250916cf07e55ae76d4d333dbc171dff6d5e32 [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.jackrabbit.vault.fs.io;
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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.version.Version;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping;
import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
import org.apache.jackrabbit.spi.commons.namespace.SessionNamespaceResolver;
import org.apache.jackrabbit.vault.fs.api.Artifact;
import org.apache.jackrabbit.vault.fs.api.ArtifactType;
import org.apache.jackrabbit.vault.fs.api.ImportInfo;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.api.NodeNameList;
import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
import org.apache.jackrabbit.vault.fs.api.PathMapping;
import org.apache.jackrabbit.vault.fs.api.ProgressTrackerListener;
import org.apache.jackrabbit.vault.fs.api.SerializationType;
import org.apache.jackrabbit.vault.fs.api.VaultInputSource;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.config.ConfigurationException;
import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
import org.apache.jackrabbit.vault.fs.config.MetaInf;
import org.apache.jackrabbit.vault.fs.config.VaultSettings;
import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl;
import org.apache.jackrabbit.vault.fs.impl.DirectoryArtifact;
import org.apache.jackrabbit.vault.fs.impl.HintArtifact;
import org.apache.jackrabbit.vault.fs.impl.io.FileArtifactHandler;
import org.apache.jackrabbit.vault.fs.impl.io.FolderArtifactHandler;
import org.apache.jackrabbit.vault.fs.impl.io.GenericArtifactHandler;
import org.apache.jackrabbit.vault.fs.impl.io.ImportInfoImpl;
import org.apache.jackrabbit.vault.fs.impl.io.InputSourceArtifact;
import org.apache.jackrabbit.vault.fs.spi.ACLManagement;
import org.apache.jackrabbit.vault.fs.spi.CNDReader;
import org.apache.jackrabbit.vault.fs.spi.DefaultNodeTypeSet;
import org.apache.jackrabbit.vault.fs.spi.NodeTypeInstaller;
import org.apache.jackrabbit.vault.fs.spi.NodeTypeSet;
import org.apache.jackrabbit.vault.fs.spi.PrivilegeDefinitions;
import org.apache.jackrabbit.vault.fs.spi.PrivilegeInstaller;
import org.apache.jackrabbit.vault.fs.spi.ProgressTracker;
import org.apache.jackrabbit.vault.fs.spi.ServiceProviderFactory;
import org.apache.jackrabbit.vault.fs.spi.UserManagement;
import org.apache.jackrabbit.vault.packaging.PackageException;
import org.apache.jackrabbit.vault.packaging.impl.ActivityLog;
import org.apache.jackrabbit.vault.packaging.registry.impl.JcrPackageRegistry;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.vault.util.Tree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Imports an {@link Archive} into a repository.
*
* file/directory combinations
* <ol>
* <li>plain file
* <pre>
* + foo
* - test.gif</pre>
* </li>
* <li>plain files + special folder
* <pre>
* + foo
* - .content.xml
* + bar
* - test.gif</pre>
* </li>
* <li>special file
* <pre>
* + foo
* - test.gif
* - test.gif.dir
* - .content.xml</pre>
* </li>
* <li>special file + sub files
* <pre>
* + foo
* - test.gif
* - test.gif.dir
* - .content.xml
* + _jcr_content
* - thumbnail.gif</pre>
* </li>
* <li>special file + sub special files
* <pre>
* + foo
* - test.gif
* - test.gif.dir
* - .content.xml
* + _jcr_content
* - thumbnail.gif
* + thumbnail.gif.dir
* - .content.xml</pre>
* </li>
* <li>file/folder structure
* <pre>
* + foo
* + en
* - .content.xml
* + _cq_content
* - thumbnail.gif
* + company
* - .content.xml</pre>
* </li>
* </ol>
*/
public class Importer {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(Importer.class);
/**
* Activity Logger to track node level updates
*/
private static final Logger activityLog = LoggerFactory.getLogger(ActivityLog.class);
/**
* workspace filter to use during import
*/
private WorkspaceFilter filter;
/**
* tree constructed of the filter roots
*/
private final Tree<PathFilterSet> filterTree = new Tree<PathFilterSet>();
/**
* tracker to use for tracking messages
*/
private ProgressTracker tracker;
/**
* the node types detected during the prepare phase that needed to by registered
*/
private final DefaultNodeTypeSet nodeTypes = new DefaultNodeTypeSet("internal");
/**
* autosave structure that tracks the number of modified nodes
*/
private AutoSave autoSave = new AutoSave();
/**
* set of paths to versionable nodes that need to be checked in after import
*/
private final Set<String> nodesToCheckin = new HashSet<String>();
/**
* map of group memberships that need to be applied after import
*/
private final Map<String, String[]> memberships = new HashMap<String, String[]>();
/**
* general flag that indicates if the import had (recoverable) errors
*/
private boolean hasErrors = false;
/** If {@link #hasErrors} = {@code true} this one contains the first exception during package import */
private Exception firstException = null;
/**
* overall handler for importing folder artifacts
*/
private final FolderArtifactHandler folderHandler = new FolderArtifactHandler();
/**
* overall handler for importing generic artifacts
*/
private final GenericArtifactHandler genericHandler = new GenericArtifactHandler();
/**
* overall handler for importing file artifacts
*/
private final FileArtifactHandler fileHandler = new FileArtifactHandler();
/**
* list of archive entries that are detected as lowlevel patches and need to be copied to the
* filesystem after import.
*/
private final List<Archive.Entry> patches = new LinkedList<Archive.Entry>();
private Map<String, TxInfo> intermediates = new LinkedHashMap<String, TxInfo>();
private Archive archive;
/**
* list of paths to subpackages that were detected during the prepare phase
*/
private final List<String> subPackages = new LinkedList<String>();
/**
* overall acl management behavior
*/
private final ACLManagement aclManagement = ServiceProviderFactory.getProvider().getACLManagement();
/**
* overall user management behavior
*/
private final UserManagement userManagement = ServiceProviderFactory.getProvider().getUserManagement();
/**
* the import options
*/
private final ImportOptions opts;
/**
* the checkpoint state of the autosave. used for recovering from stale item errors during install.
*/
private AutoSave cpAutosave;
/**
* the checkpoint tx info. used for recovering from stale item errors during install.
*/
private TxInfo cpTxInfo;
/**
* the checkpoint import info.
*/
private ImportInfo cpImportInfo;
/**
* retry counter for the batch auto recovery
*/
private int recoveryRetryCounter;
/**
* list of processed tx infos since the last auto save.
*/
private final List<TxInfo> processedInfos = new ArrayList<TxInfo>();
/**
* list of intermediate infos that were removed since the last auto save
*/
private Map<String, TxInfo> removedIntermediates = new LinkedHashMap<String, TxInfo>();
private final boolean isStrict;
private final boolean isStrictByDefault;
private final boolean overwritePrimaryTypesOfFoldersByDefault;
public Importer() {
this(new ImportOptions(), false, true);
}
public Importer(ImportOptions opts) {
this(opts, false);
}
public Importer(ImportOptions opts, boolean isStrictByDefault) {
this(opts, isStrictByDefault, true);
}
public Importer(ImportOptions opts, boolean isStrictByDefault, boolean overwritePrimaryTypesOfFoldersByDefault) {
this.opts = opts;
this.isStrict = opts.isStrict(isStrictByDefault);
this.isStrictByDefault = isStrictByDefault;
this.overwritePrimaryTypesOfFoldersByDefault = overwritePrimaryTypesOfFoldersByDefault;
}
public ImportOptions getOptions() {
return opts;
}
public List<String> getSubPackages() {
return subPackages;
}
public boolean isStrictByDefault() {
return isStrictByDefault;
}
/**
* Debug settings to allows to produce failures after each {@code failAfterEach} save.
* @param failAfterEach cardinal indicating when to fail
*/
public void setDebugFailAfterSave(int failAfterEach) {
autoSave.setDebugFailEach(failAfterEach);
}
protected void track(String action, String path) {
if ("E".equals(action)) {
log.error("{} {}", action, path);
} else {
log.debug("{} {}", action, path);
}
activityLog.debug("{} {}", action, path);
if (tracker != null) {
tracker.track(action, path);
}
}
protected void track(Exception e, String path) {
log.error("E {} ({})", path, e.toString());
if (tracker != null) {
tracker.track(e, path);
}
}
/**
* Runs the importer
*
* @param archive the archive to import
* @param importRoot the root node to import
*
* @throws org.apache.jackrabbit.vault.fs.config.ConfigurationException if the importer is not properly configured
* @throws java.io.IOException if an I/O error occurs
* @throws javax.jcr.RepositoryException if an repository error occurs
*
* @since 2.3.20
*/
public void run(Archive archive, Node importRoot)
throws IOException, RepositoryException, ConfigurationException {
run(archive, importRoot.getSession(), importRoot.getPath());
}
/**
* Runs the importer with the given session.
*
* @param archive the archive to import
* @param session the session importing the archive
* @param parentPath the repository parent path where the archive will be imported
* @throws IOException if an I/O error occurs
* @throws RepositoryException if a repository error occurs
* @throws ConfigurationException if the importer is not properly configured
*
* @since 2.7.0
*/
public void run(Archive archive, Session session, String parentPath)
throws IOException, RepositoryException, ConfigurationException {
this.archive = archive;
// init tracker
if (opts.getListener() == null) {
tracker = null;
} else {
if (tracker == null) {
tracker = new ProgressTracker();
}
tracker.setListener(opts.getListener());
}
// check format version
int version = archive.getMetaInf().getPackageFormatVersion();
if (version > MetaInf.FORMAT_VERSION_2) {
String msg = "Content format version not supported (" + version + " > " + MetaInf.FORMAT_VERSION_2 + ")";
log.warn(msg);
throw new IOException(msg);
}
// init autosave
if (opts.getAutoSaveThreshold() >= 0) {
autoSave.setThreshold(opts.getAutoSaveThreshold());
}
autoSave.setDryRun(opts.isDryRun());
autoSave.setTracker(tracker);
// enable this to test auto-recovery of batch saves
// autoSave.setDebugFailEach(1);
// autoSave.setThreshold(4);
// propagate access control handling
if (opts.getAccessControlHandling() == null) {
opts.setAccessControlHandling(AccessControlHandling.IGNORE);
}
fileHandler.setAcHandling(opts.getAccessControlHandling());
fileHandler.setCugHandling(opts.getCugHandling());
genericHandler.setAcHandling(opts.getAccessControlHandling());
genericHandler.setCugHandling(opts.getCugHandling());
folderHandler.setAcHandling(opts.getAccessControlHandling());
folderHandler.setCugHandling(opts.getCugHandling());
folderHandler.setOverwritePrimaryTypesOfFolders(opts.overwritePrimaryTypesOfFolders(overwritePrimaryTypesOfFoldersByDefault));
filter = opts.getFilter();
if (filter == null) {
filter = archive.getMetaInf().getFilter();
}
if (filter == null) {
filter = new DefaultWorkspaceFilter();
}
// check path remapping
PathMapping pathMapping = opts.getPathMapping();
if (pathMapping != null) {
filter = filter.translate(pathMapping);
this.archive = archive = new MappedArchive(archive, pathMapping);
this.archive.open(true);
}
// set import mode if possible
if (opts.getImportMode() != null) {
if (filter instanceof DefaultWorkspaceFilter) {
((DefaultWorkspaceFilter) filter).setImportMode(opts.getImportMode());
} else {
log.warn("Unable to override import mode, incompatible filter: {}", filter.getClass().getName());
}
}
// build filter tree
for (PathFilterSet set: filter.getFilterSets()) {
filterTree.put(set.getRoot(), set);
}
if ("/".equals(parentPath)) {
parentPath = "";
}
track("Collecting import information...", "");
TxInfo root = prepare(archive.getJcrRoot(), parentPath, new SessionNamespaceResolver(session));
if (filter!=null && filter.getFilterSets() != null && filter.getFilterSets().size() > 0 ) {
root = postFilter(root);
}
log.debug("Access control handling set to {}", opts.getAccessControlHandling());
log.debug("CUG handling set to {}", opts.getCugHandling());
if (opts.isDryRun()) {
track("Dry Run: Skipping node types installation (might lead to errors).", "");
track("Simulating content import...", "");
} else {
track("Installing node types...", "");
installNodeTypes(session);
track("Installing privileges...", "");
registerPrivileges(session);
log.debug("Starting content import. autosave is {}", autoSave);
track("Importing content...", "");
if (tracker != null) {
tracker.setMode(ProgressTrackerListener.Mode.PATHS);
}
}
cpAutosave = autoSave.copy();
LinkedList<TxInfo> skipList = new LinkedList<>();
while (recoveryRetryCounter++ < 10) {
try {
commit(session, root, skipList);
autoSave.save(session, false);
break;
} catch (RepositoryException e) {
if (recoveryRetryCounter == 10) {
log.error("Error while committing changes. Aborting.");
throw e;
} else {
log.warn("Error while committing changes. Retrying import from checkpoint at {}. Retries {}/10",
cpTxInfo == null ? "/" : cpTxInfo.path, recoveryRetryCounter);
autoSave = cpAutosave.copy();
// build skip list
skipList.clear();
TxInfo info = cpTxInfo;
while (info != null && info.parent != null) {
skipList.addFirst(info);
info = info.parent;
}
// reset any intermediate changes in this run
intermediates.putAll(removedIntermediates);
for (TxInfo i: removedIntermediates.values()) {
i.isIntermediate = 1;
}
removedIntermediates.clear();
processedInfos.clear();
session.refresh(false);
}
}
}
if (tracker != null) {
tracker.setMode(ProgressTrackerListener.Mode.TEXT);
}
checkinNodes(session);
applyMemberships(session);
applyPatches();
if (opts.isDryRun()) {
if (hasErrors) {
track("Package import simulation finished. (with errors, check logs!)", "");
log.error("There were errors during package install simulation. Please check the logs for details.");
} else {
track("Package import simulation finished.", "");
}
} else {
if (hasErrors) {
track("Package imported (with errors, check logs!)", "");
if (isStrict) {
throw new RepositoryException("Some errors occurred while installing packages. Please check the logs for details. First exception is logged as cause.", firstException);
}
log.error("There were errors during package install. Please check the logs for details.");
} else {
track("Package imported.", "");
}
}
}
private TxInfo postFilter(TxInfo root) {
TxInfo modifierRoot = root;
if (filter.contains(modifierRoot.path)){
return modifierRoot;
}
if (filter.isAncestor(modifierRoot.path)) {
for (String k : modifierRoot.children().keySet()) {
TxInfo child = modifierRoot.children().get(k);
modifierRoot.children().put(k, postFilter(child));
}
}
else {
modifierRoot.discard();
}
return modifierRoot;
}
public boolean hasErrors() {
return hasErrors;
}
protected VaultSettings getSettings() {
return archive.getMetaInf().getSettings();
}
private void installNodeTypes(Session session)
throws IOException, RepositoryException {
Collection<NodeTypeSet> metaTypes = archive.getMetaInf().getNodeTypes();
if (metaTypes != null) {
for (NodeTypeSet cnd: metaTypes) {
nodeTypes.add(cnd);
}
}
if (!nodeTypes.getNodeTypes().isEmpty()) {
NodeTypeInstaller installer = ServiceProviderFactory.getProvider().getDefaultNodeTypeInstaller(session);
try {
log.debug("Installing node types...");
installer.install(tracker, nodeTypes);
} catch (RepositoryException e) {
if (isStrict) {
throw e;
}
track(e, "Packaged node types");
}
} else {
log.debug("No node types provided.");
}
}
private void registerPrivileges(Session session) throws IOException, RepositoryException {
PrivilegeDefinitions privileges = archive.getMetaInf().getPrivileges();
if (privileges != null && !privileges.getDefinitions().isEmpty()) {
PrivilegeInstaller installer = ServiceProviderFactory.getProvider().getDefaultPrivilegeInstaller(session);
try {
log.debug("Registering privileges...");
installer.install(tracker, privileges);
} catch (RepositoryException e) {
if (isStrict) {
throw e;
}
track(e, "Packaged privileges");
}
} else {
log.debug("No privileges provided.");
}
}
/**
* Checks if the given file name is excluded
* @param fileName the file name
* @return {@code true} if excluded
*/
protected boolean isExcluded(String fileName) {
// hard coded exclusion of .vlt files/directories
return getSettings().getIgnoredNames().contains(fileName)
|| fileName.equals(".vlt")
|| fileName.startsWith(".vlt-");
}
private TxInfo prepare(Archive.Entry jcrRoot, String parentPath, NamespaceResolver resolver)
throws IOException, RepositoryException {
TxInfo root = new TxInfo(null, parentPath);
// special check for .content.xml in root directory
Archive.Entry contentXml = jcrRoot.getChild(Constants.DOT_CONTENT_XML);
if (contentXml != null) {
if (contentXml.isDirectory()) {
throw new IllegalArgumentException(Constants.DOT_CONTENT_XML + " is not a file");
}
// in this case, create a new info
root.artifacts.add(new InputSourceArtifact(
null,
"",
"",
ArtifactType.PRIMARY,
archive.getInputSource(contentXml),
SerializationType.XML_DOCVIEW
));
}
root.artifacts.add(new DirectoryArtifact(Text.getName(parentPath)));
prepare(jcrRoot, root, resolver);
// go over the filter roots and create intermediates for the parents if needed (bug #25370)
for (PathFilterSet sets: filter.getFilterSets()) {
String rootPath = sets.getRoot();
// make filter root relative to import root
if (parentPath.length() > 0 && rootPath.startsWith(parentPath)) {
rootPath = rootPath.substring(parentPath.length());
}
String[] segments = Text.explode(rootPath, '/');
TxInfo current = root;
StringBuilder path = new StringBuilder();
for (final String name : segments) {
path.append('/').append(name);
TxInfo child = current.children().get(name);
if (child == null) {
log.trace("Creating missing intermediate directory artifact for {}", name);
child = current.addChild(new TxInfo(current, path.toString()));
child.isIntermediate = 1;
intermediates.put(path.toString(), child);
}
current = child;
}
}
return root;
}
private void prepare(Archive.Entry directory, TxInfo parentInfo, NamespaceResolver resolver)
throws IOException, RepositoryException {
Collection<? extends Archive.Entry> files = directory.getChildren();
// first process the directories
for (Archive.Entry file: files) {
if (file.isDirectory()) {
String fileName = file.getName();
if (isExcluded(fileName)) {
continue;
}
String repoName = PlatformNameFormat.getRepositoryName(fileName);
String repoPath = parentInfo.path + "/" + repoName;
if (repoName.endsWith(".dir")) {
// fix repo path
repoName = repoName.substring(0, repoName.length() - 4);
repoPath = parentInfo.path + "/" + repoName;
}
TxInfo info = parentInfo.addChild(new TxInfo(parentInfo, repoPath));
log.trace("Creating directory artifact for {}", repoName);
Artifact parent = new DirectoryArtifact(repoName);
info.artifacts.add(parent);
Archive.Entry contentXml = file.getChild(Constants.DOT_CONTENT_XML);
if (contentXml != null) {
if (contentXml.isDirectory()) {
throw new IllegalArgumentException(Constants.DOT_CONTENT_XML + " is not a file");
}
// in this case, create a new info
info.artifacts.add(new InputSourceArtifact(
parent,
Constants.DOT_CONTENT_XML,
"",
ArtifactType.PRIMARY,
archive.getInputSource(contentXml),
SerializationType.XML_DOCVIEW
));
} else {
// this is an empty directory and potential intermediate
info.isIntermediate = 1;
intermediates.put(repoPath, info);
log.trace("Detecting intermediate directory {}", repoName);
}
prepare(file, info, resolver);
}
}
// second the files
for (Archive.Entry file: files) {
if (!file.isDirectory()) {
String fileName = file.getName();
if (isExcluded(fileName)) {
continue;
}
String repoName = PlatformNameFormat.getRepositoryName(fileName);
String repoPath = parentInfo.path + "/" + repoName;
if (file.getName().equals(Constants.DOT_CONTENT_XML)) {
continue;
}
if (opts.getPatchDirectory() != null && repoPath.startsWith(opts.getPatchParentPath())) {
patches.add(file);
if (!opts.isPatchKeepInRepo()) {
continue;
}
}
// todo: find better way to detect sub-packages
if (repoPath.startsWith(JcrPackageRegistry.DEFAULT_PACKAGE_ROOT_PATH_PREFIX) && (repoPath.endsWith(".jar") || repoPath.endsWith(".zip"))) {
subPackages.add(repoPath);
}
String repoBase = repoName;
String ext = "";
int idx = repoName.lastIndexOf('.');
if (idx > 0) {
repoBase = repoName.substring(0, idx);
ext = repoName.substring(idx);
}
SerializationType serType = SerializationType.GENERIC;
ArtifactType type = ArtifactType.PRIMARY;
VaultInputSource is = archive.getInputSource(file);
if (".xml".equals(ext)) {
// this can either be an generic exported docview or a 'user-xml' that is imported as file
// btw: this only works for input sources that can refetch their input stream
if (DocViewParser.isDocView(is)) {
// in this case, the extension was added by the exporter.
repoName = repoBase;
serType = SerializationType.XML_DOCVIEW;
} else {
ext = "";
serType = SerializationType.GENERIC;
type = ArtifactType.FILE;
}
} else if (".cnd".equals(ext)) {
if (opts.getCndPattern().matcher(repoPath).matches()) {
InputStream in = is.getByteStream();
try (Reader r = new InputStreamReader(in, "utf8")) {
CNDReader reader = ServiceProviderFactory.getProvider().getCNDReader();
// provide session namespaces
reader.read(r, is.getSystemId(), new NamespaceMapping(resolver));
nodeTypes.add(reader);
log.debug("Loaded nodetypes from {}.", repoPath);
} catch (IOException e1) {
log.error("Error while reading CND.", e1);
}
}
ext = "";
type = ArtifactType.FILE;
} else if (".binary".equals(ext)) {
serType = SerializationType.GENERIC;
type = ArtifactType.BINARY;
repoName = repoBase;
} else {
ext = "";
type = ArtifactType.FILE;
}
if (type != ArtifactType.PRIMARY) {
// check if info already exists (in case of .dir artifacts)
TxInfo parent = parentInfo.children().get(repoName);
if (parent == null) {
if (type == ArtifactType.BINARY) {
// search next parent for binary artifacts
parent = parentInfo;
while (parent != null && parent.isIntermediate > 0) {
parent = parent.parent;
}
if (parent == null) {
log.warn("No parent info found {}. using direct.");
parent = parentInfo;
}
} else {
// "normal" file
TxInfo tx = new TxInfo(parentInfo, parentInfo.path + "/" + repoName);
log.trace("Creating file artifact for {}", repoName);
tx.artifacts.add(new InputSourceArtifact(null,
repoName, ext, type, is, serType
));
parentInfo.addChild(tx);
}
}
if (parent != null) {
String path = parentInfo.path + "/" + repoName;
String relPath = parent.name + path.substring(parent.path.length());
log.trace("Attaching {} artifact {}", type, path);
parent.artifacts.add(new InputSourceArtifact(null,
relPath, ext, type, is, serType
));
}
}
if (type == ArtifactType.PRIMARY) {
// if primary artifact, add new tx info
TxInfo tx = new TxInfo(parentInfo, parentInfo.path + "/" + repoName);
log.trace("Creating primary artifact for {}", repoName);
tx.artifacts.add(new InputSourceArtifact(null,
repoName, ext, type, is, serType
));
parentInfo.addChild(tx);
}
}
}
// sort the child infos according to the workspace filter rules if possible
Tree.Node<PathFilterSet> filterNode = filterTree.getNode(parentInfo.path);
if (filterNode != null) {
parentInfo.sort(filterNode.getChildren().keySet());
}
}
private void commit(Session session, TxInfo info, LinkedList<TxInfo> skipList) throws RepositoryException, IOException {
try {
ImportInfo imp = null;
if (skipList.isEmpty()) {
if (info == cpTxInfo) {
// don't need to import again, just set import info
log.trace("skipping last checkpoint info {}", info.path);
imp = cpImportInfo;
} else {
imp = commit(session, info);
if (imp != null) {
nodesToCheckin.addAll(imp.getToVersion());
memberships.putAll(imp.getMemberships());
autoSave.modified(imp.numModified());
}
}
} else if (log.isDebugEnabled()) {
StringBuilder skips = new StringBuilder();
for (TxInfo i: skipList) {
skips.append(i.path).append(',');
}
log.trace("skip list: {}", skips);
}
if (autoSave.needsSave()) {
autoSave.save(session, true); // this is only intermediate
// save checkpoint
cpTxInfo = info;
cpAutosave = autoSave.copy();
cpImportInfo = imp;
recoveryRetryCounter = 0;
/*
todo: check retry logic if it's ok to discard all processed infos or if some ancestors should be excluded
// discard processed infos to free some memory
for (TxInfo i: processedInfos) {
i.discard();
}
*/
removedIntermediates.clear();
processedInfos.clear();
}
// copy the children collection since children could be removed during remapping
List<TxInfo> children = new ArrayList<TxInfo>(info.children().values());
// traverse children but skip the ones not in the skip list
TxInfo next = skipList.isEmpty() ? null : skipList.removeFirst();
for (TxInfo child: children) {
if (next == null || next == child) {
commit(session, child, skipList);
// continue normally after lng child was found
next = null;
} else {
log.trace("skipping {}", child.path);
}
}
// see if any child nodes need to be reordered
if (info.nameList != null) {
Node node = info.getNode(session);
if (node == null) {
log.warn("Unable to restore order of {}. Node does not exist.", info.path);
} else if (info.nameList.needsReorder(node)) {
log.trace("Restoring order of {}.", info.path);
info.nameList.restoreOrder(node);
}
}
processedInfos.add(info);
} catch (RepositoryException e) {
log.error("Error while committing {}: {}", info.path, e.toString());
throw e;
}
}
private ImportInfoImpl commit(Session session, TxInfo info) throws RepositoryException, IOException {
log.trace("committing {}", info.path);
ImportInfoImpl imp = null;
if (info.artifacts == null) {
log.debug("S {}", info.path);
} else if (info.artifacts.isEmpty()) {
// intermediate directory, check if node exists and filter
// matches. in this case remove the node (bug #25370)
// but only if intermediate is not processed yet (bug #42562)
if (filter.contains(info.path) && session.nodeExists(info.path) && info.isIntermediate < 2) {
Node node = session.getNode(info.path);
imp = new ImportInfoImpl();
if (aclManagement.isACLNode(node)) {
// Judging from isACLNode behavior, this part only applies
// to "rep:Policy" nodes so no need for special handling of CUG case.
if (opts.getAccessControlHandling() == AccessControlHandling.OVERWRITE
|| opts.getAccessControlHandling() == AccessControlHandling.CLEAR) {
imp.onDeleted(info.path);
aclManagement.clearACL(node.getParent());
}
} else {
if (filter.getImportMode(info.path) == ImportMode.REPLACE) {
imp.onDeleted(info.path);
node.remove();
} else {
imp.onNop(info.path);
}
}
}
} else if (info.artifacts.getPrimaryData() !=null && info.artifacts.size() == 1) {
// simple case, only 1 primary artifact
Node node = info.getParentNode(session);
if (node == null) {
imp = new ImportInfoImpl();
imp.onError(info.path, new IllegalStateException("Parent node not found."));
} else {
imp = genericHandler.accept(opts, isStrictByDefault, filter, node, info.artifacts.getPrimaryData().getRelativePath(), info.artifacts);
if (imp == null) {
throw new IllegalStateException("generic handler did not accept " + info.path);
}
}
} else if (info.artifacts.getDirectory() != null) {
String prefix = info.parent == null ? info.name : info.name + "/";
for (TxInfo child: info.children().values()) {
// add the directory artifacts as hint to this one.
if (child.artifacts == null) {
// in this case it's some deleted intermediate directory???
String path = prefix + child.name;
info.artifacts.add(new HintArtifact(path));
} else {
for (Artifact a: child.artifacts.values()) {
String path = prefix + a.getRelativePath();
info.artifacts.add(new HintArtifact(path));
}
}
}
Node node = info.getParentNode(session);
if (node == null) {
imp = new ImportInfoImpl();
imp.onError(info.path, new IllegalStateException("Parent node not found."));
} else {
if (info.isIntermediate == 2) {
// skip existing intermediate
log.trace("skipping intermediate node at {}", info.path);
} else if (info.artifacts.getPrimaryData() == null) {
// create nt:folder node if not exists
imp = folderHandler.accept(opts, isStrictByDefault, filter, node, info.name, info.artifacts);
if (imp == null) {
throw new IllegalStateException("folder handler did not accept " + info.path);
}
} else {
imp = genericHandler.accept(opts, isStrictByDefault, filter, node, info.artifacts.getDirectory().getRelativePath(), info.artifacts);
if (imp == null) {
throw new IllegalStateException("generic handler did not accept " + info.path);
}
}
}
} else if (info.artifacts.size(ArtifactType.FILE) > 0) {
Node node = info.getParentNode(session);
if (node == null) {
imp = new ImportInfoImpl();
imp.onError(info.path, new IllegalStateException("Parent node not found."));
} else {
imp = fileHandler.accept(opts, isStrictByDefault, filter, node, info.name, info.artifacts);
if (imp == null) {
throw new IllegalStateException("file handler did not accept " + info.path);
}
}
} else {
throw new UnsupportedOperationException("ArtifactSet not supported: " + info.artifacts);
}
if (imp != null) {
for (Map.Entry<String, ImportInfo.Info> entry: imp.getInfos().entrySet()) {
String path = entry.getKey();
ImportInfo.Type type = entry.getValue().getType();
if (type != ImportInfoImpl.Type.DEL) {
// mark intermediates as processed
TxInfo im = intermediates.remove(path);
if (im != null) {
log.debug("P {}", path);
removedIntermediates.put(path, im);
im.isIntermediate = 2;
}
}
switch (type) {
case CRE:
track("A", path);
break;
case DEL:
track("D", path);
break;
case MOD:
track("U", path);
break;
case NOP:
track("-", path);
break;
case REP:
track("R", path);
break;
case MIS:
track("!", path);
break;
case ERR:
Exception error = entry.getValue().getError();
if (error == null) {
track("E", path);
} else {
track(error, path);
}
hasErrors = true;
if (firstException == null) {
firstException = new PackageException("Error creating/updating node " + path, error);
}
break;
}
// see if any child nodes need to be reordered and remember namelist.
// only restore order if in filter scope if freshly created
NodeNameList nameList = entry.getValue().getNameList();
if (nameList != null && (filter.contains(path) || type == ImportInfo.Type.CRE)) {
TxInfo subInfo = info.findChild(path);
if (subInfo != null) {
subInfo.nameList = nameList;
}
}
}
// remap the child tree in case some of the nodes where moved during import (e.g. authorizable)
// todo: this could be a problem during error recovery
info = info.remap(imp.getRemapped());
}
log.trace("committed {}", info.path);
return imp;
}
public void checkinNodes(Session session) {
if (nodesToCheckin.isEmpty()) {
return;
}
if (opts.isDryRun()) {
track("Dry run: Would commit versions...", "");
} else {
track("Committing versions...", "");
}
for (String path: nodesToCheckin) {
try {
Node node = session.getNode(path);
try {
if (opts.isDryRun()) {
track("V", String.format("%s (---)", path));
} else {
Version v = node.checkin();
track("V", String.format("%s (%s)", path, v.getName()));
}
} catch (RepositoryException e) {
log.error("Error while checkin node {}: {}",path, e.toString());
}
} catch (RepositoryException e) {
log.error("Error while retrieving node to be versioned at {}.", path, e);
}
}
nodesToCheckin.clear();
}
public void applyMemberships(Session session) {
if (memberships.isEmpty()) {
return;
}
if (opts.isDryRun()) {
track("Dry run: Would apply merged group memberships...", "");
} else {
track("Applying merged group memberships...", "");
}
for (String id: memberships.keySet()) {
String[] members = memberships.get(id);
String authPath = userManagement.getAuthorizablePath(session, id);
if (authPath != null) {
if (!opts.isDryRun()) {
userManagement.addMembers(session, id, members);
}
track("U", String.format("%s", authPath));
}
}
try {
session.save();
} catch (RepositoryException e) {
log.error("Error while updating memberships.", e);
try {
session.refresh(false);
} catch (RepositoryException e1) {
// ignore
}
}
memberships.clear();
}
private void applyPatches() {
for (Archive.Entry e: patches) {
String name = e.getName();
File target = new File(opts.getPatchDirectory(), name);
if (opts.isDryRun()) {
log.debug("Dry run: Would copy patch {} to {}", name, target.getPath());
} else {
log.debug("Copying patch {} to {}", name, target.getPath());
try (InputStream in = archive.getInputSource(e).getByteStream();
OutputStream out = FileUtils.openOutputStream(target)) {
IOUtils.copy(in, out);
} catch (IOException e1) {
log.error("Error while copying patch.", e);
}
}
track("P", name);
}
}
private static class TxInfo {
private TxInfo parent;
private final String path;
private final String name;
private ArtifactSetImpl artifacts = new ArtifactSetImpl();
private Map<String, TxInfo> children;
private byte isIntermediate = 0;
private NodeNameList nameList;
public TxInfo(TxInfo parent, String path) {
log.trace("New TxInfo {}" , path);
this.parent = parent;
this.path = path;
this.name = Text.getName(path);
}
public TxInfo addChild(TxInfo child) {
if (children == null) {
children = new LinkedHashMap<String, TxInfo>();
}
children.put(child.name, child);
return child;
}
public Map<String, TxInfo> children() {
if (children == null) {
return Collections.emptyMap();
} else {
return children;
}
}
public void sort(Collection<String> names) {
if (children == null || children.size() <=1 || names == null || names.isEmpty()) {
return;
}
Map<String, TxInfo> ret = new LinkedHashMap<String, TxInfo>();
Iterator<String> iter = names.iterator();
while (iter.hasNext() && children.size() > 1) {
String name = iter.next();
TxInfo info = children.remove(name);
if (info != null) {
ret.put(name, info);
}
}
ret.putAll(children);
children = ret;
}
public Node getParentNode(Session s) throws RepositoryException {
String parentPath = emptyPathToRoot(Text.getRelativeParent(path, 1));
return s.nodeExists(parentPath)
? s.getNode(parentPath)
: null;
}
public Node getNode(Session s) throws RepositoryException {
String p = emptyPathToRoot(path);
return s.nodeExists(p)
? s.getNode(p)
: null;
}
public void discard() {
log.trace("discarding {}", path);
artifacts = null;
children = null;
}
public TxInfo findChild(String absPath) {
if (path.equals(absPath)) {
return this;
}
if (!absPath.startsWith(path + "/")) {
return null;
}
absPath = absPath.substring(path.length());
TxInfo root = this;
for (String name: Text.explode(absPath, '/')) {
root = root.children().get(name);
if (root == null) {
break;
}
}
return root;
}
public TxInfo remap(PathMapping mapping) {
String mappedPath = mapping.map(path, true);
if (mappedPath.equals(path)) {
return this;
}
TxInfo ret = new TxInfo(parent, mappedPath);
// todo: what should we do with the artifacts ?
ret.artifacts.addAll(artifacts);
// todo: do we need to remap the namelist, too?
ret.nameList = nameList;
ret.isIntermediate = isIntermediate;
if (children != null) {
for (TxInfo child: children.values()) {
child = child.remap(mapping);
child.parent = this;
ret.addChild(child);
}
}
// ensure that our parent links the new info
if (parent.children != null) {
parent.children.put(ret.name, ret);
}
return ret;
}
private static String emptyPathToRoot(String path) {
return path == null || path.length() == 0 ? "/" : path;
}
}
}