com.microsoft.tfs.client.common.repository.cache.pendingchange.PendingChangeCollection.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.repository.cache.pendingchange.PendingChangeCollection.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.client.common.repository.cache.pendingchange;

import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.tfs.core.clients.versioncontrol.path.LocalPath;
import com.microsoft.tfs.core.clients.versioncontrol.path.ServerPath;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.ChangeType;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.ItemType;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.PendingChange;
import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Workspace;
import com.microsoft.tfs.util.Check;

/**
 * {@link PendingChangeCollection} is an internal class intended for use only by
 * {@link PendingChangeCache}. It stores the cached pending change data and
 * provides several indexes over the cached pending changes (by server path, by
 * local path, and by parentage so that you can query all pending changes
 * beneath a given path.)
 */
public class PendingChangeCollection {
    private final Workspace workspace;

    private static final Log log = LogFactory.getLog(PendingChangeCollection.class);

    /**
     * Canonical server path ({@link String}) to {@link PendingChange}. This is
     * the authoritative list of pending changes.
     */
    private final Map<String, PendingChange> changesByServerPath = new HashMap<String, PendingChange>();

    /**
     * Canonical parent server path ({@link String}) to {@link Set} of
     * {@link PendingChange}.
     */
    private final Map<String, Set<PendingChange>> changesByParentServerPath = new HashMap<String, Set<PendingChange>>();

    /**
     * Canonical local path ({@link String}) to {@link PendingChange}.
     */
    private final Map<String, PendingChange> changesByLocalPath = new HashMap<String, PendingChange>();

    /**
     * Canonical parent local path ({@link String}) to {@link Set} of
     * {@link PendingChange}.
     */
    private final Map<String, Set<PendingChange>> changesByParentLocalPath = new HashMap<String, Set<PendingChange>>();

    /**
     * A lock acquired to read/write any of the collection fields or to
     * read/write any of the value items (Sets and Collections) inside the
     * collection fields.
     */
    private final Object lock = new Object();

    public PendingChangeCollection(final Workspace workspace) {
        Check.notNull(workspace, "workspace"); //$NON-NLS-1$

        this.workspace = workspace;
    }

    /**
     * Clears all pending change data held by this collection.
     */
    public void clear() {
        synchronized (lock) {
            changesByServerPath.clear();
            changesByParentServerPath.clear();
            changesByLocalPath.clear();
            changesByParentLocalPath.clear();
        }
    }

    /**
     * Adds a new pending change to this collection. If <code>forRefill</code>
     * is <code>true</code>, no attempt is made to remove any existing pending
     * change with the same server path. This flag should be passed as
     * <code>true</code> when refilling this collection either initially or
     * after clearing it. If <code>forRefill</code> is <code>false</code> and
     * there is an existing pending change with the same server path, the
     * existing pending change is removed and returned.
     *
     * @param newPendingChange
     *        the new {@link PendingChange} to add
     * @param forRefill
     *        <code>true</code> to skip checking for an existing pending change
     *        with the same server path
     * @return an existing pending change with the same server path that was
     *         replaced by the new pending change, or <code>null</code> if no
     *         such pending change exists
     */
    public PendingChange add(final PendingChange newPendingChange, final boolean forRefill) {
        Check.notNull(newPendingChange, "newPendingChange"); //$NON-NLS-1$
        Check.notNull(newPendingChange.getServerItem(), "newPendingChange.serverItem"); //$NON-NLS-1$

        PendingChange oldPendingChange = null;

        synchronized (lock) {
            if (!forRefill) {
                oldPendingChange = remove(newPendingChange);
            }

            addInternal(newPendingChange);
        }

        return oldPendingChange;
    }

    /**
     * Only adds the pending changes to the maps.
     *
     * You must acquire the lock before calling this method.
     *
     * @param newPendingChange
     *        the new {@link PendingChange} to add
     * @return an existing pending change with the same server path that was
     *         replaced by the new pending change, or <code>null</code> if no
     *         such pending change exists
     */
    private void addInternal(final PendingChange newPendingChange) {
        Check.notNull(newPendingChange, "newPendingChange"); //$NON-NLS-1$
        Check.notNull(newPendingChange.getServerItem(), "newPendingChange.serverItem"); //$NON-NLS-1$

        final String serverPath = ServerPath.canonicalize(newPendingChange.getServerItem());
        final String[] serverHierarchy = ServerPath.getHierarchy(serverPath);

        String localPath = null;
        String[] localHierarchy = null;

        if (newPendingChange.getLocalItem() != null) {
            localPath = LocalPath.canonicalize(newPendingChange.getLocalItem());
            localHierarchy = LocalPath.getHierarchy(localPath);
        }

        changesByServerPath.put(serverPath, newPendingChange);

        for (int i = 0; i < serverHierarchy.length; i++) {
            Set<PendingChange> changesForPath = changesByParentServerPath.get(serverHierarchy[i]);
            if (changesForPath == null) {
                changesForPath = new HashSet<PendingChange>();
                changesByParentServerPath.put(serverHierarchy[i], changesForPath);
            }
            changesForPath.add(newPendingChange);
        }

        if (localPath != null) {
            changesByLocalPath.put(localPath, newPendingChange);

            for (int i = 0; i < localHierarchy.length; i++) {
                Set<PendingChange> changesForPath = changesByParentLocalPath.get(localHierarchy[i]);
                if (changesForPath == null) {
                    changesForPath = new HashSet<PendingChange>();
                    changesByParentLocalPath.put(localHierarchy[i], changesForPath);
                }
                changesForPath.add(newPendingChange);
            }
        }
    }

    /**
     * Removes an existing pending change from this collection that has the same
     * server path as the specified pending change.
     *
     * @param changeToRemove
     *        specifies which server item to remove
     * @return the existing pending change that was removed, or
     *         <code>null</code> if no such pending change exists
     */
    public PendingChange remove(final PendingChange changeToRemove) {
        Check.notNull(changeToRemove, "changeToRemove"); //$NON-NLS-1$
        Check.notNull(changeToRemove.getServerItem(), "changeToRemove.serverItem"); //$NON-NLS-1$

        PendingChange removedChange;

        synchronized (lock) {
            removedChange = removeInternal(changeToRemove);

            /*
             * If the undone pending change was a renamed folder, this means
             * that any children have had their paths updated. (We are not given
             * new pending changes for the children in this case.)
             */
            if (removedChange != null && removedChange.getChangeType().contains(ChangeType.RENAME)
                    && removedChange.getItemType().equals(ItemType.FOLDER)) {
                retargetChildrenOfUndoneRename(removedChange);
            }
        }

        return removedChange;
    }

    private PendingChange removeInternal(final PendingChange changeToRemove) {
        Check.notNull(changeToRemove, "changeToRemove"); //$NON-NLS-1$
        Check.notNull(changeToRemove.getServerItem(), "changeToRemove.serverItem"); //$NON-NLS-1$

        String serverPath = ServerPath.canonicalize(changeToRemove.getServerItem());
        PendingChange removedChange = changesByServerPath.remove(ServerPath.canonicalize(serverPath));

        /*
         * See if this is a rename pending change - we may need to remove the
         * source item
         */
        if (removedChange == null && changeToRemove.getSourceServerItem() != null) {
            serverPath = ServerPath.canonicalize(changeToRemove.getSourceServerItem());
            removedChange = changesByServerPath.remove(ServerPath.canonicalize(serverPath));
        }

        if (removedChange == null && changeToRemove.getSourceLocalItem() != null) {
            serverPath = workspace.getMappedServerPath(changeToRemove.getSourceLocalItem());

            if (serverPath != null) {
                serverPath = ServerPath.canonicalize(serverPath);
                removedChange = changesByServerPath.remove(serverPath);
            }
        }

        if (removedChange == null) {
            return null;
        }

        /* Update the server path hierarchy */
        final String[] serverHierarchy = ServerPath.getHierarchy(serverPath);
        for (int i = 0; i < serverHierarchy.length; i++) {
            final Set<PendingChange> changesForPath = changesByParentServerPath.get(serverHierarchy[i]);
            changesForPath.remove(removedChange);

            if (changesForPath.size() == 0) {
                changesByParentServerPath.remove(serverHierarchy[i]);
            }
        }

        if (removedChange.getLocalItem() != null) {
            final String localPath = LocalPath.canonicalize(removedChange.getLocalItem());
            final String[] localHierarchy = LocalPath.getHierarchy(localPath);

            changesByLocalPath.remove(localPath);

            for (int i = 0; i < localHierarchy.length; i++) {
                final Set<PendingChange> changesForPath = changesByParentLocalPath.get(localHierarchy[i]);
                changesForPath.remove(removedChange);

                if (changesForPath.size() == 0) {
                    changesByParentLocalPath.remove(localHierarchy[i]);
                }
            }
        }

        return removedChange;
    }

    private void retargetChildrenOfUndoneRename(final PendingChange parentChange) {
        Check.notNull(parentChange, "parentChange"); //$NON-NLS-1$
        Check.notNull(parentChange.getServerItem(), "parentChange.serverItem"); //$NON-NLS-1$

        final Set<PendingChange> childChanges = changesByParentServerPath.get(parentChange.getServerItem());

        if (childChanges == null || childChanges.size() == 0) {
            return;
        }

        final String oldServerPath = parentChange.getServerItem(); // never null
        final String oldLocalPath = parentChange.getLocalItem() != null ? parentChange.getLocalItem()
                : workspace.getMappedLocalPath(oldServerPath);
        String newServerPath = parentChange.getSourceServerItem();
        String newLocalPath = parentChange.getSourceLocalItem();

        if (newServerPath == null && newLocalPath != null) {
            newServerPath = workspace.getMappedServerPath(newLocalPath);
        } else if (newLocalPath == null && newServerPath != null) {
            newLocalPath = workspace.getMappedLocalPath(newServerPath);
        }

        if (oldLocalPath == null || newLocalPath == null || newServerPath == null) {
            log.warn(MessageFormat.format("Could not retarget children of undone rename pending change for {0}", //$NON-NLS-1$
                    oldServerPath));

            return;
        }

        for (final PendingChange childChange : childChanges) {
            /*
             * Dup this pending change before modifying it - callers that added
             * this change may still have a reference.
             */
            final PendingChange newChildChange = new PendingChange(childChange);

            if (newChildChange.getServerItem() != null) {
                newChildChange.setServerItem(ServerPath.combine(newServerPath,
                        ServerPath.makeRelative(newChildChange.getServerItem(), oldServerPath)));
            }

            if (newChildChange.getLocalItem() != null) {
                newChildChange.setLocalItem(LocalPath.combine(newLocalPath,
                        LocalPath.makeRelative(newChildChange.getLocalItem(), oldLocalPath)));
            }

            removeInternal(childChange);
            addInternal(newChildChange);
        }
    }

    /**
     * @return all of the pending changes currently held by this collection
     */
    public PendingChange[] getValues() {
        /*
         * The conversion to an array must happen inside the lock because the
         * lock covers the values in the map.
         */
        synchronized (lock) {
            final Collection<PendingChange> values = changesByServerPath.values();

            if (values == null) {
                return new PendingChange[0];
            }

            return values.toArray(new PendingChange[values.size()]);
        }
    }

    /**
     * Queries for a pending change with the specified server path. For a given
     * workspace, there is at most one pending change at any time for a given
     * server path.
     *
     * @param serverPath
     *        the server path to identify a pending change with
     * @return the corresponding pending change or <code>null</code> if no such
     *         pending change exists
     */
    public PendingChange getValueByServerPath(String serverPath) {
        serverPath = ServerPath.canonicalize(serverPath);

        synchronized (lock) {
            return changesByServerPath.get(serverPath);
        }
    }

    /**
     * Queries for all pending changes for the specified server path or that
     * have the specified local path as a parent.
     *
     * @param serverPath
     *        the parent local path to identify pending changes with
     * @return each pending change that has the server path as a parent (never
     *         <code>null</code>)
     */
    public PendingChange[] getValuesByServerPathRecursive(String serverPath) {
        serverPath = ServerPath.canonicalize(serverPath);

        /*
         * The conversion to an array must happen inside the lock because the
         * lock covers the values in the map.
         */
        synchronized (lock) {
            final Set<PendingChange> changes = changesByParentServerPath.get(serverPath);

            if (changes == null) {
                return new PendingChange[0];
            }

            return changes.toArray(new PendingChange[changes.size()]);
        }
    }

    /**
     * Queries for a pending change with the specified local path. For a given
     * workspace, there is at most one pending change at any time for a given
     * local path.
     *
     * @param localPath
     *        the local path to identify a pending change with
     * @return the corresponding pending change or <code>null</code> if no such
     *         pending change exists
     */
    public PendingChange getValueByLocalPath(String localPath) {
        localPath = LocalPath.canonicalize(localPath);

        synchronized (lock) {
            return changesByLocalPath.get(localPath);
        }
    }

    /**
     * Queries for all pending changes for the specified local path or that have
     * the specified local path as a parent.
     *
     * @param localPath
     *        the parent local path to identify pending changes with
     * @return each pending change that has the local path as a parent (never
     *         <code>null</code>)
     */
    public PendingChange[] getValuesByLocalPathRecursive(String localPath) {
        localPath = LocalPath.canonicalize(localPath);

        /*
         * The conversion to an array must happen inside the lock because the
         * lock covers the values in the map.
         */
        synchronized (lock) {
            final Set<PendingChange> changes = changesByParentLocalPath.get(localPath);

            if (changes == null) {
                return new PendingChange[0];
            }

            return changes.toArray(new PendingChange[changes.size()]);
        }
    }

    /**
     * Tests whether there are changes for the specified local path or that have
     * the specified local path as a parent.
     *
     * @param localPath
     *        the parent local path to identify pending changes with
     * @return <code>true</code> if the given path has pending changes,
     *         <code>false</code> if it does not
     */
    public boolean hasValuesByLocalPathRecursive(String localPath) {
        localPath = LocalPath.canonicalize(localPath);

        synchronized (lock) {
            final Set<PendingChange> changes = changesByParentLocalPath.get(localPath);

            return changes != null && changes.size() > 0;
        }
    }

    /**
     * @return the current number of pending changes held by this collection
     */
    public int size() {
        synchronized (lock) {
            return changesByServerPath.size();
        }
    }
}