| /* |
| * 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<TxInfo>(); |
| 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; |
| } |
| } |
| |
| } |