/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2016, 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.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.opengis.filter.FilterVisitor;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
/**
* 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 AbstractFilter implements PropertyIsLike {
/** The attribute value, which must be an attribute expression. */
private org.opengis.filter.expression.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;
/** Used to indicate action with multiple values **/
protected MatchAction matchAction;
/**
* 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 MatchAction getMatchAction() {
return matchAction;
}
public void setMatchingCase(boolean matchingCase) {
this.matchingCase = matchingCase;
}
private Matcher getMatcher(String string) {
if (compPattern == null) {
String pattern = new LikeToRegexConverter(this).getPattern();
compPattern = isMatchingCase() ? Pattern.compile(pattern)
: Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
}
return compPattern.matcher(string);
}
/**
* Constructor which flags the operator as like.
*/
protected LikeFilterImpl() {
this(MatchAction.ANY);
}
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);
}
protected LikeFilterImpl(MatchAction matchAction) {
this.matchAction = matchAction;
}
public LikeFilterImpl(org.opengis.filter.expression.Expression expr, String pattern, String wildcardMulti,
String wildcardSingle, String escape, MatchAction matchAction) {
this(matchAction);
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.
* @deprecated Use {@link #setExpression(org.opengis.filter.expression.Expression)}
*/
public final void setValue(Expression attribute) throws IllegalFilterException {
setExpression(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 attribute;
}
public void setExpression(org.opengis.filter.expression.Expression e) {
this.attribute = e;
}
/**
* 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.opengis.filter.expression.Expression p, String wildcardMulti,
String wildcardSingle, String escape) {
if( p instanceof Literal){
Literal literal = (Literal) p;
Object value = literal.getValue();
if( value != null && value instanceof String){
String pattern = (String) value;
setPattern(pattern, wildcardMulti, wildcardSingle, escape);
}
else {
throw new ClassCastException("Pattern Literal must be a string:"+value);
}
}
else {
throw new ClassCastException("Pattern must be a literal String");
}
}
/**
* 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 = eval(attribute, feature);
if (null == value) {
return false;
}
//NC - support multiple values
if (value instanceof Collection) {
int count = 0;
for (Object element : (Collection<Object>) value){
Matcher matcher = getMatcher(element.toString());
boolean temp = matcher.matches();
if (temp) {
count++;
}
switch (matchAction){
case ONE: if (count > 1) return false; break;
case ALL: if (!temp) return false; break;
case ANY: if (temp) return true; break;
}
}
switch (matchAction){
case ONE: return (count == 1);
case ALL: return true;
case ANY: return false;
default: return false;
}
} else {
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();
}
/**
* 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 ((Filters.getFilterType( lFilter ) == Filters.getFilterType( this ))
&& lFilter.getExpression().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);
}
}