/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
* (C) 2009, Geomatys
*
* 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.geotoolkit.filter;
import java.io.Serializable;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import org.geotoolkit.util.StringUtilities;
import org.apache.sis.util.logging.Logging;
import org.opengis.filter.FilterVisitor;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.expression.Expression;
import static org.apache.sis.util.ArgumentChecks.*;
import org.geotoolkit.temporal.object.TemporalUtilities;
/**
* Defines a like filter, which checks to see if an attribute matches a REGEXP.
*
* @author Rob Hranac, Vision for New York
* @version $Id$
* @module
*/
public class DefaultPropertyIsLike implements PropertyIsLike,Serializable {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.filter");
/** The attribute value, which must be an attribute expression. */
private final Expression attribute;
/** The (limited) REGEXP pattern. */
private final String pattern;
/** The single wildcard for the REGEXP pattern. */
private final String wildcardSingle;
/** The multiple wildcard for the REGEXP pattern. */
private final String wildcardMulti;
/** The escape sequence for the REGEXP pattern. */
private final String escape;
/** The matcher to match patterns with. */
private transient Matcher match;
/** Used to indicate if case should be ignored or not */
private final 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(final char escape, final char multi, final char single, final String pattern)
throws IllegalArgumentException {
if ((escape == '\'') || (multi == '\'') || (single == '\'')) {
throw new IllegalArgumentException("do not use single quote (') as special char!");
}
final StringBuffer result = new StringBuffer(pattern.length() + 5);
for (int i = 0; i < pattern.length(); i++) {
final 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(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 DefaultPropertyIsLike.convertToSQL92(
escape.charAt(0),
wildcardMulti.charAt(0),
wildcardSingle.charAt(0),
pattern);
}
private Matcher getMatcher() {
if (match == null) {
// protect the vars as this is moved code
String pattern1 = this.pattern;
// 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.
final char esc = escape.charAt(0);
LOGGER.log(Level.FINER, "wildcard {0} single {1}", new Object[]{wildcardMulti, wildcardSingle});
LOGGER.log(Level.FINER, "escape {0} esc {1} esc == \\ {2}", new Object[]{escape, esc, esc == '\\'});
final String escapedWildcardMulti = fixSpecials(wildcardMulti);
final String escapedWildcardSingle = fixSpecials(wildcardSingle);
// escape any special chars which are not our wildcards
final StringBuffer tmp = new StringBuffer("");
boolean escapedMode = false;
for (int i = 0; i < pattern1.length(); i++) {
char chr = pattern1.charAt(i);
LOGGER.log(Level.FINER, "tmp = {0} looking at {1}", new Object[]{tmp, chr});
if (pattern1.regionMatches(false, i, escape, 0, escape.length())) {
// skip the escape string
LOGGER.finer("escape ");
escapedMode = true;
i += escape.length();
chr = pattern1.charAt(i);
}
if (pattern1.regionMatches(false, i, wildcardMulti, 0,
wildcardMulti.length())) { // replace with java wildcard
LOGGER.finer("multi wildcard");
if (escapedMode) {
LOGGER.finer("escaped ");
tmp.append(escapedWildcardMulti);
} else {
tmp.append(".*");
}
i += wildcardMulti.length() - 1;
escapedMode = false;
continue;
}
if (pattern1.regionMatches(false, i, wildcardSingle, 0,
wildcardSingle.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 += wildcardSingle.length() - 1;
escapedMode = false;
continue;
}
if (isSpecial(chr)) {
LOGGER.finer("special");
tmp.append(this.escape).append(chr);
escapedMode = false;
continue;
}
tmp.append(chr);
escapedMode = false;
}
pattern1 = tmp.toString();
LOGGER.log(Level.FINER, "final pattern {0}", pattern1);
java.util.regex.Pattern compPattern = java.util.regex.Pattern.compile(pattern1);
match = compPattern.matcher("");
}
return match;
}
public DefaultPropertyIsLike(final Expression expr, final String pattern, final String wildcardMulti,
final String wildcardSingle, final String escape, final boolean matchCase) {
ensureNonNull("expression", expr);
this.attribute = expr;
this.pattern = pattern;
this.wildcardMulti = wildcardMulti;
this.wildcardSingle = wildcardSingle;
this.escape = escape;
this.matchingCase = matchCase;
}
@Override
public org.opengis.filter.expression.Expression getExpression() {
return attribute;
}
@Override
public String getLiteral() {
return this.pattern;
}
@Override
public boolean evaluate(final 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;
} else if (value instanceof Date) {
value = TemporalUtilities.toISO8601((Date) value);
}
final Matcher matcher = getMatcher();
matcher.reset(String.valueOf(value));
return matcher.matches();
}
@Override
public java.lang.String getEscape() {
return escape;
}
@Override
public String getWildCard() {
return wildcardMulti;
}
@Override
public String getSingleChar() {
return wildcardSingle;
}
@Override
public boolean isMatchingCase() {
return matchingCase;
}
/**
* 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) {
final StringBuffer tmp = new StringBuffer("");
for (int i = 0; i < inString.length(); i++) {
final char chr = inString.charAt(i);
if (isSpecial(chr)) {
tmp.append(this.escape).append(chr);
} else {
tmp.append(chr);
}
}
return tmp.toString();
}
@Override
public Object accept(final FilterVisitor visitor, final Object extraData) {
return visitor.visit(this, extraData);
}
@Override
public int hashCode() {
int hash = 5;
hash = 59 * hash + (this.attribute != null ? this.attribute.hashCode() : 0);
hash = 59 * hash + (this.pattern != null ? this.pattern.hashCode() : 0);
hash = 59 * hash + (this.wildcardSingle != null ? this.wildcardSingle.hashCode() : 0);
hash = 59 * hash + (this.wildcardMulti != null ? this.wildcardMulti.hashCode() : 0);
hash = 59 * hash + (this.escape != null ? this.escape.hashCode() : 0);
hash = 59 * hash + (this.matchingCase ? 1 : 0);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final DefaultPropertyIsLike other = (DefaultPropertyIsLike) obj;
if (this.attribute != other.attribute && (this.attribute == null || !this.attribute.equals(other.attribute))) {
return false;
}
if ((this.pattern == null) ? (other.pattern != null) : !this.pattern.equals(other.pattern)) {
return false;
}
if ((this.wildcardSingle == null) ? (other.wildcardSingle != null) : !this.wildcardSingle.equals(other.wildcardSingle)) {
return false;
}
if ((this.wildcardMulti == null) ? (other.wildcardMulti != null) : !this.wildcardMulti.equals(other.wildcardMulti)) {
return false;
}
if ((this.escape == null) ? (other.escape != null) : !this.escape.equals(other.escape)) {
return false;
}
if (this.matchingCase != other.matchingCase) {
return false;
}
return true;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("PropertyIsLike (");
sb.append("pattern=").append(pattern).append(", ");
sb.append("wildcardSingle=").append(wildcardSingle).append(", ");
sb.append("wildcardMulti=").append(wildcardMulti).append(", ");
sb.append("escape=").append(escape).append(", ");
sb.append("matchingCase=").append(matchingCase);
sb.append(")\n");
sb.append(StringUtilities.toStringTree(attribute));
return sb.toString();
}
}