Mercurial > hg > release > thermostat-0.11
changeset 1148:e597d72c3ecb
Add support for IN / NOT IN operators
This commit extends our query mechanism to handle queries of the form:
key IN [ x, y, z ], and key NOT IN [ x, y, z ]. I have added two new
Expression implementations, a BinarySetMembershipExpression and a
LiteralSetExpression. LiteralSetExpression corresponds to a list of
values, which can then be used as the right operand to a
BinarySetMembershipExpression. We have to explicitly pass the type of
value we're using in our list for deserialization.
I have also cleaned up the MongoExpressionParser in the process of
adding support for these new operators. UnaryLogicalExpressions now
can only take ComparisonExpressions, due to the restriction of how
$not is used in MongoDB.
Reviewed-by: vanaltj
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-June/007100.html
line wrap: on
line diff
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonExpression.java Fri Jun 21 16:36:21 2013 +0200 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonExpression.java Fri Jun 28 12:37:39 2013 -0400 @@ -44,7 +44,8 @@ * @param <T> - the type parameter of this expression's {@link Key} */ public final class BinaryComparisonExpression<T> - extends BinaryExpression<LiteralExpression<Key<T>>, LiteralExpression<T>, BinaryComparisonOperator> { + extends BinaryExpression<LiteralExpression<Key<T>>, LiteralExpression<T>, BinaryComparisonOperator> + implements ComparisonExpression { /** * Constructs a {@link BinaryComparisonExpression} whose operands are
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalExpression.java Fri Jun 21 16:36:21 2013 +0200 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalExpression.java Fri Jun 28 12:37:39 2013 -0400 @@ -43,7 +43,8 @@ * @param <T> - the type of {@link Expression} corresponding to the right operand */ public final class BinaryLogicalExpression<S extends Expression, T extends Expression> - extends BinaryExpression<S, T, BinaryLogicalOperator> { + extends BinaryExpression<S, T, BinaryLogicalOperator> + implements LogicalExpression { /** * Constructs a {@link BinaryLogicalExpression} whose operands are
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinarySetMembershipExpression.java Fri Jun 28 12:37:39 2013 -0400 @@ -0,0 +1,66 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.storage.query; + +import com.redhat.thermostat.storage.core.Key; + +/** + * A {@link BinaryExpression} that corresponds to an algebraic comparison + * between a {@link Key} and multiple values. + * @param <T> - the type parameter of this expression's {@link Key} + */ +public final class BinarySetMembershipExpression<T> + extends BinaryExpression<LiteralExpression<Key<T>>, LiteralSetExpression<T>, BinarySetMembershipOperator> + implements ComparisonExpression { + + /** + * Constructs a {@link BinarySetMembershipExpression} whose operands are + * a {@link LiteralExpression} for a {@link Key} and a {@link LiteralSetExpression} + * for the set of values to compare against the key. + * <p> + * This constructor exists mainly for JSON serialization, use methods in + * {@link ExpressionFactory} instead of this constructor. + * @param leftOperand - left operand for this expression + * @param operator - the operator for this expression + * @param rightOperand - right operand for this expression + */ + public BinarySetMembershipExpression(LiteralExpression<Key<T>> leftOperand, + BinarySetMembershipOperator operator, LiteralSetExpression<T> rightOperand) { + super(leftOperand, operator, rightOperand); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinarySetMembershipOperator.java Fri Jun 28 12:37:39 2013 -0400 @@ -0,0 +1,49 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.storage.query; + +/** + * Operators to be used with {@link BinarySetMembershipExpression} + */ +public enum BinarySetMembershipOperator implements BinaryOperator { + /** Compares if the left-hand value is equal to any of the + * right-hand values */ + IN, + /** Compares if the left-hand value is not equal to all of the + * right-hand values */ + NOT_IN, +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/ComparisonExpression.java Fri Jun 28 12:37:39 2013 -0400 @@ -0,0 +1,41 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.storage.query; + +public interface ComparisonExpression extends Expression { + +} \ No newline at end of file
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/ExpressionFactory.java Fri Jun 21 16:36:21 2013 +0200 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/ExpressionFactory.java Fri Jun 28 12:37:39 2013 -0400 @@ -36,6 +36,8 @@ package com.redhat.thermostat.storage.query; +import java.util.Set; + import com.redhat.thermostat.storage.core.Key; import com.redhat.thermostat.storage.core.Query; @@ -115,12 +117,36 @@ } /** + * Creates a {@link BinarySetMembershipExpression} comparing if the + * provided key has a value equal to any of the provided values. + * @param key - {@link Key} whose value to compare against the provided values + * @param value - a set of values to compare against the key + * @param type - the type of values of stored in the provided set + * @return the new comparison expression + */ + public <T> BinarySetMembershipExpression<T> in(Key<T> key, Set<T> values, Class<T> type) { + return createSetMembershipExpression(key, values, BinarySetMembershipOperator.IN, type); + } + + /** + * Creates a {@link BinarySetMembershipExpression} comparing if the + * provided key has a value not equal to all of the provided values. + * @param key - {@link Key} whose value to compare against the provided values + * @param value - a set of values to compare against the key + * @param type - the type of values of stored in the provided set + * @return the new comparison expression + */ + public <T> BinarySetMembershipExpression<T> notIn(Key<T> key, Set<T> values, Class<T> type) { + return createSetMembershipExpression(key, values, BinarySetMembershipOperator.NOT_IN, type); + } + + /** * Creates a {@link UnaryLogicalExpression} which is a logical * negation of the provided expression. * @param expr - the expression to negate * @return the new negated expression */ - public <T extends Expression> UnaryLogicalExpression<T> not(T expr) { + public <T extends ComparisonExpression> UnaryLogicalExpression<T> not(T expr) { return new UnaryLogicalExpression<>(expr, UnaryLogicalOperator.NOT); } @@ -150,4 +176,10 @@ return new BinaryComparisonExpression<>(new LiteralExpression<>(key), op, new LiteralExpression<>(value)); } + private <T> BinarySetMembershipExpression<T> createSetMembershipExpression(Key<T> key, Set<T> values, + BinarySetMembershipOperator op, Class<T> type) { + return new BinarySetMembershipExpression<>(new LiteralExpression<>(key), op, + new LiteralSetExpression<>(values, type)); + } + }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/LiteralSetExpression.java Fri Jun 28 12:37:39 2013 -0400 @@ -0,0 +1,101 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.storage.query; + +import java.util.Collections; +import java.util.Set; + +/** + * An {@link Expression} that contains a set of values. + * @param <T> - the type of this expression's values + */ +public final class LiteralSetExpression<T> implements Expression { + + private Set<T> values; + private Class<T> type; + + /** + * Constructs a {@link LiteralSetExpression} given a list of values + * and its type. + * <p> + * This constructor exists mainly for JSON serialization, use methods in + * {@link ExpressionFactory} instead of this constructor. + * @param values - a list of values for this expression + * @param type - the type of values of stored in the provided set + * (needed for serialization) + */ + public LiteralSetExpression(Set<T> values, Class<T> type) { + // Don't need to write, make read-only copy + this.values = Collections.unmodifiableSet(values); + this.type = type; + } + + /** + * @return the values represented by this expression + */ + public Set<T> getValues() { + return values; + } + + /** + * @return the type of values stored in this expression + */ + public Class<T> getType() { + return type; + } + + @Override + public boolean equals(Object obj) { + boolean result = false; + if (obj != null && obj instanceof LiteralSetExpression) { + LiteralSetExpression<?> otherExpr = (LiteralSetExpression<?>) obj; + result = values.equals(otherExpr.values) + && type.equals(otherExpr.type); + } + return result; + } + + @Override + public int hashCode() { + return values.hashCode() + type.hashCode(); + } + + @Override + public String toString() { + return values.toString(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/LogicalExpression.java Fri Jun 28 12:37:39 2013 -0400 @@ -0,0 +1,41 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.storage.query; + +public interface LogicalExpression extends Expression { + +}
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalExpression.java Fri Jun 21 16:36:21 2013 +0200 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalExpression.java Fri Jun 28 12:37:39 2013 -0400 @@ -38,11 +38,12 @@ /** * A {@link UnaryExpression} which represents a boolean formula - * with one expression and a logical operator. + * with one comparison expression and a logical operator. * @param <T> - type of {@link Expression} used for the operand */ -public final class UnaryLogicalExpression<T extends Expression> extends - UnaryExpression<T, UnaryLogicalOperator> { +public final class UnaryLogicalExpression<T extends ComparisonExpression> extends + UnaryExpression<T, UnaryLogicalOperator> + implements LogicalExpression { /** * Constructs a {@link UnaryLogicalExpression} given an operand @@ -50,9 +51,8 @@ * <p> * This constructor exists mainly for JSON serialization, use methods in * {@link ExpressionFactory} instead of this constructor. - * @param leftOperand - left operand for this expression + * @param operand - the operand for this expression * @param operator - the operator for this expression - * @param rightOperand - right operand for this expression */ public UnaryLogicalExpression(T operand, UnaryLogicalOperator operator) { super(operand, operator);
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/query/ExpressionFactoryTest.java Fri Jun 21 16:36:21 2013 +0200 +++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/ExpressionFactoryTest.java Fri Jun 28 12:37:39 2013 -0400 @@ -36,8 +36,12 @@ package com.redhat.thermostat.storage.query; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import org.junit.Before; import org.junit.Test; @@ -48,6 +52,7 @@ private static final Key<String> key = new Key<>("hello", true); private static final String VALUE = "world"; + private static final Set<String> VALUES = new HashSet<>(Arrays.asList("world", "worlds")); ExpressionFactory factory; @Before @@ -104,9 +109,25 @@ } @Test + public void testIn() { + Expression expr = new BinarySetMembershipExpression<>( + new LiteralExpression<>(key), BinarySetMembershipOperator.IN, + new LiteralSetExpression<>(VALUES, String.class)); + assertEquals(expr, factory.in(key, VALUES, String.class)); + } + + @Test + public void testNotIn() { + Expression expr = new BinarySetMembershipExpression<>( + new LiteralExpression<>(key), BinarySetMembershipOperator.NOT_IN, + new LiteralSetExpression<>(VALUES, String.class)); + assertEquals(expr, factory.notIn(key, VALUES, String.class)); + } + + @Test public void testNot() { - Expression operand = mock(Expression.class); - Expression expr = new UnaryLogicalExpression<Expression>(operand, UnaryLogicalOperator.NOT); + ComparisonExpression operand = mock(ComparisonExpression.class); + Expression expr = new UnaryLogicalExpression<ComparisonExpression>(operand, UnaryLogicalOperator.NOT); assertEquals(expr, factory.not(operand)); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/LiteralSetExpressionTest.java Fri Jun 28 12:37:39 2013 -0400 @@ -0,0 +1,105 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.storage.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +public class LiteralSetExpressionTest { + + private static final Set<Object> VALUES = new HashSet<>(Arrays.asList(new Object(), new Object(), new Object())); + private LiteralSetExpression<Object> expr; + + @Before + public void setup() { + expr = new LiteralSetExpression<Object>(VALUES, Object.class); + } + + @Test + public void testGetValues() { + assertEquals(VALUES, expr.getValues()); + } + + @Test(expected=UnsupportedOperationException.class) + public void testGetValuesImmutable() { + Set<Object> values = expr.getValues(); + values.add(new Object()); + } + + @Test + public void testEquals() { + LiteralSetExpression<Object> otherExpr = new LiteralSetExpression<Object>(VALUES, Object.class); + assertEquals(expr, otherExpr); + } + + @Test + public void testDeepEquals() { + Set<Object> otherValues = new HashSet<>(VALUES); + LiteralSetExpression<Object> otherExpr = new LiteralSetExpression<Object>(otherValues, Object.class); + assertEquals(expr, otherExpr); + } + + @Test + public void testNotEquals() { + Set<Object> otherValue = new HashSet<>(Arrays.asList(new Object(), new Object(), new Object())); + LiteralSetExpression<Object> otherExpr = new LiteralSetExpression<Object>(otherValue, Object.class); + + assertFalse(expr.equals(otherExpr)); + } + + @Test + public void testNotEqualsWrongClass() { + assertFalse(expr.equals(new Object())); + } + + @Test + public void testNotEqualsNull() { + assertFalse(expr.equals(null)); + } + + @Test + public void testHashCode() { + assertEquals(VALUES.hashCode() + Object.class.hashCode(), expr.hashCode()); + } +}
--- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParser.java Fri Jun 21 16:36:21 2013 +0200 +++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParser.java Fri Jun 28 12:37:39 2013 -0400 @@ -47,7 +47,11 @@ import com.redhat.thermostat.storage.query.BinaryComparisonOperator; import com.redhat.thermostat.storage.query.BinaryLogicalExpression; import com.redhat.thermostat.storage.query.BinaryLogicalOperator; +import com.redhat.thermostat.storage.query.BinarySetMembershipExpression; +import com.redhat.thermostat.storage.query.BinarySetMembershipOperator; +import com.redhat.thermostat.storage.query.ComparisonExpression; import com.redhat.thermostat.storage.query.Expression; +import com.redhat.thermostat.storage.query.LiteralSetExpression; import com.redhat.thermostat.storage.query.LiteralExpression; import com.redhat.thermostat.storage.query.UnaryLogicalExpression; import com.redhat.thermostat.storage.query.UnaryLogicalOperator; @@ -57,7 +61,10 @@ public DBObject parse(Expression expr) { DBObject result; if (expr instanceof BinaryComparisonExpression) { - result = parseBinaryLiteralExpression((BinaryComparisonExpression<?>) expr); + result = parseBinaryComparisonExpression((BinaryComparisonExpression<?>) expr); + } + else if (expr instanceof BinarySetMembershipExpression) { + result = parseBinarySetComparisonExpression((BinarySetMembershipExpression<?>) expr); } else if (expr instanceof BinaryLogicalExpression) { // LHS OP RHS ==> { OP : [ LHS, RHS ] } @@ -74,30 +81,19 @@ } else if (expr instanceof UnaryLogicalExpression) { UnaryLogicalExpression<?> uniExpr = (UnaryLogicalExpression<?>) expr; - Expression operand = uniExpr.getOperand(); + ComparisonExpression operand = uniExpr.getOperand(); UnaryLogicalOperator op = uniExpr.getOperator(); - String strOp = getLogicalOperator(op); - // In MongoDB, NOT can only be applied to negate comparisons - if (operand instanceof BinaryComparisonExpression) { + // Special case + if (op == UnaryLogicalOperator.NOT && operand instanceof BinaryComparisonExpression) { BinaryComparisonExpression<?> binExpr = (BinaryComparisonExpression<?>) operand; if (binExpr.getOperator() == BinaryComparisonOperator.EQUALS) { + // TODO Convert to $ne? throw new IllegalArgumentException("Cannot use $not with equality"); } - DBObject object = parseBinaryLiteralExpression(binExpr); - // OP { LHS : RHS } => { LHS : { OP : RHS }} - // Apply operator to each key - Set<String> keySet = object.keySet(); - for (String key : keySet) { - Object value = object.get(key); - BasicDBObject newValue = new BasicDBObject(strOp, value); - object.put(key, newValue); - } - - result = object; } - else { - throw new IllegalArgumentException("Not a valid use of " + strOp + " in MongoDB"); - } + DBObject object = parse(operand); + insertUnaryLogicalOperator(object, op); + result = object; } else { throw new IllegalArgumentException("Unknown Expression of type " + expr.getClass()); @@ -106,7 +102,19 @@ return result; } - private <T> DBObject parseBinaryLiteralExpression(BinaryComparisonExpression<T> expr) { + private void insertUnaryLogicalOperator(DBObject object, UnaryLogicalOperator op) { + String strOp = getLogicalOperator(op); + // OP { LHS : RHS } => { LHS : { OP : RHS }} + // Apply operator to each key + Set<String> keySet = object.keySet(); + for (String key : keySet) { + Object value = object.get(key); + BasicDBObject newValue = new BasicDBObject(strOp, value); + object.put(key, newValue); + } + } + + private <T> DBObject parseBinaryComparisonExpression(BinaryComparisonExpression<T> expr) { BasicDBObjectBuilder builder = BasicDBObjectBuilder.start(); LiteralExpression<Key<T>> leftExpr = expr.getLeftOperand(); LiteralExpression<T> rightExpr = expr.getRightOperand(); @@ -119,7 +127,7 @@ // LHS OP RHS => { LHS : { OP : RHS } } builder.push(leftExpr.getValue().getName()); - String mongoOp = getLiteralOperator(op); + String mongoOp = getComparisonOperator(op); builder.add(mongoOp, rightExpr.getValue()); builder.pop(); @@ -127,7 +135,22 @@ return builder.get(); } - private String getLiteralOperator(BinaryComparisonOperator operator) { + private <T> DBObject parseBinarySetComparisonExpression(BinarySetMembershipExpression<T> expr) { + BasicDBObjectBuilder builder = BasicDBObjectBuilder.start(); + LiteralExpression<Key<T>> leftExpr = expr.getLeftOperand(); + LiteralSetExpression<T> rightExpr = expr.getRightOperand(); + BinarySetMembershipOperator op = expr.getOperator(); + // LHS OP [ RHS ] => { LHS : { OP : [ RHS ] } } + builder.push(leftExpr.getValue().getName()); + + String mongoOp = getSetMembershipOperator(op); + builder.add(mongoOp, rightExpr.getValues()); + + builder.pop(); + return builder.get(); + } + + private String getComparisonOperator(BinaryComparisonOperator operator) { String result; switch (operator) { case NOT_EQUAL_TO: @@ -151,6 +174,21 @@ return result; } + private String getSetMembershipOperator(BinarySetMembershipOperator operator) { + String result; + switch (operator) { + case IN: + result = "$in"; + break; + case NOT_IN: + result = "$nin"; + break; + default: + throw new IllegalArgumentException("MongoQuery can not handle " + operator); + } + return result; + } + private String getLogicalOperator(BinaryLogicalOperator operator) { String result; switch (operator) { @@ -177,5 +215,5 @@ } return result; } - + }
--- a/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParserTest.java Fri Jun 21 16:36:21 2013 +0200 +++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParserTest.java Fri Jun 28 12:37:39 2013 -0400 @@ -38,6 +38,10 @@ import static org.junit.Assert.assertEquals; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.junit.Before; import org.junit.Test; @@ -55,15 +59,16 @@ private final Key<String> KEY_3 = new Key<>("key", true); private MongoExpressionParser parser; + private ExpressionFactory factory; @Before public void setUp() throws Exception { parser = new MongoExpressionParser(); + factory = new ExpressionFactory(); } @Test public void testWhereEquals() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.equalTo(KEY_3, "value"); DBObject query = BasicDBObjectBuilder.start().add(KEY_3.getName(), "value").get(); assertEquals(query, parser.parse(expr)); @@ -71,7 +76,6 @@ @Test public void testWhereNotEquals() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.notEqualTo(KEY_3, "value"); DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$ne", "value").get(); assertEquals(query, parser.parse(expr)); @@ -79,7 +83,6 @@ @Test public void testWhereGreaterThan() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.greaterThan(KEY_3, "value"); DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$gt", "value").get(); assertEquals(query, parser.parse(expr)); @@ -87,7 +90,6 @@ @Test public void testWhereGreaterThanOrEqualTo() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.greaterThanOrEqualTo(KEY_3, "value"); DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$gte", "value").get(); assertEquals(query, parser.parse(expr)); @@ -95,7 +97,6 @@ @Test public void testWhereLessThan() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.lessThan(KEY_3, "value"); DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$lt", "value").get(); assertEquals(query, parser.parse(expr)); @@ -103,15 +104,29 @@ @Test public void testWhereLessThanOrEqualTo() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.lessThanOrEqualTo(KEY_3, "value"); DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$lte", "value").get(); assertEquals(query, parser.parse(expr)); } + + @Test + public void testWhereIn() { + Set<String> values = new HashSet<>(Arrays.asList("value", "values")); + Expression expr = factory.in(KEY_3, values, String.class); + DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$in", values).get(); + assertEquals(query, parser.parse(expr)); + } + + @Test + public void testWhereNotIn() { + Set<String> values = new HashSet<>(Arrays.asList("value", "values")); + Expression expr = factory.notIn(KEY_3, values, String.class); + DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$nin", values).get(); + assertEquals(query, parser.parse(expr)); + } @Test public void testMultiWhere() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.and(factory.lessThanOrEqualTo(KEY_1, 1), factory.greaterThan(KEY_1, 2)); BasicDBList list = new BasicDBList(); @@ -123,7 +138,6 @@ @Test public void testMultiWhere2() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.and(factory.lessThanOrEqualTo(KEY_1, 1), factory.greaterThan(KEY_2, 2)); BasicDBList list = new BasicDBList(); @@ -135,7 +149,6 @@ @Test public void testMultiWhere3() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.and(factory.equalTo(KEY_1, 1), factory.greaterThan(KEY_1, 2)); BasicDBList list = new BasicDBList(); @@ -147,7 +160,6 @@ @Test public void testMultiWhere4() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.and(factory.equalTo(KEY_1, 1), factory.greaterThan(KEY_2, 2)); BasicDBList list = new BasicDBList(); @@ -159,7 +171,6 @@ @Test public void testWhereOr() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.or(factory.equalTo(KEY_1, 1), factory.greaterThan(KEY_2, 2)); BasicDBList list = new BasicDBList(); @@ -170,31 +181,30 @@ } @Test - public void testWhereNot() { - ExpressionFactory factory = new ExpressionFactory(); + public void testWhereNotCompare() { Expression expr = factory.not(factory.greaterThan(KEY_1, 1)); DBObject dbObject = BasicDBObjectBuilder.start().push("test").push("$not").add("$gt", 1).get(); assertEquals(dbObject, parser.parse(expr)); } - @Test(expected=IllegalArgumentException.class) - public void testWhereNotLogicalExpr() { - ExpressionFactory factory = new ExpressionFactory(); - Expression expr = factory.not(factory.and(factory.equalTo(KEY_1, 1), factory.equalTo(KEY_2, 2))); - parser.parse(expr); + @Test + public void testWhereNotSetCompare() { + Set<Integer> values = new HashSet<>(Arrays.asList(1, 2)); + Expression expr = factory.not(factory.in(KEY_1, values, Integer.class)); + + DBObject dbObject = BasicDBObjectBuilder.start().push("test").push("$not").add("$in", values).get(); + assertEquals(dbObject, parser.parse(expr)); } @Test(expected=IllegalArgumentException.class) public void testWhereLogicalNotEquals() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.not(factory.equalTo(KEY_1, 1)); parser.parse(expr); } @Test public void testWhere3() { - ExpressionFactory factory = new ExpressionFactory(); Expression expr = factory.and( factory.lessThanOrEqualTo(KEY_3, "value"), factory.and(factory.equalTo(KEY_1, 1),
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/ExpressionSerializer.java Fri Jun 21 16:36:21 2013 +0200 +++ b/web/common/src/main/java/com/redhat/thermostat/web/common/ExpressionSerializer.java Fri Jun 28 12:37:39 2013 -0400 @@ -37,7 +37,10 @@ package com.redhat.thermostat.web.common; import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; +import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; @@ -50,8 +53,12 @@ import com.redhat.thermostat.storage.query.BinaryComparisonOperator; import com.redhat.thermostat.storage.query.BinaryLogicalExpression; import com.redhat.thermostat.storage.query.BinaryLogicalOperator; +import com.redhat.thermostat.storage.query.BinarySetMembershipExpression; +import com.redhat.thermostat.storage.query.BinarySetMembershipOperator; +import com.redhat.thermostat.storage.query.ComparisonExpression; import com.redhat.thermostat.storage.query.Expression; import com.redhat.thermostat.storage.query.LiteralExpression; +import com.redhat.thermostat.storage.query.LiteralSetExpression; import com.redhat.thermostat.storage.query.Operator; import com.redhat.thermostat.storage.query.UnaryLogicalExpression; import com.redhat.thermostat.storage.query.UnaryLogicalOperator; @@ -89,6 +96,9 @@ if (BinaryComparisonExpression.class.isAssignableFrom(clazz)) { result = deserializeBinaryComparisonExpression(json, context); } + else if (BinarySetMembershipExpression.class.isAssignableFrom(clazz)) { + result = deserializeBinarySetMembershipExpression(json, context); + } else if (BinaryLogicalExpression.class.isAssignableFrom(clazz)) { result = deserializeBinaryLogicalExpression(json, context); } @@ -98,6 +108,9 @@ else if (LiteralExpression.class.isAssignableFrom(clazz)) { result = deserializeLiteralExpression(json, context); } + else if (LiteralSetExpression.class.isAssignableFrom(clazz)) { + result = deserializeLiteralArrayExpression(json, context); + } else { throw new JsonParseException("Unknown Expression of type " + className); } @@ -118,6 +131,18 @@ BinaryComparisonOperator op = context.deserialize(jsonOp, Operator.class); return new BinaryComparisonExpression<>(left, op, right); } + + private <T> Expression deserializeBinarySetMembershipExpression(JsonElement json, + JsonDeserializationContext context) { + JsonElement jsonLeft = json.getAsJsonObject().get(PROP_OPERAND_LEFT); + JsonElement jsonRight = json.getAsJsonObject().get(PROP_OPERAND_RIGHT); + JsonElement jsonOp = json.getAsJsonObject().get(PROP_OPERATOR); + + LiteralExpression<Key<T>> left = context.deserialize(jsonLeft, Expression.class); + LiteralSetExpression<T> right = context.deserialize(jsonRight, Expression.class); + BinarySetMembershipOperator op = context.deserialize(jsonOp, Operator.class); + return new BinarySetMembershipExpression<>(left, op, right); + } private <S extends Expression, T extends Expression> Expression deserializeBinaryLogicalExpression(JsonElement json, JsonDeserializationContext context) { @@ -131,7 +156,7 @@ return new BinaryLogicalExpression<>(left, op, right); } - private <T extends Expression> Expression deserializeUnaryLogicalExpression(JsonElement json, + private <T extends ComparisonExpression> Expression deserializeUnaryLogicalExpression(JsonElement json, JsonDeserializationContext context) { JsonElement jsonOperand = json.getAsJsonObject().get(PROP_OPERAND); JsonElement jsonOp = json.getAsJsonObject().get(PROP_OPERATOR); @@ -149,13 +174,38 @@ Class<?> valueClass = Class.forName(valueClassName); return makeLiteralExpression(context, jsonValue, valueClass); } - + private <T> Expression makeLiteralExpression(JsonDeserializationContext context, JsonElement jsonValue, Class<T> valueClass) throws ClassNotFoundException { T value = context.deserialize(jsonValue, valueClass); return new LiteralExpression<>(value); } + private Expression deserializeLiteralArrayExpression(JsonElement json, + JsonDeserializationContext context) throws ClassNotFoundException { + JsonElement jsonValue = json.getAsJsonObject().get(PROP_VALUE); + if (jsonValue instanceof JsonArray) { + JsonElement jsonValueClass = json.getAsJsonObject().get(PROP_VALUE_CLASS); + String valueClassName = jsonValueClass.getAsString(); + Class<?> type = Class.forName(valueClassName); + JsonArray jsonArr = (JsonArray) jsonValue; + return makeLiteralArrayExpression(context, jsonArr, type); + } + else { + throw new JsonParseException("No JsonArray supplied for " + PROP_VALUE); + } + } + + private <T> Expression makeLiteralArrayExpression(JsonDeserializationContext context, + JsonArray jsonArr, Class<T> valueClass) { + Set<T> values = new HashSet<>(); + for (JsonElement element : jsonArr) { + T value = context.deserialize(element, valueClass); + values.add(value); + } + return new LiteralSetExpression<>(values, valueClass); + } + @Override public JsonElement serialize(Expression src, Type typeOfSrc, JsonSerializationContext context) { @@ -181,6 +231,16 @@ result.add(PROP_OPERATOR, op); result.add(PROP_OPERAND_RIGHT, right); } + else if (src instanceof BinarySetMembershipExpression) { + BinarySetMembershipExpression<?> binExpr = (BinarySetMembershipExpression<?>) src; + JsonElement left = context.serialize(binExpr.getLeftOperand()); + JsonElement op = context.serialize(binExpr.getOperator()); + JsonElement right = context.serialize(binExpr.getRightOperand()); + result = new JsonObject(); + result.add(PROP_OPERAND_LEFT, left); + result.add(PROP_OPERATOR, op); + result.add(PROP_OPERAND_RIGHT, right); + } else if (src instanceof UnaryLogicalExpression) { UnaryLogicalExpression<?> unaryExpr = (UnaryLogicalExpression<?>) src; JsonElement operand = context.serialize(unaryExpr.getOperand()); @@ -197,11 +257,30 @@ // Store the type of value to properly deserialize it later result.addProperty(PROP_VALUE_CLASS, litExpr.getValue().getClass().getCanonicalName()); } + else if (src instanceof LiteralSetExpression) { + LiteralSetExpression<?> litArrExpr = (LiteralSetExpression<?>) src; + result = serializeLiteralArrayExpression(litArrExpr, context); + } else { throw new JsonParseException("Unknown expression of type " + src.getClass()); } result.addProperty(PROP_CLASS_NAME, src.getClass().getCanonicalName()); return result; } + + private <T> JsonObject serializeLiteralArrayExpression(LiteralSetExpression<T> expr, + JsonSerializationContext context) { + JsonObject result = new JsonObject(); + Set<T> list = expr.getValues(); + Class<T> type = expr.getType(); + JsonArray arr = new JsonArray(); + for (T element : list) { + arr.add(context.serialize(element, type)); + } + result.add(PROP_VALUE, arr); + // Store type of list elements + result.addProperty(PROP_VALUE_CLASS, type.getCanonicalName()); + return result; + } }
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/ExpressionSerializerTest.java Fri Jun 21 16:36:21 2013 +0200 +++ b/web/common/src/test/java/com/redhat/thermostat/web/common/ExpressionSerializerTest.java Fri Jun 28 12:37:39 2013 -0400 @@ -38,18 +38,24 @@ import static org.junit.Assert.assertEquals; +import java.util.Arrays; +import java.util.HashSet; + import org.junit.Before; import org.junit.Test; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.redhat.thermostat.storage.core.Key; import com.redhat.thermostat.storage.query.BinaryComparisonExpression; import com.redhat.thermostat.storage.query.BinaryLogicalExpression; +import com.redhat.thermostat.storage.query.BinarySetMembershipExpression; import com.redhat.thermostat.storage.query.Expression; import com.redhat.thermostat.storage.query.ExpressionFactory; +import com.redhat.thermostat.storage.query.LiteralSetExpression; import com.redhat.thermostat.storage.query.LiteralExpression; import com.redhat.thermostat.storage.query.Operator; import com.redhat.thermostat.storage.query.UnaryLogicalExpression; @@ -85,6 +91,22 @@ } @Test + public void testDeserializeLiteralArrayExpression() { + Expression expr = new LiteralSetExpression<String>(new HashSet<>(Arrays.asList("hello", "goodbye")), + String.class); + + JsonObject json = new JsonObject(); + JsonArray arr = new JsonArray(); + arr.add(gson.toJsonTree("hello")); + arr.add(gson.toJsonTree("goodbye")); + json.add(ExpressionSerializer.PROP_VALUE, arr); + json.addProperty(ExpressionSerializer.PROP_VALUE_CLASS, String.class.getCanonicalName()); + json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, LiteralSetExpression.class.getCanonicalName()); + + assertEquals(expr, gson.fromJson(json, Expression.class)); + } + + @Test public void testDeserializeBinaryComparisonExpression() { ExpressionFactory factory = new ExpressionFactory(); BinaryComparisonExpression<String> expr = factory.equalTo(key, "world"); @@ -99,6 +121,21 @@ } @Test + public void testDeserializeBinarySetMembershipExpression() { + ExpressionFactory factory = new ExpressionFactory(); + BinarySetMembershipExpression<String> expr = factory.in(key, + new HashSet<>(Arrays.asList("world", "goodbye")), String.class); + + JsonObject json = new JsonObject(); + json.add(ExpressionSerializer.PROP_OPERAND_LEFT, gson.toJsonTree(expr.getLeftOperand())); + json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator())); + json.add(ExpressionSerializer.PROP_OPERAND_RIGHT, gson.toJsonTree(expr.getRightOperand())); + json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, BinarySetMembershipExpression.class.getCanonicalName()); + + assertEquals(expr, gson.fromJson(json, Expression.class)); + } + + @Test public void testDeserializeBinaryLogicalExpression() { ExpressionFactory factory = new ExpressionFactory(); BinaryLogicalExpression<BinaryComparisonExpression<String>, BinaryComparisonExpression<String>> expr = factory.and(factory.equalTo(key, "world"), factory.greaterThan(key, "goodbye")); @@ -153,6 +190,22 @@ } @Test + public void testSerializeLiteralArrayExpression() { + Expression expr = new LiteralSetExpression<String>(new HashSet<>(Arrays.asList("hello", "goodbye")), + String.class); + + JsonObject json = new JsonObject(); + JsonArray arr = new JsonArray(); + arr.add(gson.toJsonTree("hello")); + arr.add(gson.toJsonTree("goodbye")); + json.add(ExpressionSerializer.PROP_VALUE, arr); + json.addProperty(ExpressionSerializer.PROP_VALUE_CLASS, String.class.getCanonicalName()); + json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, LiteralSetExpression.class.getCanonicalName()); + + assertEquals(gson.toJson(json), gson.toJson(expr)); + } + + @Test public void testSerializeBinaryComparisonExpression() { ExpressionFactory factory = new ExpressionFactory(); BinaryComparisonExpression<String> expr = factory.equalTo(key, "world"); @@ -167,6 +220,21 @@ } @Test + public void testSerializeBinarySetMembershipExpression() { + ExpressionFactory factory = new ExpressionFactory(); + BinarySetMembershipExpression<String> expr = factory.in(key, new HashSet<>(Arrays.asList("world", "goodbye")), + String.class); + + JsonObject json = new JsonObject(); + json.add(ExpressionSerializer.PROP_OPERAND_LEFT, gson.toJsonTree(expr.getLeftOperand())); + json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator())); + json.add(ExpressionSerializer.PROP_OPERAND_RIGHT, gson.toJsonTree(expr.getRightOperand())); + json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, BinarySetMembershipExpression.class.getCanonicalName()); + + assertEquals(gson.toJson(json), gson.toJson(expr)); + } + + @Test public void testSerializeBinaryLogicalExpression() { ExpressionFactory factory = new ExpressionFactory(); BinaryLogicalExpression<BinaryComparisonExpression<String>, BinaryComparisonExpression<String>> expr = factory.and(factory.equalTo(key, "world"), factory.greaterThan(key, "goodbye"));