/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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;
* version 2.1 of the License.
*
* 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.
*/
package org.geotools.filter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.geotools.factory.CommonFactoryFinder;
import org.opengis.filter.FilterVisitor;
import org.opengis.filter.PropertyIsLike;
/**
* Defines a like filter, which checks to see if an attribute matches a REGEXP.
*
* @author Rob Hranac, Vision for New York
* @source $URL$
* @version $Id$
*/
public class LikeFilterImpl extends AbstractFilterImpl implements LikeFilter {
/** The attribute value, which must be an attribute expression. */
private Expression attribute = null;
/** The (limited) REGEXP pattern. */
private String pattern = null;
/** The single wildcard for the REGEXP pattern. */
private String wildcardSingle = ".?";
/** The multiple wildcard for the REGEXP pattern. */
private String wildcardMulti = ".*";
/** The escape sequence for the REGEXP pattern. */
private String escape = "\\";
/** the pattern compiled into a java regex */
private Pattern compPattern = null;
/** Used to indicate if case should be ignored or not */
boolean matchingCase;
/**
* Given OGC PropertyIsLike Filter information, construct
* an SQL-compatible 'like' pattern.
*
* SQL % --> match any number of characters
* _ --> match a single character
*
* NOTE; the SQL command is 'string LIKE pattern [ESCAPE escape-character]'
* We could re-define the escape character, but I'm not doing to do that in this code
* since some databases will not handle this case.
*
* Method:
* 1.
*
* Examples: ( escape ='!', multi='*', single='.' )
* broadway* -> 'broadway%'
* broad_ay -> 'broad_ay'
* broadway -> 'broadway'
*
* broadway!* -> 'broadway*' (* has no significance and is escaped)
* can't -> 'can''t' ( ' escaped for SQL compliance)
*
*
* NOTE: we also handle "'" characters as special because they are
* end-of-string characters. SQL will convert ' to '' (double single quote).
*
* NOTE: we dont handle "'" as a 'special' character because it would be
* too confusing to have a special char as another special char.
* Using this will throw an error (IllegalArgumentException).
*
* @param escape
* @param multi
* @param single
* @param pattern
*
*/
public static String convertToSQL92(char escape, char multi, char single, boolean matchCase,
String pattern ) throws IllegalArgumentException {
if ( (escape == '\'') || (multi == '\'') || (single == '\'') )
throw new IllegalArgumentException("do not use single quote (') as special char!");
StringBuffer result = new StringBuffer(pattern.length()+5);
for (int i = 0; i < pattern.length(); i++)
{
char chr = pattern.charAt(i);
if (chr == escape)
{
// emit the next char and skip it
if (i!= (pattern.length()-1) )
result.append( pattern.charAt(i+1) );//
i++; // skip next char
}
else if (chr == single)
{
result.append('_');
}
else if (chr == multi)
{
result.append('%');
}
else if (chr == '\'')
{
result.append('\'');
result.append('\'');
}
else
{
result.append(matchCase ? chr : Character.toUpperCase(chr));
}
}
return result.toString();
}
/**
* see convertToSQL92
*
* @throws IllegalArgumentException
*/
public String getSQL92LikePattern() throws IllegalArgumentException
{
if (escape.length() !=1)
{
throw new IllegalArgumentException("Like Pattern --> escape char should be of length exactly 1");
}
if (wildcardSingle.length() !=1)
{
throw new IllegalArgumentException("Like Pattern --> wildcardSingle char should be of length exactly 1");
}
if (wildcardMulti.length() !=1)
{
throw new IllegalArgumentException("Like Pattern --> wildcardMulti char should be of length exactly 1");
}
return LikeFilterImpl.convertToSQL92(
escape.charAt(0),
wildcardMulti.charAt(0),
wildcardSingle.charAt(0),
matchingCase,
pattern);
}
public void setWildCard(String wildCard) {
this.wildcardMulti = wildCard;
compPattern = null;
}
public void setSingleChar(String singleChar) {
this.wildcardSingle = singleChar;
compPattern = null;
}
public void setEscape(String escape) {
this.escape = escape;
compPattern = null;
}
public void setMatchCase(boolean matchingCase){
this.matchingCase = matchingCase;
compPattern = null;
}
public boolean isMatchingCase() {
return matchingCase;
}
public void setMatchingCase(boolean matchingCase) {
this.matchingCase = matchingCase;
}
private Matcher getMatcher(String string){
if(compPattern == null){
// protect the vars as this is moved code
String pattern1 = new String(this.pattern);
String wildcardMulti1 = new String(this.wildcardMulti);
String wildcardSingle1 = new String(this.wildcardSingle);
String escape1 = new String(this.escape);
// The following things happen for both wildcards:
// (1) If a user-defined wildcard exists, replace with Java wildcard
// (2) If a user-defined escape exists, Java wildcard + user-escape
// Then, test for matching pattern and return result.
char esc = escape1.charAt(0);
LOGGER.finer("wildcard " + wildcardMulti1 + " single " + wildcardSingle1);
LOGGER.finer("escape " + escape1 + " esc " + esc + " esc == \\ "
+ (esc == '\\'));
String escapedWildcardMulti = fixSpecials(wildcardMulti1);
String escapedWildcardSingle = fixSpecials(wildcardSingle1);
// escape any special chars which are not our wildcards
StringBuffer tmp = new StringBuffer("");
boolean escapedMode = false;
for (int i = 0; i < pattern1.length(); i++) {
char chr = pattern1.charAt(i);
LOGGER.finer("tmp = " + tmp + " looking at " + chr);
if (pattern1.regionMatches(false, i, escape1, 0, escape1.length())) {
// skip the escape string
LOGGER.finer("escape ");
escapedMode = true;
i += escape1.length();
chr = pattern1.charAt(i);
}
if (pattern1.regionMatches(false, i, wildcardMulti1, 0,
wildcardMulti1.length())) { // replace with java wildcard
LOGGER.finer("multi wildcard");
if (escapedMode) {
LOGGER.finer("escaped ");
tmp.append(escapedWildcardMulti);
} else {
tmp.append(".*");
}
i += (wildcardMulti1.length() - 1);
escapedMode = false;
continue;
}
if (pattern1.regionMatches(false, i, wildcardSingle1, 0,
wildcardSingle1.length())) {
// replace with java single wild card
LOGGER.finer("single wildcard");
if (escapedMode) {
LOGGER.finer("escaped ");
tmp.append(escapedWildcardSingle);
} else {
// From the OpenGIS filter encoding spec,
// "the single singleChar character matches exactly one character"
tmp.append(".{1}");
}
i += (wildcardSingle1.length() - 1);
escapedMode = false;
continue;
}
if (isSpecial(chr)) {
LOGGER.finer("special");
tmp.append(this.escape + chr);
escapedMode = false;
continue;
}
tmp.append(chr);
escapedMode = false;
}
pattern1 = tmp.toString();
LOGGER.finer("final pattern " + pattern1);
compPattern = isMatchingCase()
? Pattern.compile(pattern1)
: Pattern.compile(pattern1, Pattern.CASE_INSENSITIVE);
}
return compPattern.matcher(string);
}
/**
* Constructor which flags the operator as like.
*/
protected LikeFilterImpl() {
super(CommonFactoryFinder.getFilterFactory(null));
filterType = LIKE;
}
public LikeFilterImpl(org.opengis.filter.expression.Expression expr, String pattern, String wildcardMulti,
String wildcardSingle, String escape) {
this();
setExpression(expr);
setLiteral(pattern);
setWildCard(wildcardMulti);
setSingleChar(wildcardSingle);
setEscape(escape);
}
/**
* Sets the expression to be evalutated as being like the pattern
*
* @param attribute The value of the attribute for comparison.
*
* @throws IllegalFilterException Filter is illegal.
*/
public final void setValue(Expression attribute) throws IllegalFilterException {
setExpression(attribute);
}
/**
* Gets the Value (left hand side) of this filter.
*
* @return The expression that is the value of the filter.
*
* @deprecated use {@link #getExpression()}.
*/
public final org.geotools.filter.Expression getValue() {
return attribute;
}
/**
* Gets the expression for hte filter.
* <p>
* This method calls th deprecated {@link #getValue()} for backwards
* compatability with subclasses.
* </p>
*/
public org.opengis.filter.expression.Expression getExpression() {
return getValue();
}
public void setExpression(org.opengis.filter.expression.Expression e) {
Expression attribute = (Expression)e;
if ((attribute.getType() != ExpressionType.ATTRIBUTE_STRING)
|| permissiveConstruction) {
this.attribute = attribute;
} else {
throw new IllegalFilterException(
"Attempted to add something other than a string attribute "
+ "expression to a like filter.");
}
}
/**
* Sets the match pattern for this FilterLike.
*
* @param p the expression which evaluates to the match pattern for this
* filter
* @param wildcardMulti the string that represents a mulitple character
* (1->n) wildcard
* @param wildcardSingle the string that represents a single character (1)
* wildcard
* @param escape the string that represents an escape character
*
* @deprecated use one of
* {@link PropertyIsLike#setExpression(Expression)}
* {@link PropertyIsLike#setWildCard(String)}
* {@link PropertyIsLike#setSingleChar(String)}
* {@link PropertyIsLike#setEscape(String)}
*/
public final void setPattern(org.geotools.filter.Expression p, String wildcardMulti,
String wildcardSingle, String escape) {
setPattern(p.toString(), wildcardMulti, wildcardSingle, escape);
}
/**
* Sets the match pattern for this FilterLike.
*
* @param pattern the string which contains the match pattern for this
* filter
* @param wildcardMulti the string that represents a mulitple character
* (1->n) wildcard
* @param wildcardSingle the string that represents a single character (1)
* wildcard
* @param escape the string that represents an escape character
*
* @deprecated use one of
* {@link PropertyIsLike#setLiteral(String)}
* {@link PropertyIsLike#setWildCard(String)}
* {@link PropertyIsLike#setSingleChar(String)}
* {@link PropertyIsLike#setEscape(String)}
*/
public final void setPattern(String pattern, String wildcardMulti,
String wildcardSingle, String escape) {
setLiteral(pattern);
setWildCard(wildcardMulti);
setSingleChar(wildcardSingle);
setEscape(escape);
}
/**
* Accessor method to retrieve the pattern.
*
* @return the pattern being matched.
*
* @deprecated use {@link #getLiteral()}
*/
public final String getPattern() {
return getLiteral();
}
/**
* Returns the pattern.
*/
public String getLiteral() {
return this.pattern;
}
/**
* Sets the pattern.
*/
public void setLiteral(String literal) {
this.pattern = literal;
compPattern = null;
}
/**
* Determines whether or not a given feature matches this pattern.
*
* @param feature Specified feature to examine.
*
* @return Flag confirming whether or not this feature is inside the
* filter.
*
* @task REVISIT: could the pattern be null such that a null = null?
*/
public boolean evaluate(Object feature) {
//Checks to ensure that the attribute has been set
if (attribute == null) {
return false;
}
// Note that this converts the attribute to a string
// for comparison. Unlike the math or geometry filters, which
// require specific types to function correctly, this filter
// using the mandatory string representation in Java
// Of course, this does not guarantee a meaningful result, but it
// does guarantee a valid result.
//LOGGER.finest("pattern: " + pattern);
//LOGGER.finest("string: " + attribute.getValue(feature));
//return attribute.getValue(feature).toString().matches(pattern);
Object value = attribute.evaluate(feature);
if (null == value) {
return false;
}
Matcher matcher = getMatcher(value.toString());
return matcher.matches();
}
/**
* Return this filter as a string.
*
* @return String representation of this like filter.
*/
public String toString() {
return "[ " + attribute.toString() + " is like " + pattern + " ]";
}
/**
* Getter for property escape.
*
* @return Value of property escape.
*/
public java.lang.String getEscape() {
return escape;
}
/**
* Getter for property wildcardMulti.
*
* @return Value of property wildcardMulti.
*
* @deprecated use {@link #getWildCard()}.
*
*/
public final String getWildcardMulti() {
return wildcardMulti;
}
/**
* <p>
* THis method calls {@link #getWildcardMulti()} for subclass backwards
* compatability.
* </p>
*
* @see org.opengis.filter.PropertyIsLike#getWildCard().
*/
public String getWildCard() {
return getWildcardMulti();
}
/**
* Getter for property wildcardSingle.
*
* @return Value of property wildcardSingle.
*
* @deprecated use {@link #getSingleChar()}
*/
public final String getWildcardSingle() {
return wildcardSingle;
}
/**
* <p>
* THis method calls {@link #getWildcardSingle()()} for subclass backwards
* compatability.
* </p>
*
* @see org.opengis.filter.PropertyIsLike#getSingleChar()().
*/
public String getSingleChar() {
return getWildcardSingle();
}
/**
* convienience method to determine if a character is special to the regex
* system.
*
* @param chr the character to test
*
* @return is the character a special character.
*/
private boolean isSpecial(final char chr) {
return ((chr == '.') || (chr == '?') || (chr == '*') || (chr == '^')
|| (chr == '$') || (chr == '+') || (chr == '[') || (chr == ']')
|| (chr == '(') || (chr == ')') || (chr == '|') || (chr == '\\')
|| (chr == '&'));
}
/**
* convienience method to escape any character that is special to the regex
* system.
*
* @param inString the string to fix
*
* @return the fixed string
*/
private String fixSpecials(final String inString) {
StringBuffer tmp = new StringBuffer("");
for (int i = 0; i < inString.length(); i++) {
char chr = inString.charAt(i);
if (isSpecial(chr)) {
tmp.append(this.escape + chr);
} else {
tmp.append(chr);
}
}
return tmp.toString();
}
/**
* Compares this filter to the specified object. Returns true if the
* passed in object is the same as this filter. Checks to make sure the
* filter types, the value, and the pattern are the same. &
*
* @param obj - the object to compare this LikeFilter against.
*
* @return true if specified object is equal to this filter; false
* otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof LikeFilterImpl) {
LikeFilterImpl lFilter = (LikeFilterImpl) obj;
//REVISIT: check for nulls.
return ((lFilter.getFilterType() == this.filterType)
&& lFilter.getValue().equals(this.attribute)
&& lFilter.getPattern().equals(this.pattern));
}
return false;
}
/**
* Override of hashCode method.
*
* @return the hash code for this like filter implementation.
*/
public int hashCode() {
int result = 17;
result = (37 * result)
+ ((attribute == null) ? 0 : attribute.hashCode());
result = (37 * result) + ((pattern == null) ? 0 : pattern.hashCode());
return result;
}
/**
* Used by FilterVisitors to perform some action on this filter instance.
* Typicaly used by Filter decoders, but may also be used by any thing
* which needs infomration from filter structure. Implementations should
* always call: visitor.visit(this); It is importatant that this is not
* left to a parent class unless the parents API is identical.
*
* @param visitor The visitor which requires access to this filter, the
* method must call visitor.visit(this);
*/
public Object accept(FilterVisitor visitor, Object extraData) {
return visitor.visit(this,extraData);
}
}