com.microsoft.tfs.client.eclipse.tpignore.TPIgnoreCache.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.eclipse.tpignore.TPIgnoreCache.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.eclipse.tpignore;

import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;

import com.microsoft.tfs.util.Check;

/**
 * Handles the in-memory persistence of the exclusions from multiple
 * <code>.tpignore</code> files (one per project) using
 * <code>IgnorableResourcesFileParser</code> as reader. This cache object keeps
 * the resource patterns fresh by periodically (and transparently) reading from
 * the files on disk.
 */
public final class TPIgnoreCache {
    private static final Log log = LogFactory.getLog(TPIgnoreCache.class);

    private static class RefreshAction {
        public final static int NOTHING = 1;
        public final static int LOAD_OR_REFRESH = 2;
        public final static int REMOVE_EXISTING = 3;
        public final static int RETURN_EXISTING = 4;
    }

    /**
     * Caches our patterns we have read from disk. The key is the .tpignore
     * {@link IFile} and the result is a cache entry (which contains compiled
     * patterns and the time they were read from disk).
     */
    private final HashMap<IFile, TPIgnoreCacheEntry> ignoreFileToEntriesMap = new HashMap<IFile, TPIgnoreCacheEntry>();

    private static Pattern[] NO_MATCH_PATTERN_ARRAY = new Pattern[0];

    /**
     * Creates a cache to consult for the latest news in resources that should
     * be ignored during automatic adds.
     */
    public TPIgnoreCache() {
    }

    /**
     * Gets the {@link IFile} the .tpignore file for the given resource might
     * exist.
     *
     * @param resource
     *        the resource to get the .tpignore file for (must not be
     *        <code>null</code>)
     * @return the .tpignore {@link IFile} for the specified resource (the
     *         .tpignore file may not exist), or <code>null</code> if the
     *         resource cannot have a .tpignore file (workspace root)
     */
    public static IFile getIgnoreFile(final IResource resource) {
        Check.notNull(resource, "resource"); //$NON-NLS-1$

        if (resource.getProject() == null) {
            return null;
        }

        return resource.getProject().getFile(TPIgnoreDocument.DEFAULT_FILENAME);
    }

    /**
     * Creates a regular expression to match the given resource. If the resource
     * is a project or folder, the pattern matches every path which starts with
     * that folder path. For files, the expression matches that file exactly.
     *
     * @param resource
     *        the resource to create a pattern for (must not be
     *        <code>null</code>)
     * @return the pattern string
     */
    public static String createIgnorePatternForResource(final IResource resource) {
        Check.notNull(resource, "resource"); //$NON-NLS-1$

        final String path = TPIgnoreCache.createResourceMatchString(resource);

        String patternString = Pattern.quote(path);

        /*
         * Make folders match recursively, other types match only exactly.
         */
        if (resource.getType() == IResource.FOLDER || resource.getType() == IResource.PROJECT) {
            patternString = patternString + ".*"; //$NON-NLS-1$
        }

        return patternString;
    }

    /**
     * Gets the normalized matching string for a resource. The string always
     * starts with a slash, then the Eclipse project-relative path, then a slash
     * only if the resource is a folder or project.
     *
     * @param resource
     *        the resource to get the match string for (must not be
     *        <code>null</code>)
     * @return the match string
     */
    public static String createResourceMatchString(final IResource resource) {
        String matchPath = resource.getProjectRelativePath().toString();

        if (matchPath.startsWith("/") == false) //$NON-NLS-1$
        {
            matchPath = "/" + matchPath; //$NON-NLS-1$
        }

        if ((resource.getType() == IResource.FOLDER || resource.getType() == IResource.PROJECT)
                && matchPath.endsWith("/") == false) //$NON-NLS-1$
        {
            matchPath = matchPath + "/"; //$NON-NLS-1$
        }

        return matchPath;
    }

    /**
     * Gets pattern strings from {@link Pattern} objects.
     *
     * @param patterns
     *        the patterns to get strings for (must not be <code>null</code>)
     * @return an array of strings equal in size to the given collection
     *         containing the pattern strings
     */
    public static String[] getPatternStrings(final Collection<Pattern> patterns) {
        Check.notNull(patterns, "patterns"); //$NON-NLS-1$

        final String[] ret = new String[patterns.size()];

        int i = 0;
        for (final Pattern pattern : patterns) {
            ret[i++] = pattern.toString();
        }

        return ret;
    }

    /**
     * Gets the unique (as computed by {@link TPIgnorePatternComparator})
     * patterns in this cache which match the given resource.
     *
     * @param resource
     *        the resource to find matching patterns for (must not be
     *        <code>null</code>)
     * @return the {@link Pattern}s which match, {@link #NO_MATCH_PATTERN_ARRAY}
     *         if none match
     */
    public Pattern[] getMatchingPatterns(final IResource resource) {
        Check.notNull(resource, "resource"); //$NON-NLS-1$

        if (canMatch(resource)) {
            final Pattern[] patterns = getExclusionPatterns(resource.getProject());

            if (patterns == null || patterns.length == 0) {
                return NO_MATCH_PATTERN_ARRAY;
            }

            final String matchPath = createResourceMatchString(resource);

            final Set<Pattern> matches = new TreeSet<Pattern>(new TPIgnorePatternComparator());

            for (final Pattern pattern : patterns) {
                if (pattern.matcher(matchPath).matches()) {
                    log.debug(MessageFormat.format("item ''{0}'' matched exclusion pattern ''{1}''", //$NON-NLS-1$
                            matchPath, pattern.toString()));

                    matches.add(pattern);
                }
            }

            if (matches.size() > 0) {
                return matches.toArray(new Pattern[matches.size()]);
            }
        }

        return NO_MATCH_PATTERN_ARRAY;
    }

    /**
     * Tests whether the given resource matches any exclusion pattern in this
     * cache.
     *
     * @param resource
     *        the resource to test whether it matches (not null).
     * @return true if the the given resource matches the exclusions set for the
     *         resource's project, false if it does not.
     */
    public boolean matchesAnyPattern(final IResource resource) {
        Check.notNull(resource, "resource"); //$NON-NLS-1$

        if (canMatch(resource)) {
            final Pattern[] patterns = getExclusionPatterns(resource.getProject());

            if (patterns == null || patterns.length == 0) {
                return false;
            }

            final String matchPath = createResourceMatchString(resource);

            for (final Pattern pattern : patterns) {
                if (pattern.matcher(matchPath).matches()) {
                    log.debug(MessageFormat.format("item ''{0}'' matched exclusion pattern ''{1}''", //$NON-NLS-1$
                            matchPath, pattern.toString()));

                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @return <code>true</code> if the specified resource can match cache
     *         entries, <code>false</code> if it is not valid to match it
     */
    private boolean canMatch(final IResource resource) {
        return resource.getType() == IResource.FILE || resource.getType() == IResource.FOLDER
                || resource.getType() == IResource.PROJECT;
    }

    /**
     * Gets the exclusion patterns for the given project.
     *
     * @param project
     *        the Eclipse project to get exclusions for (not null).
     * @return an array of the regular expression patterns loaded from the
     *         .tpignore file for the given project, or null if none were loaded
     *         because the file did not exist or an error occurred reading it.
     */
    private Pattern[] getExclusionPatterns(final IProject project) {
        Check.notNull(project, "project"); //$NON-NLS-1$

        final IFile ignoreFile = getIgnoreFile(project);
        if (ignoreFile == null) {
            return null;
        }

        final boolean ignoreFileExists = ignoreFile.exists();
        final long ignoreFileLastModified = ignoreFile.getModificationStamp();

        Pattern[] ret = null;

        synchronized (ignoreFileToEntriesMap) {
            final boolean alreadyHaveEntry = ignoreFileToEntriesMap.containsKey(ignoreFile);

            TPIgnoreCacheEntry existingEntry = null;

            int refreshAction = RefreshAction.NOTHING;

            if (ignoreFileExists && alreadyHaveEntry) {
                existingEntry = ignoreFileToEntriesMap.get(ignoreFile);

                // We have the file on disk and an entry, so update it if the
                // dates are new, otherwise return what we have.
                if (existingEntry.getLoadedFromDiskTime() != ignoreFileLastModified) {
                    refreshAction = RefreshAction.LOAD_OR_REFRESH;
                } else {
                    refreshAction = RefreshAction.RETURN_EXISTING;
                }
            } else if (ignoreFileExists && alreadyHaveEntry == false) {
                refreshAction = RefreshAction.LOAD_OR_REFRESH;
            } else if (ignoreFileExists == false && alreadyHaveEntry) {
                refreshAction = RefreshAction.REMOVE_EXISTING;
            } else {
                /*
                 * File does not exist and we have no entry, so nothing to do.
                 */
            }

            switch (refreshAction) {
            case RefreshAction.LOAD_OR_REFRESH:
                log.trace("LOAD_OR_REFRESH"); //$NON-NLS-1$

                ret = TPIgnoreFileParser.load(ignoreFile);

                /*
                 * Null patterns means error reading file, so remove the
                 * entry so it can be parsed again later.
                 */
                if (ret == null) {
                    ignoreFileToEntriesMap.remove(ignoreFile);
                } else {
                    /*
                     * Query the file's mod time again so we're most
                     * accurate in heading off future re-reads.
                     */
                    ignoreFileToEntriesMap.put(ignoreFile,
                            new TPIgnoreCacheEntry(ignoreFile.getModificationStamp(), ret));
                }

                break;
            case RefreshAction.REMOVE_EXISTING:
                log.trace("REMOVE_EXISTING"); //$NON-NLS-1$
                ignoreFileToEntriesMap.remove(ignoreFile);
                break;
            case RefreshAction.RETURN_EXISTING:
                log.trace("RETURN_EXISTING"); //$NON-NLS-1$
                Check.notNull(existingEntry, "existingEntry"); //$NON-NLS-1$
                ret = existingEntry.getPatterns();
                break;
            case RefreshAction.NOTHING:
                log.trace("NOTHING"); //$NON-NLS-1$
                // Nothing to load or return.
                break;
            }
        }

        return ret;
    }
}