/* * 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.exoplatform.services.jcr.impl.core.query.misc; import org.exoplatform.services.jcr.datamodel.InternalQName; import org.exoplatform.services.jcr.datamodel.QPath; import org.exoplatform.services.jcr.datamodel.QPathEntry; import javax.jcr.RepositoryException; /** * Pattern to match normalized {@link QPath}s. * A pattern matches either a constant path, a name of a path element, a selection of * either of two patterns or a sequence of two patterns. The matching process is greedy. * That is, whenever a match is not unique only the longest match is considered. * Matching consumes as many elements from the beginning of an input path as possible and * returns what's left as an instance of {@link MatchResult}. * Use the {@link java.util.regex.Matcher} class for matching a whole path or finding matches inside a path. */ public abstract class Pattern { /** * Matches this pattern against the input. * @param input path to match with this pattern * @return result from the matching <code>pattern</code> against <code>input</code> * @throws IllegalArgumentException if <code>input</code> is not normalized */ public MatchResult match(QPath input) { try { return match(new Context(input)).getMatchResult(); } catch (RepositoryException e) { throw (IllegalArgumentException)new IllegalArgumentException("QPath not normalized").initCause(e); } } protected abstract Context match(Context input) throws RepositoryException; /** * Construct a new pattern which matches an exact path * @param path * @return A pattern which matches <code>path</code> and nothing else * @throws IllegalArgumentException if <code>path</code> is <code>null</code> */ public static Pattern path(QPath path) { if (path == null) { throw new IllegalArgumentException("path cannot be null"); } return new PathPattern(path); } /** * Construct a new pattern which matches a path element of a given name * @param name * @return A pattern which matches a path element with name <code>name</code> * @throws IllegalArgumentException if <code>name</code> is <code>null</code> */ public static Pattern name(QPathEntry name) { if (name == null) { throw new IllegalArgumentException("name cannot be null"); } return new NamePattern(name); } /** * Constructs a pattern which matches a path elements against regular expressions. * @param namespaceUri A regular expression used for matching the name space URI of * a path element. * @param localName A regular expression used for matching the local name of a path * element * @return A pattern which matches a path element if namespaceUri matches the * name space URI of the path element and localName matches the local name of the * path element. * @throws IllegalArgumentException if either <code>namespaceUri</code> or * <code>localName</code> is <code>null</code> * * @see java.util.regex.Pattern */ public static Pattern name(String namespaceUri, String localName) { if (namespaceUri == null || localName == null) { throw new IllegalArgumentException("neither namespaceUri nor localName can be null"); } return new RegexPattern(namespaceUri, localName); } private static final Pattern ALL_PATTERN = new Pattern() { protected Context match(Context input) { return input.matchToEnd(); } public String toString() { return "[ALL]"; } }; /** * A pattern which matches all input. * @return */ public static Pattern all() { return ALL_PATTERN; } private static final Pattern NOTHING_PATTERN = new Pattern() { protected Context match(Context input) { return input.match(0); } public String toString() { return "[NOTHING]"; } }; /** * A pattern which matches nothing. * @return */ public static Pattern nothing() { return NOTHING_PATTERN; } /** * A pattern which matches <code>pattern1</code> followed by <code>pattern2</code> and * returns the longer of the two matches. * @param pattern1 * @param pattern2 * @return * @throws IllegalArgumentException if either argument is <code>null</code> */ public static Pattern selection(Pattern pattern1, Pattern pattern2) { if (pattern1 == null || pattern2 == null) { throw new IllegalArgumentException("Neither pattern can be null"); } return new SelectPattern(pattern1, pattern2); } /** * A pattern which matches <code>pattern1</code> followed by <code>pattern2</code>. * @param pattern1 * @param pattern2 * @return */ public static Pattern sequence(Pattern pattern1, Pattern pattern2) { if (pattern1 == null || pattern2 == null) { throw new IllegalArgumentException("Neither pattern can be null"); } return new SequencePattern(pattern1, pattern2); } /** * A pattern which matches <code>pattern</code> as many times as possible * @param pattern * @return */ public static Pattern repeat(Pattern pattern) { if (pattern == null) { throw new IllegalArgumentException("Pattern can not be null"); } return new RepeatPattern(pattern); } /** * A pattern which matches <code>pattern</code> as many times as possible * but at least <code>min</code> times and at most <code>max</code> times. * @param pattern * @param min * @param max * @return */ public static Pattern repeat(Pattern pattern, int min, int max) { if (pattern == null) { throw new IllegalArgumentException("Pattern can not be null"); } return new RepeatPattern(pattern, min, max); } // -----------------------------------------------------< Context >--- private static class Context { private final QPath path; private final int length; private final int pos; private final boolean isMatch; public Context(QPath path) { super(); this.path = path; length = path.getEntries().length; isMatch = false; pos = 0; } public Context(Context context, int pos, boolean matched) { path = context.path; length = context.length; this.pos = pos; this.isMatch = matched; if (pos > length) { throw new IllegalArgumentException("Cannot match beyond end of input"); } } public Context matchToEnd() { return new Context(this, length, true); } public Context match(int count) { return new Context(this, pos + count, true); } public Context noMatch() { return new Context(this, this.pos, false); } public boolean isMatch() { return isMatch; } public QPath getRemainder() throws RepositoryException { if (pos >= length) { return null; } else { //throw new RepositoryException("not implemented"); return subPath(path, pos, length); } } /** * @see Path#subPath(int, int) */ public QPath subPath(QPath source, int from, int to) throws IllegalArgumentException, RepositoryException { QPathEntry[] elements = source.getEntries(); if (from < 0 || to > elements.length || from >= to) { throw new IllegalArgumentException(); } // if (!isNormalized()) // { // throw new RepositoryException("Cannot extract sub-Path from a non-normalized Path: " + this); // } QPathEntry[] dest = new QPathEntry[to - from]; System.arraycopy(elements, from, dest, 0, dest.length); //Builder pb = new Builder(dest); return new QPath(dest); } public boolean isExhausted() { return pos == length; } public MatchResult getMatchResult() { return new MatchResult(path, isMatch ? pos : 0); } public String toString() { return pos + " @ " + path; } } // -----------------------------------------------------< SelectPattern >--- private static class SelectPattern extends Pattern { private final Pattern pattern1; private final Pattern pattern2; public SelectPattern(Pattern pattern1, Pattern pattern2) { super(); this.pattern1 = pattern1; this.pattern2 = pattern2; } protected Context match(Context input) throws RepositoryException { Context remainder1 = pattern1.match(input); Context remainder2 = pattern2.match(input); return remainder1.pos > remainder2.pos ? remainder1 : remainder2; } public String toString() { return new StringBuilder().append("(").append(pattern1).append("|").append(pattern2).append(")").toString(); } } // -----------------------------------------------------< SequencePattern >--- private static class SequencePattern extends Pattern { private final Pattern pattern1; private final Pattern pattern2; public SequencePattern(Pattern pattern1, Pattern pattern2) { super(); this.pattern1 = pattern1; this.pattern2 = pattern2; } protected Context match(Context input) throws RepositoryException { Context context1 = pattern1.match(input); if (context1.isMatch()) { return pattern2.match(context1); } else { return input.noMatch(); } } public String toString() { return new StringBuilder().append("(").append(pattern1).append(", ").append(pattern2).append(")").toString(); } } // -----------------------------------------------------< RepeatPattern >--- private static class RepeatPattern extends Pattern { private final Pattern pattern; private final int min; private final int max; private boolean hasBounds; public RepeatPattern(Pattern pattern) { this(pattern, 0, 0); this.hasBounds = false; } public RepeatPattern(Pattern pattern, int min, int max) { super(); this.pattern = pattern; this.min = min; this.max = max; this.hasBounds = true; } protected Context match(Context input) throws RepositoryException { Context nextInput; Context output = input.match(0); int matchCount = -1; do { nextInput = output; output = pattern.match(nextInput); matchCount++; } while (output.isMatch() && (output.pos > nextInput.pos)); if (!hasBounds() || (min <= matchCount && matchCount <= max)) { return nextInput; } else { return input.noMatch(); } } private boolean hasBounds() { return hasBounds; } public String toString() { return new StringBuilder().append("(").append(pattern).append(")*").toString(); } } // -----------------------------------------------------< PathPattern >--- private static class PathPattern extends Pattern { private final QPath path; private final QPathEntry[] patternElements; public PathPattern(QPath path) { super(); this.path = path; patternElements = path.getEntries(); } protected Context match(Context input) throws RepositoryException { if (input.isExhausted()) { return input; } QPath inputPath = input.getRemainder(); // if (!inputPath.isNormalized()) { // throw new IllegalArgumentException("Not normalized"); // } QPathEntry[] inputElements = inputPath.getEntries(); int inputLength = inputElements.length; int patternLength = patternElements.length; if (patternLength > inputLength) { return input.noMatch(); } for (int k = 0; k < patternLength; k++) { if (!patternElements[k].equals(inputElements[k])) { return input.noMatch(); } } return input.match(patternLength); } public String toString() { return new StringBuilder().append("\"").append(path).append("\"").toString(); } } // -----------------------------------------------------< AbstractNamePattern >--- private static abstract class AbstractNamePattern extends Pattern { protected abstract boolean matches(QPathEntry element); protected Context match(Context input) throws RepositoryException { if (input.isExhausted()) { return input.noMatch(); } QPath inputPath = input.getRemainder(); // if (!inputPath.isNormalized()) { // throw new IllegalArgumentException("Not normalized"); // } QPathEntry[] inputElements = inputPath.getEntries(); if (inputElements.length < 1 || !matches(inputElements[0])) { return input.noMatch(); } return input.match(1); } } // -----------------------------------------------------< NameNamePattern >--- private static class NamePattern extends AbstractNamePattern { private final InternalQName name; public NamePattern(InternalQName name) { super(); this.name = name; } protected boolean matches(QPathEntry element) { return name.equals(element); } public String toString() { return new StringBuilder().append("\"").append(name).append("\"").toString(); } } // -----------------------------------------------------< StringNamePattern >--- private static class RegexPattern extends AbstractNamePattern { private final java.util.regex.Pattern namespaceUri; private final java.util.regex.Pattern localName; private final String localNameStr; private final String namespaceUriStr; public RegexPattern(String namespaceUri, String localName) { super(); this.namespaceUri = java.util.regex.Pattern.compile(namespaceUri); this.localName = java.util.regex.Pattern.compile(localName); this.namespaceUriStr = namespaceUri; this.localNameStr = localName; } protected boolean matches(QPathEntry element) { InternalQName name = element; boolean nsMatches = namespaceUri.matcher(name.getNamespace()).matches(); boolean localMatches = localName.matcher(name.getName()).matches(); return nsMatches && localMatches; } public String toString() { return new StringBuilder().append("\"{").append(namespaceUriStr).append("}").append(localNameStr).append("\"") .toString(); } } }