/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.query.sql.lang; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.teiid.api.exception.query.ExpressionEvaluationException; import org.teiid.core.util.EquivalenceUtil; import org.teiid.core.util.HashCodeUtil; import org.teiid.core.util.LRUCache; import org.teiid.core.util.PropertiesUtils; import org.teiid.language.Like.MatchMode; import org.teiid.query.QueryPlugin; import org.teiid.query.sql.LanguageVisitor; import org.teiid.query.sql.lang.PredicateCriteria.Negatable; import org.teiid.query.sql.symbol.Expression; /** * This class represents a criteria involving a string expression to be matched * against a string expression match value. The match value may contain a few * special characters: % represents 0 or more characters and _ represents a single * match character. The escape character can be used to escape an actual % or _ within a * match string. */ public class MatchCriteria extends PredicateCriteria implements Negatable { /** The default wildcard character - '%' */ public static final char WILDCARD_CHAR = '%'; /** The default single match character - '_' */ public static final char MATCH_CHAR = '_'; /** The left-hand expression. */ private Expression leftExpression; /** The right-hand expression. */ private Expression rightExpression; /** The internal null escape character */ public static final char NULL_ESCAPE_CHAR = 0; static char DEFAULT_ESCAPE_CHAR = PropertiesUtils.getBooleanProperty(System.getProperties(), "org.teiid.backslashDefaultMatchEscape", false)?'\\':NULL_ESCAPE_CHAR; //$NON-NLS-1$ /** The escape character or '' if there is none */ private char escapeChar = DEFAULT_ESCAPE_CHAR; /** Negation flag. Indicates whether the criteria expression contains a NOT. */ private boolean negated; private MatchMode mode = MatchMode.LIKE; /** * Constructs a default instance of this class. */ public MatchCriteria() {} /** * Constructs an instance of this class from a left and right expression * * @param leftExpression The expression to check * @param rightExpression The match expression */ public MatchCriteria( Expression leftExpression, Expression rightExpression ) { setLeftExpression(leftExpression); setRightExpression(rightExpression); } /** * Constructs an instance of this class from a left and right expression * and an escape character * * @param leftExpression The expression to check * @param rightExpression The match expression * @param escapeChar The escape character, to allow literal use of wildcard and single match chars */ public MatchCriteria( Expression leftExpression, Expression rightExpression, char escapeChar ) { this(leftExpression, rightExpression); setEscapeChar(escapeChar); } /** * Set left expression. * @param expression expression */ public void setLeftExpression(Expression expression) { this.leftExpression = expression; } /** * Get left expression. * @return Left expression */ public Expression getLeftExpression() { return this.leftExpression; } /** * Set right expression. * @param expression expression */ public void setRightExpression(Expression expression) { this.rightExpression = expression; } /** * Get right expression. * @return right expression */ public Expression getRightExpression() { return this.rightExpression; } /** * Get the escape character, which can be placed before the wildcard or single match * character in the expression to prevent it from being used as a wildcard or single * match. The escape character must not be used elsewhere in the expression. * For example, to match "35%" without activating % as a wildcard, set the escape character * to '$' and use the expression "35$%". * @return Escape character, if not set will return {@link #NULL_ESCAPE_CHAR} */ public char getEscapeChar() { return this.escapeChar; } /** * Set the escape character which can be used when the wildcard or single * character should be used literally. * @param escapeChar New escape character */ public void setEscapeChar(char escapeChar) { this.escapeChar = escapeChar; } /** * Returns whether this criteria is negated. * @return flag indicating whether this criteria contains a NOT */ public boolean isNegated() { return negated; } /** * Sets the negation flag for this criteria. * @param negationFlag true if this criteria contains a NOT; false otherwise */ public void setNegated(boolean negationFlag) { negated = negationFlag; } @Override public void negate() { this.negated = !this.negated; } public void acceptVisitor(LanguageVisitor visitor) { visitor.visit(this); } /** * Get hash code. WARNING: The hash code is based on data in the criteria. * If data values are changed, the hash code will change - don't hash this * object and change values. * @return Hash code */ public int hashCode() { int hc = 0; hc = HashCodeUtil.hashCode(hc, getLeftExpression()); hc = HashCodeUtil.hashCode(hc, getRightExpression()); return hc; } /** * Override equals() method. * @param obj Other object * @return True if equal */ public boolean equals(Object obj) { // Use super.equals() to check obvious stuff and variable if(obj == this) { return true; } if(!(obj instanceof MatchCriteria)) { return false; } MatchCriteria mc = (MatchCriteria)obj; if (isNegated() != mc.isNegated()) { return false; } if (this.mode != mc.mode) { return false; } return getEscapeChar() == mc.getEscapeChar() && EquivalenceUtil.areEqual(getLeftExpression(), mc.getLeftExpression()) && EquivalenceUtil.areEqual(getRightExpression(), mc.getRightExpression()); } /** * Deep copy of object * @return Deep copy of object */ public Object clone() { Expression leftCopy = null; if(getLeftExpression() != null) { leftCopy = (Expression) getLeftExpression().clone(); } Expression rightCopy = null; if(getRightExpression() != null) { rightCopy = (Expression) getRightExpression().clone(); } MatchCriteria criteriaCopy = new MatchCriteria(leftCopy, rightCopy, getEscapeChar()); criteriaCopy.setNegated(isNegated()); criteriaCopy.mode = mode; return criteriaCopy; } private final static LRUCache<List<?>, Pattern> patternCache = new LRUCache<List<?>, Pattern>(100); public static Pattern getPattern(String newPattern, String originalPattern, int flags) throws ExpressionEvaluationException { List<?> key = Arrays.asList(newPattern, flags); Pattern p = patternCache.get(key); if (p == null) { try { p = Pattern.compile(newPattern, Pattern.DOTALL); patternCache.put(key, p); } catch(PatternSyntaxException e) { throw new ExpressionEvaluationException(QueryPlugin.Event.TEIID30448, e, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30448, new Object[]{originalPattern, e.getMessage()})); } } return p; } /** * <p>Utility to convert the pattern into a different match syntax</p> */ public static class PatternTranslator { private char[] reserved; private char newEscape; private char[] toReplace; private String[] replacements; private int flags; private final LRUCache<List<?>, Pattern> cache = new LRUCache<List<?>, Pattern>(100); /** * @param newWildCard replacement for % * @param newSingleMatch replacement for _ * @param reserved sorted array of reserved chars in the new match syntax * @param newEscape escape char in the new syntax */ public PatternTranslator(char[] toReplace, String[] replacements, char[] reserved, char newEscape, int flags) { this.reserved = reserved; this.newEscape = newEscape; this.toReplace = toReplace; this.replacements = replacements; this.flags = flags; } public Pattern translate(String pattern, char escape) throws ExpressionEvaluationException { List<?> key = Arrays.asList(pattern, escape); Pattern result = null; synchronized (cache) { result = cache.get(key); } if (result == null) { String newPattern = getPatternString(pattern, escape); result = getPattern(newPattern, pattern, flags); synchronized (cache) { cache.put(key, result); } } return result; } public String getPatternString(String pattern, char escape) throws ExpressionEvaluationException { int startChar = 0; StringBuffer newPattern = new StringBuffer(pattern.length()); if (pattern.length() > 0 && pattern.charAt(0) == '%') { startChar = 1; } else { newPattern.append('^'); } boolean escaped = false; boolean endsWithMatchAny = false; for (int i = startChar; i < pattern.length(); i++) { char character = pattern.charAt(i); if (character == escape && character != NULL_ESCAPE_CHAR) { if (escaped) { appendCharacter(newPattern, character); escaped = false; } else { escaped = true; } } else { int index = Arrays.binarySearch(toReplace, character); if (index >= 0) { if (escaped) { appendCharacter(newPattern, character); escaped = false; } else { if (character == '%' && i == pattern.length() - 1) { endsWithMatchAny = true; continue; } newPattern.append(replacements[index]); } } else { if (escaped) { throw new ExpressionEvaluationException(QueryPlugin.Event.TEIID30449, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30449, new Object[] {pattern, new Character(escape)})); } appendCharacter(newPattern, character); } } } if (escaped) { throw new ExpressionEvaluationException(QueryPlugin.Event.TEIID30449, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30449, new Object[] {pattern, new Character(escape)})); } if (!endsWithMatchAny) { newPattern.append('$'); } return newPattern.toString(); } private void appendCharacter(StringBuffer newPattern, char character) { if (Arrays.binarySearch(this.reserved, character) >= 0) { newPattern.append(this.newEscape); } newPattern.append(character); } } public MatchMode getMode() { return mode; } public void setMode(MatchMode mode) { this.mode = mode; } } // END CLASS