/*
* The MIT License
*
* Copyright (c) 2011-2012, CloudBees, Inc., Stephen Connolly.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.cloudbees.plugins.credentials;
import com.cloudbees.plugins.credentials.common.UsernameCredentials;
import com.cloudbees.plugins.credentials.matchers.AllOfMatcher;
import com.cloudbees.plugins.credentials.matchers.AnyOfMatcher;
import com.cloudbees.plugins.credentials.matchers.BeanPropertyMatcher;
import com.cloudbees.plugins.credentials.matchers.CQLBaseListener;
import com.cloudbees.plugins.credentials.matchers.CQLLexer;
import com.cloudbees.plugins.credentials.matchers.CQLParser;
import com.cloudbees.plugins.credentials.matchers.CQLSyntaxException;
import com.cloudbees.plugins.credentials.matchers.ConstantMatcher;
import com.cloudbees.plugins.credentials.matchers.IdMatcher;
import com.cloudbees.plugins.credentials.matchers.InstanceOfMatcher;
import com.cloudbees.plugins.credentials.matchers.NotMatcher;
import com.cloudbees.plugins.credentials.matchers.ScopeMatcher;
import com.cloudbees.plugins.credentials.matchers.UsernameMatcher;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EmptyStackException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
/**
* Some standard matchers and filtering utility methods.
*
* @since 1.5
*/
public class CredentialsMatchers {
/**
* Utility class.
*/
private CredentialsMatchers() {
throw new UnsupportedOperationException("Utility class");
}
/**
* Creates a matcher that always matches.
*
* @return a matcher that always matches.
*/
@NonNull
public static CredentialsMatcher always() {
return new ConstantMatcher(true);
}
/**
* Creates a matcher that never matches.
*
* @return a matcher that never matches.
*/
@NonNull
public static CredentialsMatcher never() {
return new ConstantMatcher(false);
}
/**
* Creates a matcher that inverts the supplied matcher.
*
* @param matcher matcher to invert.
* @return a matcher that is the opposite of the supplied matcher.
*/
@NonNull
public static CredentialsMatcher not(@NonNull CredentialsMatcher matcher) {
return new NotMatcher(matcher);
}
/**
* Creates a matcher that matches credentials of the specified type.
*
* @param clazz the type of credential to match.
* @return a matcher that matches credentials of the specified type.
*/
@NonNull
public static CredentialsMatcher instanceOf(@NonNull Class clazz) {
return new InstanceOfMatcher(clazz);
}
/**
* Creates a matcher that matches {@link com.cloudbees.plugins.credentials.common.IdCredentials} with the
* supplied {@link com.cloudbees.plugins.credentials.common.IdCredentials#getId()}
*
* @param id the {@link com.cloudbees.plugins.credentials.common.IdCredentials#getId()} to match.
* @return a matcher that matches {@link com.cloudbees.plugins.credentials.common.IdCredentials} with the
* supplied {@link com.cloudbees.plugins.credentials.common.IdCredentials#getId()}
*/
@NonNull
public static CredentialsMatcher withId(@NonNull String id) {
return new IdMatcher(id);
}
/**
* Creates a matcher that matches {@link Credentials} with the supplied {@link CredentialsScope}.
*
* @param scope the {@link CredentialsScope} to match.
* @return a matcher that matches {@link Credentials} with the supplied {@link CredentialsScope}.
*/
@NonNull
public static CredentialsMatcher withScope(@NonNull CredentialsScope scope) {
return new ScopeMatcher(scope);
}
/**
* Creates a matcher that matches {@link Credentials} with the supplied {@link CredentialsScope}.
*
* @param scopes the {@link CredentialsScope}s to match.
* @return a matcher that matches {@link Credentials} with the supplied {@link CredentialsScope}s.
*/
@NonNull
public static CredentialsMatcher withScopes(@NonNull CredentialsScope... scopes) {
return new ScopeMatcher(scopes);
}
/**
* Creates a matcher that matches {@link Credentials} with the supplied {@link CredentialsScope}.
*
* @param scopes the {@link CredentialsScope}s to match.
* @return a matcher that matches {@link Credentials} with the supplied {@link CredentialsScope}s.
*/
@NonNull
public static CredentialsMatcher withScopes(@NonNull Collection<CredentialsScope> scopes) {
return new ScopeMatcher(scopes);
}
/**
* Creates a matcher that matches {@link com.cloudbees.plugins.credentials.common.UsernameCredentials} with the
* supplied {@link com.cloudbees.plugins.credentials.common.UsernameCredentials#getUsername()}
*
* @param username the {@link com.cloudbees.plugins.credentials.common.UsernameCredentials#getUsername()} to match.
* @return a matcher that matches {@link com.cloudbees.plugins.credentials.common.UsernameCredentials} with the
* supplied {@link com.cloudbees.plugins.credentials.common.UsernameCredentials#getUsername()}
*/
@NonNull
public static CredentialsMatcher withUsername(@NonNull String username) {
return new UsernameMatcher(username);
}
/**
* Creates a matcher that matches a named Java Bean property against the supplied expected value.
*
* @param name the name of the property to match.
* @param expected the expected value of the property.
* @param <T> the type of expected value.
* @return a matcher that matches a named Java Bean property against the supplied expected value.
* @since 2.1.0
*/
public static <T extends Serializable> CredentialsMatcher withProperty(@NonNull String name,
@CheckForNull T expected) {
return new BeanPropertyMatcher<T>(name, expected);
}
/**
* Creates a matcher that matches when all of the supplied matchers match.
*
* @param matchers the matchers to match.
* @return a matcher that matches when all of the supplied matchers match.
*/
@NonNull
public static CredentialsMatcher allOf(@NonNull CredentialsMatcher... matchers) {
return new AllOfMatcher(Arrays.asList(matchers));
}
/**
* Creates a matcher that matches when any of the supplied matchers match.
*
* @param matchers the matchers to match.
* @return a matcher that matches when any of the supplied matchers match.
*/
@NonNull
public static CredentialsMatcher anyOf(@NonNull CredentialsMatcher... matchers) {
return new AnyOfMatcher(Arrays.asList(matchers));
}
/**
* Creates a matcher that matches when both of the supplied matchers match.
*
* @param matcher1 the first matcher to match.
* @param matcher2 the second matcher to match.
* @return a matcher that matches when both of the supplied matchers match.
*/
@NonNull
public static CredentialsMatcher both(@NonNull CredentialsMatcher matcher1, @NonNull CredentialsMatcher matcher2) {
return new AllOfMatcher(Arrays.asList(matcher1, matcher2));
}
/**
* Creates a matcher that matches when either of the supplied matchers match.
*
* @param matcher1 the first matcher to match.
* @param matcher2 the second matcher to match.
* @return a matcher that matches when either of the supplied matchers match.
*/
@NonNull
public static CredentialsMatcher either(@NonNull CredentialsMatcher matcher1,
@NonNull CredentialsMatcher matcher2) {
return new AnyOfMatcher(Arrays.asList(matcher1, matcher2));
}
/**
* Creates a matcher that matches when none of the supplied matchers match.
*
* @param matchers the matchers to match.
* @return a matcher that matches when none of the supplied matchers match.
*/
@NonNull
public static CredentialsMatcher noneOf(@NonNull CredentialsMatcher... matchers) {
return not(anyOf(matchers));
}
/**
* Filters credentials using the supplied matcher.
*
* @param credentials the credentials to filter.
* @param matcher the matcher to match on.
* @param <C> the type of credentials.
* @return only those credentials that match the supplied matcher.
*/
@NonNull
public static <C extends Credentials> Collection<C> filter(@NonNull Collection<C> credentials,
@NonNull CredentialsMatcher matcher) {
Collection<C> result = credentials instanceof Set ? new LinkedHashSet<C>() : new ArrayList<C>();
for (C credential : credentials) {
if (credential != null && matcher.matches(credential)) {
result.add(credential);
}
}
return result;
}
/**
* Filters credentials using the supplied matcher.
*
* @param credentials the credentials to filter.
* @param matcher the matcher to match on.
* @param <C> the type of credentials.
* @return only those credentials that match the supplied matcher.
*/
@NonNull
public static <C extends Credentials> Set<C> filter(@NonNull Set<C> credentials,
@NonNull CredentialsMatcher matcher) {
Set<C> result = new LinkedHashSet<C>();
for (C credential : credentials) {
if (credential != null && matcher.matches(credential)) {
result.add(credential);
}
}
return result;
}
/**
* Filters credentials using the supplied matcher.
*
* @param credentials the credentials to filter.
* @param matcher the matcher to match on.
* @param <C> the type of credentials.
* @return only those credentials that match the supplied matcher.
*/
@NonNull
public static <C extends Credentials> List<C> filter(@NonNull List<C> credentials,
@NonNull CredentialsMatcher matcher) {
List<C> result = new ArrayList<C>();
for (C credential : credentials) {
if (credential != null && matcher.matches(credential)) {
result.add(credential);
}
}
return result;
}
/**
* Filters credentials using the supplied matcher.
*
* @param credentials the credentials to filter.
* @param matcher the matcher to match on.
* @param <C> the type of credentials.
* @return only those credentials that match the supplied matcher.
*/
@NonNull
public static <C extends Credentials> Iterable<C> filter(@NonNull Iterable<C> credentials,
@NonNull CredentialsMatcher matcher) {
List<C> result = new ArrayList<C>();
for (C credential : credentials) {
if (credential != null && matcher.matches(credential)) {
result.add(credential);
}
}
return result;
}
/**
* Filters a map keyed by credentials using the supplied matcher.
*
* @param credentialMap the map keyed by credentials to filter.
* @param matcher the matcher to match on.
* @param <C> the type of credentials.
* @param <V> the type of the map values.
* @return only those entries with keys that that match the supplied matcher.
*/
@NonNull
public static <C extends Credentials, V> Map<C, V> filterKeys(@NonNull Map<C, V> credentialMap,
@NonNull CredentialsMatcher matcher) {
Map<C, V> result = new LinkedHashMap<C, V>();
for (Map.Entry<C, V> credential : credentialMap.entrySet()) {
if (credential.getKey() != null && matcher.matches(credential.getKey())) {
result.put(credential.getKey(), credential.getValue());
}
}
return result;
}
/**
* Filters a map based on credential values using the supplied matcher.
*
* @param credentialMap the map with credentials values to filter.
* @param matcher the matcher to match on.
* @param <K> the type of the map keys.
* @param <C> the type of credentials.
* @return only those entries with keys that that match the supplied matcher.
*/
@NonNull
public static <C extends Credentials, K> Map<K, C> filterValues(@NonNull Map<K, C> credentialMap,
@NonNull CredentialsMatcher matcher) {
Map<K, C> result = new LinkedHashMap<K, C>();
for (Map.Entry<K, C> credential : credentialMap.entrySet()) {
if (credential.getValue() != null && matcher.matches(credential.getValue())) {
result.put(credential.getKey(), credential.getValue());
}
}
return result;
}
/**
* Returns the first credential from a collection that matches the supplied matcher or if none match then the
* specified default.
*
* @param credentials the credentials to select from.
* @param matcher the matcher.
* @param defaultIfNone the default value if no match found.
* @param <C> the type of credential.
* @return a matching credential or the supplied default.
*/
@CheckForNull
public static <C extends Credentials> C firstOrDefault(@NonNull Iterable<C> credentials,
@NonNull CredentialsMatcher matcher,
@CheckForNull C defaultIfNone) {
for (C c : credentials) {
if (matcher.matches(c)) {
return c;
}
}
return defaultIfNone;
}
/**
* Returns the first credential from a collection that matches the supplied matcher or {@code null} if none match.
*
* @param credentials the credentials to select from.
* @param matcher the matcher.
* @param <C> the type of credential.
* @return a matching credential or the supplied default.
*/
@CheckForNull
public static <C extends Credentials> C firstOrNull(@NonNull Iterable<C> credentials,
@NonNull CredentialsMatcher matcher) {
return firstOrDefault(credentials, matcher, null);
}
/**
* Attempts to describe the supplied {@link CredentialsMatcher} in terms of a Credentials Query Language. The basic
* form of the query language should follow Java expression syntax assuming that there is one variable in scope,
* namely the credential. The Java Bean style properties will be exposed as variables in the context. Example:
* {@code (instanceof com.cloudbees.plugins.credentials.common.UsernameCredentials) && !(username == "bob")}
* will match all instances of {@link com.cloudbees.plugins.credentials.common.UsernameCredentials} with
* {@link UsernameCredentials#getUsername()} not equal to {@literal bob}.
* See also {@link CQLParser}.
*
* @param matcher the {@link CredentialsMatcher} to describe.
* @return the CQL description or {@code null} if the {@link CredentialsMatcher} cannot be mapped to CQL.
* @since 2.1.0
*/
@CheckForNull
public static String describe(CredentialsMatcher matcher) {
return matcher instanceof CredentialsMatcher.CQL ? ((CredentialsMatcher.CQL) matcher).describe() : null;
}
/**
* Attempts to parse a Credentials Query Language expression and construct the corresponding matcher.
*
* @param cql the Credentials Query Language expression to parse.
* @return a {@link CredentialsMatcher} for this expression.
* @throws CQLSyntaxException if the expression could not be parsed.
* @since 2.1.0
*/
@NonNull
public static CredentialsMatcher parse(final String cql) {
if (StringUtils.isEmpty(cql)) {
return always();
}
CQLLexer lexer = new CQLLexer(new ANTLRInputStream(cql));
CommonTokenStream tokens = new CommonTokenStream(lexer);
CQLParser parser = new CQLParser(tokens);
parser.removeErrorListeners();
parser.addErrorListener(new BaseErrorListener() {
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,
int charPositionInLine,
String msg, RecognitionException e) {
StringBuilder expression = new StringBuilder(cql.length() + msg.length() + charPositionInLine + 256);
String[] lines = StringUtils.split(cql, '\n');
for (int i = 0; i < line; i++) {
expression.append(" ").append(lines[i]).append('\n');
}
expression.append(" ").append(StringUtils.repeat(" ", charPositionInLine)).append("^ ").append(msg);
for (int i = line; i < lines.length; i++) {
expression.append("\n ").append(lines[i]);
}
throw new CQLSyntaxException(
String.format("CQL syntax error: line %d:%d%n%s", line, charPositionInLine, expression),
charPositionInLine);
}
});
CQLParser.ExpressionContext expressionContext = parser.expression();
ParseTreeWalker walker = new ParseTreeWalker();
MatcherBuildingListener listener = new MatcherBuildingListener();
try {
walker.walk(listener, expressionContext);
return listener.getMatcher();
} catch (EmptyStackException e) {
throw new IllegalStateException("There should not be an empty stack when starting from an expression", e);
} catch (CQLSyntaxError e) {
throw new CQLSyntaxException(
String.format("CQL syntax error:%n %s%n %s%s unexpected symbol %s", cql,
StringUtils.repeat(" ", e.interval.a),
StringUtils.repeat("^", e.interval.length()),
e.text
), e.interval.a);
}
}
/**
* A listener to build the matcher.
*/
private static class MatcherBuildingListener extends CQLBaseListener {
/**
* The current primary value.
*/
private CredentialsMatcher primary;
/**
* The current literal value.
*/
private Serializable literal;
/**
* The stack of expressions.
*/
private Stack<CredentialsMatcher> expression = new Stack<CredentialsMatcher>();
/**
* Returns the {@link CredentialsMatcher}.
*
* @return the {@link CredentialsMatcher}.
*/
private CredentialsMatcher getMatcher() {
return expression.pop();
}
/**
* {@inheritDoc}
*/
@Override
public void exitExpression(CQLParser.ExpressionContext ctx) {
if (ctx.AND() != null) {
CredentialsMatcher second = expression.pop();
CredentialsMatcher first = expression.pop();
expression.push(CredentialsMatchers.allOf(first, second));
} else if (ctx.OR() != null) {
CredentialsMatcher second = expression.pop();
CredentialsMatcher first = expression.pop();
expression.push(CredentialsMatchers.anyOf(first, second));
} else {
expression.push(primary);
}
}
/**
* {@inheritDoc}
*/
@Override
public void exitConstantTest(CQLParser.ConstantTestContext ctx) {
primary = new ConstantMatcher(Boolean.parseBoolean(ctx.BooleanLiteral().getSymbol().getText()));
}
/**
* {@inheritDoc}
*/
@Override
public void exitPropertyTest(CQLParser.PropertyTestContext ctx) {
primary = new BeanPropertyMatcher<Serializable>(ctx.Identifier().getText(), literal);
}
/**
* {@inheritDoc}
*/
@Override
public void exitNegativeTest(CQLParser.NegativeTestContext ctx) {
primary = new NotMatcher(primary);
}
/**
* {@inheritDoc}
*/
@Override
public void exitInstanceOfTest(CQLParser.InstanceOfTestContext ctx) {
try {
primary = new InstanceOfMatcher(Class.forName(ctx.qualifiedName().getText()));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* {@inheritDoc}
*/
@Override
public void exitGroupedTest(CQLParser.GroupedTestContext ctx) {
primary = expression.pop();
}
/**
* {@inheritDoc}
*/
@Override
public void exitLiteral(CQLParser.LiteralContext ctx) {
if (ctx.BooleanLiteral() != null) {
literal = Boolean.valueOf(ctx.BooleanLiteral().getText());
} else if (ctx.StringLiteral() != null) {
String text = ctx.StringLiteral().getText();
literal = StringEscapeUtils.unescapeJava(text.substring(1, text.length() - 1));
} else if (ctx.CharacterLiteral() != null) {
String text = ctx.StringLiteral().getText();
literal = StringEscapeUtils.unescapeJava(text.substring(1, text.length() - 1)).charAt(0);
} else if (ctx.IntegerLiteral() != null) {
literal = Integer.valueOf(ctx.IntegerLiteral().getText());
} else if (ctx.FloatingPointLiteral() != null) {
literal = Double.valueOf(ctx.FloatingPointLiteral().getText());
} else if (ctx.NullLiteral() != null) {
literal = null;
} else if (ctx.enumLiteral() != null) {
String enumClass = ctx.enumLiteral().qualifiedName().getText();
String enumConst = ctx.enumLiteral().Identifier().getText();
try {
Class<?> enumClazz = Class.forName(enumClass);
Field field = enumClazz.getDeclaredField(enumConst);
literal = (Serializable) field.get(null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void visitErrorNode(ErrorNode node) {
throw new CQLSyntaxError(node);
}
}
/**
* Internal exception to track an error not caught during the parse.
*
* @since 2.1.0
*/
private static class CQLSyntaxError extends RuntimeException {
/**
* The eror node's text.
*/
private final String text;
/**
* The error node's location.
*/
private final Interval interval;
/**
* Constructor.
*
* @param node the error node.
*/
private CQLSyntaxError(ErrorNode node) {
this.text = node.getText();
int offset = 0;
ParseTree n = node;
while (n != null) {
offset += n.getSourceInterval().a;
n = n.getParent();
}
this.interval = new Interval(offset + node.getSourceInterval().a, offset + node.getSourceInterval().b);
}
}
}