@@ -6,9 +6,7 @@ import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.ECField; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKey; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.ECSearchable; | |||
import org.cobbzilla.wizard.model.search.SearchBound; | |||
import org.cobbzilla.wizard.model.search.SearchBoundBuilder; | |||
import org.cobbzilla.wizard.model.search.SearchField; | |||
import org.cobbzilla.wizard.model.search.*; | |||
import java.lang.reflect.Field; | |||
import java.util.ArrayList; | |||
@@ -51,22 +49,26 @@ public class SqlDefaultSearchField implements SearchField { | |||
@Override public SearchBound[] getBounds() { | |||
final List<SearchBound> bounds = new ArrayList<>(); | |||
EntityFieldType fieldType = search.type(); | |||
if (fieldType == none_set) { | |||
if (!empty(search.bounds())) { | |||
try { | |||
final SearchBoundBuilder builder = instantiate(search.bounds()); | |||
return builder.build(bound, value, params, locale); | |||
} catch (Exception e) { | |||
return die("getBounds(" + bound + "): error invoking custom SearchBoundBuilder: " + search.bounds() + ": " + e); | |||
} | |||
if (!empty(search.bounds())) { | |||
try { | |||
final SearchBoundBuilder builder = instantiate(search.bounds()); | |||
return builder.build(bound, value, params, locale); | |||
} catch (Exception e) { | |||
return die("getBounds(" + bound + "): error invoking custom SearchBoundBuilder: " + search.bounds() + ": " + e); | |||
} | |||
} else if (!empty(search.operators())) { | |||
for (SearchBoundComparison op : search.operators()) { | |||
bounds.add(op.bind(name(), SearchFieldType.string)); | |||
} | |||
} else if (fieldType == none_set) { | |||
final ECField ecField = f.getAnnotation(ECField.class); | |||
final ECForeignKey ecForeignKey = f.getAnnotation(ECForeignKey.class); | |||
if (ecForeignKey != null) { | |||
fieldType = reference; | |||
} else { | |||
final ECField ecField = f.getAnnotation(ECField.class); | |||
final ECForeignKey ecForeignKey = f.getAnnotation(ECForeignKey.class); | |||
if (ecForeignKey != null) { | |||
fieldType = reference; | |||
} else { | |||
fieldType = ecField != null && ecField.type() != none_set ? ecField.type() : guessFieldType(f); | |||
} | |||
fieldType = ecField != null && ecField.type() != none_set ? ecField.type() : guessFieldType(f); | |||
} | |||
} | |||
if (fieldType != null) { | |||
@@ -2,8 +2,10 @@ package org.cobbzilla.wizard.model.entityconfig; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.model.entityconfig.validation.*; | |||
import org.cobbzilla.wizard.model.search.SearchFieldType; | |||
import org.cobbzilla.wizard.validation.ValidationResult; | |||
import org.cobbzilla.wizard.validation.Validator; | |||
@@ -40,94 +42,94 @@ public enum EntityFieldType { | |||
email (new EntityConfigFieldValidator_email()), | |||
/** an integer-valued number */ | |||
integer (new EntityConfigFieldValidator_integer()), | |||
integer (SearchFieldType.integer, new EntityConfigFieldValidator_integer()), | |||
/** a real number */ | |||
decimal (new EntityConfigFieldValidator_decimal()), | |||
decimal (SearchFieldType.decimal, new EntityConfigFieldValidator_decimal()), | |||
/** an integer-valued monetary amount */ | |||
money_integer (null), | |||
money_integer (SearchFieldType.integer), | |||
/** a real-valued monetary amount */ | |||
money_decimal (null), | |||
money_decimal (SearchFieldType.decimal), | |||
/** a boolean value */ | |||
flag (new EntityConfigFieldValidator_boolean()), | |||
flag (SearchFieldType.flag, new EntityConfigFieldValidator_boolean()), | |||
/** a date value */ | |||
date (null), | |||
date (SearchFieldType.integer), | |||
/** a date value in the past (before current date) */ | |||
date_past (null), | |||
date_past (SearchFieldType.integer), | |||
/** a date value in the future (or current date) */ | |||
date_future (null), | |||
date_future (SearchFieldType.integer), | |||
/** a field for age */ | |||
age (null), | |||
age (SearchFieldType.integer), | |||
/** a 4-digit year field */ | |||
year (null), | |||
year (SearchFieldType.integer), | |||
/** a 4-digit year field that starts with the current year and goes into the past */ | |||
year_past (null), | |||
year_past (SearchFieldType.integer), | |||
/** a 4-digit year field that starts with the current year and goes into the future */ | |||
year_future (null), | |||
year_future (SearchFieldType.integer), | |||
/** a 4-digit year and 2-digit month field (YYYY-MM) */ | |||
year_and_month (null), | |||
year_and_month (SearchFieldType.integer), | |||
/** a 4-digit year and 2-digit month field (YYYY-MM) field that starts with the current year and goes into the past */ | |||
year_and_month_past (null), | |||
year_and_month_past (SearchFieldType.integer), | |||
/** a 4-digit year and 2-digit month field (YYYY-MM) field that starts with the current year and goes into the future */ | |||
year_and_month_future (null), | |||
year_and_month_future (SearchFieldType.integer), | |||
/** a date or date/time value, represented as milliseconds since 1/1/1970 */ | |||
epoch_time (new EntityConfigFieldValidator_integer()), | |||
epoch_time (SearchFieldType.integer, new EntityConfigFieldValidator_integer()), | |||
/** a date or date/time value, represented as milliseconds since 1/1/1970 */ | |||
expiration_time (new EntityConfigFieldValidator_integer()), | |||
expiration_time (SearchFieldType.integer, new EntityConfigFieldValidator_integer()), | |||
/** millisecond value for a time duration */ | |||
time_duration (new EntityConfigFieldValidator_integer()), | |||
time_duration (SearchFieldType.integer, new EntityConfigFieldValidator_integer()), | |||
/** a time-zone (for example America/New York) */ | |||
time_zone (null), | |||
time_zone (), | |||
/** a locale (for example en_US) */ | |||
locale (null), | |||
locale (), | |||
/** a 3-letter currency code (for example USD) */ | |||
currency (null), | |||
currency (), | |||
/** an IPv4 address */ | |||
ip4 (null), | |||
ip4 (), | |||
/** an IPv6 address */ | |||
ip6 (null), | |||
ip6 (), | |||
/** a hostname */ | |||
hostname (null), | |||
hostname (), | |||
/** a fully-qualified domain name */ | |||
fqdn (null), | |||
fqdn (), | |||
/** a 2-letter US state abbreviation */ | |||
us_state (null), | |||
us_state (), | |||
/** a US ZIP code */ | |||
us_zip (null), | |||
us_zip (), | |||
/** HTTP URL */ | |||
http_url (new EntityConfigFieldValidator_httpUrl()), | |||
/** a reference to another EntityConfig instance */ | |||
reference (null), | |||
reference (), | |||
/** a base64-encoded PNG image */ | |||
base64_png (null), | |||
base64_png (), | |||
/** an embedded sub-object */ | |||
embedded (new EntityConfigFieldValidator_embedded()), | |||
@@ -135,8 +137,24 @@ public enum EntityFieldType { | |||
/** a US phone number */ | |||
us_phone (new EntityConfigFieldValidator_USPhone()); | |||
@Getter private SearchFieldType searchFieldType; | |||
private EntityConfigFieldValidator fieldValidator; | |||
EntityFieldType (EntityConfigFieldValidator validator) { | |||
this.searchFieldType = SearchFieldType.string; | |||
this.fieldValidator = validator; | |||
} | |||
EntityFieldType (SearchFieldType searchFieldType) { | |||
this.searchFieldType = searchFieldType; | |||
this.fieldValidator = null; | |||
} | |||
EntityFieldType () { | |||
this.searchFieldType = SearchFieldType.string; | |||
this.fieldValidator = null; | |||
} | |||
/** Jackson-hook to create a new instance based on a string, case-insensitively */ | |||
@JsonCreator public static EntityFieldType fromString(String val) { return valueOf(val.toLowerCase()); } | |||
@@ -1,6 +1,7 @@ | |||
package org.cobbzilla.wizard.model.entityconfig.annotations; | |||
import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; | |||
import org.cobbzilla.wizard.model.search.SearchBoundComparison; | |||
import org.cobbzilla.wizard.model.search.SqlViewFieldSetter; | |||
import org.jasypt.hibernate4.encryptor.HibernatePBEStringEncryptor; | |||
@@ -20,6 +21,7 @@ public @interface ECSearchable { | |||
String sortField() default ""; | |||
EntityFieldType type() default EntityFieldType.none_set; | |||
String bounds() default ""; | |||
SearchBoundComparison[] operators() default {}; | |||
String entity() default ""; | |||
ECForeignKeySearchDepth fkDepth() default ECForeignKeySearchDepth.inherit; | |||
@@ -6,6 +6,8 @@ import org.cobbzilla.util.collection.ComparisonOperator; | |||
import org.cobbzilla.util.time.TimePeriodType; | |||
import org.cobbzilla.util.time.TimeUtil; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.List; | |||
@@ -27,7 +29,20 @@ public enum SearchBoundComparison { | |||
lt (sqlCompare(ComparisonOperator.lt.sql, SearchBoundComparison::parseCompareArgument)), | |||
le (sqlCompare(ComparisonOperator.le.sql, SearchBoundComparison::parseCompareArgument)), | |||
like (sqlCompare( "ilike", SearchBoundComparison::parseLikeArgument)), | |||
like (sqlCompare(Constants.ILIKE, SearchBoundComparison::parseLikeArgument)), | |||
like_any((bound, params, value, locale) -> { | |||
final String[] values = value.split(Constants.ILIKE_SEP); | |||
final List<Object> valueList = Arrays.asList(values); | |||
final String[] operators = new String[values.length]; | |||
Arrays.fill(operators, Constants.ILIKE); | |||
final SearchBoundValueFunction[] valueFuncs = new SearchBoundValueFunction[values.length]; | |||
Arrays.fill(valueFuncs, (SearchBoundValueFunction) (bound1, value1, locale1) -> value1); | |||
for (int i=0; i<values.length; i++) { | |||
params.add(valueFuncs[i].paramValue(bound, values[i], locale)); | |||
} | |||
return sqlOrCompare(operators, valueFuncs, values).generateSqlAndAddParams(bound, new ArrayList<>(valueList), null, locale); | |||
}), | |||
is_null (sqlNullCompare(true)), | |||
not_null(sqlNullCompare(false)), | |||
@@ -126,4 +141,8 @@ public enum SearchBoundComparison { | |||
public static String customPrefix(String op) { return SearchBoundComparison.custom.name() + OP_SEP + op + OP_SEP; } | |||
public static class Constants { | |||
public static final String ILIKE = "ilike"; | |||
public static final String ILIKE_SEP = ","; | |||
} | |||
} |
@@ -38,6 +38,18 @@ public interface SearchBoundSqlFunction { | |||
}; | |||
} | |||
static SearchBoundSqlFunction sqlOrCompare(String[] operators, SearchBoundValueFunction[] valueFunctions, String[] values) { | |||
return (bound, params, value, locale) -> { | |||
final StringBuilder b = new StringBuilder(); | |||
for (int i = 0; i < operators.length; i++) { | |||
if (b.length() > 0) b.append(") OR ("); | |||
b.append(bound.getName()).append(" ").append(operators[i]).append(" ?"); | |||
params.add(valueFunctions[i].paramValue(bound, values[i], locale)); | |||
} | |||
return b.insert(0, "(").append(")").toString(); | |||
}; | |||
} | |||
static SearchBoundSqlFunction sqlNullCompare(boolean isNull) { | |||
return (bound, params, value, locale) -> bound.getName() + " IS " + (!isNull ? "NOT " : "") + " NULL"; | |||
} | |||