@@ -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.ECField; | ||||
import org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKey; | import org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKey; | ||||
import org.cobbzilla.wizard.model.entityconfig.annotations.ECSearchable; | 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.lang.reflect.Field; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
@@ -51,22 +49,26 @@ public class SqlDefaultSearchField implements SearchField { | |||||
@Override public SearchBound[] getBounds() { | @Override public SearchBound[] getBounds() { | ||||
final List<SearchBound> bounds = new ArrayList<>(); | final List<SearchBound> bounds = new ArrayList<>(); | ||||
EntityFieldType fieldType = search.type(); | 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 { | } 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) { | if (fieldType != null) { | ||||
@@ -2,8 +2,10 @@ package org.cobbzilla.wizard.model.entityconfig; | |||||
import com.fasterxml.jackson.annotation.JsonCreator; | import com.fasterxml.jackson.annotation.JsonCreator; | ||||
import lombok.AllArgsConstructor; | import lombok.AllArgsConstructor; | ||||
import lombok.Getter; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.wizard.model.entityconfig.validation.*; | 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.ValidationResult; | ||||
import org.cobbzilla.wizard.validation.Validator; | import org.cobbzilla.wizard.validation.Validator; | ||||
@@ -40,94 +42,94 @@ public enum EntityFieldType { | |||||
email (new EntityConfigFieldValidator_email()), | email (new EntityConfigFieldValidator_email()), | ||||
/** an integer-valued number */ | /** an integer-valued number */ | ||||
integer (new EntityConfigFieldValidator_integer()), | |||||
integer (SearchFieldType.integer, new EntityConfigFieldValidator_integer()), | |||||
/** a real number */ | /** a real number */ | ||||
decimal (new EntityConfigFieldValidator_decimal()), | |||||
decimal (SearchFieldType.decimal, new EntityConfigFieldValidator_decimal()), | |||||
/** an integer-valued monetary amount */ | /** an integer-valued monetary amount */ | ||||
money_integer (null), | |||||
money_integer (SearchFieldType.integer), | |||||
/** a real-valued monetary amount */ | /** a real-valued monetary amount */ | ||||
money_decimal (null), | |||||
money_decimal (SearchFieldType.decimal), | |||||
/** a boolean value */ | /** a boolean value */ | ||||
flag (new EntityConfigFieldValidator_boolean()), | |||||
flag (SearchFieldType.flag, new EntityConfigFieldValidator_boolean()), | |||||
/** a date value */ | /** a date value */ | ||||
date (null), | |||||
date (SearchFieldType.integer), | |||||
/** a date value in the past (before current date) */ | /** 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) */ | /** a date value in the future (or current date) */ | ||||
date_future (null), | |||||
date_future (SearchFieldType.integer), | |||||
/** a field for age */ | /** a field for age */ | ||||
age (null), | |||||
age (SearchFieldType.integer), | |||||
/** a 4-digit year field */ | /** 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 */ | /** 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 */ | /** 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) */ | /** 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 */ | /** 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 */ | /** 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 */ | /** 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 */ | /** 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 */ | /** 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) */ | /** a time-zone (for example America/New York) */ | ||||
time_zone (null), | |||||
time_zone (), | |||||
/** a locale (for example en_US) */ | /** a locale (for example en_US) */ | ||||
locale (null), | |||||
locale (), | |||||
/** a 3-letter currency code (for example USD) */ | /** a 3-letter currency code (for example USD) */ | ||||
currency (null), | |||||
currency (), | |||||
/** an IPv4 address */ | /** an IPv4 address */ | ||||
ip4 (null), | |||||
ip4 (), | |||||
/** an IPv6 address */ | /** an IPv6 address */ | ||||
ip6 (null), | |||||
ip6 (), | |||||
/** a hostname */ | /** a hostname */ | ||||
hostname (null), | |||||
hostname (), | |||||
/** a fully-qualified domain name */ | /** a fully-qualified domain name */ | ||||
fqdn (null), | |||||
fqdn (), | |||||
/** a 2-letter US state abbreviation */ | /** a 2-letter US state abbreviation */ | ||||
us_state (null), | |||||
us_state (), | |||||
/** a US ZIP code */ | /** a US ZIP code */ | ||||
us_zip (null), | |||||
us_zip (), | |||||
/** HTTP URL */ | /** HTTP URL */ | ||||
http_url (new EntityConfigFieldValidator_httpUrl()), | http_url (new EntityConfigFieldValidator_httpUrl()), | ||||
/** a reference to another EntityConfig instance */ | /** a reference to another EntityConfig instance */ | ||||
reference (null), | |||||
reference (), | |||||
/** a base64-encoded PNG image */ | /** a base64-encoded PNG image */ | ||||
base64_png (null), | |||||
base64_png (), | |||||
/** an embedded sub-object */ | /** an embedded sub-object */ | ||||
embedded (new EntityConfigFieldValidator_embedded()), | embedded (new EntityConfigFieldValidator_embedded()), | ||||
@@ -135,8 +137,24 @@ public enum EntityFieldType { | |||||
/** a US phone number */ | /** a US phone number */ | ||||
us_phone (new EntityConfigFieldValidator_USPhone()); | us_phone (new EntityConfigFieldValidator_USPhone()); | ||||
@Getter private SearchFieldType searchFieldType; | |||||
private EntityConfigFieldValidator fieldValidator; | 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 */ | /** Jackson-hook to create a new instance based on a string, case-insensitively */ | ||||
@JsonCreator public static EntityFieldType fromString(String val) { return valueOf(val.toLowerCase()); } | @JsonCreator public static EntityFieldType fromString(String val) { return valueOf(val.toLowerCase()); } | ||||
@@ -1,6 +1,7 @@ | |||||
package org.cobbzilla.wizard.model.entityconfig.annotations; | package org.cobbzilla.wizard.model.entityconfig.annotations; | ||||
import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; | import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; | ||||
import org.cobbzilla.wizard.model.search.SearchBoundComparison; | |||||
import org.cobbzilla.wizard.model.search.SqlViewFieldSetter; | import org.cobbzilla.wizard.model.search.SqlViewFieldSetter; | ||||
import org.jasypt.hibernate4.encryptor.HibernatePBEStringEncryptor; | import org.jasypt.hibernate4.encryptor.HibernatePBEStringEncryptor; | ||||
@@ -20,6 +21,7 @@ public @interface ECSearchable { | |||||
String sortField() default ""; | String sortField() default ""; | ||||
EntityFieldType type() default EntityFieldType.none_set; | EntityFieldType type() default EntityFieldType.none_set; | ||||
String bounds() default ""; | String bounds() default ""; | ||||
SearchBoundComparison[] operators() default {}; | |||||
String entity() default ""; | String entity() default ""; | ||||
ECForeignKeySearchDepth fkDepth() default ECForeignKeySearchDepth.inherit; | 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.TimePeriodType; | ||||
import org.cobbzilla.util.time.TimeUtil; | import org.cobbzilla.util.time.TimeUtil; | ||||
import java.util.ArrayList; | |||||
import java.util.Arrays; | |||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.List; | import java.util.List; | ||||
@@ -27,7 +29,20 @@ public enum SearchBoundComparison { | |||||
lt (sqlCompare(ComparisonOperator.lt.sql, SearchBoundComparison::parseCompareArgument)), | lt (sqlCompare(ComparisonOperator.lt.sql, SearchBoundComparison::parseCompareArgument)), | ||||
le (sqlCompare(ComparisonOperator.le.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)), | is_null (sqlNullCompare(true)), | ||||
not_null(sqlNullCompare(false)), | 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 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) { | static SearchBoundSqlFunction sqlNullCompare(boolean isNull) { | ||||
return (bound, params, value, locale) -> bound.getName() + " IS " + (!isNull ? "NOT " : "") + " NULL"; | return (bound, params, value, locale) -> bound.getName() + " IS " + (!isNull ? "NOT " : "") + " NULL"; | ||||
} | } | ||||