/** * Copyright (c) 2013 Puppet Labs, Inc. and other contributors, as listed below. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Apache License, Version 2.0 * which accompanies this distribution, and is available at * http://www.apache.org/licenses/LICENSE-2.0 * * Contributors: * Puppet Labs */ package com.puppetlabs.puppetdb.javaclient.query; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import com.puppetlabs.puppetdb.javaclient.impl.GsonProvider; import com.puppetlabs.puppetdb.javaclient.model.Fact; import com.puppetlabs.puppetdb.javaclient.model.Node; import com.puppetlabs.puppetdb.javaclient.model.Resource; import com.puppetlabs.puppetdb.javaclient.query.OrderBy.OrderByField; /** * Helper class for building queries consisting of one or several expressions. */ public abstract class Query { static abstract class AbstractExpression<T> implements Expression<T> { @Override public void appendTo(Map<String, String> queryParameters) { StringBuilder bld = new StringBuilder(); toJSON(bld); queryParameters.put("query", bld.toString()); } } static class Binary<T> extends OpExpression<T> { private final Identifier<T> lhs; private final Expression<?> rhs; Binary(Identifier<T> lhs, Expression<?> rhs, String operator) { super(operator); if(lhs == null || rhs == null) throw new IllegalArgumentException("An '" + operator + "' cannot be operate on null expressions"); this.lhs = lhs; this.rhs = rhs; } @Override void appendPredicateValue(StringBuilder bld) { lhs.toJSON(bld); bld.append(','); rhs.toJSON(bld); } } static class Ident<T> extends Literal<T> implements Field<T> { Ident(String name) { super(name); } } static class Literal<T> extends AbstractExpression<T> { private final Object literal; Literal(Object literal) { this.literal = literal; } @Override public void toJSON(StringBuilder bld) { bld.append(GsonProvider.toJSON(literal)); } } static class NAry<T> extends OpExpression<T> { private final Expression<?>[] expressions; NAry(Expression<T> e1, Expression<T> e2, Expression<T> e3, String operator) { super(operator); this.expressions = new Expression<?>[] { e1, e2, e3 }; } NAry(Expression<T> e1, Expression<T> e2, String operator) { super(operator); this.expressions = new Expression<?>[] { e1, e2 }; } NAry(List<Expression<T>> expressions, String operator) { super(operator); int ec; if(expressions == null || (ec = expressions.size()) < 2) throw new IllegalArgumentException("An '" + operator + "' operator must be applied to least two expressions"); this.expressions = expressions.toArray(new Expression<?>[ec]); } @Override void appendPredicateValue(StringBuilder bld) { int top = expressions.length; expressions[0].toJSON(bld); for(int idx = 1; idx < top; ++idx) { bld.append(','); expressions[idx].toJSON(bld); } } } static abstract class OpExpression<T> extends AbstractExpression<T> { private final String operator; OpExpression(String operator) { this.operator = operator; } abstract void appendPredicateValue(StringBuilder query); @Override public void toJSON(StringBuilder bld) { bld.append("[\""); bld.append(operator); bld.append("\","); appendPredicateValue(bld); bld.append(']'); } @Override public String toString() { StringBuilder bld = new StringBuilder(); toJSON(bld); return bld.toString(); } } static class QualifiedIdent<T> extends Literal<T> implements Identifier<T> { QualifiedIdent(String qualifier, String name) { super(new String[] { qualifier, name }); } } static class Unary<T> extends OpExpression<T> { private final Expression<T> expression; Unary(Expression<T> expression, String operator) { super(operator); if(expression == null) throw new IllegalArgumentException("An '" + operator + "' cannot be applied on a null expression"); this.expression = expression; } @Override void appendPredicateValue(StringBuilder bld) { expression.toJSON(bld); } } /** * <b>Matches if: all</b> of its arguments would match.. * * @param e1 * The first expression * @param e2 * The second expression * @return The <code>or</code> expression */ public static <T> Expression<T> and(Expression<T> e1, Expression<T> e2) { return new NAry<T>(e1, e2, "and"); } /** * <b>Matches if: all</b> of its arguments would match.. * * @param e1 * The first expression * @param e2 * The second expression * @param e3 * The third expression * @return The <code>or</code> expression */ public static <T> Expression<T> and(Expression<T> e1, Expression<T> e2, Expression<T> e3) { return new NAry<T>(e1, e2, e3, "and"); } /** * <b>Matches if: all</b> of its arguments would match.. * * @param expressions * A list of expressions (at least two) * @return The <code>or</code> expression or the first expression of the list when the list contains only one element */ public static <T> Expression<T> and(List<Expression<T>> expressions) { return new NAry<T>(expressions, "and"); } /** * <b>Matches if:</b> the field’s actual value is exactly the same as the provided value. Note that this * does not coerce values — the provided value must be the same data type as the field. In particular, be aware that: * <ul> * <li>Most fields are strings.</li> * <li>Some fields are booleans.</li> * </ul> * * @param identifier * The identifier denoting a field, named fact, or named parameter * @param value * The value to compare with the identified value * @return The <code>=</code> expression */ public static <T> Expression<T> eq(Identifier<T> identifier, Object value) { return new Binary<T>(identifier, new Literal<Object>(value), "="); } /** * Creates an identifier that identifies a fact * * @param factName * The name of the fact * @return The created identifier */ public static <T> Identifier<Node> fact(String factName) { return new QualifiedIdent<Node>("fact", factName); } /** * Creates an identifier that identifies a field * * @param fieldName * The name of the field * @return The created identifier */ public static <T> Field<T> field(String fieldName) { return new Ident<T>(fieldName); } /** * <b>Matches if:</b> the field is greater than the provided value. Coerces the field to float or integer; if it * can’t be coerced, the operator will not match. * * @param identifier * The identifier denoting a field, named fact, or named parameter * @param value * The value to compare with the identified value * @return The <code>></code> expression */ public static <T> Expression<T> gt(Identifier<T> identifier, Number value) { return new Binary<T>(identifier, new Literal<Object>(value), ">"); } /** * <b>Matches if:</b> the field is greater than or equal to the provided value. Coerces the field to float or integer; if it * can’t be coerced, the operator will not match. * * @param identifier * The identifier denoting a field, named fact, or named parameter * @param value * The value to compare with the identified value * @return The <code>>=</code> expression */ public static <T> Expression<T> gtEq(Identifier<T> identifier, Number value) { return new Binary<T>(identifier, new Literal<Object>(value), ">="); } private static <T, S> Expression<T> in(Field<T> field, Field<S> subQueryField, Expression<S> subQuery, String subQueryId) { return new Binary<T>(field, new Binary<S>(subQueryField, new Unary<S>(subQuery, subQueryId), "extract"), "in"); } /** * Return a subquery expression that will query for all instances for which <code>field</code> can be found * by selecting <code>subQueryField</code> from the set of facts returned by <code>subQuery</code>. * * @param field * The field to use as the left hand side of the IN clause * @param subQueryField * The field to select in the subquery * @param subQuery * The subquery expression * @return The <code>in</code> expression */ public static <T> Expression<T> inFacts(Field<T> field, Field<Fact> subQueryField, Expression<Fact> subQuery) { return in(field, subQueryField, subQuery, "select-facts"); } /** * Return a subquery expression that will query for all instances where the value of <code>field</code> can be found * by in the set obtained by selecting <code>subQueryField</code> using <code>subQuery</code>. * * @param field * The field to use as the left hand side of the IN clause * @param subQueryField * The field to select in the subquery * @param subQuery * The subquery expression * @return The <code>in</code> expression */ public static <T> Expression<T> inResources(Field<T> field, Field<Resource> subQueryField, Expression<Resource> subQuery) { return in(field, subQueryField, subQuery, "select-resources"); } /** * <b>Matches if:</b> the field is less than the provided value. Coerces the field to float or integer; if it * can’t be coerced, the operator will not match. * * @param identifier * The identifier denoting a field, named fact, or named parameter * @param value * The value to compare with the identified value * @return The <code><</code> expression */ public static <T> Expression<T> lt(Identifier<T> identifier, Number value) { return new Binary<T>(identifier, new Literal<Object>(value), "<"); } /** * <b>Matches if:</b> the field is less than or equal to the provided value. Coerces the field to float or integer; if it * can’t be coerced, the operator will not match. * * @param identifier * The identifier denoting a field, named fact, or named parameter * @param value * The value to compare with the identified value * @return The <code><=</code> expression */ public static <T> Expression<T> ltEq(Identifier<T> identifier, Number value) { return new Binary<T>(identifier, new Literal<Object>(value), "<="); } /** * <p> * <b>Matches if:</b> the field’s actual value matches the provided regular expression. * <p> * The following example would match if the <code>certname</code> field’s actual value resembled something like * <code>www03.example.com</code> * </p> * * <pre> * match("certname", Pattern.compile("www\\d+\\.example\\.com")) * </pre> * * @param identifier * The identifier denoting a field, named fact, or named parameter * @param pattern * The regular expression to match with the identified value * @return The match expression */ public static <T> Expression<T> match(Identifier<T> identifier, String pattern) { return new Binary<T>(identifier, new Literal<Object>(pattern), "~"); } /** * <b>Matches if:</b> its argument would not match. * * @param expression * The expression that will be negated * @return The <code>not</code> expression */ public static <T> Expression<T> not(Expression<T> expression) { return new Unary<T>(expression, "not"); } /** * <b>Matches if: at least one</b> of its arguments would match.. * * @param e1 * The first expression * @param e2 * The second expression * @return The <code>or</code> expression */ public static <T> Expression<T> or(Expression<T> e1, Expression<T> e2) { return new NAry<T>(e1, e2, "or"); } /** * <b>Matches if: at least one</b> of its arguments would match.. * * @param e1 * The first expression * @param e2 * The second expression * @param e3 * The third expression * @return The <code>or</code> expression */ public static <T> Expression<T> or(Expression<T> e1, Expression<T> e2, Expression<T> e3) { return new NAry<T>(e1, e2, e3, "or"); } /** * <b>Matches if: at least one</b> of its arguments would match.. * * @param expressions * A list of expressions (at least two) * @return The <code>or</code> expression or the first expression of the list when the list contains only one element */ public static <T> Expression<T> or(List<Expression<T>> expressions) { return new NAry<T>(expressions, "or"); } /** * Order the result obtained using the given <code>expression</code> using <code>fields</code>. * * @param expression * The query expression * @param fields * The fields to order by * @return The created OrderBy instance */ public static <T> OrderBy<T> orderBy(Expression<T> expression, List<OrderByField<T>> fields) { return new OrderBy<T>(expression, fields); } /** * Order the result obtained using the given <code>expression</code> using <code>fields</code>. * * @param expression * The query expression * @param field * The field to order by * @return The created OrderBy instance */ public static <T> OrderBy<T> orderBy(Expression<T> expression, OrderByField<T> field) { return orderBy(expression, Collections.singletonList(field)); } /** * Order the result obtained using the given <code>expression</code> using <code>fields</code>. * * @param expression * The query expression * @param field * The primary field to order by * @param field * The secondary field to order by * @return The created OrderBy instance */ public static <T> OrderBy<T> orderBy(Expression<T> expression, OrderByField<T> field1, OrderByField<T> field2) { List<OrderByField<T>> list = Arrays.asList(field1, field2); return orderBy(expression, list); } /** * Order the result using the given <code>field</code> in either ascending or descending order given * the boolean <code>descending</code> parameter. * * @param field * The field to order by * @param descending * <code>false</code> for ascending order, <code>true</code> for descending order * @return */ public static <T> OrderByField<T> orderByField(Field<T> field, boolean descending) { return new OrderByField<T>(field, descending); } /** * Paginate the ordered result obtained using the given <code>orderBy</code> in accordance with <code>offset</code> and * <code>limit</code>. If the <code>includeTotal</code> parameter is set, then the total number of entries can be retrieved * from the in the result when the query is executed. * * @param orderBy * @param offset * @param limit * @param includeTotal * @return */ public static <T> Parameters<T> paging(OrderBy<T> orderBy, int offset, int limit, boolean includeTotal) { return new Paging<T>(orderBy, offset, limit, includeTotal); } /** * Creates an identifier that identifies a resource parameter * * @param parameterName * The name of the resource parameter * @return The created identifier */ public static Identifier<Resource> parameter(String parameterName) { return new QualifiedIdent<Resource>("parameter", parameterName); } }