/* * 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.jackrabbit.core.security.authorization; import javax.jcr.Item; import javax.jcr.RepositoryException; import org.apache.jackrabbit.util.Text; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>GlobPattern</code> defines a simplistic pattern matching. It consists * of a mandatory (leading) path and an optional "glob" that may contain one or * more wildcard characters ("<code>*</code>") according to the glob matching * defined by {@link javax.jcr.Node#getNodes(String[])}. In contrast to that * method the <code>GlobPattern</code> operates on path (not only names). * <p> * Please note the following special cases: * <pre> * NodePath | Restriction | Matches * ----------------------------------------------------------------------------- * /foo | null | matches /foo and all children of /foo * /foo | "" | matches /foo only * </pre> * <p> * Examples including wildcard char: * <pre> * NodePath = "/foo" * Restriction | Matches * ----------------------------------------------------------------------------- * * | all siblings of foo and foo's and the siblings' descendants * /*cat | all children of /foo whose path ends with "cat" * /*/cat | all non-direct descendants of /foo named "cat" * /cat* | all descendant path of /foo that have the direct foo-descendant segment starting with "cat" * *cat | all siblings and descendants of foo that have a name ending with cat * */cat | all descendants of /foo and foo's siblings that have a name segment "cat" * cat/* | all descendants of '/foocat' * /cat/* | all descendants of '/foo/cat' * *cat/* | all descendants of /foo that have an intermediate segment ending with 'cat' * </pre> */ public final class GlobPattern { private static Logger log = LoggerFactory.getLogger(GlobPattern.class); private static final char WILDCARD_CHAR = '*'; private static final int MAX_WILDCARD = 20; private final String nodePath; private final String restriction; private final Pattern pattern; private GlobPattern(String nodePath, String restriction) { if (nodePath == null) { throw new IllegalArgumentException(); } this.nodePath = nodePath; this.restriction = restriction; if (restriction != null && restriction.length() > 0) { StringBuilder b = new StringBuilder(nodePath); b.append(restriction); int lastPos = restriction.lastIndexOf(WILDCARD_CHAR); if (lastPos >= 0) { String end; if (lastPos != restriction.length()-1) { end = restriction.substring(lastPos + 1); } else { end = null; } pattern = new WildcardPattern(b.toString(), end); } else { pattern = new PathPattern(b.toString()); } } else { pattern = new PathPattern(); } } public static GlobPattern create(String nodePath, String restrictions) { return new GlobPattern(nodePath, restrictions); } public static GlobPattern create(String nodePath) { return create(nodePath, null); } public boolean matches(String toMatch) { if (toMatch == null) { return false; } else { return pattern.matches(toMatch); } } public boolean matches(Item itemToMatch) { try { // TODO: missing proper impl return matches(itemToMatch.getPath()); } catch (RepositoryException e) { log.error("Unable to determine match. {}", e.getMessage()); return false; } } //-------------------------------------------------------------< Object >--- /** * @see Object#hashCode() */ @Override public int hashCode() { int h = 37 * 17 + nodePath.hashCode(); if (restriction != null) { h = 37 * h + restriction.hashCode(); } return h; } /** * @see Object#toString() */ @Override public String toString() { return nodePath + " : " + restriction; } /** * @see Object#equals(Object) */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof GlobPattern) { GlobPattern other = (GlobPattern) obj; return nodePath.equals(other.nodePath) && ((restriction == null) ? other.restriction == null : restriction.equals(other.restriction)); } return false; } //------------------------------------------------------< inner classes >--- /** * Base for PathPattern and WildcardPattern */ private abstract class Pattern { abstract boolean matches(String toMatch); } /** * Path pattern: The restriction is missing or doesn't contain any wildcard character. */ private final class PathPattern extends Pattern { private final String patternStr; private PathPattern() { this(null); } private PathPattern(String patternStr) { this.patternStr = patternStr; } @Override boolean matches(String toMatch) { if (restriction == null) { return Text.isDescendantOrEqual(nodePath, toMatch); } else if (restriction.length() == 0) { return nodePath.equals(toMatch); } else { // no wildcard contained in restriction: use path defined // by nodePath + restriction to calculate the match return Text.isDescendantOrEqual(patternStr, toMatch); } } } /** * Wildcard pattern: The specified restriction contains one or more wildcard character(s). */ private final class WildcardPattern extends Pattern { private final String patternEnd; private final char[] patternChars; private WildcardPattern(String patternStr, String patternEnd) { patternChars = patternStr.toCharArray(); this.patternEnd = patternEnd; } @Override boolean matches(String toMatch) { if (patternEnd != null && !toMatch.endsWith(patternEnd)) { // shortcut: verify if end of pattern matches end of toMatch return false; } char[] tm = (toMatch.endsWith("/")) ? toMatch.substring(0, toMatch.length()-1).toCharArray() : toMatch.toCharArray(); // shortcut didn't reveal mismatch -> need to process the internal match method. return matches(patternChars, 0, tm, 0, MAX_WILDCARD); } /** * * @param pattern The pattern * @param pOff * @param s * @param sOff * @return <code>true</code> if matches, <code>false</code> otherwise */ private boolean matches(char[] pattern, int pOff, char[] s, int sOff, int cnt) { if (cnt <= 0) { throw new IllegalArgumentException("Illegal glob pattern " + GlobPattern.this); } /* NOTE: code has been copied (and slightly modified) from ChildrenCollectorFilter#internalMatches. TODO: move them to a common utility. */ int pLength = pattern.length; int sLength = s.length; while (true) { // end of pattern reached: matches only if sOff points at the end // of the string to match. if (pOff >= pLength) { return sOff >= sLength; } // the end of the string to match has been reached but pattern // doesn't have '*' at patternIndex -> no match if (sOff >= sLength && pattern[pOff] != WILDCARD_CHAR) { return false; } // the next character of the pattern is '*' // -> recursively test if the rest of the specified string matches if (pattern[pOff] == WILDCARD_CHAR) { if (++pOff >= pLength) { return true; } cnt--; while (true) { if (matches(pattern, pOff, s, sOff, cnt)) { return true; } if (sOff >= sLength) { return false; } sOff++; } } // not yet reached end of patter nor string and not wildcard character. // the 2 strings don't match in case the characters at the current // position are not the same. if (pOff < pLength && sOff < sLength) { if (pattern[pOff] != s[sOff]) { return false; } } pOff++; sOff++; } } } }