| /* |
| * 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.sling.api.resource.path; |
| |
| import java.util.regex.Pattern; |
| |
| import org.jetbrains.annotations.NotNull; |
| |
| /** |
| * Simple helper class for path matching. |
| * |
| * @since 1.0.0 (Sling API Bundle 2.11.0) |
| */ |
| public class Path implements Comparable<Path> { |
| |
| /** |
| * The prefix to be used for glob patterns |
| * @since 1.2.0 (Sling API Bundle 2.15.0) |
| */ |
| public static final String GLOB_PREFIX = "glob:"; |
| |
| private final String path; |
| 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>A glob pattern must start with the {@code glob:} prefix (e.g. <code>glob:**/*.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 match zero or more characters crossing directory boundaries.</li> |
| * </ul> |
| * |
| * @param path The resource path or a glob pattern. |
| * @throws NullPointerException If {@code otherPath} is {@code null} |
| * @throws IllegalArgumentException If the provided path is not absolute, or if the glob pattern does not start with a slash. |
| */ |
| public Path(@NotNull final String path) { |
| if ( path.equals("/") ) { |
| this.path = "/"; |
| } else if ( path.endsWith("/") ) { |
| this.path = path.substring(0, path.length() - 1); |
| } else { |
| this.path = path; |
| } |
| if (this.path.startsWith(GLOB_PREFIX)) { |
| final String patternPath = path.substring(GLOB_PREFIX.length()); |
| this.isPattern = true; |
| this.regexPattern = Pattern.compile(toRegexPattern(patternPath)); |
| int lastSlash = 0; |
| int pos = 1; |
| while ( patternPath.length() > pos ) { |
| final char c = patternPath.charAt(pos); |
| if ( c == '/') { |
| lastSlash = pos; |
| } else if ( c == '*') { |
| break; |
| } |
| pos++; |
| } |
| |
| this.prefix = (pos == patternPath.length() ? patternPath : patternPath.substring(0, lastSlash + 1)); |
| } else { |
| this.isPattern = false; |
| this.regexPattern = null; |
| this.prefix = this.path.equals("/") ? "/" : this.path.concat("/"); |
| } |
| if ( !this.prefix.startsWith("/") ) { |
| throw new IllegalArgumentException("Path must be absolute: " + path); |
| } |
| } |
| |
| /** |
| * If this {@code Path} object holds a path (and not a pattern), this method |
| * checks whether the provided path is equal to this path or a sub path of it. |
| * If a glob pattern is provided as the argument, it performs the same check |
| * and respects the provided pattern. This means it returns {@code true} if this |
| * path is a parent to any potential path matching the provided pattern. For |
| * example if this path is {@code /apps/foo} and the provided pattern is |
| * {@code glob:/apps/foo/bar/*.jsp} this method will return true. Same if |
| * the provided pattern is {@code glob:/apps/**/hello.html}. |
| * If this {@code Path} object holds a pattern, it checks whether the |
| * provided path matches the pattern. If this path object holds a pattern |
| * and a pattern is provided as the argument, it returns only {@code true} |
| * if the pattern is the same. |
| * If the provided argument is not an absolute path (e.g. if it is a relative |
| * path or a pattern), this method returns {@code false}. |
| * |
| * @param otherPath Absolute path to check. |
| * @return {@code true} If other path is within the sub tree of this path |
| * or matches the pattern. |
| * @see Path#isPattern() |
| * @throws NullPointerException If {@code otherPath} is {@code null} |
| * @throws IllegalArgumentException If the provided path is not absolute, or if the glob pattern does not start with a slash. |
| */ |
| public boolean matches(final String otherPath) { |
| if ( otherPath.startsWith(GLOB_PREFIX) ) { |
| if ( this.isPattern ) { |
| // both are patterns, then they must be equal. |
| // need to compare Pattern.pattern() as that class does |
| // not implement a semantic equals(...) method |
| final Path oPath = new Path(otherPath); |
| return this.regexPattern.pattern().equals(oPath.regexPattern.pattern()); |
| } |
| |
| // this is path, provided argument is a pattern |
| |
| // simple check, if this is root, everything is a sub pattern |
| if ( "/".equals(this.path) ) { |
| return true; |
| } |
| // simplest case - the prefix of the glob pattern matches already |
| // for example: this path = /apps |
| // glob = /apps/** |
| // then we iterate by removing the last path segment |
| String subPath = otherPath; |
| while ( subPath != null ) { |
| final Path oPath = new Path(subPath); |
| if ( oPath.matches(this.path) ) { |
| return true; |
| } |
| final int lastSlash = subPath.lastIndexOf('/'); |
| if ( lastSlash == GLOB_PREFIX.length() ) { |
| subPath = null; |
| } else { |
| subPath = subPath.substring(0, lastSlash); |
| } |
| } |
| |
| return false; |
| } |
| |
| // provided argument is a path |
| if ( !otherPath.startsWith("/") ) { |
| throw new IllegalArgumentException("Path must be absolute: " + otherPath); |
| } |
| if (isPattern) { |
| return this.regexPattern.matcher(otherPath).matches(); |
| } |
| return this.path.equals(otherPath) || otherPath.startsWith(this.prefix); |
| } |
| |
| /** |
| * Return the path if this {@code Path} object holds a path, |
| * returns the pattern otherwise. |
| * @return The path or pattern. |
| * @see #isPattern() |
| */ |
| public String getPath() { |
| return this.path; |
| } |
| |
| /** |
| * Returns {code true} if this {@code Path} object is holding a pattern |
| * @return {code true} for a pattern, {@code false} for a path. |
| * @since 1.2.0 (Sling API Bundle 2.15.0) |
| */ |
| public boolean isPattern() { |
| return this.isPattern; |
| } |
| |
| @Override |
| public int compareTo(@NotNull final Path o) { |
| return this.getPath().compareTo(o.getPath()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return this.getPath().hashCode(); |
| } |
| |
| @Override |
| public boolean equals(final Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null || !(obj instanceof Path)) { |
| return false; |
| } |
| 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(Pattern.quote(Character.toString(currentChar))); |
| } else { |
| 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; |
| } |
| |
| @Override |
| public String toString() { |
| return "Path [path=" + path + "]"; |
| } |
| } |