/*
* Copyright 2012 Eric F. Savage, code@efsavage.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ajah.spring.jdbc.criteria;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import lombok.extern.java.Log;
import org.joda.time.LocalDate;
import com.ajah.util.AjahUtils;
import com.ajah.util.CollectionUtils;
import com.ajah.util.StringUtils;
import com.ajah.util.lang.NameValuePair;
/**
* Object-oriented way to construct a WHERE statement.
*
* @author <a href="http://efsavage.com">Eric F. Savage</a>,
* <a href="mailto:code@efsavage.com">code@efsavage.com</a>.
*
*/
@Log
// TODO add gt/lt/ge/le/ne
// TODO handle Date params
public class Criteria extends AbstractCriteria<Criteria> {
private List<NameValuePair<String>> gts = null;
private List<NameValuePair<String>> gtes = null;
private List<NameValuePair<String>> ltes = null;
private List<NameValuePair<String>> likes = null;
private List<NameValuePair<String>> reverseLikes = null;
private List<NameValuePair<String>> joins = null;
private List<NameValuePair<Order>> orderBys = null;
private List<SubCriteria> ands = null;
private List<SubCriteria> ors = null;
private long offset = 0;
private long rowCount = 0;
/**
* A subclause, included as an AND, but may contain ORs.
*
* @param subCriteria
* The subCriteria to include
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria and(final SubCriteria subCriteria) {
AjahUtils.requireParam(subCriteria, "field");
if (this.ands == null) {
this.ands = new ArrayList<>();
}
this.ands.add(subCriteria);
return this;
}
/**
* Add an "ORDER BY" clause for the field with an ascending order.
*
* @param field
* The field to sort by.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria asc(final String field) {
AjahUtils.requireParam(field, "field");
return orderBy(field, Order.ASC);
}
/**
* Add an "ORDER BY" clause for the field with an ascending order.
*
* @param field
* The field to sort by.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria desc(final String field) {
AjahUtils.requireParam(field, "field");
return orderBy(field, Order.DESC);
}
/**
* Returns the LIMIT number of this Criteria. The default value is 0, which
* will yield a query without a LIMIT clause.
*
* @return The number of records this criteria should yield, or 0 which
* means unlimited.
*/
public Limit getLimit() {
return new Limit(this.offset, this.rowCount);
}
/**
* Returns the SQL for the ORDER BY portion of this query, or an empty
* string.
*
* @return The SQL for the ORDER BY portion of this query, or an empty
* string.
*/
public String getOrderBySql() {
if (CollectionUtils.isEmpty(this.orderBys)) {
return " ";
}
final StringBuilder sql = new StringBuilder();
boolean first = true;
for (final NameValuePair<Order> orderBy : this.orderBys) {
if (first) {
sql.append(" ORDER BY ");
first = false;
} else {
sql.append(",");
}
sql.append(orderBy.getName());
if (orderBy.getValue() == null) {
// Do nothing
} else if (orderBy.getValue() != Order.ASC) {
sql.append(" ");
sql.append(orderBy.getValue().name());
}
}
return sql.toString();
}
/**
* @see com.ajah.spring.jdbc.criteria.AbstractCriteria#getThis()
*/
@Override
protected Criteria getThis() {
return this;
}
public Where getWhere() {
return getWhere(null);
}
/**
* Constructs a {@link Where} object from this instance, suitable for
* creating a prepared SQL statement.
*
* @return A where clause that is equivalent to this criteria.
*/
public Where getWhere(final String tableName) {
final List<String> values = new ArrayList<>();
final StringBuilder where = new StringBuilder();
boolean first = true;
final String tablePrefix = StringUtils.isBlank(tableName) ? "" : "`" + tableName + "`.";
if (!CollectionUtils.isEmpty(this.eqs)) {
for (final NameValuePair<String> eq : this.eqs) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(tablePrefix);
if (eq.getName().contains("`") || eq.getName().contains(".")) {
where.append(eq.getName());
} else {
where.append("`");
where.append(eq.getName());
where.append("`");
}
if (eq.getValue() == null) {
where.append(" IS NULL");
} else {
where.append("=?");
values.add(eq.getValue());
}
}
}
if (!CollectionUtils.isEmpty(this.neqs)) {
for (final NameValuePair<String> neq : this.neqs) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(tablePrefix);
where.append("`");
where.append(neq.getName());
where.append("`");
if (neq.getValue() == null) {
where.append(" IS NOT NULL");
} else {
where.append("<>?");
values.add(neq.getValue());
}
}
}
if (!CollectionUtils.isEmpty(this.gtes)) {
for (final NameValuePair<String> gte : this.gtes) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(tablePrefix);
where.append("`");
where.append(gte.getName());
where.append("`");
where.append(">=?");
values.add(gte.getValue());
}
}
if (!CollectionUtils.isEmpty(this.gts)) {
for (final NameValuePair<String> gt : this.gts) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(tablePrefix);
where.append("`");
where.append(gt.getName());
where.append("`");
where.append(">?");
values.add(gt.getValue());
}
}
if (!CollectionUtils.isEmpty(this.ltes)) {
for (final NameValuePair<String> lte : this.ltes) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(tablePrefix);
where.append("`");
where.append(lte.getName());
where.append("`");
where.append("<=?");
values.add(lte.getValue());
}
}
if (!CollectionUtils.isEmpty(this.joins)) {
for (final NameValuePair<String> join : this.joins) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(join.getName());
where.append("=");
where.append(join.getValue());
}
}
if (!CollectionUtils.isEmpty(this.likes)) {
for (final NameValuePair<String> like : this.likes) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(tablePrefix);
if (like.getName().contains("`") || like.getName().contains(".")) {
where.append(like.getName());
} else {
where.append("`");
where.append(like.getName());
where.append("`");
}
where.append(" LIKE '");
where.append(like.getValue().replaceAll("'", "\\'"));
where.append("'");
}
}
if (!CollectionUtils.isEmpty(this.reverseLikes)) {
for (final NameValuePair<String> reverseLike : this.reverseLikes) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(tablePrefix);
where.append("'");
where.append(reverseLike.getValue().replaceAll("'", "\\'"));
where.append("'");
where.append(" LIKE ");
where.append(reverseLike.getName());
}
}
if (!CollectionUtils.isEmpty(this.ands)) {
for (final SubCriteria and : this.ands) {
if (first) {
first = false;
} else {
where.append(" AND ");
}
where.append(and.getWhere().getSql(false));
values.addAll(and.getWhere().getValues());
}
}
if (!CollectionUtils.isEmpty(this.ors)) {
for (final SubCriteria or : this.ors) {
if (first) {
first = false;
} else {
where.append(" OR ");
}
where.append(or.getWhere().getSql(false));
values.addAll(or.getWhere().getValues());
}
}
if (log.isLoggable(Level.FINEST)) {
log.finest(where.toString());
}
return new Where(where.toString(), values);
}
/**
* A greater-than match.
*
* @param field
* The field to match
* @param value
* The value the field must be greater than.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria gt(final String field, final long value) {
AjahUtils.requireParam(field, "field");
return gt(field, String.valueOf(value));
}
/**
* A greater-than field match.
*
* @param field
* The field to match
* @param value
* The value the field must be greater than .
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria gt(final String field, final String value) {
AjahUtils.requireParam(field, "field");
if (this.gts == null) {
this.gts = new ArrayList<>();
}
this.gts.add(new NameValuePair<>(field, value));
return this;
}
/**
* A greater-than or equal to match.
*
* @param field
* The field to match
* @param value
* The value the field must be greater than or equal to.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria gte(final String field, final long value) {
AjahUtils.requireParam(field, "field");
return gte(field, String.valueOf(value));
}
/**
* A greater-than or equal to match.
*
* @param field
* The field to match
* @param value
* The value the field must be greater than or equal to.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria gte(final String field, final BigDecimal value) {
AjahUtils.requireParam(field, "field");
return gte(field, value.toString());
}
/**
* A greater-than or equal to match.
*
* @param field
* The field to match
* @param value
* The value the field must be greater than or equal to.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria gte(final String field, final Date value) {
return gte(field, value.getTime());
}
/**
* A greater-than or equal-to field match.
*
* @param field
* The field to match
* @param value
* The value the field must be greater than or equal to.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria gte(final String field, final String value) {
AjahUtils.requireParam(field, "field");
if (this.gtes == null) {
this.gtes = new ArrayList<>();
}
this.gtes.add(new NameValuePair<>(field, value));
return this;
}
/**
* A join match.
*
* @param table1
* The first table to join.
* @param table2
* The second table to join.
* @param field
* The field to join on (same field name in both tables).
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria join(final String table1, final String table2, final String field) {
return join(table1, table2, field, field);
}
/**
* A join match.
*
* @param table1
* The first table to join.
* @param table2
* The second table to join.
* @param field1
* The field of the first table to join on.
* @param field2
* The field of the second table to join on.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria join(final String table1, final String table2, final String field1, final String field2) {
if (this.joins == null) {
this.joins = new ArrayList<>();
}
this.joins.add(new NameValuePair<>("`" + table1 + "`.`" + field1 + "`", "`" + table2 + "`.`" + field2 + "`"));
return this;
}
/**
* A LIKE field match. Uses the query as-is (i.e. add your own wildcards).
*
* @param field
* The field to match
* @param pattern
* The pattern the field must match.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria like(final String field, final String pattern) {
AjahUtils.requireParam(field, "field");
AjahUtils.requireParam(pattern, "pattern");
if (this.likes == null) {
this.likes = new ArrayList<>();
}
this.likes.add(new NameValuePair<>(field, pattern));
return this;
}
/**
* A greater-than or equal to match.
*
* @param field
* The field to match
* @param value
* The value the field must be greater than or equal to.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria lte(final String field, final long value) {
AjahUtils.requireParam(field, "field");
return lte(field, String.valueOf(value));
}
/**
* A less-than or equal-to field match.
*
* @param field
* The field to match
* @param value
* The value the field must be less than or equal to.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria lte(final String field, final String value) {
AjahUtils.requireParam(field, "field");
if (this.ltes == null) {
this.ltes = new ArrayList<>();
}
this.ltes.add(new NameValuePair<>(field, value));
return this;
}
/**
* Sets the offset, i.e. the position of the first result.
*
* @param offsetIndex
* The offset, i.e. the position of the first result.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria offset(final long offsetIndex) {
this.offset = offsetIndex;
return this;
}
/**
* A subclause, included as an AND, but may contain ORs.
*
* @param subCriteria
* The subCriteria to include
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria or(final SubCriteria subCriteria) {
AjahUtils.requireParam(subCriteria, "subCriteria");
if (this.ors == null) {
this.ors = new ArrayList<>();
}
this.ors.add(subCriteria);
return this;
}
/**
* Add an "ORDER BY" clause.
*
* @param field
* The field to sort by.
* @param order
* The order by which to sort.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria orderBy(final String field, final Order order) {
AjahUtils.requireParam(field, "field");
AjahUtils.requireParam(order, "order");
if (this.orderBys == null) {
this.orderBys = new ArrayList<>();
}
this.orderBys.add(new NameValuePair<>(field, order));
return this;
}
/**
* Adds an ORDER BY RAND() sort.
*
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria randomOrder() {
if (this.orderBys == null) {
this.orderBys = new ArrayList<>();
}
this.orderBys.add(new NameValuePair<>("RAND()", (Order) null));
return this;
}
/**
* A reverse LIKE field match. Intended for when the wildcard is in the
* database, e.g. 'value' like field. Uses the query as-is (i.e. add your
* own wildcards).
*
* @param field
* The field to match
* @param pattern
* The pattern the field must match.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria reverseLike(final String field, final String pattern) {
AjahUtils.requireParam(field, "field");
AjahUtils.requireParam(pattern, "pattern");
if (this.reverseLikes == null) {
this.reverseLikes = new ArrayList<>();
}
this.reverseLikes.add(new NameValuePair<>(field, pattern));
return this;
}
/**
* Sets the maximum number of rows to fetch.
*
* @param maximumRowsFetched
* The maximum number of rows to fetch.
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria rows(final long maximumRowsFetched) {
this.rowCount = maximumRowsFetched;
return this;
}
/**
* Sets the number of rows to fetch to 1.
*
* @see Criteria#rows(long)
* @return Criteria instance the method was invoked on (for chaining).
*/
public Criteria unique() {
this.rowCount = 1;
return this;
}
public Criteria eq(String field, LocalDate from) {
return eq(field, from.toString());
}
}