Package org.eclipse.jgit.fnmatch

Source Code of org.eclipse.jgit.fnmatch.FileNameMatcher

/*
* Copyright (C) 2008, Florian Koeberle <florianskarten@web.de>
* Copyright (C) 2008, Florian Köberle <florianskarten@web.de>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
*   notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
*   copyright notice, this list of conditions and the following
*   disclaimer in the documentation and/or other materials provided
*   with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
*   names of its contributors may be used to endorse or promote
*   products derived from this software without specific prior
*   written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.eclipse.jgit.fnmatch;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jgit.errors.InvalidPatternException;
import org.eclipse.jgit.errors.NoClosingBracketException;

/**
* This class can be used to match filenames against fnmatch like patterns. It
* is not thread save.
* <p>
* Supported are the wildcard characters * and ? and groups with:
* <ul>
* <li> characters e.g. [abc]</li>
* <li> ranges e.g. [a-z]</li>
* <li> the following character classes
* <ul>
* <li>[:alnum:]</li>
* <li>[:alpha:]</li>
* <li>[:blank:]</li>
* <li>[:cntrl:]</li>
* <li>[:digit:]</li>
* <li>[:graph:]</li>
* <li>[:lower:]</li>
* <li>[:print:]</li>
* <li>[:punct:]</li>
* <li>[:space:]</li>
* <li>[:upper:]</li>
* <li>[:word:]</li>
* <li>[:xdigit:]</li>
* </ul>
* e. g. [[:xdigit:]] </li>
* </ul>
* </p>
*/
public class FileNameMatcher {
  static final List<Head> EMPTY_HEAD_LIST = Collections.emptyList();

  private static final Pattern characterClassStartPattern = Pattern
      .compile("\\[[.:=]"); //$NON-NLS-1$

  private List<Head> headsStartValue;

  private List<Head> heads;

  /**
   * {{@link #extendStringToMatchByOneCharacter(char)} needs a list for the
   * new heads, allocating a new array would be bad for the performance, as
   * the method gets called very often.
   *
   */
  private List<Head> listForLocalUseage;

  /**
   *
   * @param headsStartValue
   *            must be a list which will never be modified.
   */
  private FileNameMatcher(final List<Head> headsStartValue) {
    this(headsStartValue, headsStartValue);
  }

  /**
   *
   * @param headsStartValue
   *            must be a list which will never be modified.
   * @param heads
   *            a list which will be cloned and then used as current head
   *            list.
   */
  private FileNameMatcher(final List<Head> headsStartValue,
      final List<Head> heads) {
    this.headsStartValue = headsStartValue;
    this.heads = new ArrayList<Head>(heads.size());
    this.heads.addAll(heads);
    this.listForLocalUseage = new ArrayList<Head>(heads.size());
  }

  /**
   * @param patternString
   *            must contain a pattern which fnmatch would accept.
   * @param invalidWildgetCharacter
   *            if this parameter isn't null then this character will not
   *            match at wildcards(* and ? are wildcards).
   * @throws InvalidPatternException
   *             if the patternString contains a invalid fnmatch pattern.
   */
  public FileNameMatcher(final String patternString,
      final Character invalidWildgetCharacter)
      throws InvalidPatternException {
    this(createHeadsStartValues(patternString, invalidWildgetCharacter));
  }

  /**
   * A Copy Constructor which creates a new {@link FileNameMatcher} with the
   * same state and reset point like <code>other</code>.
   *
   * @param other
   *            another {@link FileNameMatcher} instance.
   */
  public FileNameMatcher(FileNameMatcher other) {
    this(other.headsStartValue, other.heads);
  }

  private static List<Head> createHeadsStartValues(
      final String patternString, final Character invalidWildgetCharacter)
      throws InvalidPatternException {

    final List<AbstractHead> allHeads = parseHeads(patternString,
        invalidWildgetCharacter);

    List<Head> nextHeadsSuggestion = new ArrayList<Head>(2);
    nextHeadsSuggestion.add(LastHead.INSTANCE);
    for (int i = allHeads.size() - 1; i >= 0; i--) {
      final AbstractHead head = allHeads.get(i);

      // explanation:
      // a and * of the pattern "a*b"
      // need *b as newHeads
      // that's why * extends the list for it self and it's left neighbor.
      if (head.isStar()) {
        nextHeadsSuggestion.add(head);
        head.setNewHeads(nextHeadsSuggestion);
      } else {
        head.setNewHeads(nextHeadsSuggestion);
        nextHeadsSuggestion = new ArrayList<Head>(2);
        nextHeadsSuggestion.add(head);
      }
    }
    return nextHeadsSuggestion;
  }

  private static int findGroupEnd(final int indexOfStartBracket,
      final String pattern) throws InvalidPatternException {
    int firstValidCharClassIndex = indexOfStartBracket + 1;
    int firstValidEndBracketIndex = indexOfStartBracket + 2;

    if (indexOfStartBracket + 1 >= pattern.length())
      throw new NoClosingBracketException(indexOfStartBracket, "[", "]", //$NON-NLS-1$ //$NON-NLS-2$
          pattern);

    if (pattern.charAt(firstValidCharClassIndex) == '!') {
      firstValidCharClassIndex++;
      firstValidEndBracketIndex++;
    }

    final Matcher charClassStartMatcher = characterClassStartPattern
        .matcher(pattern);

    int groupEnd = -1;
    while (groupEnd == -1) {

      final int possibleGroupEnd = pattern.indexOf(']',
          firstValidEndBracketIndex);
      if (possibleGroupEnd == -1)
        throw new NoClosingBracketException(indexOfStartBracket, "[", //$NON-NLS-1$
            "]", pattern); //$NON-NLS-1$

      final boolean foundCharClass = charClassStartMatcher
          .find(firstValidCharClassIndex);

      if (foundCharClass
          && charClassStartMatcher.start() < possibleGroupEnd) {

        final String classStart = charClassStartMatcher.group(0);
        final String classEnd = classStart.charAt(1) + "]"; //$NON-NLS-1$

        final int classStartIndex = charClassStartMatcher.start();
        final int classEndIndex = pattern.indexOf(classEnd,
            classStartIndex + 2);

        if (classEndIndex == -1)
          throw new NoClosingBracketException(classStartIndex,
              classStart, classEnd, pattern);

        firstValidCharClassIndex = classEndIndex + 2;
        firstValidEndBracketIndex = firstValidCharClassIndex;
      } else {
        groupEnd = possibleGroupEnd;
      }
    }
    return groupEnd;
  }

  private static List<AbstractHead> parseHeads(final String pattern,
      final Character invalidWildgetCharacter)
      throws InvalidPatternException {

    int currentIndex = 0;
    List<AbstractHead> heads = new ArrayList<AbstractHead>();
    while (currentIndex < pattern.length()) {
      final int groupStart = pattern.indexOf('[', currentIndex);
      if (groupStart == -1) {
        final String patternPart = pattern.substring(currentIndex);
        heads.addAll(createSimpleHeads(patternPart,
            invalidWildgetCharacter));
        currentIndex = pattern.length();
      } else {
        final String patternPart = pattern.substring(currentIndex,
            groupStart);
        heads.addAll(createSimpleHeads(patternPart,
            invalidWildgetCharacter));

        final int groupEnd = findGroupEnd(groupStart, pattern);
        final String groupPart = pattern.substring(groupStart + 1,
            groupEnd);
        heads.add(new GroupHead(groupPart, pattern));
        currentIndex = groupEnd + 1;
      }
    }
    return heads;
  }

  private static List<AbstractHead> createSimpleHeads(
      final String patternPart, final Character invalidWildgetCharacter) {
    final List<AbstractHead> heads = new ArrayList<AbstractHead>(
        patternPart.length());
    for (int i = 0; i < patternPart.length(); i++) {
      final char c = patternPart.charAt(i);
      switch (c) {
      case '*': {
        final AbstractHead head = createWildCardHead(
            invalidWildgetCharacter, true);
        heads.add(head);
        break;
      }
      case '?': {
        final AbstractHead head = createWildCardHead(
            invalidWildgetCharacter, false);
        heads.add(head);
        break;
      }
      default:
        final CharacterHead head = new CharacterHead(c);
        heads.add(head);
      }
    }
    return heads;
  }

  private static AbstractHead createWildCardHead(
      final Character invalidWildgetCharacter, final boolean star) {
    if (invalidWildgetCharacter != null)
      return new RestrictedWildCardHead(invalidWildgetCharacter
          .charValue(), star);
    else
      return new WildCardHead(star);
  }

  private void extendStringToMatchByOneCharacter(final char c) {
    final List<Head> newHeads = listForLocalUseage;
    newHeads.clear();
    List<Head> lastAddedHeads = null;
    for (int i = 0; i < heads.size(); i++) {
      final Head head = heads.get(i);
      final List<Head> headsToAdd = head.getNextHeads(c);
      // Why the next performance optimization isn't wrong:
      // Some times two heads return the very same list.
      // We save future effort if we don't add these heads again.
      // This is the case with the heads "a" and "*" of "a*b" which
      // both can return the list ["*","b"]
      if (headsToAdd != lastAddedHeads) {
        newHeads.addAll(headsToAdd);
        lastAddedHeads = headsToAdd;
      }
    }
    listForLocalUseage = heads;
    heads = newHeads;
  }

  /**
   *
   * @param stringToMatch
   *            extends the string which is matched against the patterns of
   *            this class.
   */
  public void append(final String stringToMatch) {
    for (int i = 0; i < stringToMatch.length(); i++) {
      final char c = stringToMatch.charAt(i);
      extendStringToMatchByOneCharacter(c);
    }
  }

  /**
   * Resets this matcher to it's state right after construction.
   */
  public void reset() {
    heads.clear();
    heads.addAll(headsStartValue);
  }

  /**
   *
   * @return a {@link FileNameMatcher} instance which uses the same pattern
   *         like this matcher, but has the current state of this matcher as
   *         reset and start point.
   */
  public FileNameMatcher createMatcherForSuffix() {
    final List<Head> copyOfHeads = new ArrayList<Head>(heads.size());
    copyOfHeads.addAll(heads);
    return new FileNameMatcher(copyOfHeads);
  }

  /**
   *
   * @return true, if the string currently being matched does match.
   */
  public boolean isMatch() {
    final ListIterator<Head> headIterator = heads
        .listIterator(heads.size());
    while (headIterator.hasPrevious()) {
      final Head head = headIterator.previous();
      if (head == LastHead.INSTANCE) {
        return true;
      }
    }
    return false;
  }

  /**
   *
   * @return false, if the string being matched will not match when the string
   *         gets extended.
   */
  public boolean canAppendMatch() {
    for (int i = 0; i < heads.size(); i++) {
      if (heads.get(i) != LastHead.INSTANCE) {
        return true;
      }
    }
    return false;
  }
}
TOP

Related Classes of org.eclipse.jgit.fnmatch.FileNameMatcher

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.