/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.wc.admin;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLock;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.fs.FSFS;
import org.tmatesoft.svn.core.internal.io.fs.FSHotCopier;
import org.tmatesoft.svn.core.internal.io.fs.FSPacker;
import org.tmatesoft.svn.core.internal.io.fs.FSRecoverer;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryUtil;
import org.tmatesoft.svn.core.internal.io.fs.FSRevisionRoot;
import org.tmatesoft.svn.core.internal.io.fs.FSRoot;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNUUIDGenerator;
import org.tmatesoft.svn.core.internal.wc.DefaultDumpFilterHandler;
import org.tmatesoft.svn.core.internal.wc.DefaultLoadHandler;
import org.tmatesoft.svn.core.internal.wc.ISVNLoadHandler;
import org.tmatesoft.svn.core.internal.wc.SVNAdminDeltifier;
import org.tmatesoft.svn.core.internal.wc.SVNAdminHelper;
import org.tmatesoft.svn.core.internal.wc.SVNCancellableEditor;
import org.tmatesoft.svn.core.internal.wc.SVNDumpEditor;
import org.tmatesoft.svn.core.internal.wc.SVNDumpStreamParser;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNPropertiesManager;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.ISVNLockHandler;
import org.tmatesoft.svn.core.io.SVNCapability;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.replicator.SVNRepositoryReplicator;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNRepositoryPool;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnOperationFactory;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * The <b>SVNAdminClient</b> class provides methods that brings repository-side functionality
 * and repository synchronizing features.
 *
 * <p>
 * Repository administrative methods are analogues of the corresponding commands of the native
 * Subversion 'svnadmin' utility, while repository synchronizing methods are the ones for the
 * 'svnsync' utility.
 *
 * <p>
 * Here's a list of the <b>SVNAdminClient</b>'s methods
 * matched against corresponing commands of the Subversion svnsync and svnadmin command-line utilities:
 *
 * <table cellpadding="3" cellspacing="1" border="0" width="40%" bgcolor="#999933">
 * <tr bgcolor="#ADB8D9" align="left">
 * <td><b>SVNKit</b></td>
 * <td><b>Subversion</b></td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doInitialize()</td><td>'svnsync initialize'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doSynchronize()</td><td>'svnsync synchronize'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doInfo()</td><td>'svnsync info'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doCopyRevisionProperties()</td><td>'svnsync copy-revprops'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doDump()</td><td>'svnadmin dump'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doListTransactions()</td><td>'svnadmin lstxns'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doLoad()</td><td>'svnadmin load'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doRemoveTransactions()</td><td>'svnadmin rmtxns'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doVerify()</td><td>'svnadmin verify'</td>
 * </tr>
 * <tr bgcolor="#EAEAEA" align="left">
 * <td>doPack()</td><td>'svnadmin pack'</td>
 * </tr>
 * </table>
 *
 * @version 1.3
 * @author  TMate Software Ltd.
 * @since   1.2
 */
public class SVNAdminClient extends SVNAdminBasicClient {

    private ISVNLogEntryHandler mySyncHandler;
    private DefaultDumpFilterHandler myDumpFilterHandler;
    private ISVNAdminEventHandler myEventHandler;
    private FSHotCopier myHotCopier;
    private SVNDumpStreamParser myDumpStreamParser;
    private SVNDumpEditor myDumpEditor;

    private static final int LOCK_RETRY_COUNT = 10;

    /**
     * Creates a new admin client.
     *
     * @param authManager   an auth manager
     * @param options       an options driver
     */
    public SVNAdminClient(ISVNAuthenticationManager authManager, ISVNOptions options) {
        super(authManager, options);
    }

    /**
     * Creates a new admin client.
     *
     * @param repositoryPool a repository pool
     * @param options        an options driver
     */
    public SVNAdminClient(ISVNRepositoryPool repositoryPool, ISVNOptions options) {
        super(repositoryPool, options);
    }

    public SVNAdminClient(SvnOperationFactory of) {
        super(of);
    }

    /**
     * Sets a replication handler that will receive a log entry object
     * per each replayed revision.
     *
     * <p>
     * Log entries dispatched to the handler may not contain changed paths and
     * committed log message until this features are implemented in future releases.
     *
     * @param handler a replay handler
     */
    public void setReplayHandler(ISVNLogEntryHandler handler) {
        mySyncHandler = handler;
    }

    /**
     * Sets an event handler for this object.
     * {@link ISVNAdminEventHandler} should be provided to <b>SVNAdminClent</b>
     * via this method also. But it does not mean that you can have two handler set, only
     * one handler can be used at a time.
     *
     * @param handler an event handler
     */
    public void setEventHandler(ISVNEventHandler handler) {
        super.setEventHandler(handler);
        if (handler instanceof ISVNAdminEventHandler) {
            myEventHandler = (ISVNAdminEventHandler) handler;
        }
    }

    /**
     * Creates an FSFS-type repository.
     *
     * This implementation uses {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory#createLocalRepository(File, String, boolean, boolean)}}.
     * <p>
     * If <code>uuid</code> is <span class="javakeyword">null</span> a new uuid will be generated, otherwise
     * the specified will be used.
     *
     * <p>
     * If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>, an empty
     * pre-revprop-change hook will be placed into the repository /hooks subdir. This enables changes to
     * revision properties of the newly created repository.
     *
     * <p>
     * If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
     * exists, deletes that path and creates a repository in its place.
     *
     * @param  path                        a repository root dir path
     * @param  uuid                        a repository uuid
     * @param  enableRevisionProperties    enables/disables changes to revision properties
     * @param  force                       forces operation to run
     * @return                             a local URL (file:///) of a newly created repository
     * @throws SVNException
     * @see                                #doCreateRepository(File, String, boolean, boolean, boolean)
     * @since                              1.1.0
     */
    public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force) throws SVNException {
        return doCreateRepository(path, uuid, enableRevisionProperties, force, false);
    }

    /**
     * Creates an FSFS-type repository.
     *
     * This implementation uses {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory#createLocalRepository(File, String, boolean, boolean)}}.
     * <p>
     * If <code>uuid</code> is <span class="javakeyword">null</span> a new uuid will be generated, otherwise
     * the specified will be used.
     *
     * <p>
     * If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>, an empty
     * pre-revprop-change hook will be placed into the repository /hooks subdir. This enables changes to
     * revision properties of the newly created repository.
     *
     * <p>
     * If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
     * exists, deletes that path and creates a repository in its place.
     *
     * <p>
     * Set <code>pre14Compatible</code> to <span class="javakeyword">true</span> if you want a new repository
     * to be compatible with pre-1.4 servers.
     *
     * @param  path                        a repository root dir path
     * @param  uuid                        a repository uuid
     * @param  enableRevisionProperties    enables/disables changes to revision properties
     * @param  force                       forces operation to run
     * @param  pre14Compatible             <span class="javakeyword">true</span> to
     *                                     create a repository with pre-1.4 format
     * @return                             a local URL (file:///) of a newly created repository
     * @throws SVNException
     * @since                              1.1.1
     */
    public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force, boolean pre14Compatible) throws SVNException {
        return doCreateRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, false);
    }

    /**
     * Creates an FSFS-type repository.
     *
     * This implementation uses {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory#createLocalRepository(File, String, boolean, boolean)}}.
     * <p>
     * If <code>uuid</code> is <span class="javakeyword">null</span> a new uuid will be generated, otherwise
     * the specified will be used.
     *
     * <p>
     * If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>, an empty
     * pre-revprop-change hook will be placed into the repository /hooks subdir. This enables changes to
     * revision properties of the newly created repository.
     *
     * <p>
     * If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
     * exists, deletes that path and creates a repository in its place.
     *
     * <p>
     * Set <code>pre14Compatible</code> to <span class="javakeyword">true</span> if you want a new repository
     * to be compatible with pre-1.4 servers, <code>pre15Compatible</code> - with pre-1.5 servers and
     * <code>pre16Compatible</code> - with pre-1.6 servers.
     *
     * <p>
     * There must be only one option (either <code>pre14Compatible</code> or <code>pre15Compatible</code> or <code>pre16Compatible</code>)
     * set to <span class="javakeyword">true</span> at a time.
     *
     * @param  path                        a repository root dir path
     * @param  uuid                        a repository uuid
     * @param  enableRevisionProperties    enables/disables changes to revision properties
     * @param  force                       forces operation to run
     * @param  pre14Compatible             <span class="javakeyword">true</span> to
     *                                     create a repository with pre-1.4 format
     * @param  pre15Compatible             <span class="javakeyword">true</span> to
     *                                     create a repository with pre-1.5 format
     * @param  pre16Compatible             <span class="javakeyword">true</span> to
     *                                     create a repository with pre-1.6 format
     * @return                             a local URL (file:///) of a newly created repository
     * @throws SVNException
     * @since                              1.3, SVN 1.5.0
     */
    public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force,
            boolean pre14Compatible, boolean pre15Compatible, boolean pre16Compatible) throws SVNException {
        return doCreateRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, pre15Compatible, pre16Compatible, false, false);
    }

    public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force,
            boolean pre14Compatible, boolean pre15Compatible, boolean pre16Compatible, boolean pre17Compatible, boolean with17Compatible) throws SVNException {
        return doCreateRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, pre15Compatible, pre16Compatible, pre17Compatible, with17Compatible,
                false);
    }

    public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force,
                                     boolean pre14Compatible, boolean pre15Compatible, boolean pre16Compatible, boolean pre17Compatible, boolean with17Compatible,
                                     boolean pre110Compatible) throws SVNException {
        return SVNRepositoryFactory.createLocalRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, pre15Compatible, pre16Compatible, pre17Compatible, with17Compatible,
                pre110Compatible);
    }

    /**
     * Creates an FSFS-type repository.
     *
     * This implementation uses {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory#createLocalRepository(File, String, boolean, boolean)}}.
     * <p>
     * If <code>uuid</code> is <span class="javakeyword">null</span> a new uuid will be generated, otherwise
     * the specified will be used.
     *
     * <p>
     * If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>, an empty
     * pre-revprop-change hook will be placed into the repository /hooks subdir. This enables changes to
     * revision properties of the newly created repository.
     *
     * <p>
     * If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
     * exists, deletes that path and creates a repository in its place.
     *
     * <p>
     * Set <code>pre14Compatible</code> to <span class="javakeyword">true</span> if you want a new repository
     * to be compatible with pre-1.4 servers, <code>pre15Compatible</code> - with pre-1.5 servers.
     *
     * <p>
     * There must be only one option (either <code>pre14Compatible</code> or <code>pre15Compatible</code>)
     * set to <span class="javakeyword">true</span> at a time.
     *
     * @param  path                        a repository root dir path
     * @param  uuid                        a repository uuid
     * @param  enableRevisionProperties    enables/disables changes to revision properties
     * @param  force                       forces operation to run
     * @param  pre14Compatible             <span class="javakeyword">true</span> to
     *                                     create a repository with pre-1.4 format
     * @param  pre15Compatible             <span class="javakeyword">true</span> to
     *                                     create a repository with pre-1.5 format
     * @return                             a local URL (file:///) of a newly created repository
     * @throws SVNException
     * @since                              1.2, SVN 1.5.0
     */
    public SVNURL doCreateRepository(File path, String uuid, boolean enableRevisionProperties, boolean force,
            boolean pre14Compatible, boolean pre15Compatible) throws SVNException {
        return doCreateRepository(path, uuid, enableRevisionProperties, force, pre14Compatible, pre15Compatible, false);
    }


    /**
     * Copies revision properties from the source repository starting at <code>startRevision</code> and up to
     * <code>endRevision</code> to corresponding revisions of the destination repository represented by
     * <code>toURL</code>.
     *
     * <p>
     * This method is equivalent to the command 'copy-revprops' of the native Subversion <i>svnsync</i> utility.
     * Note that the destination repository given as <code>toURL</code> must be synchronized with a source
     * repository. Please, see {@link #doInitialize(SVNURL, SVNURL)}} how to initialize such a synchronization.
     *
     * <p/>
     * If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
     * receive an {@link SVNAdminEvent} with the {@link SVNAdminEventAction#REVISION_PROPERTIES_COPIED} action
     * when the properties get copied.
     *
     * @param  toURL          a url to the destination repository which must be synchronized
     *                        with another repository
     * @param  startRevision  start revision
     * @param  endRevision    end revision
     * @throws SVNException   in the following cases:
     *                        <ul>
     *                        <li/>exception with {@link SVNErrorCode#IO_ERROR} error code - if any of revisions
     *                        between <code>startRevision</code> and <code>endRevision</code> inclusively was not
     *                        synchronized yet
     *                        </ul>
     * @since                 1.2.0, new in Subversion 1.5.0
     */
    public void doCopyRevisionProperties(SVNURL toURL, long startRevision, long endRevision) throws SVNException {
        SVNRepository toRepos = null;
        SessionInfo info = null;
        SVNException error = null;
        SVNException error2 = null;
        try {
            toRepos = createRepository(toURL, null, true);
            checkIfRepositoryIsAtRoot(toRepos, toURL);
            lock(toRepos);
            info = openSourceRepository(toRepos);

            if (!SVNRevision.isValidRevisionNumber(startRevision)) {
                startRevision = info.myLastMergedRevision;
            }
            if (!SVNRevision.isValidRevisionNumber(endRevision)) {
                endRevision = info.myLastMergedRevision;
            }

            if (startRevision > info.myLastMergedRevision) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                        "Cannot copy revprops for a revision ({0}) that has not been synchronized yet",
                        String.valueOf(startRevision));
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }

            if (endRevision > info.myLastMergedRevision) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                        "Cannot copy revprops for a revision ({0}) that has not been synchronized yet",
                        String.valueOf(endRevision));
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }

            int normalizedRevPropsCount = 0;
            long step = startRevision > endRevision ? -1 : 1;
            for (long i = startRevision; i != endRevision + step; i += step) {
                checkCancelled();
                SVNProperties normalizedProps = copyRevisionProperties(info.myRepository, toRepos, i, false);
                normalizedRevPropsCount += normalizedProps.size();
            }
            handleNormalizedProperties(normalizedRevPropsCount, 0);
        } catch (SVNException svne) {
            error = svne;
        } finally {
            try {
                unlock(toRepos);
                if (toRepos != null) {
                    toRepos.closeSession();
                }
                if (info != null && info.myRepository != null) {
                    info.myRepository.closeSession();
                }
            } catch (SVNException svne) {
                error2 = svne;
            }
        }

        if (error != null) {
            throw error;
        } else if (error2 != null) {
            throw error2;
        }
    }

    /**
     * Initializes synchronization between source and target repositories.
     *
     * <p>
     * This method is equivalent to the command 'initialize' ('init') of the native Subversion <i>svnsync</i>
     * utility. Initialization places information of a source repository to a destination one (setting special
     * revision properties in revision 0) as well as copies all revision props from revision 0 of the source
     * repository to revision 0 of the destination one.
     *
     * @param  fromURL         a source repository url
     * @param  toURL           a destination repository url
     * @throws SVNException    in the following cases:
     *                         <ul>
     *                         <li/>exception with {@link SVNErrorCode#IO_ERROR} error code - if
     *                         either the target repository's last revision is different from <code>0</code>, or
     *                         no {@link SVNRevisionProperty#FROM_URL} property value found in the target
     *                         repository
     *                         <li/>exception with {@link SVNErrorCode#RA_PARTIAL_REPLAY_NOT_SUPPORTED} error
     *                         code - if the source repository does not support partial replay
     *                         </ul>
     * @since                  1.1, new in Subversion 1.4
     */
    public void doInitialize(SVNURL fromURL, SVNURL toURL) throws SVNException {
        SVNRepository toRepos = null;
        SVNRepository fromRepos = null;
        SVNException error = null;
        SVNException error2 = null;

        try {
            toRepos = createRepository(toURL, null, true);
            checkIfRepositoryIsAtRoot(toRepos, toURL);
            lock(toRepos);
            long latestRevision = toRepos.getLatestRevision();
            if (latestRevision != 0) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                        "Cannot initialize a repository with content in it");
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }

            SVNPropertyValue fromURLProp = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL);
            if (fromURLProp != null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                        "Destination repository is already synchronizing from ''{0}''", fromURLProp);
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }

            fromRepos = createRepository(fromURL, null, false);
            SVNURL rootURL = fromRepos.getRepositoryRoot(true);
            if (SVNPathUtil.getPathAsChild(rootURL.toString(), fromURL.toString()) != null) {
                boolean supportsPartialReplay = false;
                try {
                    supportsPartialReplay = fromRepos.hasCapability(SVNCapability.PARTIAL_REPLAY);
                } catch (SVNException svne) {
                    if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.UNSUPPORTED_FEATURE) {
                        throw svne;
                    }
                }
                if (!supportsPartialReplay) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_PARTIAL_REPLAY_NOT_SUPPORTED);
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                }
            }
            toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL,
                    SVNPropertyValue.create(fromURL.toString()));
            String uuid = fromRepos.getRepositoryUUID(true);
            toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID, SVNPropertyValue.create(uuid));
            toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION,
                    SVNPropertyValue.create("0"));
            SVNProperties normalizedProps = copyRevisionProperties(fromRepos, toRepos, 0, false);
            handleNormalizedProperties(normalizedProps.size(), 0);
        } catch (SVNException svne) {
            error = svne;
        } finally {
            try {
                unlock(toRepos);
                if (toRepos != null) {
                    toRepos.closeSession();
                }
                if (fromRepos != null) {
                    fromRepos.closeSession();
                }
            } catch (SVNException svne) {
                error2 = svne;
            }
        }

        if (error != null) {
            throw error;
        } else if (error2 != null) {
            throw error2;
        }
    }

    /**
     * Returns information about the synchronization repository located at <code>toURL</code>.
     *
     * @param  toURL          destination repository url
     * @return                synchronization information
     * @throws SVNException
     * @since  1.3, SVN 1.6
     */
    public SVNSyncInfo doInfo(SVNURL toURL) throws SVNException {
        SVNRepository toRepos = null;
        try {
            toRepos = createRepository(toURL, null, true);
            checkIfRepositoryIsAtRoot(toRepos, toURL);
            SVNPropertyValue fromURL = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL);
            if (fromURL == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Repository ''{0}'' is not initialized for synchronization",
                        toURL);
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }

            SVNPropertyValue fromUUID = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID);
            SVNPropertyValue lastMergedRevProp = toRepos.getRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION);
            long lastMergedRev = lastMergedRevProp != null ? Long.parseLong(lastMergedRevProp.getString()) : SVNRepository.INVALID_REVISION;
            return new SVNSyncInfo(fromURL.getString(), fromUUID != null ? fromUUID.getString() : null, lastMergedRev);
        } finally {
            if (toRepos != null) {
                toRepos.closeSession();
            }
        }
    }

    /**
     * Compacts a repository into a more efficient storage model.
     *
     * <p/>
     * Compacting does not occur if there are no full shards. Also compacting does not work
     * on pre-1.6 repositories.
     *
     * @param  repositoryRoot  root of the repository to pack
     * @throws SVNException
     * @since  1.3, SVN 1.6
     */
    public void doPack(File repositoryRoot) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            FSPacker packer = new FSPacker(myEventHandler);
            packer.pack(fsfs);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }

    }

    /**
     * Completely synchronizes two repositories.
     *
     * <p>
     * This method initializes the destination repository and then copies all revision
     * changes (including revision properties)
     * from the given source repository to the destination one. First it
     * tries to use synchronization features similar to the native Subversion
     * 'svnsync' capabilities. But if a server does not support
     * <code>replay</code> functionality, SVNKit uses its own repository
     * replication feature (see {@link org.tmatesoft.svn.core.replicator.SVNRepositoryReplicator}})
     *
     * @param  fromURL        a url of a repository to copy from
     * @param  toURL          a destination repository url
     * @throws SVNException
     * @since                 1.1
     */
    public void doCompleteSynchronize(SVNURL fromURL, SVNURL toURL) throws SVNException {
        try {
            doInitialize(fromURL, toURL);
            doSynchronize(toURL);
            return;
        } catch (SVNException svne) {
            if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.RA_NOT_IMPLEMENTED) {
                throw svne;
            }
        }

        SVNRepositoryReplicator replicator = SVNRepositoryReplicator.newInstance();
        SVNRepository fromRepos = null;
        SVNRepository toRepos = null;
        try {
            fromRepos = createRepository(fromURL, null, true);
            toRepos = createRepository(toURL, null, false);
            replicator.replicateRepository(fromRepos, toRepos, 1, -1);
        } finally {
            if (fromRepos != null) {
                fromRepos.closeSession();
            }
            if (toRepos != null) {
                toRepos.closeSession();
            }
        }
    }

    /**
     * Synchronizes the repository at the given url.
     *
     * <p>
     * Synchronization means copying revision changes and revision properties from the source
     * repository (that the destination one is synchronized with) to the destination one starting at
     * the last merged revision. This method is equivalent to the command 'synchronize' ('sync') of
     * the native Subversion <i>svnsync</i> utility.
     *
     * @param  toURL          a destination repository url
     * @throws SVNException
     * @since                 1.1, new in Subversion 1.4
     */
    public void doSynchronize(SVNURL toURL) throws SVNException {
        SVNRepository toRepos = null;
        SVNRepository fromRepos = null;
        SVNException error = null;
        SVNException error2 = null;

        try {
            toRepos = createRepository(toURL, null, true);
            checkIfRepositoryIsAtRoot(toRepos, toURL);
            lock(toRepos);

            SessionInfo info = openSourceRepository(toRepos);
            fromRepos = info.myRepository;
            long lastMergedRevision = info.myLastMergedRevision;
            SVNPropertyValue currentlyCopying = toRepos.getRevisionPropertyValue(0,
                    SVNRevisionProperty.CURRENTLY_COPYING);
            long toLatestRevision = toRepos.getLatestRevision();
            int normalizedRevPropsCount = 0;

            if (currentlyCopying != null) {
                long copyingRev = -1;
                try {
                    copyingRev = Long.parseLong(currentlyCopying.getString());
                } catch (NumberFormatException nfe) {
                    SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.IO_ERROR, nfe), SVNLogType.WC);
                }
                if (copyingRev < lastMergedRevision || copyingRev > lastMergedRevision + 1 ||
                        (toLatestRevision != lastMergedRevision && toLatestRevision != copyingRev)) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                            "Revision being currently copied ({0}), last merged revision ({1}), and destination HEAD ({2}) are inconsistent; have you committed to the destination without using svnsync?",
                            new Object[] { String.valueOf(copyingRev), String.valueOf(lastMergedRevision),
                            String.valueOf(toLatestRevision) });
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                } else if (copyingRev == toLatestRevision) {
                    if (copyingRev > lastMergedRevision) {
                        SVNProperties normalizedProps = copyRevisionProperties(fromRepos, toRepos, toLatestRevision, true);
                        normalizedRevPropsCount += normalizedProps.size();
                        lastMergedRevision = copyingRev;
                    }
                    toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION, SVNPropertyValue.create(SVNProperty.toString(lastMergedRevision)));
                    toRepos.setRevisionPropertyValue(0, SVNRevisionProperty.CURRENTLY_COPYING, null);
                }
            } else {
                if (toLatestRevision != lastMergedRevision) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                            "Destination HEAD ({0}) is not the last merged revision ({1}); have you committed to the destination without using svnsync?",
                            new Object[] { String.valueOf(toLatestRevision), String.valueOf(lastMergedRevision) });
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                }
            }

            long fromLatestRevision = fromRepos.getLatestRevision();
            if (fromLatestRevision < lastMergedRevision) {
                return;
            }

            boolean hasCommitRevPropCapability = toRepos.hasCapability(SVNCapability.COMMIT_REVPROPS);
            checkCancelled();

            long startRevision = lastMergedRevision + 1;
            long endRevision = fromLatestRevision;

            SVNReplayHandler replayHandler = new SVNReplayHandler(toRepos, hasCommitRevPropCapability,
                    mySyncHandler, getDebugLog(), this, this);

            fromRepos.replayRange(startRevision, endRevision, 0, true, replayHandler);
            handleNormalizedProperties(normalizedRevPropsCount + replayHandler.getNormalizedRevPropsCount(),
                    replayHandler.getNormalizedNodePropsCount());
        } catch (SVNException svne) {
            error = svne;
        } finally {
            try {
                unlock(toRepos);
                if (toRepos != null) {
                    toRepos.closeSession();
                }
                if (fromRepos != null) {
                    fromRepos.closeSession();
                }
            } catch (SVNException svne) {
                error2 = svne;
            }
        }

        if (error != null) {
            throw error;
        } else if (error2 != null) {
            throw error2;
        }
    }

    /**
     * Walks through the repository tree found under <code>repositoryRoot</code> and reports all found locks
     * via calls to the caller's {@link ISVNAdminEventHandler} handler implementation.
     *
     * <p/>
     * On each locked path found this method dispatches an {@link SVNAdminEventAction#LOCK_LISTED}
     * {@link SVNAdminEvent event} to the caller's handler providing the
     * {@link SVNAdminEvent#getLock() lock information}.
     *
     * @param  repositoryRoot   repository root location
     * @throws SVNException
     * @since                   1.2.0, SVN 1.5.0
     */
    public void doListLocks(File repositoryRoot) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            File digestFile = fsfs.getDigestFileFromRepositoryPath("/");
            ISVNLockHandler handler = new ISVNLockHandler() {
                public void handleLock(String path, SVNLock lock, SVNErrorMessage error) throws SVNException {
                    checkCancelled();
                    if (myEventHandler != null) {
                        SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.LOCK_LISTED, lock, error, null);
                        myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                    }

                }
                public void handleUnlock(String path, SVNLock lock, SVNErrorMessage error) throws SVNException {
                }
            };
            fsfs.walkDigestFiles(digestFile, handler, false);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Removes locks from the specified <code>paths</code> in the repository found under
     * <code>repositoryRoot</code>.
     *
     * <p/>
     * If any path of the <code>paths</code> is not locked, then an {@link SVNAdminEvent event} with the
     * {@link SVNAdminEventAction#NOT_LOCKED} action is dispatched to the caller's {@link ISVNAdminEventHandler}
     * handler.
     *
     * <p/>
     * If, on the contrary, a path is locked, it is unlocked and an event with the
     * {@link SVNAdminEventAction#UNLOCKED} action is dispatched to the caller's handler providing the
     * {@link SVNAdminEvent#getLock() lock information}.
     *
     * <p/>
     * If some error occurs while unlocking, an event with the {@link SVNAdminEventAction#UNLOCK_FAILED} action
     * is dispatched to the caller's handler providing the {@link SVNAdminEvent#getError() error description}.
     *
     * @param  repositoryRoot       repository root location
     * @param  paths                paths to unlock
     * @throws SVNException
     * @since                       1.2.0, SVN 1.5.0
     */
    public void doRemoveLocks(File repositoryRoot, String[] paths) throws SVNException {
        if (paths == null) {
            return;
        }

        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            for (int i = 0; i < paths.length; i++) {
                String path = paths[i];
                if (path == null) {
                    continue;
                }
                checkCancelled();

                SVNLock lock = null;
                try {
                    lock = fsfs.getLockHelper(path, false);
                    if (lock == null) {
                        if (myEventHandler != null) {
                            SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.NOT_LOCKED, null, null, "Path '" + path + "' isn't locked.");
                            myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                        }
                        continue;
                    }

                    fsfs.unlockPath(path, lock.getID(), null, true, false);
                    if (myEventHandler != null) {
                        SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.UNLOCKED, lock, null, "Removed lock on '" + path + "'.");
                        myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                    }
                } catch (SVNException svne) {
                    if (myEventHandler != null) {
                        SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.UNLOCK_FAILED, lock, svne.getErrorMessage(), "svnadmin: " + svne.getErrorMessage().getFullMessage());
                        myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                    }
                }
            }
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Lists all uncommitted transactions.
     * On each uncommetted transaction found this method fires an {@link SVNAdminEvent}
     * with action set to {@link SVNAdminEventAction#TRANSACTION_LISTED} to the registered
     * {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
     * pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
     * information can be retrieved out of {@link SVNAdminEvent}:
     * <ul>
     * <li>transaction name - use {@link SVNAdminEvent#getTxnName() SVNAdminEvent.getTxnName()} to get it</li>
     * <li>transaction directory - use {@link SVNAdminEvent#getTxnDir() SVNAdminEvent.getTxnDir()} to get it</li>
     * </ul>
     *
     * @param  repositoryRoot   a repository root directory path
     * @throws SVNException
     * @since                   1.1.1
     */
    public void doListTransactions(File repositoryRoot) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            Map txns = fsfs.listTransactions();

            for(Iterator names = txns.keySet().iterator(); names.hasNext();) {
                String txnName = (String) names.next();
                File txnDir = (File) txns.get(txnName);
                if (myEventHandler != null) {
                    SVNAdminEvent event = new SVNAdminEvent(txnName, txnDir, SVNAdminEventAction.TRANSACTION_LISTED);
                    myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                }
            }
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Removes the specified outstanding transactions from a repository.
     * On each transaction removed this method fires an {@link SVNAdminEvent}
     * with action set to {@link SVNAdminEventAction#TRANSACTION_REMOVED} to the registered
     * {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
     * pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
     * information can be retrieved out of {@link SVNAdminEvent}:
     * <ul>
     * <li>transaction name - use {@link SVNAdminEvent#getTxnName() SVNAdminEvent.getTxnName()} to get it</li>
     * <li>transaction directory - use {@link SVNAdminEvent#getTxnDir() SVNAdminEvent.getTxnDir()} to get it</li>
     * </ul>
     *
     * @param  repositoryRoot   a repository root directory path
     * @param  transactions     an array with transaction names
     * @throws SVNException
     * @since                   1.1.1
     */
    public void doRemoveTransactions(File repositoryRoot, String[] transactions) throws SVNException {
        if (transactions == null) {
            return;
        }

        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            for (int i = 0; i < transactions.length; i++) {
                String txnName = transactions[i];
                fsfs.openTxn(txnName);
                fsfs.purgeTxn(txnName);
                SVNDebugLog.getDefaultLog().logFine(SVNLogType.FSFS, "Transaction '" + txnName + "' removed.\n");
                if (myEventHandler != null) {
                    SVNAdminEvent event = new SVNAdminEvent(txnName, fsfs.getTransactionDir(txnName), SVNAdminEventAction.TRANSACTION_REMOVED);
                    myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                }
            }
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Verifies the data stored in the repository. This method uses the dump implementation
     * (non incremental, beginning with revision 0, ending at the latest one)
     * passing a dummy output stream to it. This allows to check the integrity of the
     * repository data.
     *
     * <p/>
     * This is identical to <code>doVerify(repositoryRoot, SVNRevision.create(0), SVNRevision.HEAD)</code>.
     *
     * @param  repositoryRoot   a repository root directory path
     * @throws SVNException     verification failed - a repository may be corrupted
     * @since                   1.1.1
     */
    public void doVerify(File repositoryRoot) throws SVNException {
        doVerify(repositoryRoot, SVNRevision.create(0), SVNRevision.HEAD);
    }

    /**
     * Verifies repository contents found under <code>repositoryRoot</code> starting at <code>startRevision</code>
     * and up to <code>endRevision</code>. This method uses the dump implementation
     * (non incremental) passing a dummy output stream to it. This allows to check the integrity of the
     * repository data.
     *
     * <p/>
     * If <code>startRevision</code> is {@link SVNRevision#isValid() invalid}, it defaults to <code>0</code>.
     * If <code>endRevision</code> is {@link SVNRevision#isValid() invalid}, it defaults to the HEAD revision.
     *
     * @param  repositoryRoot   a repository root directory path
     * @param  startRevision    revision to start verification at
     * @param  endRevision      revision to stop verification at
     * @throws SVNException     verification failed - a repository may be corrupted
     * @since                   1.2.0, SVN 1.5.0
     */
    public void doVerify(File repositoryRoot, SVNRevision startRevision, SVNRevision endRevision) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            long youngestRevision = fsfs.getYoungestRevision();

            long lowerRev = SVNAdminHelper.getRevisionNumber(startRevision, youngestRevision, fsfs);
            long upperRev = SVNAdminHelper.getRevisionNumber(endRevision, youngestRevision, fsfs);

            if (!SVNRevision.isValidRevisionNumber(upperRev)) {
                upperRev = lowerRev;
            }

            verify(fsfs, lowerRev, upperRev);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Dumps contents of the repository to the provided output stream in a
     * 'dumpfile' portable format.
     *
     * <p>
     * On each revision dumped this method fires an {@link SVNAdminEvent}
     * with action set to {@link SVNAdminEventAction#REVISION_DUMPED} to the registered
     * {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
     * pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
     * information can be retrieved out of {@link SVNAdminEvent}:
     * <ul>
     * <li>dumped revision - use {@link SVNAdminEvent#getRevision() SVNAdminEvent.getRevision()} to get it</li>
     * </ul>
     *
     * @param  repositoryRoot   a repository root directory path
     * @param  dumpStream       an output stream to write dumped contents to
     * @param  startRevision    the first revision to start dumping from
     * @param  endRevision      the last revision to end dumping at
     * @param  isIncremental    if <span class="javakeyword">true</span>
     *                          then the first revision dumped will be a
     *                          diff against the previous revision; otherwise
     *                          the first revision is a fulltext.
     * @param  useDeltas        if <span class="javakeyword">true</span>
     *                          deltas will be written instead of fulltexts
     * @throws SVNException
     * @since                   1.1.1
     */
    public void doDump(File repositoryRoot, OutputStream dumpStream, SVNRevision startRevision, SVNRevision endRevision, boolean isIncremental, boolean useDeltas) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            long youngestRevision = fsfs.getYoungestRevision();

            long lowerR = SVNAdminHelper.getRevisionNumber(startRevision, youngestRevision, fsfs);
            long upperR = SVNAdminHelper.getRevisionNumber(endRevision, youngestRevision, fsfs);

            if (!SVNRevision.isValidRevisionNumber(lowerR)) {
                lowerR = 0;
                upperR = youngestRevision;
            } else if (!SVNRevision.isValidRevisionNumber(upperR)) {
                upperR = lowerR;
            }

            if (lowerR > upperR) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CL_ARG_PARSING_ERROR, "First revision cannot be higher than second");
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }

            dump(fsfs, dumpStream, lowerR, upperR, isIncremental, useDeltas);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Reads the provided dump stream committing new revisions to a repository.
     *
     * <p>
     * On each revision loaded this method fires an {@link SVNAdminEvent}
     * with action set to {@link SVNAdminEventAction#REVISION_LOADED} to the registered
     * {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
     * pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
     * information can be retrieved out of {@link SVNAdminEvent}:
     * <ul>
     * <li>original revision - use {@link SVNAdminEvent#getOriginalRevision() SVNAdminEvent.getOriginalRevision()} to get it</li>
     * <li>new committed revision - use {@link SVNAdminEvent#getRevision() SVNAdminEvent.getRevision()} to get it</li>
     * </ul>
     *
     * <p>
     * A call to this method is equivalent to
     * <code>doLoad(repositoryRoot, dumpStream, false, false, SVNUUIDAction.DEFAULT, null)</code>.
     *
     * @param  repositoryRoot   the root directory path of the repository where
     *                          new revisions will be committed
     * @param  dumpStream       stream with dumped contents of a repository
     * @throws SVNException
     * @see                     #doLoad(File, InputStream, boolean, boolean, SVNUUIDAction, String)
     * @since                   1.1.1
     */
    public void doLoad(File repositoryRoot, InputStream dumpStream) throws SVNException {
        doLoad(repositoryRoot, dumpStream, false, false, SVNUUIDAction.DEFAULT, null);
    }

    /**
     * Reads the provided dump stream committing new revisions to a repository.
     *
     * <p>
     * On each revision loaded this method fires an {@link SVNAdminEvent}
     * with action set to {@link SVNAdminEventAction#REVISION_LOADED} to the registered
     * {@link ISVNAdminEventHandler} (if any). To register your <b>ISVNAdminEventHandler</b>
     * pass it to {@link #setEventHandler(ISVNEventHandler)}. For this operation the following
     * information can be retrieved out of {@link SVNAdminEvent}:
     * <ul>
     * <li>original revision - use {@link SVNAdminEvent#getOriginalRevision() SVNAdminEvent.getOriginalRevision()} to get it</li>
     * <li>new committed revision - use {@link SVNAdminEvent#getRevision() SVNAdminEvent.getRevision()} to get it</li>
     * </ul>
     *
     * @param  repositoryRoot    the root directory path of the repository where
     *                           new revisions will be committed
     * @param  dumpStream        stream with dumped contents of a repository
     * @param  usePreCommitHook  if <span class="javakeyword">true</span>
     *                           then calls a pre-commit hook before committing
     * @param  usePostCommitHook if <span class="javakeyword">true</span>
     *                           then calls a post-commit hook after committing
     * @param  uuidAction        one of the three possible ways to treat uuids
     * @param  parentDir         if not <span class="javakeyword">null</span>
     *                           then loads at this directory in the repository
     * @throws SVNException
     * @since                       1.1.1
     */
    public void doLoad(File repositoryRoot, InputStream dumpStream, boolean usePreCommitHook,
            boolean usePostCommitHook, SVNUUIDAction uuidAction, String parentDir) throws SVNException {
        CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
        FSFS fsfs = null;
        try {
            fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
            ISVNLoadHandler handler = createLoadHandler(fsfs, usePreCommitHook, usePostCommitHook,
                    uuidAction, parentDir);
            SVNDumpStreamParser parser = getDumpStreamParser();
            parser.parseDumpStream(dumpStream, handler, decoder);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Recovers the repository found under <code>repositoryRoot</code>.
     * This method can recover only FSFS type repositories and is identical to the <code>'svnadmin recover'</code>
     * command.
     *
     * <p/>
     * If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
     * receive an {@link SVNAdminEvent} with the {@link SVNAdminEventAction#RECOVERY_STARTED} action before
     * the recovery starts.
     *
     * @param  repositoryRoot    repository root location
     * @throws SVNException
     * @since                    1.2.0, SVN 1.5.0
     */
    public void doRecover(File repositoryRoot) throws SVNException {
        FSFS fsfs = null;
        try {
            fsfs = SVNAdminHelper.openRepositoryForRecovery(repositoryRoot);

            if (myEventHandler != null) {
                SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.RECOVERY_STARTED);
                myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
            }
            FSRecoverer recoverer = new FSRecoverer(fsfs, this);
            recoverer.runRecovery();
        } finally {
            if (fsfs != null) {
                fsfs.close();
            }
        }
    }

    /**
     * Upgrades the repository located at <code>repositoryRoot</code> to the latest supported
     * schema version. This method is identical to the <code>'svnadmin upgrade'</code> command.
     *
     * <p/>
     * This functionality is provided as a convenience for repository administrators who wish to make use of
     * new Subversion functionality without having to undertake a potentially costly full repository dump
     * and load operation. As such, the upgrade performs only the minimum amount of work needed to accomplish
     * this while still maintaining the integrity of the repository. It does not guarantee the most optimized
     * repository state as a dump and subsequent load would.
     *
     * <p/>
     * If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
     * receive an {@link SVNAdminEvent} with the {@link SVNAdminEventAction#UPGRADE} action before
     * the upgrade starts.
     *
     * @param  repositoryRoot   repository root location
     * @throws SVNException
     * @since                   1.2.0, SVN 1.5.0
     */
    public void doUpgrade(File repositoryRoot)throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            if (myEventHandler != null) {
                SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.UPGRADE);
                myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
            }

            File reposFormatFile = fsfs.getRepositoryFormatFile();
            int format = fsfs.getReposFormat();
            SVNFileUtil.writeVersionFile(reposFormatFile, format);
            fsfs.upgrade();
            SVNFileUtil.writeVersionFile(reposFormatFile, FSFS.REPOSITORY_FORMAT);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Resets the repository UUID with the provided <code>uuid</code> for the repository located at
     * <code>repositoryRoot</code>. This method is identical to the <code>'svnadmin setuuid'</code> command.
     *
     * <p/>
     * If no <code>uuid</code> is specified, then <code>SVNKit</code> will generate a new one and will use it to
     * reset the original UUID.
     *
     * @param  repositoryRoot   repository root location
     * @param  uuid             new UUID to set
     * @throws SVNException     exception with {@link SVNErrorCode#BAD_UUID} error code - if the <code>uuid</code>
     *                          is malformed
     * @since                   1.2.0, SVN 1.5.0
     */
    public void doSetUUID(File repositoryRoot, String uuid) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            if (uuid == null) {
                uuid = SVNUUIDGenerator.generateUUIDString();
            } else {
                String[] components = uuid.split("-");
                if (components.length != 5) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_UUID, "Malformed UUID ''{0}''",
                            uuid);
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                }
            }
            fsfs.setUUID(uuid);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Makes a hot copy of a repository located at <code>srcRepositoryRoot</code> to one located at
     * <code>newRepositoryRoot</code>. This method is identical to the <code>'svnadmin hotcopy'</code> command.
     *
     * @param  srcRepositoryRoot   repository to copy data from
     * @param  newRepositoryRoot   repository to copy data to
     * @throws SVNException
     * @since                       1.2.0, SVN 1.5.0
     */
    public void doHotCopy(File srcRepositoryRoot, File newRepositoryRoot) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(srcRepositoryRoot, false);
        try {
            FSHotCopier copier = getHotCopier();
            copier.runHotCopy(fsfs, newRepositoryRoot);
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Returns the HEAD revision of the repository located at <code>repositoryRoot</code>.
     *
     * <p/>
     * Identical to the <code>'svnlook youngest'</code> svn command.
     *
     * @param  repositoryRoot  repository root location
     * @return                 the last revision
     * @throws SVNException
     * @since                  1.2.0
     */
    public long getYoungestRevision(File repositoryRoot) throws SVNException {
        FSFS fsfs = SVNAdminHelper.openRepository(repositoryRoot, true);
        try {
            return fsfs.getYoungestRevision();
        } finally {
            SVNAdminHelper.closeRepository(fsfs);
        }
    }

    /**
     * Filters out nodes with or without the given <code>prefixes</code> from <code>dumpStream</code>
     * to <code>resultDumpStream</code>. This method is similar to the functionality provided by the
     * <code>'svndumpfilter'</code> utility.
     *
     * <p/>
     * If <code>exclude</code> is <span class="javakeyword">true</span> then filters out nodes with
     * <code>prefixes</code>, otherwise nodes without <code>prefixes</code>.
     *
     * <p/>
     * If the caller has {@link #setEventHandler(ISVNEventHandler) provided} an event handler, the handler will
     * be called with different actions:
     * <ul>
     * <li/>{@link SVNAdminEventAction#DUMP_FILTER_TOTAL_REVISIONS_DROPPED} - use {@link SVNAdminEvent#getDroppedRevisionsCount()}
     * to retrieve the total number of dropped revisions.
     * <li/>{@link SVNAdminEventAction#DUMP_FILTER_DROPPED_RENUMBERED_REVISION} - is sent only when
     * <code>renumberRevisions</code> is <span class="javakeyword">true</span> and informs that an original
     * revision (which is provided as {@link SVNAdminEvent#getRevision()}) was dropped.
     * <li/>{@link SVNAdminEventAction#DUMP_FILTER_RENUMBERED_REVISION} - is sent only when
     * <code>renumberRevisions</code> is <span class="javakeyword">true</span> and informs that the original
     * revision (provided as {@link SVNAdminEvent#getOriginalRevision()}) was renumbered to {@link SVNAdminEvent#getRevision()}.
     * <li/>{@link SVNAdminEventAction#DUMP_FILTER_DROPPED_NODE} - says that {@link SVNAdminEvent#getPath()} was
     * dropped.
     * <li/>{@link SVNAdminEventAction#DUMP_FILTER_TOTAL_NODES_DROPPED} - use {@link SVNAdminEvent#getDroppedNodesCount()}
     * to retrieve the total number of dropped nodes.
     * <li/>{@link SVNAdminEventAction#DUMP_FILTER_REVISION_COMMITTED} - is sent to inform that the original
     * revision {@link SVNAdminEvent#getOriginalRevision()} resulted in {@link SVNAdminEvent#getRevision()}
     * in the output.
     * <li/>{@link SVNAdminEventAction#DUMP_FILTER_REVISION_SKIPPED} - is sent to inform that the original
     * revision {@link SVNAdminEvent#getRevision()} is dropped (skipped).
     * </ul>
     *
     * @param  dumpStream                   the input repository dump stream
     * @param  resultDumpStream             the resultant (filtered) dump stream
     * @param  exclude                      whether to exclude or include paths with the specified
     *                                      <code>prefixes</code>
     * @param  renumberRevisions            if <span class="javakeyword">true</span>, renumbers revisions left
     *                                      after filtering
     * @param  dropEmptyRevisions           if <span class="javakeyword">true</span>, then removes revisions
     *                                      emptied by filtering
     * @param  preserveRevisionProperties   if <span class="javakeyword">true</span>, then does not filter
     *                                      revision properties
     * @param  prefixes                     prefixes of the path to filter
     * @param  skipMissingMergeSources      if <span class="javakeyword">true</span>, then skips missig merge
     *                                      sources
     * @throws SVNException
     * @since                               1.2.0, SVN 1.5.0
     */
    public void doFilter(InputStream dumpStream, OutputStream resultDumpStream, boolean exclude,
            boolean renumberRevisions, boolean dropEmptyRevisions, boolean preserveRevisionProperties,
            Collection prefixes, boolean skipMissingMergeSources) throws SVNException {
        CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();

        writeDumpData(resultDumpStream, SVNAdminHelper.DUMPFILE_MAGIC_HEADER + ": 2\n\n");

        DefaultDumpFilterHandler handler = getDumpFilterHandler(resultDumpStream, exclude, renumberRevisions,
                dropEmptyRevisions, preserveRevisionProperties, prefixes, skipMissingMergeSources);
        SVNDumpStreamParser parser = getDumpStreamParser();
        parser.parseDumpStream(dumpStream, handler, decoder);

        if (myEventHandler != null) {
            if (handler.getDroppedRevisionsCount() > 0) {
                String message = MessageFormat.format("Dropped {0} revision(s).", new Object[] {
                        String.valueOf(handler.getDroppedRevisionsCount()) });
                SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.DUMP_FILTER_TOTAL_REVISIONS_DROPPED,
                        message);
                event.setDroppedRevisionsCount(handler.getDroppedRevisionsCount());
                myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
            }

            if (renumberRevisions) {
                Map renumberHistory = handler.getRenumberHistory();
                Long[] reNumberedRevisions = (Long[]) renumberHistory.keySet().toArray(new Long[renumberHistory.size()]);
                Arrays.sort(reNumberedRevisions);
                for (int i = reNumberedRevisions.length; i > 0; i--) {
                    Long revision = reNumberedRevisions[i - 1];
                    DefaultDumpFilterHandler.RevisionItem revItem = (DefaultDumpFilterHandler.RevisionItem) renumberHistory.get(revision);
                    if (revItem.wasDropped()) {
                        String message = MessageFormat.format("{0} => (dropped)", new Object[] { revision.toString() });
                        SVNAdminEvent event = new SVNAdminEvent(revision.longValue(),
                                SVNAdminEventAction.DUMP_FILTER_DROPPED_RENUMBERED_REVISION, message);
                        myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                    } else {
                        String message = MessageFormat.format("{0} => {1}", new Object[] { revision.toString(),
                                String.valueOf(revItem.getRevision()) });
                        SVNAdminEvent event = new SVNAdminEvent(revItem.getRevision(), revision.longValue(),
                                SVNAdminEventAction.DUMP_FILTER_RENUMBERED_REVISION, message);
                        myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                    }
                }
            }

            Map droppedNodes = handler.getDroppedNodes();
            if (!droppedNodes.isEmpty()) {
                String message = MessageFormat.format("Dropped {0} node(s)", new Object[] {
                        String.valueOf(droppedNodes.size()) });
                SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.DUMP_FILTER_TOTAL_NODES_DROPPED,
                        message);
                event.setDroppedNodesCount(droppedNodes.size());
                myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                String[] paths = (String[]) droppedNodes.keySet().toArray(new String[droppedNodes.size()]);
                Arrays.sort(paths, SVNPathUtil.PATH_COMPARATOR);
                for (int i = 0; i < paths.length; i++) {
                    String path = paths[i];
                    message = "'" + path + "'";
                    event = new SVNAdminEvent(SVNAdminEventAction.DUMP_FILTER_DROPPED_NODE, path, message);
                    myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                }
            }
        }
    }

    protected void handlePropertesCopied(boolean foundSyncProps, long revision) throws SVNException {
        if (myEventHandler != null) {
            String message = null;
            if (foundSyncProps) {
                message = MessageFormat.format("Copied properties for revision {0} ({1}* properties skipped).",
                        new Object[] { String.valueOf(revision), SVNProperty.SVN_SYNC_PREFIX });
            } else {
                message = MessageFormat.format("Copied properties for revision {0}.", new Object[] {
                        String.valueOf(revision) });
            }
            SVNAdminEvent event = new SVNAdminEvent(revision, SVNAdminEventAction.REVISION_PROPERTIES_COPIED,
                    message);
            myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
        }
    }

    protected void handleNormalizedProperties(int normalizedRevPropsCount, int normalizedNodePropsCount) throws SVNException {
        if (myEventHandler != null && (normalizedRevPropsCount > 0 || normalizedNodePropsCount > 0)) {
            String message = MessageFormat.format("NOTE: Normalized {0}* properties to LF line endings ({1} rev-props, {2} node-props).",
                    new Object[] { SVNProperty.SVN_PREFIX, String.valueOf(normalizedRevPropsCount), String.valueOf(normalizedNodePropsCount) });
            SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.NORMALIZED_PROPERTIES, message);
            myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
        }
    }

    private FSHotCopier getHotCopier() {
        if (myHotCopier == null) {
            myHotCopier = new FSHotCopier();
        }
        return myHotCopier;
    }

    private void verify(FSFS fsfs, long startRev, long endRev) throws SVNException {
        long youngestRev = fsfs.getYoungestRevision();
        if (!SVNRevision.isValidRevisionNumber(startRev)) {
            startRev = 0;
        }
        if (!SVNRevision.isValidRevisionNumber(endRev)) {
            endRev = youngestRev;
        }

        if (startRev > endRev) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS, "Start revision {0} is greater than end revision {1}",
                    new Object[] { String.valueOf(startRev), String.valueOf(endRev) });
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        if (endRev > youngestRev) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS,
                    "End revision {0} is invalid (youngest revision is {1})", new Object[] { String.valueOf(endRev),
                    String.valueOf(youngestRev) });
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        for (long rev = startRev; rev <= endRev; rev++) {
            FSRevisionRoot toRoot = fsfs.createRevisionRoot(rev);
            ISVNEditor editor = getDumpEditor(fsfs, toRoot, rev, startRev, "/", SVNFileUtil.DUMMY_OUT, false, true);
            editor = SVNCancellableEditor.newInstance(editor, getEventDispatcher(), getDebugLog());
            FSRepositoryUtil.replay(fsfs, toRoot, "", SVNRepository.INVALID_REVISION, false, editor);
            fsfs.getRevisionProperties(rev);
            String message = "* Verified revision " + rev + ".";

            if (myEventHandler != null) {
                SVNAdminEvent event = new SVNAdminEvent(rev, SVNAdminEventAction.REVISION_DUMPED, message);
                myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
            }
        }
    }

    private void dump(FSFS fsfs, OutputStream dumpStream, long start, long end, boolean isIncremental, boolean useDeltas) throws SVNException {
        boolean isDumping = dumpStream != null && dumpStream != SVNFileUtil.DUMMY_OUT;
        long youngestRevision = fsfs.getYoungestRevision();
        SVNAdminDeltifier deltifier = new SVNAdminDeltifier(fsfs, SVNDepth.INFINITY,
                false, false, false, null);

        if (!SVNRevision.isValidRevisionNumber(start)) {
            start = 0;
        }

        if (!SVNRevision.isValidRevisionNumber(end)) {
            end = youngestRevision;
        }

        if (dumpStream == null) {
            dumpStream = SVNFileUtil.DUMMY_OUT;
        }

        if (start > end) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS,
                    "Start revision {0} is greater than end revision {1}", new Object[] { String.valueOf(start),
                    String.valueOf(end) });
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        if (end > youngestRevision) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS,
                    "End revision {0} is invalid (youngest revision is {1})", new Object[] { String.valueOf(end),
                    String.valueOf(youngestRevision) });
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        if (start == 0 && isIncremental) {
            isIncremental = false;
        }

        String uuid = fsfs.getUUID();
        int version = SVNAdminHelper.DUMPFILE_FORMAT_VERSION;

        if (!useDeltas) {
            //for compatibility with SVN 1.0.x
            version--;
        }

        writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_MAGIC_HEADER + ": " + version + "\n\n");
        writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_UUID + ": " + uuid + "\n\n");

        for (long i = start; i <= end; i++) {
            long fromRev, toRev;

            checkCancelled();

            if (i == start && !isIncremental) {
                if (i == 0) {
                    writeRevisionRecord(dumpStream, fsfs, 0);
                    toRev = 0;
                    String message = (isDumping ? "* Dumped" : "* Verified") + " revision " + toRev + ".";
                    if (myEventHandler != null) {
                        SVNAdminEvent event = new SVNAdminEvent(toRev, SVNAdminEventAction.REVISION_DUMPED, message);
                        myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                    }
                    continue;
                }

                fromRev = 0;
                toRev = i;
            } else {
                fromRev = i - 1;
                toRev = i;
            }

            writeRevisionRecord(dumpStream, fsfs, toRev);
            boolean useDeltasForRevision = useDeltas && (isIncremental || i != start);
            FSRevisionRoot toRoot = fsfs.createRevisionRoot(toRev);
            ISVNEditor dumpEditor = getDumpEditor(fsfs, toRoot, toRev, start, "/", dumpStream, useDeltasForRevision, false);

            if (i == start && !isIncremental) {
                FSRevisionRoot fromRoot = fsfs.createRevisionRoot(fromRev);
                deltifier.setEditor(dumpEditor);
                deltifier.deltifyDir(fromRoot, "/", "", toRoot, "/");
            } else {
                FSRepositoryUtil.replay(fsfs, toRoot, "", -1, false, dumpEditor);
            }
            String message = (isDumping ? "* Dumped" : "* Verified") + " revision " + toRev + ".";
            if (myEventHandler != null) {
                SVNAdminEvent event = new SVNAdminEvent(toRev, SVNAdminEventAction.REVISION_DUMPED, message);
                myEventHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
            }
        }
    }

    private void writeRevisionRecord(OutputStream dumpStream, FSFS fsfs, long revision) throws SVNException {
        SVNProperties revProps = fsfs.getRevisionProperties(revision);

        String revisionDate = revProps.getStringValue(SVNRevisionProperty.DATE);
        if (revisionDate != null) {
            SVNDate date = SVNDate.parseDate(revisionDate);
            revProps.put(SVNRevisionProperty.DATE, date.format());
        }

        ByteArrayOutputStream encodedProps = new ByteArrayOutputStream();
        SVNAdminHelper.writeProperties(revProps, null, encodedProps);

        writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_REVISION_NUMBER + ": " + revision + "\n");
        String propContents = null;
        try {
            propContents = new String(encodedProps.toByteArray(), "UTF-8");
        } catch (UnsupportedEncodingException uee) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, uee.getLocalizedMessage());
            SVNErrorManager.error(err, uee, SVNLogType.FSFS);
        }
        writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_PROP_CONTENT_LENGTH + ": " + propContents.length() + "\n");
        writeDumpData(dumpStream, SVNAdminHelper.DUMPFILE_CONTENT_LENGTH + ": " + propContents.length() + "\n\n");
        writeDumpData(dumpStream, propContents);
        writeDumpData(dumpStream, "\n");
    }

    private void writeDumpData(OutputStream out, String data) throws SVNException {
        try {
            out.write(data.getBytes("UTF-8"));
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        }
    }

    private DefaultLoadHandler createLoadHandler(FSFS fsfs, boolean usePreCommitHook,
            boolean usePostCommitHook, SVNUUIDAction uuidAction, String parentDir) {
        DefaultLoadHandler handler = new DefaultLoadHandler(usePreCommitHook, usePostCommitHook, uuidAction,
                parentDir, myEventHandler);
        handler.setFSFS(fsfs);
        handler.setUsePreCommitHook(usePreCommitHook);
        handler.setUsePostCommitHook(usePostCommitHook);
        handler.setUUIDAction(uuidAction);
        handler.setParentDir(parentDir);
        return handler;
    }

    private DefaultDumpFilterHandler getDumpFilterHandler(OutputStream os, boolean exclude,
            boolean renumberRevisions, boolean dropEmptyRevisions, boolean preserveRevisionProperties,
            Collection prefixes, boolean skipMissingMergeSources) {
        if (myDumpFilterHandler == null) {
            myDumpFilterHandler = new DefaultDumpFilterHandler(os, myEventHandler, exclude, renumberRevisions,
                    dropEmptyRevisions, preserveRevisionProperties, prefixes, skipMissingMergeSources);
        } else {
            myDumpFilterHandler.reset(os, myEventHandler, exclude, renumberRevisions, dropEmptyRevisions,
                    preserveRevisionProperties, prefixes, skipMissingMergeSources);
        }
        return myDumpFilterHandler;
    }

    private SVNDumpStreamParser getDumpStreamParser() {
        if (myDumpStreamParser == null) {
            myDumpStreamParser = new SVNDumpStreamParser(this);
        }
        return myDumpStreamParser;
    }

    private SVNDumpEditor getDumpEditor(FSFS fsfs, FSRoot root, long toRevision, long oldestDumpedRevision, String rootPath, OutputStream dumpStream,
            boolean useDeltas, boolean isVerify) {
        if (myDumpEditor == null) {
            myDumpEditor = new SVNDumpEditor(fsfs, root, toRevision, oldestDumpedRevision, rootPath, dumpStream, useDeltas, isVerify);
        } else {
            myDumpEditor.reset(fsfs, root, toRevision, oldestDumpedRevision, rootPath, dumpStream, useDeltas, isVerify);
        }
        return myDumpEditor;
    }

    private SVNProperties copyRevisionProperties(SVNRepository fromRepository, SVNRepository toRepository,
            long revision, boolean sync) throws SVNException {
        int filteredCount = 0;

        SVNProperties existingRevProps = null;
        if (sync) {
            existingRevProps = toRepository.getRevisionProperties(revision, null);
        }

        SVNProperties revProps = fromRepository.getRevisionProperties(revision, null);
        SVNProperties normalizedProps = normalizeRevisionProperties(revProps);
        filteredCount += SVNAdminHelper.writeRevisionProperties(toRepository, revision, revProps);

        if (sync) {
            SVNAdminHelper.removePropertiesNotInSource(toRepository, revision, revProps, existingRevProps);
        }
        handlePropertesCopied(filteredCount > 0, revision);
        return normalizedProps;
    }

    private SessionInfo openSourceRepository(SVNRepository targetRepos) throws SVNException {
        SVNPropertyValue fromURL = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_URL);
        SVNPropertyValue fromUUID = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.FROM_UUID);
        SVNPropertyValue lastMergedRev = targetRepos.getRevisionPropertyValue(0, SVNRevisionProperty.LAST_MERGED_REVISION);

        if (fromURL == null || fromUUID == null || lastMergedRev == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                    "Destination repository has not been initialized");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        SVNURL srcURL = SVNURL.parseURIEncoded(fromURL.getString());
        SVNRepository srcRepos = createRepository(srcURL, fromUUID.getString(), false);
        try {
            return new SessionInfo(srcRepos, Long.parseLong(lastMergedRev.getString()));
        } catch (NumberFormatException nfe) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.IO_ERROR, nfe), SVNLogType.FSFS);
        }
        return null;
    }

    private void checkIfRepositoryIsAtRoot(SVNRepository repos, SVNURL url) throws SVNException {
        SVNURL reposRoot = repos.getRepositoryRoot(true);
        if (!reposRoot.equals(url)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Session is rooted at ''{0}'' but the repos root is ''{1}''",  url, reposRoot);
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
    }

    private void lock(SVNRepository repos) throws SVNException {
        String hostName = null;
        try {
            hostName = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Can't get local hostname");
            SVNErrorManager.error(err, e, SVNLogType.FSFS);
        }

        if (hostName.length() > 256) {
            hostName = hostName.substring(0, 256);
        }

        String lockToken = hostName + ":" + SVNUUIDGenerator.formatUUID(SVNUUIDGenerator.generateUUID());
        int i = 0;
        SVNErrorMessage childError = null;
        for (i = 0; i < LOCK_RETRY_COUNT; i++) {
            checkCancelled();
            SVNPropertyValue reposLockToken = repos.getRevisionPropertyValue(0, SVNRevisionProperty.LOCK);
            if (reposLockToken != null) {
                if (lockToken.equals(reposLockToken.getString())) {
                    return;
                }
                childError = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                        "Failed to get lock on destination repos, currently held by ''{0}''", reposLockToken.getString());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //
                }
            } else if (i < LOCK_RETRY_COUNT - 1) {
                repos.setRevisionPropertyValue(0, SVNRevisionProperty.LOCK, SVNPropertyValue.create(lockToken));
            }
        }

        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                "Couldn''t get lock on destination repos after {0} attempts", String.valueOf(i));
        if (childError != null) {
            err.setChildErrorMessage(childError);
        }
        SVNErrorManager.error(err, SVNLogType.FSFS);
    }

    private void unlock(SVNRepository repos) throws SVNException {
        repos.setRevisionPropertyValue(0, SVNRevisionProperty.LOCK, null);
    }

    private class SessionInfo {

        SVNRepository myRepository;
        long myLastMergedRevision;

        public SessionInfo(SVNRepository repos, long lastMergedRev) {
            myRepository = repos;
            myLastMergedRevision = lastMergedRev;
        }
    }

    public static SVNProperties normalizeRevisionProperties(SVNProperties revProps) throws SVNException {
        SVNProperties normalizedProps = new SVNProperties();
        for (Iterator propNamesIter = revProps.nameSet().iterator(); propNamesIter.hasNext();) {
            String propName = (String) propNamesIter.next();
            if (SVNPropertiesManager.propNeedsTranslation(propName)) {
                SVNPropertyValue value = revProps.getSVNPropertyValue(propName);
                String normalizedValue = normalizeString(SVNPropertyValue.getPropertyAsString(value));
                if (normalizedValue != null) {
                    normalizedProps.put(propName, SVNPropertyValue.create(normalizedValue));
                }
            }
        }
        revProps.putAll(normalizedProps);
        return normalizedProps;
    }

    public static String normalizeString(String string) throws SVNException {
        if (string != null && string.indexOf(SVNProperty.EOL_CR_BYTES[0]) != -1) {
            return SVNTranslator.translateString(string, SVNProperty.EOL_LF_BYTES, null, true, false);
        }
        return null;
    }

}
