/*
 * 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.core;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;

import org.apache.commons.io.FileUtils;
import org.apache.jackrabbit.core.config.RepositoryConfig;
import org.apache.jackrabbit.core.lock.LockManagerImpl;
import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.persistence.PersistenceCopier;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QNodeTypeDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Tool for backing up or migrating the entire contents (workspaces,
 * version histories, namespaces, node types, etc.) of a repository to
 * a new repository. The target repository (if it exists) is overwritten.
 * <p>
 * No cluster journal records are written in the target repository. If the
 * target repository is clustered, it should be the only node in the cluster.
 * <p>
 * The target repository needs to be fully reindexed after the copy operation.
 * The static copy() methods will remove the target search index folders from
 * their default locations to trigger automatic reindexing when the repository
 * is next started.
 *
 * @since Apache Jackrabbit 1.6
 */
public class RepositoryCopier {

    /**
     * Logger instance
     */
    private static final Logger logger =
        LoggerFactory.getLogger(RepositoryCopier.class);

    /**
     * Source repository context.
     */
    private final RepositoryContext source;

    /**
     * Target repository context.
     */
    private final RepositoryContext target;

    /**
     * Copies the contents of the repository in the given source directory
     * to a repository in the given target directory.
     *
     * @param source source repository directory
     * @param target target repository directory
     * @throws RepositoryException if the copy operation fails
     * @throws IOException if the target repository can not be initialized
     */
    public static void copy(File source, File target)
            throws RepositoryException, IOException {
        copy(RepositoryConfig.create(source), RepositoryConfig.install(target));
    }

    /**
     * Copies the contents of the repository with the given configuration
     * to a repository in the given target directory.
     *
     * @param source source repository configuration
     * @param target target repository directory
     * @throws RepositoryException if the copy operation fails
     * @throws IOException if the target repository can not be initialized
     */
    public static void copy(RepositoryConfig source, File target)
            throws RepositoryException, IOException {
        copy(source, RepositoryConfig.install(target));
    }

    /**
     * Copies the contents of the source repository with the given
     * configuration to a target repository with the given configuration.
     *
     * @param source source repository configuration
     * @param target target repository directory
     * @throws RepositoryException if the copy operation fails
     */
    public static void copy(RepositoryConfig source, RepositoryConfig target)
            throws RepositoryException {
        RepositoryImpl repository = RepositoryImpl.create(source);
        try {
            copy(repository, target);
        } finally {
            repository.shutdown();
        }
    }

    /**
     * Copies the contents of the given source repository to a repository in
     * the given target directory.
     * <p>
     * The source repository <strong>must not be modified</strong> while
     * the copy operation is running to avoid an inconsistent copy.
     *
     * @param source source repository directory
     * @param target target repository directory
     * @throws RepositoryException if the copy operation fails
     * @throws IOException if the target repository can not be initialized
     */
    public static void copy(RepositoryImpl source, File target)
            throws RepositoryException, IOException {
        copy(source, RepositoryConfig.install(target));
    }

    /**
     * Copies the contents of the given source repository to a target
     * repository with the given configuration.
     * <p>
     * The source repository <strong>must not be modified</strong> while
     * the copy operation is running to avoid an inconsistent copy.
     *
     * @param source source repository directory
     * @param target target repository directory
     * @throws RepositoryException if the copy operation fails
     * @throws IOException if the target repository can not be initialized
     */
    public static void copy(RepositoryImpl source, RepositoryConfig target)
            throws RepositoryException {
        RepositoryImpl repository = RepositoryImpl.create(target);
        try {
            new RepositoryCopier(source, repository).copy();
        } finally {
            repository.shutdown();
        }

        // Remove index directories to force re-indexing on next startup
        // TODO: There should be a cleaner way to do this
        File targetDir = new File(target.getHomeDir());
        File repoDir = new File(targetDir, "repository");
        FileUtils.deleteQuietly(new File(repoDir, "index"));
        File[] workspaces = new File(targetDir, "workspaces").listFiles();
        if (workspaces != null) {
            for (File workspace : workspaces) {
                FileUtils.deleteQuietly(new File(workspace, "index"));
            }
        }
    }

    /**
     * Creates a tool for copying the full contents of the source repository
     * to the given target repository. Any existing content in the target
     * repository will be overwritten.
     *
     * @param source source repository
     * @param target target repository
     */
    public RepositoryCopier(RepositoryImpl source, RepositoryImpl target) {
        // TODO: It would be better if we were given the RepositoryContext
        // instances directly. Perhaps we should use something like
        // RepositoryImpl.getRepositoryCopier(RepositoryImpl target)
        // instead of this public constructor to achieve that.
        this.source = source.getRepositoryContext();
        this.target = target.getRepositoryContext();
    }

    /**
     * Copies the full content from the source to the target repository.
     * <p>
     * The source repository <strong>must not be modified</strong> while
     * the copy operation is running to avoid an inconsistent copy.
     * <p>
     * This method leaves the search indexes of the target repository in
     * an 
     * Note that both the source and the target repository must be closed
     * during the copy operation as this method requires exclusive access
     * to the repositories.
     *
     * @throws RepositoryException if the copy operation fails
     */
    public void copy() throws RepositoryException {
        logger.info(
                "Copying repository content from {} to {}",
                source.getRepository().repConfig.getHomeDir(),
                target.getRepository().repConfig.getHomeDir());
        try {
            copyNamespaces();
            copyNodeTypes();
            copyVersionStore();
            copyWorkspaces();
        } catch (Exception e) {
            throw new RepositoryException("Failed to copy content", e);
        }
    }

    private void copyNamespaces() throws RepositoryException {
        NamespaceRegistry sourceRegistry = source.getNamespaceRegistry();
        NamespaceRegistry targetRegistry = target.getNamespaceRegistry();

        logger.info("Copying registered namespaces");
        Collection<String> existing = Arrays.asList(targetRegistry.getURIs());
        for (String uri : sourceRegistry.getURIs()) {
            if (!existing.contains(uri)) {
                // TODO: what if the prefix is already taken?
                targetRegistry.registerNamespace(
                        sourceRegistry.getPrefix(uri), uri);
            }
        }
    }

    private void copyNodeTypes() throws RepositoryException {
        NodeTypeRegistry sourceRegistry = source.getNodeTypeRegistry();
        NodeTypeRegistry targetRegistry = target.getNodeTypeRegistry();

        logger.info("Copying registered node types");
        Collection<Name> existing =
            Arrays.asList(targetRegistry.getRegisteredNodeTypes());
        Collection<QNodeTypeDefinition> register = new ArrayList<QNodeTypeDefinition>();
        for (Name name : sourceRegistry.getRegisteredNodeTypes()) {
            // TODO: what about modified node types?
            if (!existing.contains(name)) {
                register.add(sourceRegistry.getNodeTypeDef(name));
            }
        }
        try {
            targetRegistry.registerNodeTypes(register);
        } catch (InvalidNodeTypeDefException e) {
            throw new RepositoryException("Unable to copy node types", e);
        }
    }

    private void copyVersionStore() throws RepositoryException {
        logger.info("Copying version histories");
        PersistenceCopier copier = new PersistenceCopier(
                source.getInternalVersionManager().getPersistenceManager(),
                target.getInternalVersionManager().getPersistenceManager(),
                target.getDataStore());
        copier.copy(RepositoryImpl.VERSION_STORAGE_NODE_ID);
        copier.copy(RepositoryImpl.ACTIVITIES_NODE_ID);
    }

    private void copyWorkspaces() throws RepositoryException {
        Collection<String> existing =
            Arrays.asList(target.getRepository().getWorkspaceNames());
        for (String name : source.getRepository().getWorkspaceNames()) {
            logger.info("Copying workspace {}" , name);

            if (!existing.contains(name)) {
                target.getRepository().createWorkspace(name);
            }

            // Copy all the workspace content
            PersistenceCopier copier = new PersistenceCopier(
                    source.getRepository().getWorkspaceInfo(name).getPersistenceManager(),
                    target.getRepository().getWorkspaceInfo(name).getPersistenceManager(),
                    target.getDataStore());
            copier.excludeNode(RepositoryImpl.SYSTEM_ROOT_NODE_ID);
            copier.copy(RepositoryImpl.ROOT_NODE_ID);

            // Copy all the active open-scoped locks
            LockManagerImpl sourceLockManager =
                source.getRepository().getLockManager(name);
            LockManagerImpl targetLockManager =
                target.getRepository().getLockManager(name);
            targetLockManager.copyOpenScopedLocksFrom(sourceLockManager);
        }
    }

}
