001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.expression.spel.ast;
018
019import java.util.concurrent.ConcurrentHashMap;
020import java.util.concurrent.ConcurrentMap;
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
023import java.util.regex.PatternSyntaxException;
024
025import org.springframework.expression.EvaluationException;
026import org.springframework.expression.spel.ExpressionState;
027import org.springframework.expression.spel.SpelEvaluationException;
028import org.springframework.expression.spel.SpelMessage;
029import org.springframework.expression.spel.support.BooleanTypedValue;
030
031/**
032 * Implements the matches operator. Matches takes two operands:
033 * The first is a String and the second is a Java regex.
034 * It will return {@code true} when {@link #getValue} is called
035 * if the first operand matches the regex.
036 *
037 * @author Andy Clement
038 * @author Juergen Hoeller
039 * @since 3.0
040 */
041public class OperatorMatches extends Operator {
042
043        private static final int PATTERN_ACCESS_THRESHOLD = 1000000;
044
045        private final ConcurrentMap<String, Pattern> patternCache = new ConcurrentHashMap<String, Pattern>();
046
047
048        public OperatorMatches(int pos, SpelNodeImpl... operands) {
049                super("matches", pos, operands);
050        }
051
052
053        /**
054         * Check the first operand matches the regex specified as the second operand.
055         * @param state the expression state
056         * @return {@code true} if the first operand matches the regex specified as the
057         * second operand, otherwise {@code false}
058         * @throws EvaluationException if there is a problem evaluating the expression
059         * (e.g. the regex is invalid)
060         */
061        @Override
062        public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
063                SpelNodeImpl leftOp = getLeftOperand();
064                SpelNodeImpl rightOp = getRightOperand();
065                String left = leftOp.getValue(state, String.class);
066                Object right = getRightOperand().getValue(state);
067
068                if (left == null) {
069                        throw new SpelEvaluationException(leftOp.getStartPosition(),
070                                        SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, left);
071                }
072                if (!(right instanceof String)) {
073                        throw new SpelEvaluationException(rightOp.getStartPosition(),
074                                        SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, right);
075                }
076
077                try {
078                        String rightString = (String) right;
079                        Pattern pattern = this.patternCache.get(rightString);
080                        if (pattern == null) {
081                                pattern = Pattern.compile(rightString);
082                                this.patternCache.putIfAbsent(rightString, pattern);
083                        }
084                        Matcher matcher = pattern.matcher(new MatcherInput(left, new AccessCount()));
085                        return BooleanTypedValue.forValue(matcher.matches());
086                }
087                catch (PatternSyntaxException ex) {
088                        throw new SpelEvaluationException(
089                                        rightOp.getStartPosition(), ex, SpelMessage.INVALID_PATTERN, right);
090                }
091                catch (IllegalStateException ex) {
092                        throw new SpelEvaluationException(
093                                        rightOp.getStartPosition(), ex, SpelMessage.FLAWED_PATTERN, right);
094                }
095        }
096
097
098        private static class AccessCount {
099
100                private int count;
101
102                public void check() throws IllegalStateException {
103                        if (this.count++ > PATTERN_ACCESS_THRESHOLD) {
104                                throw new IllegalStateException("Pattern access threshold exceeded");
105                        }
106                }
107        }
108
109
110        private static class MatcherInput implements CharSequence {
111
112                private final CharSequence value;
113
114                private AccessCount access;
115
116                public MatcherInput(CharSequence value, AccessCount access) {
117                        this.value = value;
118                        this.access = access;
119                }
120
121                public char charAt(int index) {
122                        this.access.check();
123                        return this.value.charAt(index);
124                }
125
126                public CharSequence subSequence(int start, int end) {
127                        return new MatcherInput(this.value.subSequence(start, end), this.access);
128                }
129
130                public int length() {
131                        return this.value.length();
132                }
133
134                @Override
135                public String toString() {
136                        return this.value.toString();
137                }
138        }
139
140}