SLING-5837 - Allow ResourceChangeListeners to define glob patterns for resource matching

* glob patterns have to start with the 'glob:' prefix; syntax is limited to only the '*' and '**' characters
* updated Javadoc

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1753534 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/sling/api/resource/observation/ResourceChangeListener.java b/src/main/java/org/apache/sling/api/resource/observation/ResourceChangeListener.java
index 61abd2a..e97634d 100644
--- a/src/main/java/org/apache/sling/api/resource/observation/ResourceChangeListener.java
+++ b/src/main/java/org/apache/sling/api/resource/observation/ResourceChangeListener.java
@@ -60,11 +60,11 @@
      *
      * <p>If the whole tree of all search paths should be observed, the special value {@code .} should be used.</p>
      *
-     * <p>The following rules are used to interpret glob patterns:</p>
+     * <p>A glob pattern should start with the {@code glob:} prefix (e.g. <code>glob:**&#47;*.html</code>). The following rules are used
+     * to interpret glob patterns:</p>
      * <ul>
      *     <li>The {@code *} character matches zero or more characters of a name component without crossing directory boundaries.</li>
-     *     <li>The {@code **} characters matches zero or more characters crossing directory boundaries.</li>
-     *     <li>The {@code ?} character matches exactly one character of a name component.</li>
+     *     <li>The {@code **} characters match zero or more characters crossing directory boundaries.</li>
      * </ul>
      *
      * <p>If one of the paths is a sub resource of another specified path, the sub path is ignored.</p>
diff --git a/src/main/java/org/apache/sling/api/resource/path/Path.java b/src/main/java/org/apache/sling/api/resource/path/Path.java
index b68eb84..2bac5db 100644
--- a/src/main/java/org/apache/sling/api/resource/path/Path.java
+++ b/src/main/java/org/apache/sling/api/resource/path/Path.java
@@ -18,9 +18,8 @@
  */
 package org.apache.sling.api.resource.path;
 
-import java.nio.file.FileSystems;
-import java.nio.file.PathMatcher;
-import java.nio.file.Paths;
+import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
 
 /**
  * Simple helper class for path matching.
@@ -34,26 +33,29 @@
     private final String prefix;
 
     private final boolean isPattern;
+    private final Pattern regexPattern;
 
     /**
      * <p>Create a new path object either from a concrete path or from a glob pattern.</p>
      *
-     * <p>The following rules are used to interpret glob patterns:</p>
+     * <p>A glob pattern should start with the {@code glob:} prefix (e.g. <code>glob:**&#47;*.html</code>). The following rules are used
+     * to interpret glob patterns:</p>
      * <ul>
      *     <li>The {@code *} character matches zero or more characters of a name component without crossing directory boundaries.</li>
-     *     <li>The {@code **} characters matches zero or more characters crossing directory boundaries.</li>
-     *     <li>The {@code ?} character matches exactly one character of a name component.</li>
+     *     <li>The {@code **} characters match zero or more characters crossing directory boundaries.</li>
      * </ul>
      *
      * @param path the resource path or a glob pattern.
      */
-    public Path(final String path) {
+    public Path(@Nonnull final String path) {
         this.path = path;
         this.prefix = path.equals("/") ? "/" : path.concat("/");
-        if (path.contains("?") || path.contains("*")) {
+        if (path.startsWith("glob:")) {
             isPattern = true;
+            regexPattern = Pattern.compile(toRegexPattern(path.substring(5)));
         } else {
             isPattern = false;
+            regexPattern = null;
         }
 
     }
@@ -65,8 +67,7 @@
      */
     public boolean matches(final String otherPath) {
         if (isPattern) {
-            PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + path);
-            return matcher.matches(Paths.get(otherPath));
+            return regexPattern.matcher(otherPath).matches();
         }
         return this.path.equals(otherPath) || otherPath.startsWith(this.prefix);
     }
@@ -80,7 +81,7 @@
     }
 
     @Override
-    public int compareTo(final Path o) {
+    public int compareTo(@Nonnull final Path o) {
         return this.getPath().compareTo(o.getPath());
     }
 
@@ -100,4 +101,40 @@
         return this.getPath().equals(((Path)obj).getPath());
     }
 
+    private static String toRegexPattern(String pattern) {
+        StringBuilder stringBuilder = new StringBuilder("^");
+        int index = 0;
+        while (index < pattern.length()) {
+            char currentChar = pattern.charAt(index++);
+            switch (currentChar) {
+                case '*':
+                    if (getCharAtIndex(pattern, index) == '*') {
+                        stringBuilder.append(".*");
+                        ++index;
+                    } else {
+                        stringBuilder.append("[^/]*");
+                    }
+                    break;
+                case '/':
+                    stringBuilder.append(currentChar);
+                    break;
+                default:
+                    if (isRegexMeta(currentChar)) {
+                        stringBuilder.append('\\');
+                    }
+
+                    stringBuilder.append(currentChar);
+            }
+        }
+        return stringBuilder.append('$').toString();
+    }
+
+    private static char getCharAtIndex(String string, int index) {
+        return index < string.length() ? string.charAt(index) : 0;
+    }
+
+    private static boolean isRegexMeta(char character) {
+        return ".^$+{[]|()".indexOf(character) != -1;
+    }
+
 }
diff --git a/src/test/java/org/apache/sling/api/resource/path/PathTest.java b/src/test/java/org/apache/sling/api/resource/path/PathTest.java
index 4579913..0d6a799 100644
--- a/src/test/java/org/apache/sling/api/resource/path/PathTest.java
+++ b/src/test/java/org/apache/sling/api/resource/path/PathTest.java
@@ -49,11 +49,15 @@
     }
 
     @Test public void testPatternMatching() {
-        final Path path = new Path("/apps/**/*.html");
-        assertTrue(path.matches("/apps/project/a.html"));
-        assertTrue(path.matches("/apps/project/1/a.html"));
-        assertTrue(path.matches("/apps/project/1/2/a.html"));
-        assertFalse(path.matches("/apps/a.html"));
-        assertFalse(path.matches("/apps/project/a.html/b"));
+        final Path path_1 = new Path("glob:/apps/**/*.html");
+        assertTrue(path_1.matches("/apps/project/a.html"));
+        assertTrue(path_1.matches("/apps/project/1/a.html"));
+        assertTrue(path_1.matches("/apps/project/1/2/a.html"));
+        assertFalse(path_1.matches("/apps/a.html"));
+        assertFalse(path_1.matches("/apps/project/a.html/b"));
+
+        final Path path_2 = new Path("glob:/apps/*.html");
+        assertTrue(path_2.matches("/apps/a.html"));
+        assertFalse(path_2.matches("/apps/a/a.html"));
     }
 }