You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gn...@apache.org on 2017/05/04 11:26:26 UTC

svn commit: r1793780 - in /felix/trunk/gogo/runtime/src: main/java/org/apache/felix/gogo/runtime/ test/java/org/apache/felix/gogo/runtime/

Author: gnodet
Date: Thu May  4 11:26:26 2017
New Revision: 1793780

URL: http://svn.apache.org/viewvc?rev=1793780&view=rev
Log:
[FELIX-5634][gogo][runtime] The file name generation may loop into subtrees for nothing

Added:
    felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java
    felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java
    felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java
Modified:
    felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java

Modified: felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java
URL: http://svn.apache.org/viewvc/felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java?rev=1793780&r1=1793779&r2=1793780&view=diff
==============================================================================
--- felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java (original)
+++ felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java Thu May  4 11:26:26 2017
@@ -24,7 +24,6 @@ import java.nio.file.FileVisitResult;
 import java.nio.file.FileVisitor;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.PathMatcher;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -601,7 +600,7 @@ public class Expander extends BaseTokeni
             dir = currentDir;
             prefix = "";
         }
-        final PathMatcher matcher = dir.getFileSystem().getPathMatcher("glob:" + arg);
+        final GlobPathMatcher matcher = new GlobPathMatcher(arg.toString());
         Files.walkFileTree(dir,
                 EnumSet.of(FileVisitOption.FOLLOW_LINKS),
                 Integer.MAX_VALUE,
@@ -619,11 +618,18 @@ public class Expander extends BaseTokeni
                             return FileVisitResult.SKIP_SUBTREE;
                         }
                         Path r = dir.relativize(file);
-                        if (matcher.matches(r))
+                        if (matcher.matches(r.toString(), true))
                         {
                             expanded.add(prefix + r.toString());
                         }
-                        return FileVisitResult.CONTINUE;
+                        if (matcher.matches(r.toString(), false))
+                        {
+                            return FileVisitResult.CONTINUE;
+                        }
+                        else
+                        {
+                            return FileVisitResult.SKIP_SUBTREE;
+                        }
                     }
 
                     @Override
@@ -632,7 +638,7 @@ public class Expander extends BaseTokeni
                         if (!Files.isHidden(file))
                         {
                             Path r = dir.relativize(file);
-                            if (matcher.matches(r))
+                            if (matcher.matches(r.toString(), true))
                             {
                                 expanded.add(prefix + r.toString());
                             }

Added: felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java
URL: http://svn.apache.org/viewvc/felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java?rev=1793780&view=auto
==============================================================================
--- felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java (added)
+++ felix/trunk/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java Thu May  4 11:26:26 2017
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.gogo.runtime;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+
+/**
+ * Freely adapted from Spring's AntPathMatcher.
+ * We don't use the file system's glob PathMatcher
+ * because it can't detect directories which can't be
+ * a start of a match.
+ */
+public class GlobPathMatcher {
+
+    /** Default path separator: "/" */
+    public static final String DEFAULT_PATH_SEPARATOR = "/";
+
+    private String pattern;
+    private String pathSeparator;
+    private boolean caseSensitive;
+
+    private String[] pattDirs;
+    private Pattern[] pattPats;
+
+    /**
+     * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}.
+     */
+    public GlobPathMatcher(String pattern) {
+        this(pattern, DEFAULT_PATH_SEPARATOR, true);
+    }
+
+    /**
+     * A convenient, alternative constructor to use with a custom path separator.
+     * @param pathSeparator the path separator to use, must not be {@code null}.
+     */
+    public GlobPathMatcher(String pattern, String pathSeparator, boolean caseSensitive) {
+        Objects.requireNonNull(pathSeparator, "'pathSeparator' is required");
+        this.pattern = pattern;
+        this.pathSeparator = pathSeparator;
+        this.caseSensitive = caseSensitive;
+        this.pattDirs = tokenizePath(pattern);
+        this.pattPats = new Pattern[pattDirs.length];
+        for (int i = 0; i < pattDirs.length; i++) {
+            pattPats[i] = createMatcherPattern(pattDirs[i]);
+        }
+    }
+
+
+    /**
+     * Actually match the given {@code path} against the given {@code pattern}.
+     * @param path the path String to test
+     * @param fullMatch whether a full pattern match is required (else a pattern match
+     * as far as the given base path goes is sufficient)
+     * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't
+     */
+    public boolean matches(String path, boolean fullMatch) {
+        if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
+            return false;
+        }
+
+        String[] pathDirs = tokenizePath(path);
+
+        int pattIdxStart = 0;
+        int pattIdxEnd = pattDirs.length - 1;
+        int pathIdxStart = 0;
+        int pathIdxEnd = pathDirs.length - 1;
+
+        // Match all elements up to the first **
+        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+            String pattDir = pattDirs[pattIdxStart];
+            if ("**".equals(pattDir)) {
+                break;
+            }
+            if (!matchStrings(pattIdxStart, pathDirs[pathIdxStart])) {
+                return false;
+            }
+            pattIdxStart++;
+            pathIdxStart++;
+        }
+
+        if (pathIdxStart > pathIdxEnd) {
+            // Path is exhausted, only match if rest of pattern is * or **'s
+            if (pattIdxStart > pattIdxEnd) {
+                return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));
+            }
+            if (!fullMatch) {
+                return true;
+            }
+            if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
+                return true;
+            }
+            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+                if (!pattDirs[i].equals("**")) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        else if (pattIdxStart > pattIdxEnd) {
+            // String not exhausted, but pattern is. Failure.
+            return false;
+        }
+        else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
+            // Path start definitely matches due to "**" part in pattern.
+            return true;
+        }
+
+        // up to last '**'
+        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+            String pattDir = pattDirs[pattIdxEnd];
+            if (pattDir.equals("**")) {
+                break;
+            }
+            if (!matchStrings(pattIdxEnd, pathDirs[pathIdxEnd])) {
+                return false;
+            }
+            pattIdxEnd--;
+            pathIdxEnd--;
+        }
+        if (pathIdxStart > pathIdxEnd) {
+            // String is exhausted
+            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+                if (!pattDirs[i].equals("**")) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+            int patIdxTmp = -1;
+            for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
+                if (pattDirs[i].equals("**")) {
+                    patIdxTmp = i;
+                    break;
+                }
+            }
+            if (patIdxTmp == pattIdxStart + 1) {
+                // '**/**' situation, so skip one
+                pattIdxStart++;
+                continue;
+            }
+            // Find the pattern between padIdxStart & padIdxTmp in str between
+            // strIdxStart & strIdxEnd
+            int patLength = (patIdxTmp - pattIdxStart - 1);
+            int strLength = (pathIdxEnd - pathIdxStart + 1);
+            int foundIdx = -1;
+
+            strLoop:
+            for (int i = 0; i <= strLength - patLength; i++) {
+                for (int j = 0; j < patLength; j++) {
+                    String subStr = pathDirs[pathIdxStart + i + j];
+                    if (!matchStrings(pattIdxStart + j + 1, subStr)) {
+                        continue strLoop;
+                    }
+                }
+                foundIdx = pathIdxStart + i;
+                break;
+            }
+
+            if (foundIdx == -1) {
+                return false;
+            }
+
+            pattIdxStart = patIdxTmp;
+            pathIdxStart = foundIdx + patLength;
+        }
+
+        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+            if (!pattDirs[i].equals("**")) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Tokenize the given path String into parts, based on this matcher's settings.
+     * @param path the path to tokenize
+     * @return the tokenized path parts
+     */
+    private String[] tokenizePath(String path) {
+        StringTokenizer st = new StringTokenizer(path, pathSeparator);
+        List<String> tokens = new ArrayList<>();
+        while (st.hasMoreTokens()) {
+            String token = st.nextToken();
+            if (token.length() > 0) {
+                tokens.add(token);
+            }
+        }
+        return tokens.toArray(new String[tokens.size()]);
+    }
+
+    private boolean matchStrings(int pattIdx, String str) {
+        return pattPats[pattIdx].matcher(str).matches();
+    }
+
+    private Pattern createMatcherPattern(String pattern) {
+        StringBuilder sb = new StringBuilder(pattern.length());
+        int inGroup = 0;
+        int inClass = 0;
+        int firstIndexInClass = -1;
+        char[] arr = pattern.toCharArray();
+        for (int i = 0; i < arr.length; i++) {
+            char ch = arr[i];
+            switch (ch) {
+                case '\\':
+                    if (++i >= arr.length) {
+                        sb.append('\\');
+                    } else {
+                        char next = arr[i];
+                        switch (next) {
+                            case ',':
+                                // escape not needed
+                                break;
+                            case 'Q':
+                            case 'E':
+                                // extra escape needed
+                                sb.append("\\\\");
+                                break;
+                            default:
+                                sb.append('\\');
+                                break;
+                        }
+                        sb.append(next);
+                    }
+                    break;
+                case '*':
+                    sb.append(inClass == 0 ? ".*" : "*");
+                    break;
+                case '?':
+                    sb.append(inClass == 0 ? '.' : '?');
+                    break;
+                case '[':
+                    inClass++;
+                    firstIndexInClass = i + 1;
+                    sb.append('[');
+                    break;
+                case ']':
+                    inClass--;
+                    sb.append(']');
+                    break;
+                case '.':
+                case '(':
+                case ')':
+                case '+':
+                case '|':
+                case '^':
+                case '$':
+                case '@':
+                case '%':
+                    if (inClass == 0 || (firstIndexInClass == i && ch == '^')) {
+                        sb.append('\\');
+                    }
+                    sb.append(ch);
+                    break;
+                case '!':
+                    sb.append(firstIndexInClass == i ? '^' : '!');
+                    break;
+                case '{':
+                    inGroup++;
+                    sb.append('(');
+                    break;
+                case '}':
+                    inGroup--;
+                    sb.append(')');
+                    break;
+                case ',':
+                    sb.append(inGroup > 0 ? '|' : ',');
+                    break;
+                default:
+                    sb.append(ch);
+            }
+        }
+        return (caseSensitive ? Pattern.compile(sb.toString()) :
+                Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE));
+    }
+
+
+}
\ No newline at end of file

Added: felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java?rev=1793780&view=auto
==============================================================================
--- felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java (added)
+++ felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java Thu May  4 11:26:26 2017
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.gogo.runtime;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ExpanderTest {
+
+    @Test
+    public void testGenerateFiles() throws IOException {
+        final Path testdir = Paths.get(".").toAbsolutePath().resolve("target/testdir").normalize();
+        Evaluate evaluate = new Evaluate() {
+            @Override
+            public Object eval(Token t) throws Exception {
+                return null;
+            }
+            @Override
+            public Object get(String key) {
+                if ("HOME".equals(key)) {
+                    return testdir.resolve("Users/gogo");
+                }
+                return null;
+            }
+            @Override
+            public Object put(String key, Object value) {
+                return null;
+            }
+            @Override
+            public Object expr(Token t) {
+                return null;
+            }
+            @Override
+            public Path currentDir() {
+                return testdir.resolve("Users/gogo/karaf/home");
+            }
+        };
+        deleteRecursive(testdir);
+        Files.createDirectories(testdir);
+        Files.createDirectories(evaluate.currentDir());
+        Path home = Paths.get(evaluate.get("HOME").toString());
+        Files.createDirectories(home);
+        Files.createFile(home.resolve("test1.txt"));
+        Files.createDirectories(home.resolve("child"));
+        Files.createFile(home.resolve("child/test2.txt"));
+
+        Expander expander = new Expander("", evaluate, false, false, false, false, false);
+        List<? extends CharSequence> files = expander.generateFileNames("~/*.[tx][v-z][!a]");
+        assertNotNull(files);
+        assertEquals(1, files.size());
+        assertEquals("test1.txt", home.relativize(Paths.get(files.get(0).toString())).toString());
+    }
+
+    private static void deleteRecursive(Path file) throws IOException {
+        if (file != null) {
+            if (Files.isDirectory(file)) {
+                for (Path child : Files.newDirectoryStream(file)) {
+                    deleteRecursive(child);
+                }
+            }
+            Files.deleteIfExists(file);
+        }
+    }
+}

Added: felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java?rev=1793780&view=auto
==============================================================================
--- felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java (added)
+++ felix/trunk/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java Thu May  4 11:26:26 2017
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.gogo.runtime;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Part of these test case have been kindly borrowed from
+ * https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java
+ */
+public class GlobPathMatcherTest {
+
+    @Test
+    public void isMatchWithCaseSensitiveWithDefaultPathSeparator() throws Exception {
+
+        final Builder.Matcher pathMatcher = new Builder().build();
+
+        // test exact matching
+        assertTrue(pathMatcher.isMatch("test", "test"));
+        assertTrue(pathMatcher.isMatch("/test", "/test"));
+        assertTrue(pathMatcher.isMatch("http://example.org", "http://example.org")); // SPR-14141
+        assertFalse(pathMatcher.isMatch("/test.jpg", "test.jpg"));
+        assertFalse(pathMatcher.isMatch("test", "/test"));
+        assertFalse(pathMatcher.isMatch("/test", "test"));
+
+        // test matching with ?'s
+        assertTrue(pathMatcher.isMatch("t?st", "test"));
+        assertTrue(pathMatcher.isMatch("??st", "test"));
+        assertTrue(pathMatcher.isMatch("tes?", "test"));
+        assertTrue(pathMatcher.isMatch("te??", "test"));
+        assertTrue(pathMatcher.isMatch("?es?", "test"));
+        assertFalse(pathMatcher.isMatch("tes?", "tes"));
+        assertFalse(pathMatcher.isMatch("tes?", "testt"));
+        assertFalse(pathMatcher.isMatch("tes?", "tsst"));
+
+        // test matching with *'s
+        assertTrue(pathMatcher.isMatch("*", "test"));
+        assertTrue(pathMatcher.isMatch("test*", "test"));
+        assertTrue(pathMatcher.isMatch("test*", "testTest"));
+        assertTrue(pathMatcher.isMatch("test/*", "test/Test"));
+        assertTrue(pathMatcher.isMatch("test/*", "test/t"));
+        assertTrue(pathMatcher.isMatch("test/*", "test/"));
+        assertTrue(pathMatcher.isMatch("*test*", "AnothertestTest"));
+        assertTrue(pathMatcher.isMatch("*test", "Anothertest"));
+        assertTrue(pathMatcher.isMatch("*.*", "test."));
+        assertTrue(pathMatcher.isMatch("*.*", "test.test"));
+        assertTrue(pathMatcher.isMatch("*.*", "test.test.test"));
+        assertTrue(pathMatcher.isMatch("test*aaa", "testblaaaa"));
+        assertFalse(pathMatcher.isMatch("test*", "tst"));
+        assertFalse(pathMatcher.isMatch("test*", "tsttest"));
+        assertFalse(pathMatcher.isMatch("test*", "test/"));
+        assertFalse(pathMatcher.isMatch("test*", "test/t"));
+        assertFalse(pathMatcher.isMatch("test/*", "test"));
+        assertFalse(pathMatcher.isMatch("*test*", "tsttst"));
+        assertFalse(pathMatcher.isMatch("*test", "tsttst"));
+        assertFalse(pathMatcher.isMatch("*.*", "tsttst"));
+        assertFalse(pathMatcher.isMatch("test*aaa", "test"));
+        assertFalse(pathMatcher.isMatch("test*aaa", "testblaaab"));
+
+        // test matching with ?'s and /'s
+        assertTrue(pathMatcher.isMatch("/?", "/a"));
+        assertTrue(pathMatcher.isMatch("/?/a", "/a/a"));
+        assertTrue(pathMatcher.isMatch("/a/?", "/a/b"));
+        assertTrue(pathMatcher.isMatch("/??/a", "/aa/a"));
+        assertTrue(pathMatcher.isMatch("/a/??", "/a/bb"));
+        assertTrue(pathMatcher.isMatch("/?", "/a"));
+
+        // test matching with **'s
+        assertTrue(pathMatcher.isMatch("/**", "/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/*/**", "/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/**/*", "/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla"));
+        assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla/bla"));
+        assertTrue(pathMatcher.isMatch("/**/test", "/bla/bla/test"));
+        assertTrue(pathMatcher.isMatch("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla"));
+        assertTrue(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbla/test"));
+        assertTrue(pathMatcher.isMatch("/*bla/test", "/XXXbla/test"));
+        assertFalse(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbl/test"));
+        assertFalse(pathMatcher.isMatch("/*bla/test", "XXXblab/test"));
+        assertFalse(pathMatcher.isMatch("/*bla/test", "XXXbl/test"));
+
+        assertFalse(pathMatcher.isMatch("/????", "/bala/bla"));
+        assertFalse(pathMatcher.isMatch("/**/*bla", "/bla/bla/bla/bbb"));
+
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/"));
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing"));
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg"));
+
+        assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/"));
+        assertTrue(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing"));
+        assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing"));
+        assertFalse(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing"));
+
+        assertFalse(pathMatcher.isMatch("/x/x/**/bla", "/x/x/x/"));
+
+        assertTrue(pathMatcher.isMatch("/foo/bar/**", "/foo/bar"));
+
+        assertTrue(pathMatcher.isMatch("", ""));
+
+        assertTrue(pathMatcher.isMatch("/foo/bar/**", "/foo/bar"));
+        assertTrue(pathMatcher.isMatch("/resource/1", "/resource/1"));
+        assertTrue(pathMatcher.isMatch("/resource/*", "/resource/1"));
+        assertTrue(pathMatcher.isMatch("/resource/*/", "/resource/1/"));
+        assertTrue(pathMatcher.isMatch("/top-resource/*/resource/*/sub-resource/*", "/top-resource/1/resource/2/sub-resource/3"));
+        assertTrue(pathMatcher.isMatch("/top-resource/*/resource/*/sub-resource/*", "/top-resource/999999/resource/8888888/sub-resource/77777777"));
+        assertTrue(pathMatcher.isMatch("/*/*/*/*/secret.html", "/this/is/protected/path/secret.html"));
+        assertTrue(pathMatcher.isMatch("/*/*/*/*/*.html", "/this/is/protected/path/secret.html"));
+        assertTrue(pathMatcher.isMatch("/*/*/*/*", "/this/is/protected/path"));
+        assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "org/springframework/web/views/hello.jsp"));
+        assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "org/springframework/web/default.jsp"));
+        assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "org/springframework/default.jsp"));
+        assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "org/springframework/servlet/bla.jsp"));
+        assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "org/springframework/testing/servlet/bla.jsp"));
+        assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "org/servlet/bla.jsp"));
+        assertTrue(pathMatcher.isMatch("**/hello.jsp", "org/springframework/servlet/web/views/hello.jsp"));
+        assertTrue(pathMatcher.isMatch("**/**/hello.jsp", "org/springframework/servlet/web/views/hello.jsp"));
+
+        assertFalse(pathMatcher.isMatch("/foo/bar/**", "/foo /bar"));
+        assertFalse(pathMatcher.isMatch("/foo/bar/**", "/foo          /bar"));
+        assertFalse(pathMatcher.isMatch("/foo/bar/**", "/foo          /               bar"));
+        assertFalse(pathMatcher.isMatch("/foo/bar/**", "       /      foo          /               bar"));
+        assertFalse(pathMatcher.isMatch("org/**/servlet/bla.jsp", "   org   /      servlet    /   bla   .   jsp"));
+    }
+
+    @Test
+    public void isMatchWithCustomSeparator() throws Exception {
+        final Builder.Matcher pathMatcher = new Builder().withPathSeparator(".").build();
+
+        assertTrue(pathMatcher.isMatch(".foo.bar.**", ".foo.bar"));
+        assertTrue(pathMatcher.isMatch(".resource.1", ".resource.1"));
+        assertTrue(pathMatcher.isMatch(".resource.*", ".resource.1"));
+        assertTrue(pathMatcher.isMatch(".resource.*.", ".resource.1."));
+        assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "org.springframework.web.views.hello.jsp"));
+        assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "org.springframework.web.default.jsp"));
+        assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "org.springframework.default.jsp"));
+        assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "org.springframework.servlet.bla.jsp"));
+        assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "org.springframework.testing.servlet.bla.jsp"));
+        assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "org.servlet.bla.jsp"));
+        assertTrue(pathMatcher.isMatch("http://example.org", "http://example.org"));
+        assertTrue(pathMatcher.isMatch("**.hello.jsp", "org.springframework.servlet.web.views.hello.jsp"));
+        assertTrue(pathMatcher.isMatch("**.**.hello.jsp", "org.springframework.servlet.web.views.hello.jsp"));
+
+        // test matching with ?'s and .'s
+        assertTrue(pathMatcher.isMatch(".?", ".a"));
+        assertTrue(pathMatcher.isMatch(".?.a", ".a.a"));
+        assertTrue(pathMatcher.isMatch(".a.?", ".a.b"));
+        assertTrue(pathMatcher.isMatch(".??.a", ".aa.a"));
+        assertTrue(pathMatcher.isMatch(".a.??", ".a.bb"));
+        assertTrue(pathMatcher.isMatch(".?", ".a"));
+
+        // test matching with **'s
+        assertTrue(pathMatcher.isMatch(".**", ".testing.testing"));
+        assertTrue(pathMatcher.isMatch(".*.**", ".testing.testing"));
+        assertTrue(pathMatcher.isMatch(".**.*", ".testing.testing"));
+        assertTrue(pathMatcher.isMatch(".bla.**.bla", ".bla.testing.testing.bla"));
+        assertTrue(pathMatcher.isMatch(".bla.**.bla", ".bla.testing.testing.bla.bla"));
+        assertTrue(pathMatcher.isMatch(".**.test", ".bla.bla.test"));
+        assertTrue(pathMatcher.isMatch(".bla.**.**.bla", ".bla.bla.bla.bla.bla.bla"));
+        assertFalse(pathMatcher.isMatch(".bla*bla.test", ".blaXXXbl.test"));
+        assertFalse(pathMatcher.isMatch(".*bla.test", "XXXblab.test"));
+        assertFalse(pathMatcher.isMatch(".*bla.test", "XXXbl.test"));
+    }
+
+    @Test
+    public void isMatchWithIgnoreCase() throws Exception {
+        final Builder.Matcher pathMatcher = new Builder().withIgnoreCase().build();
+
+        assertTrue(pathMatcher.isMatch("/foo/bar/**", "/FoO/baR"));
+        assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "ORG/SpringFramework/web/views/hello.JSP"));
+        assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "Org/SERVLET/bla.jsp"));
+        assertTrue(pathMatcher.isMatch("/?", "/A"));
+        assertTrue(pathMatcher.isMatch("/?/a", "/a/A"));
+        assertTrue(pathMatcher.isMatch("/a/??", "/a/Bb"));
+        assertTrue(pathMatcher.isMatch("/?", "/a"));
+        assertTrue(pathMatcher.isMatch("/**", "/testing/teSting"));
+        assertTrue(pathMatcher.isMatch("/*/**", "/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/**/*", "/tEsting/testinG"));
+        assertTrue(pathMatcher.isMatch("http://example.org", "HtTp://exAmple.org"));
+        assertTrue(pathMatcher.isMatch("HTTP://EXAMPLE.ORG", "HtTp://exAmple.org"));
+    }
+
+    @Test
+    public void isMatchWithIgnoreCaseWithCustomPathSeparator() throws Exception {
+        final Builder.Matcher pathMatcher = new Builder()
+                .withIgnoreCase()
+                .withPathSeparator(".").build();
+
+        assertTrue(pathMatcher.isMatch(".foo.bar.**", ".FoO.baR"));
+        assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "ORG.SpringFramework.web.views.hello.JSP"));
+        assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "Org.SERVLET.bla.jsp"));
+        assertTrue(pathMatcher.isMatch(".?", ".A"));
+        assertTrue(pathMatcher.isMatch(".?.a", ".a.A"));
+        assertTrue(pathMatcher.isMatch(".a.??", ".a.Bb"));
+        assertTrue(pathMatcher.isMatch(".?", ".a"));
+        assertTrue(pathMatcher.isMatch(".**", ".testing.teSting"));
+        assertTrue(pathMatcher.isMatch(".*.**", ".testing.testing"));
+        assertTrue(pathMatcher.isMatch(".**.*", ".tEsting.testinG"));
+        assertTrue(pathMatcher.isMatch("http:..example.org", "HtTp:..exAmple.org"));
+        assertTrue(pathMatcher.isMatch("HTTP:..EXAMPLE.ORG", "HtTp:..exAmple.org"));
+    }
+
+    @Test
+    public void isMatchWithMatchStart() {
+        final Builder.Matcher pathMatcher = new Builder().withMatchStart().build();
+
+        // test exact matching
+        assertTrue(pathMatcher.isMatch("test", "test"));
+        assertTrue(pathMatcher.isMatch("/test", "/test"));
+        assertFalse(pathMatcher.isMatch("/test.jpg", "test.jpg"));
+        assertFalse(pathMatcher.isMatch("test", "/test"));
+        assertFalse(pathMatcher.isMatch("/test", "test"));
+
+        // test matching with ?'s
+        assertTrue(pathMatcher.isMatch("t?st", "test"));
+        assertTrue(pathMatcher.isMatch("??st", "test"));
+        assertTrue(pathMatcher.isMatch("tes?", "test"));
+        assertTrue(pathMatcher.isMatch("te??", "test"));
+        assertTrue(pathMatcher.isMatch("?es?", "test"));
+        assertFalse(pathMatcher.isMatch("tes?", "tes"));
+        assertFalse(pathMatcher.isMatch("tes?", "testt"));
+        assertFalse(pathMatcher.isMatch("tes?", "tsst"));
+
+        // test matching with *'s
+        assertTrue(pathMatcher.isMatch("*", "test"));
+        assertTrue(pathMatcher.isMatch("test*", "test"));
+        assertTrue(pathMatcher.isMatch("test*", "testTest"));
+        assertTrue(pathMatcher.isMatch("test/*", "test/Test"));
+        assertTrue(pathMatcher.isMatch("test/*", "test/t"));
+        assertTrue(pathMatcher.isMatch("test/*", "test/"));
+        assertTrue(pathMatcher.isMatch("*test*", "AnothertestTest"));
+        assertTrue(pathMatcher.isMatch("*test", "Anothertest"));
+        assertTrue(pathMatcher.isMatch("*.*", "test."));
+        assertTrue(pathMatcher.isMatch("*.*", "test.test"));
+        assertTrue(pathMatcher.isMatch("*.*", "test.test.test"));
+        assertTrue(pathMatcher.isMatch("test*aaa", "testblaaaa"));
+        assertFalse(pathMatcher.isMatch("test*", "tst"));
+        assertFalse(pathMatcher.isMatch("test*", "test/"));
+        assertFalse(pathMatcher.isMatch("test*", "tsttest"));
+        assertFalse(pathMatcher.isMatch("test*", "test/"));
+        assertFalse(pathMatcher.isMatch("test*", "test/t"));
+        assertTrue(pathMatcher.isMatch("test/*", "test"));
+        assertTrue(pathMatcher.isMatch("test/t*.txt", "test"));
+        assertFalse(pathMatcher.isMatch("*test*", "tsttst"));
+        assertFalse(pathMatcher.isMatch("*test", "tsttst"));
+        assertFalse(pathMatcher.isMatch("*.*", "tsttst"));
+        assertFalse(pathMatcher.isMatch("test*aaa", "test"));
+        assertFalse(pathMatcher.isMatch("test*aaa", "testblaaab"));
+
+        // test matching with ?'s and /'s
+        assertTrue(pathMatcher.isMatch("/?", "/a"));
+        assertTrue(pathMatcher.isMatch("/?/a", "/a/a"));
+        assertTrue(pathMatcher.isMatch("/a/?", "/a/b"));
+        assertTrue(pathMatcher.isMatch("/??/a", "/aa/a"));
+        assertTrue(pathMatcher.isMatch("/a/??", "/a/bb"));
+        assertTrue(pathMatcher.isMatch("/?", "/a"));
+
+        // test matching with **'s
+        assertTrue(pathMatcher.isMatch("/**", "/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/*/**", "/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/**/*", "/testing/testing"));
+        assertTrue(pathMatcher.isMatch("test*/**", "test/"));
+        assertTrue(pathMatcher.isMatch("test*/**", "test/t"));
+        assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla"));
+        assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla/bla"));
+        assertTrue(pathMatcher.isMatch("/**/test", "/bla/bla/test"));
+        assertTrue(pathMatcher.isMatch("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla"));
+        assertTrue(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbla/test"));
+        assertTrue(pathMatcher.isMatch("/*bla/test", "/XXXbla/test"));
+        assertFalse(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbl/test"));
+        assertFalse(pathMatcher.isMatch("/*bla/test", "XXXblab/test"));
+        assertFalse(pathMatcher.isMatch("/*bla/test", "XXXbl/test"));
+
+        assertFalse(pathMatcher.isMatch("/????", "/bala/bla"));
+        assertTrue(pathMatcher.isMatch("/**/*bla", "/bla/bla/bla/bbb"));
+
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/"));
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing"));
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing"));
+        assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg"));
+
+        assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/"));
+        assertTrue(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing"));
+        assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing"));
+        assertTrue(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing"));
+
+        assertTrue(pathMatcher.isMatch("/x/x/**/bla", "/x/x/x/"));
+
+        assertTrue(pathMatcher.isMatch("", ""));
+    }
+
+    public static class Builder {
+
+        private boolean matchStart;
+        private boolean ignoreCase;
+        private String pathSeparator = GlobPathMatcher.DEFAULT_PATH_SEPARATOR;
+
+        public Matcher build() {
+            return new Matcher();
+        }
+
+        public Builder withMatchStart() {
+            this.matchStart = true;
+            return this;
+        }
+
+        public Builder withIgnoreCase() {
+            this.ignoreCase = true;
+            return this;
+        }
+
+        public Builder withPathSeparator(String sep) {
+            this.pathSeparator = sep;
+            return this;
+        }
+
+        private class Matcher {
+            public boolean isMatch(String pattern, String str) {
+                GlobPathMatcher matcher = new GlobPathMatcher(pattern, pathSeparator, !ignoreCase);
+                return matcher.matches(str, !matchStart);
+            }
+        }
+    }
+}
\ No newline at end of file