blob: aa0c9fa1a1c5aad6e4ff30e11571f3f118b9f275 [file] [log] [blame]
/*
* Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.uri;
import java.util.List;
import java.util.Map;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A pattern for matching a string against a regular expression
* and returning capturing group values for any capturing groups present in
* the expression.
*
* @author Paul Sandoz
* @author Gerard Davison (gerard.davison at oracle.com)
*/
public class PatternWithGroups {
private static final int[] EMPTY_INT_ARRAY = new int[0];
/**
* The empty pattern that matches the null or empty string.
*/
public static final PatternWithGroups EMPTY = new PatternWithGroups();
/**
* The regular expression for matching and obtaining capturing group values.
*/
private final String regex;
/**
* The compiled regular expression of {@link #regex}.
*/
private final Pattern regexPattern;
/**
* The array of group indexes to capturing groups.
*/
private final int[] groupIndexes;
/**
* Construct an empty pattern.
*/
protected PatternWithGroups() {
this.regex = "";
this.regexPattern = null;
this.groupIndexes = EMPTY_INT_ARRAY;
}
/**
* Construct a new pattern.
*
* @param regex the regular expression. If the expression is {@code null} or an empty string then the pattern will only match
* a {@code null} or empty string.
* @throws java.util.regex.PatternSyntaxException if the regular expression could not be compiled.
*/
public PatternWithGroups(final String regex) throws PatternSyntaxException {
this(regex, EMPTY_INT_ARRAY);
}
/**
* Construct a new pattern.
*
* @param regex the regular expression. If the expression is {@code null} or an empty string then the pattern will
* only
* match
* a {@code null} or empty string.
* @param groupIndexes the array of group indexes to capturing groups.
* @throws java.util.regex.PatternSyntaxException if the regular expression could not be compiled.
*/
public PatternWithGroups(final String regex, final int[] groupIndexes) throws PatternSyntaxException {
this(compile(regex), groupIndexes);
}
private static Pattern compile(final String regex) throws PatternSyntaxException {
return (regex == null || regex.isEmpty()) ? null : Pattern.compile(regex);
}
/**
* Construct a new pattern.
*
* @param regexPattern the regular expression pattern.
* @throws IllegalArgumentException if the regexPattern is {@code null}.
*/
public PatternWithGroups(final Pattern regexPattern) throws IllegalArgumentException {
this(regexPattern, EMPTY_INT_ARRAY);
}
/**
* Construct a new pattern.
*
* @param regexPattern the regular expression pattern.
* @param groupIndexes the array of group indexes to capturing groups.
* @throws IllegalArgumentException if the regexPattern is {@code null}.
*/
public PatternWithGroups(final Pattern regexPattern, final int[] groupIndexes) throws IllegalArgumentException {
if (regexPattern == null) {
throw new IllegalArgumentException();
}
this.regex = regexPattern.toString();
this.regexPattern = regexPattern;
this.groupIndexes = groupIndexes.clone();
}
/**
* Get the regular expression.
*
* @return the regular expression.
*/
public final String getRegex() {
return regex;
}
/**
* Get the group indexes to capturing groups.
* <p>
* Any nested capturing groups will be ignored and the
* the group index will refer to the top-level capturing
* groups associated with the templates variables.
*
* @return the group indexes to capturing groups.
*/
public final int[] getGroupIndexes() {
return groupIndexes.clone();
}
private static final class EmptyStringMatchResult implements MatchResult {
@Override
public int start() {
return 0;
}
@Override
public int start(final int group) {
if (group != 0) {
throw new IndexOutOfBoundsException();
}
return start();
}
@Override
public int end() {
return 0;
}
@Override
public int end(final int group) {
if (group != 0) {
throw new IndexOutOfBoundsException();
}
return end();
}
@Override
public String group() {
return "";
}
@Override
public String group(final int group) {
if (group != 0) {
throw new IndexOutOfBoundsException();
}
return group();
}
@Override
public int groupCount() {
return 0;
}
}
private static final EmptyStringMatchResult EMPTY_STRING_MATCH_RESULT = new EmptyStringMatchResult();
private final class GroupIndexMatchResult implements MatchResult {
private final MatchResult result;
GroupIndexMatchResult(final MatchResult r) {
this.result = r;
}
@Override
public int start() {
return result.start();
}
@Override
public int start(final int group) {
if (group > groupCount()) {
throw new IndexOutOfBoundsException();
}
return (group > 0) ? result.start(groupIndexes[group - 1]) : result.start();
}
@Override
public int end() {
return result.end();
}
@Override
public int end(final int group) {
if (group > groupCount()) {
throw new IndexOutOfBoundsException();
}
return (group > 0) ? result.end(groupIndexes[group - 1]) : result.end();
}
@Override
public String group() {
return result.group();
}
@Override
public String group(final int group) {
if (group > groupCount()) {
throw new IndexOutOfBoundsException();
}
return (group > 0) ? result.group(groupIndexes[group - 1]) : result.group();
}
@Override
public int groupCount() {
return groupIndexes.length;
}
}
/**
* Match against the pattern.
*
* @param cs the char sequence to match against the template.
* @return the match result, otherwise null if no match occurs.
*/
public final MatchResult match(final CharSequence cs) {
// Check for match against the empty pattern
if (cs == null) {
return (regexPattern == null) ? EMPTY_STRING_MATCH_RESULT : null;
} else if (regexPattern == null) {
return null;
}
// Match regular expression
Matcher m = regexPattern.matcher(cs);
if (!m.matches()) {
return null;
}
if (cs.length() == 0) {
return EMPTY_STRING_MATCH_RESULT;
}
return (groupIndexes.length > 0) ? new GroupIndexMatchResult(m) : m;
}
/**
* Match against the pattern.
* <p/>
* If a matched then the capturing group values (if any) will be added to a list passed in as parameter.
*
* @param cs the char sequence to match against the template.
* @param groupValues the list to add the values of a pattern's capturing groups if matching is successful. The values are
* added in the same order as the pattern's capturing groups. The list is cleared before values are added.
* @return {@code true} if the char sequence matches the pattern, otherwise {@code false}.
*
* @throws IllegalArgumentException if the group values is {@code null}.
*/
public final boolean match(final CharSequence cs, final List<String> groupValues) throws IllegalArgumentException {
if (groupValues == null) {
throw new IllegalArgumentException();
}
// Check for match against the empty pattern
if (cs == null || cs.length() == 0) {
return regexPattern == null;
} else if (regexPattern == null) {
return false;
}
// Match the regular expression
Matcher m = regexPattern.matcher(cs);
if (!m.matches()) {
return false;
}
groupValues.clear();
if (groupIndexes.length > 0) {
for (int i = 0; i < groupIndexes.length; i++) {
groupValues.add(m.group(groupIndexes[i]));
}
} else {
for (int i = 1; i <= m.groupCount(); i++) {
groupValues.add(m.group(i));
}
}
// TODO check for consistency of different capturing groups
// that must have the same value
return true;
}
/**
* Match against the pattern.
* <p/>
* If a matched then the capturing group values (if any) will be added to a list passed in as parameter.
*
* @param cs the char sequence to match against the template.
* @param groupNames the list names associated with a pattern's capturing groups. The names MUST be in the same order as the
* pattern's capturing groups and the size MUST be equal to or less than the number of capturing groups.
* @param groupValues the map to add the values of a pattern's capturing groups if matching is successful. A values is put
* into the map using the group name associated with the capturing group. The map is cleared before values
* are added.
* @return {@code true} if the matches the pattern, otherwise {@code false}.
*
* @throws IllegalArgumentException if group values is {@code null}.
*/
public final boolean match(final CharSequence cs, final List<String> groupNames, final Map<String,
String> groupValues) throws IllegalArgumentException {
if (groupValues == null) {
throw new IllegalArgumentException();
}
// Check for match against the empty pattern
if (cs == null || cs.length() == 0) {
return regexPattern == null;
} else if (regexPattern == null) {
return false;
}
// Match the regular expression
Matcher m = regexPattern.matcher(cs);
if (!m.matches()) {
return false;
}
// Assign the matched group values to group names
groupValues.clear();
for (int i = 0; i < groupNames.size(); i++) {
String name = groupNames.get(i);
String currentValue = m.group((groupIndexes.length > 0) ? groupIndexes[i] : i + 1);
// Group names can have the same name occurring more than once,
// check that groups values are same.
String previousValue = groupValues.get(name);
if (previousValue != null && !previousValue.equals(currentValue)) {
return false;
}
groupValues.put(name, currentValue);
}
return true;
}
@Override
public final int hashCode() {
return regex.hashCode();
}
@Override
public final boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PatternWithGroups that = (PatternWithGroups) obj;
if (this.regex != that.regex && (this.regex == null || !this.regex.equals(that.regex))) {
return false;
}
return true;
}
@Override
public final String toString() {
return regex;
}
}