/*
 * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

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.

Supported are the wildcard characters * and ? and groups with:

  • characters e.g. [abc]
  • ranges e.g. [a-z]
  • the following character classes
    • [:alnum:]
    • [:alpha:]
    • [:blank:]
    • [:cntrl:]
    • [:digit:]
    • [:graph:]
    • [:lower:]
    • [:print:]
    • [:punct:]
    • [:space:]
    • [:upper:]
    • [:word:]
    • [:xdigit:]
    e. g. [[:xdigit:]]
Any character can be escaped by prepending it with a \
/** * 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> * Any character can be escaped by prepending it with a \ */
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;
{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.
/** * {{@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;
Params:
  • headsStartValue – must be a list which will never be modified.
/** * * @param headsStartValue * must be a list which will never be modified. */
private FileNameMatcher(List<Head> headsStartValue) { this(headsStartValue, headsStartValue); }
Params:
  • headsStartValue – must be a list which will never be modified.
  • heads – a list which will be cloned and then used as current head list.
/** * * @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<>(heads.size()); this.heads.addAll(heads); this.listForLocalUseage = new ArrayList<>(heads.size()); }
Constructor for FileNameMatcher
Params:
  • patternString – must contain a pattern which fnmatch would accept.
  • invalidWildgetCharacter – if this parameter isn't null then this character will not match at wildcards(* and ? are wildcards).
Throws:
/** * Constructor for FileNameMatcher * * @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 org.eclipse.jgit.errors.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 FileNameMatcher with the same state and reset point like other.
Params:
/** * A Copy Constructor which creates a new * {@link org.eclipse.jgit.fnmatch.FileNameMatcher} with the same state and * reset point like <code>other</code>. * * @param other * another {@link org.eclipse.jgit.fnmatch.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<>(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<>(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 = indexOfUnescaped(pattern, ']', 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<>(); while (currentIndex < pattern.length()) { final int groupStart = indexOfUnescaped(pattern, '[', 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<>( patternPart.length()); boolean escaped = false; for (int i = 0; i < patternPart.length(); i++) { final char c = patternPart.charAt(i); if (escaped) { final CharacterHead head = new CharacterHead(c); heads.add(head); escaped = false; } else { switch (c) { case '*': { final AbstractHead head = createWildCardHead( invalidWildgetCharacter, true); heads.add(head); break; } case '?': { final AbstractHead head = createWildCardHead( invalidWildgetCharacter, false); heads.add(head); break; } case '\\': escaped = true; 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); } return new WildCardHead(star); }
Params:
  • c – new character to append
Returns:true to continue, false if the matcher can stop appending
/** * @param c new character to append * @return true to continue, false if the matcher can stop appending */
private boolean extendStringToMatchByOneCharacter(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) { if (!headsToAdd.isEmpty()) newHeads.addAll(headsToAdd); lastAddedHeads = headsToAdd; } } listForLocalUseage = heads; heads = newHeads; return !newHeads.isEmpty(); } private static int indexOfUnescaped(final String searchString, final char ch, final int fromIndex) { for (int i = fromIndex; i < searchString.length(); i++) { char current = searchString.charAt(i); if (current == ch) return i; if (current == '\\') i++; // Skip the next char as it is escaped } } return -1; }
Append to the string which is matched against the patterns of this class
Params:
  • stringToMatch – extends the string which is matched against the patterns of this class.
/** * Append to the string which is matched against the patterns of this class * * @param stringToMatch * extends the string which is matched against the patterns of * this class. */
public void append(String stringToMatch) { for (int i = 0; i < stringToMatch.length(); i++) { final char c = stringToMatch.charAt(i); if (!extendStringToMatchByOneCharacter(c)) break; } }
Resets this matcher to it's state right after construction.
/** * Resets this matcher to it's state right after construction. */
public void reset() { heads.clear(); heads.addAll(headsStartValue); }
Create a FileNameMatcher instance which uses the same pattern like this matcher, but has the current state of this matcher as reset and start point
Returns:a FileNameMatcher instance which uses the same pattern like this matcher, but has the current state of this matcher as reset and start point.
/** * Create a {@link org.eclipse.jgit.fnmatch.FileNameMatcher} instance which * uses the same pattern like this matcher, but has the current state of * this matcher as reset and start point * * @return a {@link org.eclipse.jgit.fnmatch.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<>(heads.size()); copyOfHeads.addAll(heads); return new FileNameMatcher(copyOfHeads); }
Whether the matcher matches
Returns:whether the matcher matches
/** * Whether the matcher matches * * @return whether the matcher matches */
public boolean isMatch() { if (heads.isEmpty()) return false; final ListIterator<Head> headIterator = heads .listIterator(heads.size()); while (headIterator.hasPrevious()) { final Head head = headIterator.previous(); if (head == LastHead.INSTANCE) { return true; } } return false; }
Whether a match can be appended
Returns:a boolean.
/** * Whether a match can be appended * * @return a boolean. */
public boolean canAppendMatch() { for (Head head : heads) { if (head != LastHead.INSTANCE) { return true; } } return false; } }