package org.molgenis.data; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import static java.util.stream.Collectors.toList; import static java.util.stream.StreamSupport.stream; /** * With this class an equation model can be described for a database-field (eg a column). */ @XmlRootElement public class QueryRule { /** * The operator being applied to the field and value */ @XmlElement protected Operator operator; /** * The field-name (eq column-name) in the database */ @XmlElement protected String field = null; /** * The value to compare entries of the field in the database with */ @XmlElement protected Object value = null; protected List<QueryRule> nestedRules; public QueryRule() { } public QueryRule(List<QueryRule> nestedRules) { this.nestedRules = nestedRules; operator = Operator.NESTED; } public QueryRule(QueryRule copy) { operator = copy.operator; field = copy.field; value = copy.value; } /** * Different types of rules that can be applied. */ public enum Operator { /** * 'field' like 'value', searches all fields if field is not defined */ SEARCH("search"), /** * 'field' equal to 'value' * <p> * When 'field type' is 'Mref' its results are derived from the 'Contains' behavior. <br> * Examples: <br> * 1. ref1 OR ref2 can result in: * <ul> * <li>re1</li> * <li>ref1, ref2</li> * <li>ref1, ref2, ref3;</li> * <li>ref2</li> * <li>ref2, ref3</li> * </ul> * 2. ref1 AND ref2 can result in: * <ul> * <li>ref1, ref2</li> * <li>ref1, ref2, ref3</li> * </ul> */ EQUALS("="), /** * 'field' in 'value' (value being a list). */ IN("IN"), /** * 'field' less-than 'value' */ LESS("<"), /** * 'field' equal-or-less-than 'value' */ LESS_EQUAL("<="), /** * 'field' greater-than 'value' */ GREATER(">"), /** * 'field' equal-or-greater-than 'value' */ GREATER_EQUAL(">="), /** * 'field' equal-or-greater-than 'from value' and equal-or-less-than 'to value' (value being a list with 'from * value' as first element and 'to value' as second element */ RANGE("RANGE"), /** * 'field' like 'value' (works like equals with wildcard before and after value) */ LIKE("LIKE"), /** * 'field' not-equal to 'value' */ NOT("!="), /** * AND operation */ AND("AND"), /** * OR operation */ OR("OR"), /** * indicates that 'value' is a nested array of QueryRule. The parameter 'field' is ommitted. */ NESTED(""), /** * Boolean query */ SHOULD("SHOULD"), /** * Disjunction max query */ DIS_MAX("DIS_MAX"), /** * Fuzzy match operator */ FUZZY_MATCH("FUZZY_MATCH"), /** * Fuzzy match operator */ FUZZY_MATCH_NGRAM("FUZZY_MATCH_NGRAM"); private String label; /** * Translate String label of the operator to Operator. * * @param label of the operator */ Operator(String label) { this.label = label; } /** * Get the String label of the Operator. */ @Override public String toString() { return label; } } // constructor /** * Standard constructor. * <p> * With this constructor the field, operator and value are set in one go, so there is no need for additional * statements. * * @param field The field-name. * @param operator The operator to use for comparing entries in the field with the value. * @param value The value. */ public QueryRule(String field, Operator operator, Object value) { if (operator == Operator.AND || operator == Operator.OR) { throw new IllegalArgumentException( "QueryRule(): Operator." + operator + " cannot be used with two arguments"); } this.field = field; this.operator = operator; setValue(value); } /** * Specific constructor for rules that do not apply to a field such as LIMIT and OFFSET. * * @param operator * @param value */ @SuppressWarnings("unchecked") public QueryRule(Operator operator, Object value) { if (operator == Operator.SEARCH) { this.operator = operator; setValue(value); } else if (Operator.NESTED.equals(operator)) { boolean okay = true; if (value instanceof List) { for (Object o : (List<?>) value) { if (!(o instanceof QueryRule)) okay = false; } } else { okay = false; } if (!okay) throw new IllegalArgumentException("QueryRule(NESTED, value): value should be List<QueryRule>"); this.nestedRules = (List<QueryRule>) value; this.operator = operator; } else { throw new IllegalArgumentException( "QueryRule(): Operator." + operator + " cannot be used with one argument"); } } public QueryRule(Operator operator, QueryRule nestedRules) { if (operator == Operator.NOT) { this.operator = operator; this.nestedRules = Arrays.asList(nestedRules); } else { throw new IllegalArgumentException( "QueryRule(): Operator." + operator + " cannot be used with one argument"); } } /** * Specific constructor for rules that don't have a value or field such as LAST */ public QueryRule(Operator operator) { if (operator == Operator.AND || operator == Operator.OR || operator == Operator.NOT) { this.operator = operator; } else { throw new IllegalArgumentException( "QueryRule(): Operator '" + operator + "' cannot be used without arguments"); } } public QueryRule(String field, Operator equals, String value) { this(field, equals, (Object) value); } /** * Returns the field-name set for this rule. * * @return The field-name. */ public String getField() { return field; } /** * Sets a new field-name for this rule. * * @param field The new field-name. */ public void setField(String field) { this.field = field; } /** * Returns the operator set for this rule. * * @return The operator. */ public Operator getOperator() { return operator; } /** * Sets a new operator for this rule. * * @param operator The new operator. */ public void setOperator(Operator operator) { this.operator = operator; } /** * Returns the value set for this rule. * * @return The value. */ public Object getValue() { return value; } /** * Sets a new value for this rule. * * @param value The new value. */ public void setValue(Object value) { if (value instanceof Iterable<?>) { this.value = stream(((Iterable<?>) value).spliterator(), false).map(this::toValue).collect(toList()); } else { this.value = toValue(value); } } private Object toValue(Object value) { if (value instanceof Entity) { return ((Entity) value).getIdValue(); } return value; } /** * Convenience function to return value as nested rule array. * * @return Nested rule set */ public List<QueryRule> getNestedRules() { if (nestedRules == null) { return Collections.emptyList(); } return nestedRules; } @Override public String toString() { StringBuilder strBuilder = new StringBuilder(); if (field != null) { strBuilder.append('\'').append(field).append('\''); } if (operator != null && operator != Operator.NESTED) { if (strBuilder.length() > 0) { strBuilder.append(' '); } strBuilder.append(operator); } if (operator != Operator.AND && operator != Operator.OR && operator != Operator.NOT && operator != Operator.NESTED && operator != Operator.DIS_MAX && operator != Operator.SHOULD) { if (strBuilder.length() > 0) { strBuilder.append(' '); } if (operator != Operator.IN) { strBuilder.append('\'').append(value).append('\''); } else { strBuilder.append(value); } } if (nestedRules != null && !nestedRules.isEmpty()) { if (strBuilder.length() > 0) { strBuilder.append(' '); } strBuilder.append('('); for (Iterator<QueryRule> it = nestedRules.iterator(); it.hasNext(); ) { strBuilder.append(it.next()); if (it.hasNext()) { strBuilder.append(", "); } } strBuilder.append(')'); } return strBuilder.toString(); } public static QueryRule eq(String name, Object value) { return new QueryRule(name, Operator.EQUALS, value); } @Override public int hashCode() { int result = operator != null ? operator.hashCode() : 0; result = 31 * result + (field != null ? field.hashCode() : 0); result = 31 * result + (value != null ? value.hashCode() : 0); result = 31 * result + (nestedRules != null ? nestedRules.hashCode() : 0); return result; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; QueryRule queryRule = (QueryRule) o; if (field != null ? !field.equals(queryRule.field) : queryRule.field != null) return false; if (nestedRules != null ? !nestedRules.equals(queryRule.nestedRules) : queryRule.nestedRules != null) return false; if (operator != queryRule.operator) return false; if (value != null ? !value.equals(queryRule.value) : queryRule.value != null) return false; return true; } }