package com.redhat.lightblue.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ContainerNode;
/**
* A query expression
*
* Usage:
* <pre>
* // { field: x, op: =, rvalue: "value" }
* Query.withValue("x",Query.eq,"value")
* Query.withValue("x",Query.eq,Literal.value("value"))
* Query.withValue("x = value")
*
* // { field:x op:=, rfield: y }
* Query.withField("x",Query.eq,"y")
* Query.withField("x=y")
*
* // { field:x, op:$in, values=[1,2,3] }
* Query.withValues("x",Query.in,Literal.values(1,2,3))
* Query.withValues("x $in [1,2,3]")
*
* // { field:x, op:$in, rfield:y
* Query.withFieldValues("x",Query.in,"y")
* Query.withFieldValues("x $in y")
*
* // { field:x, regex: pattern, caseInsensitive: true, extended: false, multiline: false, dotall:true }
* Query.regex(x,pattern,true,false,false,true);
* Query.regex(x,pattern,Query.CASE_INSENSITIVE|Query.DOTALL)
*
* // { array: <array>, contains: <op>, values: [values] }
* Query.arrayContains("array",Query.all,Literal.values(1,2,3))
*
* // { array: <array>, elemMatch:{field:x, op:=, rvalue:1 } }
* Query.arrayMatch("array",Query.withValue("x=1"))
*
* // Logical connectors:
* Query.and(Query.withValue("x=1"),Query.withValue("y=2"),Query.not(Query.withValue("z=3")))
* Query.or(Query.withValue("x=1"),Query.withValue("y=2"),Query.not(Query.withValue("z=3")))
* Query.logical(Query.and,Query.withValue("x=1"),Query.withValue("y=2"),Query.not(Query.withValue("z=3")))
*
* // Literal query:
* Query.query("{ \"field\":\"x\", \"op\":\"=\", \"rvalue\":1}")
*
* </pre>
*/
public class Query extends Expression
implements Update.UpdateQuery {
public static final BinOp eq = BinOp.eq;
public static final BinOp neq = BinOp.neq;
public static final BinOp lt = BinOp.lt;
public static final BinOp gt = BinOp.gt;
public static final BinOp lte = BinOp.lte;
public static final BinOp gte = BinOp.gte;
public static final NaryOp in = NaryOp.in;
public static final NaryOp nin = NaryOp.nin;
public static final ArrOp any = ArrOp.any;
public static final ArrOp all = ArrOp.all;
public static final ArrOp non = ArrOp.none;
public static final LogOp and = LogOp.and;
public static final LogOp or = LogOp.or;
public static final int CASE_INSENSITIVE = 1;
public static final int EXTENDED = 2;
public static final int MULTILINE = 4;
public static final int DOTALL = 8;
public enum BinOp {
eq("="),
neq("!="),
lt("<"),
gt(">"),
lte("<="),
gte(">=");
private String s;
private BinOp(String s) {
this.s = s;
}
public String toString() {
return s;
}
public static BinOp getOp(String x) {
for (BinOp v : values()) {
if (v.toString().equals(x)) {
return v;
}
}
return null;
}
}
public enum NaryOp {
in("$in"),
nin("$nin");
private String s;
private NaryOp(String s) {
this.s = s;
}
@Override
public String toString() {
return s;
}
public static NaryOp getOp(String x) {
for (NaryOp v : values()) {
if (v.toString().equals(x)) {
return v;
}
}
return null;
}
}
public enum ArrOp {
any("$any"),
all("$all"),
none("$none");
private String s;
private ArrOp(String s) {
this.s = s;
}
@Override
public String toString() {
return s;
}
}
public enum LogOp {
and("$and"),
or("$or");
private String s;
private LogOp(String s) {
this.s = s;
}
@Override
public String toString() {
return s;
}
}
/**
* Constructs a query object from a json array or object
*/
public Query(ContainerNode node) {
super(node);
}
private Query(boolean arrayNode) {
super(arrayNode);
}
/**
* <pre>
* { field: <field>, op: <op>, rvalue: <value> }
* </pre>
*/
public static Query withValue(String field,
BinOp op,
Literal value) {
Query q = new Query(false);
q.add("field", field).add("op", op.toString()).add("rvalue", value.toJson());
return q;
}
/**
* <pre>
* { field: <field>, regex: <pattern>, caseInsensitive: <caseInsensitive>, ... }
* </pre>
*/
public static Query withMatchingString(String field, String value, boolean caseInsensitive) {
return caseInsensitive ? regex(field, escapeRegExPattern(value), caseInsensitive, false, false, false) : withValue(field, eq, value);
}
/**
* <pre>
* { field: <field>, regex: <^string$>, caseInsensitive: <caseInsensitive>, ... }
* </pre>
*/
public static Query withString(String field, String value, boolean caseInsensitive) {
return caseInsensitive ? regex(field, "^" + escapeRegExPattern(value) + "$", caseInsensitive, false, false, false) : withValue(field, eq, value);
}
/**
* <pre>
* { field: <field>, regex: <^string$>, caseInsensitive: true, ... }
* </pre>
*/
public static Query withStringIgnoreCase(String field, String value) {
return Query.withString(field, value, true);
}
/**
* <pre>
* { "$or": [{ field: <field>, regex: <^string$>, caseInsensitive: <caseInsensitive>, ... }, ... ]}
* </pre>
*/
public static Query withStrings(String field, String[] values, boolean caseInsensitive) {
if (caseInsensitive) {
List<Query> regexList = new ArrayList<Query>();
for (String value : values) {
regexList.add(withString(field, value, true));
}
return Query.or(regexList);
} else {
return Query.withValues(field, Query.in, Literal.values(values));
}
}
/**
* <pre>
* { "$or": [{ field: <field>, regex: <^string$>, caseInsensitive: true, ... }, ... ]}
* </pre>
*/
public static Query withStringsIgnoreCase(String field, String[] values) {
return Query.withStrings(field, values, true);
}
/**
* <pre>
* { field: <field>, op: <op>, rvalue: <value> }
* </pre>
*/
public static Query withValue(String field,
BinOp op,
Object value) {
return withValue(field, op, Literal.value(value));
}
/**
* <pre>
* { field: <field>, op: <op>, rvalue: <value> }
* </pre>
*/
public static Query withValue(String field,
BinOp op,
int value) {
return withValue(field, op, Literal.value(value));
}
/**
* <pre>
* { field: <field>, op: <op>, rvalue: <value> }
* </pre>
*/
public static Query withValue(String field,
BinOp op,
long value) {
return withValue(field, op, Literal.value(value));
}
/**
* <pre>
* { field: <field>, op: <op>, rvalue: <value> }
* </pre>
*/
public static Query withValue(String field,
BinOp op,
double value) {
return withValue(field, op, Literal.value(value));
}
/**
* <pre>
* { field: <field>, op: <op>, rvalue: <value> }
* </pre>
*/
public static Query withValue(String field,
BinOp op,
boolean value) {
return withValue(field, op, Literal.value(value));
}
/**
* <pre>
* { field: <field>, op: <op>, rfield: <rfield> }
* </pre>
*/
public static Query withField(String field,
BinOp op,
String rfield) {
Query q = new Query(false);
q.add("field", field).add("op", op.toString()).add("rfield", rfield);
return q;
}
/**
* <pre>
* { field:<field>, op:<op>, rvalue:<value> }
* { field:<field>, op:<op>, values:[values] }
* </pre>
*/
public static Query withValue(String expression) {
String[] parts = split(expression);
if (parts != null) {
String field = parts[0];
String operator = parts[1];
String value = parts[2];
BinOp binOp = BinOp.getOp(operator);
if (binOp != null) {
return withValue(field, binOp, value);
}
NaryOp naryOp = NaryOp.getOp(operator);
if (naryOp != null) {
Literal[] values = Literal.values(value.substring(1, value.length() - 1).split("\\s*,\\s*"));
return withValues(field, naryOp, values);
}
}
throw new IllegalArgumentException("'" + expression + "' is incorrect");
}
/**
* <pre>
* { field:<field>, op:<op>, rfield:<rfield> }
* </pre>
*/
public static Query withField(String expression) {
String[] parts = split(expression);
if (parts != null) {
String field = parts[0];
String operator = parts[1];
String rfield = parts[2];
BinOp binOp = BinOp.getOp(operator);
if (binOp != null) {
return withField(field, binOp, rfield);
}
NaryOp naryOp = NaryOp.getOp(operator);
if (naryOp != null) {
return withFieldValues(field, naryOp, rfield);
}
}
throw new IllegalArgumentException("'" + expression + "' is incorrect");
}
private static String[] split(String expression) {
int opIndex = -1;
String operator = null;
for (BinOp x : BinOp.values()) {
int ix = expression.indexOf(x.toString());
if (ix != -1) {
if (opIndex == -1 || ix < opIndex) {
opIndex = ix;
operator = x.toString();
}
}
}
if (opIndex == -1) {
for (NaryOp x : NaryOp.values()) {
int ix = expression.indexOf(x.toString());
if (ix != -1) {
if (opIndex == -1 || ix < opIndex) {
opIndex = ix;
operator = x.toString();
}
}
}
}
if (opIndex != -1) {
return new String[]{expression.substring(0, opIndex).trim(),
operator,
expression.substring(opIndex + operator.length()).trim()};
} else {
return null;
}
}
/**
* <pre>
* { field: <field>, regex: <pattern>, ... }
* </pre>
*/
public static Query regex(String field,
String pattern,
boolean caseInsensitive,
boolean extended,
boolean multiline,
boolean dotall) {
Query q = new Query(false);
q.add("field", field).add("regex", pattern).
add("caseInsensitive", caseInsensitive).
add("extended", extended).
add("multiline", multiline).
add("dotall", dotall);
return q;
}
/**
* <pre>
* { field: <field>, regex: <pattern>, ... }
* </pre>
*/
public static Query regex(String field,
String pattern,
int options) {
return regex(field, pattern, is(options, CASE_INSENSITIVE), is(options, EXTENDED), is(options, MULTILINE), is(options, DOTALL));
}
private static boolean is(int options, int value) {
return (options & value) == value;
}
/**
* <pre>
* { field: <field>, op: <in/nin>, values: [ values ] }
* </pre>
*/
public static Query withValues(String field,
NaryOp op,
Literal... values) {
Query q = new Query(false);
q.add("field", field).add("op", op.toString()).add("values", Literal.toJson(values));
return q;
}
/**
* <pre>
* { field: <field>, op: <in/nin>, rfield: <rfield> }
* </pre>
*/
public static Query withFieldValues(String field,
NaryOp op,
String rfield) {
Query q = new Query(false);
q.add("field", field).add("op", op.toString()).add("rfield", rfield);
return q;
}
/**
* <pre>
* { array: <array>, contains: <op>, values: [values] }
* </pre>
*/
public static Query arrayContains(String array,
ArrOp op,
Literal... values) {
Query q = new Query(false);
q.add("array", array).add("contains", op.toString()).add("values", Literal.toJson(values));
return q;
}
/**
* <pre>
* { array: <array>, elemMatch:<x> }
* </pre>
*/
public static Query arrayMatch(String array,
Query x) {
Query q = new Query(false);
q.add("array", array).add("elemMatch", x.toJson());
return q;
}
/**
* Return a query from a json node
*/
public static Query query(ContainerNode query) {
return new Query(query);
}
/**
* <pre>
* { $not : { query } }
* </pre>
*/
public static Query not(Query query) {
Query q = new Query(false);
q.add("$not", query.toJson());
return q;
}
/**
* <pre>
* { $and : [ expressions ] }
* </pre>
*/
public static Query and(Query... expressions) {
return logical(LogOp.and, expressions);
}
/**
* <pre>
* { $and : [ expressions ] }
* </pre>
*/
public static Query and(Collection<Query> expressions) {
return logical(LogOp.and, expressions);
}
/**
* <pre>
* { $or : [ expressions ] }
* </pre>
*/
public static Query or(Query... expressions) {
return logical(LogOp.or, expressions);
}
/**
* <pre>
* { $or : [ expressions ] }
* </pre>
*/
public static Query or(Collection<Query> expressions) {
return logical(LogOp.or, expressions);
}
/**
* <pre>
* { $and : [ expressions ] }
* { $or : [ expressions ] }
* </pre>
*/
public static Query logical(LogOp op, Query... expressions) {
return logical(op, Arrays.asList(expressions));
}
/**
* <pre>
* { $and : [ expressions ] }
* { $or : [ expressions ] }
* </pre>
*/
public static Query logical(LogOp op, Collection<Query> expressions) {
Query q = new Query(true);
for (Query x : expressions) {
((ArrayNode) q.node).add(x.toJson());
}
Query a = new Query(false);
a.add(op.toString(), q.toJson());
return a;
}
private static final String ESCAPECHARS = ".^$*+?()[{\\|";
public static String escapeRegExPattern(String s) {
StringBuilder bld = new StringBuilder();
int n = s.length();
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
if (ESCAPECHARS.indexOf(c) != -1) {
bld.append("\\");
}
bld.append(c);
}
return bld.toString();
}
}