diff --git a/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/ExpressionsTest.java b/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/ExpressionsTest.java index 942bd247..c3ee9a46 100644 --- a/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/ExpressionsTest.java +++ b/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/ExpressionsTest.java @@ -7,6 +7,7 @@ import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -17,9 +18,11 @@ public class ExpressionsTest { + private final String textEntity1 = "Entity 1"; + private final TestEntity testEntity1 = TestEntity.builder() .number(3) - .text("Entity 1") + .text(textEntity1) .refEntity(TestRefEntity .builder() .text("Description 1") @@ -39,6 +42,8 @@ public class ExpressionsTest { .build()) .keyName("Key 2") .refEntities(Collections.emptyList()) + .address("Address") + .col(Collections.singleton("Address")) .build(); @Test @@ -120,5 +125,343 @@ public void testValueInExpression() { Assert.assertFalse(exp.apply(testEntity2)); } + @Test + public void testNullExpression() { + Function exp = Expressions + .compile(TestEntity.$.address.isNull()); + + Assert.assertTrue(exp.apply(testEntity1)); + Assert.assertFalse(exp.apply(testEntity2)); + } + + @Test + public void testWhenEqGetNullPropertyValue() { + Function exp = Expressions + .compile(TestEntity.$.address.eq("Address")); + + Assert.assertFalse(exp.apply(testEntity1)); + Assert.assertTrue(exp.apply(testEntity2)); + } + + @Test + public void testAndEvaluation() { + Function exp = Expressions + .compile(TestEntity.$.address.isNotNull().and(TestEntity.$.address.eq("Address"))); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testOrEvaluation() { + Function exp = Expressions + .compile(TestEntity.$.address.isNull().or(TestEntity.$.address.eq("Address"))); + + Assert.assertTrue(exp.apply(testEntity1)); + Assert.assertTrue(exp.apply(testEntity2)); + } + + //Null handling tests + @Test + public void testAsStringWithNull() { + Function exp = Expressions + .compile(TestEntity.$.code.asString()); + + Assert.assertNull(exp.apply(testEntity1)); + } + + @Test + public void testAddWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.code.add(TestEntity.$.number)); + + Assert.assertEquals(Integer.valueOf(3), exp.apply(testEntity1)); + } + + @Test + public void testAddWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.number.add(TestEntity.$.code)); + + Assert.assertEquals(Integer.valueOf(3), exp.apply(testEntity1)); + } + + @Test + public void testSubtractWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.code.sub(TestEntity.$.number)); + + Assert.assertEquals(Integer.valueOf(-3), exp.apply(testEntity1)); + } + + @Test + public void testSubtractWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.number.sub(TestEntity.$.code)); + + Assert.assertEquals(Integer.valueOf(3), exp.apply(testEntity1)); + } + + + @Test + public void testMultiplyWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.code.mul(TestEntity.$.number)); + + Assert.assertEquals(Integer.valueOf(0), exp.apply(testEntity1)); + } + + @Test + public void testMultiplyWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.number.mul(TestEntity.$.code)); + + Assert.assertEquals(Integer.valueOf(0), exp.apply(testEntity1)); + } + + @Test + public void testDivideWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.code.div(TestEntity.$.number)); + + Assert.assertEquals(Integer.valueOf(0), exp.apply(testEntity1)); + } + + @Test + public void testDivideWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.number.div(TestEntity.$.code)); + + Assert.assertEquals(Integer.valueOf(3), exp.apply(testEntity1)); + } + + @Test + public void testNegateWithNull() { + Function exp = Expressions + .compile(TestEntity.$.code.negate()); + + Assert.assertNull(exp.apply(testEntity1)); + } + + @Test + public void testEqualsWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.code.eq(TestEntity.$.number)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testEqualsWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.number.eq(TestEntity.$.code)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testEqualsWithNull3() { + Function exp = Expressions + .compile(TestEntity.$.code.eq(TestEntity.$.code)); + + Assert.assertTrue(exp.apply(testEntity1)); + } + + @Test + public void testGreaterThanWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.code.greaterThan(TestEntity.$.number)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testGreaterThanWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.number.greaterThan(TestEntity.$.code)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testGreaterThanWithNull3() { + Function exp = Expressions + .compile(TestEntity.$.code.greaterThan(TestEntity.$.code)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testLessThanWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.code.lessThan(TestEntity.$.number)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testLessThanWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.number.lessThan(TestEntity.$.code)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testLessThanWithNull3() { + Function exp = Expressions + .compile(TestEntity.$.code.lessThan(TestEntity.$.code)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testIsEmptyWithNull() { + Function exp = Expressions + .compile(TestEntity.$.address.isEmpty()); + + Assert.assertTrue(exp.apply(testEntity1)); + Assert.assertFalse(exp.apply(testEntity2)); + } + + @Test + public void testContainsWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.address.contains("A")); + + Assert.assertFalse(exp.apply(testEntity1)); + Assert.assertTrue(exp.apply(testEntity2)); + } + + @Test + public void testContainsWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.text.contains(TestEntity.$.address)); + + Assert.assertTrue(exp.apply(testEntity1)); + } + + @Test + public void testStartsWithWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.address.startsWith("A")); + + Assert.assertFalse(exp.apply(testEntity1)); + Assert.assertTrue(exp.apply(testEntity2)); + } + + @Test + public void testStartsWithWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.text.startsWith(TestEntity.$.address)); + + Assert.assertTrue(exp.apply(testEntity1)); + } + + @Test + public void testEndsWithWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.address.endsWith("ss")); + + Assert.assertFalse(exp.apply(testEntity1)); + Assert.assertTrue(exp.apply(testEntity2)); + } + + @Test + public void testEndsWithWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.text.endsWith(TestEntity.$.address)); + + Assert.assertTrue(exp.apply(testEntity1)); + } + + @Test + public void testMatchesWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.address.matches("^[Aa]")); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testMatchesWithWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.text.matches(TestEntity.$.address)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testLengthWithNull() { + Function exp = Expressions + .compile(TestEntity.$.address.length()); + + Assert.assertEquals(Integer.valueOf(0),exp.apply(testEntity1)); + } + + @Test + public void testConcatWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.address.concat(TestEntity.$.text)); + + Assert.assertEquals(textEntity1,exp.apply(testEntity1)); + } + + @Test + public void testConcatWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.text.concat(TestEntity.$.address)); + + Assert.assertEquals(textEntity1,exp.apply(testEntity1)); + } + + @Test + public void testToLowerWithNull() { + Function exp = Expressions + .compile(TestEntity.$.address.toLower()); + + Assert.assertNull(exp.apply(testEntity1)); + } + + @Test + public void testToUpperWithNull() { + Function exp = Expressions + .compile(TestEntity.$.address.toUpper()); + + Assert.assertNull(exp.apply(testEntity1)); + } + + @Test + public void testTrimWithNull() { + Function exp = Expressions + .compile(TestEntity.$.address.trim()); + + Assert.assertNull(exp.apply(testEntity1)); + } + + @Test + public void testSearchTextWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.address.searchText("Ad")); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + @Test + public void testValueInTextWithNull1() { + Function exp = Expressions + .compile(TestEntity.$.address.in()); + + Assert.assertFalse(exp.apply(testEntity2)); + } + + @Test + public void testValueInTextWithNull2() { + Function exp = Expressions + .compile(TestEntity.$.text.in(TestEntity.$.col)); + + Assert.assertFalse(exp.apply(testEntity1)); + } + + } diff --git a/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/TestEntityPrototype.java b/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/TestEntityPrototype.java index 84aa5ada..ce765b8c 100644 --- a/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/TestEntityPrototype.java +++ b/rxrepo-apt/src/test/java/com/slimgears/rxrepo/queries/TestEntityPrototype.java @@ -8,6 +8,7 @@ import com.slimgears.util.autovalue.annotations.Key; import com.slimgears.util.autovalue.annotations.UseCopyAnnotator; +import javax.annotation.Nullable; import java.util.Collection; @AutoValuePrototype @@ -19,4 +20,7 @@ public interface TestEntityPrototype { @Indexable @Filterable int number(); @Filterable TestRefEntity refEntity(); Collection refEntities(); + @Nullable String address(); + @Nullable Integer code(); + @Nullable Collection col(); } diff --git a/rxrepo-core/src/main/java/com/slimgears/rxrepo/expressions/StringExpression.java b/rxrepo-core/src/main/java/com/slimgears/rxrepo/expressions/StringExpression.java index bb444995..37415878 100644 --- a/rxrepo-core/src/main/java/com/slimgears/rxrepo/expressions/StringExpression.java +++ b/rxrepo-core/src/main/java/com/slimgears/rxrepo/expressions/StringExpression.java @@ -1,9 +1,6 @@ package com.slimgears.rxrepo.expressions; -import com.slimgears.rxrepo.expressions.internal.BooleanBinaryOperationExpression; -import com.slimgears.rxrepo.expressions.internal.NumericUnaryOperationExpression; -import com.slimgears.rxrepo.expressions.internal.StringBinaryOperationExpression; -import com.slimgears.rxrepo.expressions.internal.StringUnaryOperationExpression; +import com.slimgears.rxrepo.expressions.internal.*; public interface StringExpression extends ComparableExpression { default BooleanExpression contains(ObjectExpression substr) { @@ -14,6 +11,10 @@ default BooleanExpression contains(String substr) { return contains(ConstantExpression.of(substr)); } + default BooleanExpression isEmpty() { + return BooleanUnaryOperationExpression.create(Type.IsEmpty, this); + } + default BooleanExpression startsWith(ObjectExpression substr) { return BooleanBinaryOperationExpression.create(Type.StartsWith, this, substr); } diff --git a/rxrepo-core/src/main/java/com/slimgears/rxrepo/util/Expressions.java b/rxrepo-core/src/main/java/com/slimgears/rxrepo/util/Expressions.java index 04ba2ca6..37b4ccab 100644 --- a/rxrepo-core/src/main/java/com/slimgears/rxrepo/util/Expressions.java +++ b/rxrepo-core/src/main/java/com/slimgears/rxrepo/util/Expressions.java @@ -1,5 +1,6 @@ package com.slimgears.rxrepo.util; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.slimgears.rxrepo.expressions.Expression; import com.slimgears.rxrepo.expressions.ExpressionVisitor; @@ -9,14 +10,11 @@ import com.slimgears.util.reflect.TypeToken; import com.slimgears.util.stream.Optionals; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Optional; +import java.util.*; import java.util.function.BiFunction; +import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Stream; @SuppressWarnings("WeakerAccess") public class Expressions { @@ -60,6 +58,14 @@ private static Function fromBinary(BiFunction< return funcs -> val -> func.apply((T1)funcs[0].apply(val), (T2)funcs[1].apply(val)); } + private static Function fromShortCircuitAnd() { + return funcs -> val -> (boolean)funcs[0].apply(val) && (boolean)funcs[1].apply(val); + } + + private static Function fromShortCircuitOr() { + return funcs -> val -> (boolean)funcs[0].apply(val) || (boolean)funcs[1].apply(val); + } + @SuppressWarnings("unchecked") private static Function fromNumericBinary(BiFunction func) { return funcs -> val -> func.apply((T)funcs[0].apply(val), (T)funcs[1].apply(val)); @@ -81,35 +87,36 @@ private static Function notSupported() { private static class InternalVisitor extends ExpressionVisitor { @SuppressWarnings("unchecked") private final static ImmutableMap> expressionTypeReducersMap = ImmutableMap.>builder() - .put(Expression.Type.AsString, fromUnary(Object::toString)) - .put(Expression.Type.Add, fromNumericBinary(GenericMath::add)) - .put(Expression.Type.Sub, fromNumericBinary(GenericMath::subtract)) - .put(Expression.Type.Mul, fromNumericBinary(GenericMath::multiply)) - .put(Expression.Type.Div, fromNumericBinary(GenericMath::divide)) - .put(Expression.Type.Negate, fromNumericUnary(GenericMath::negate)) - .put(Expression.Type.And, fromBinary(Boolean::logicalAnd)) - .put(Expression.Type.Or, fromBinary(Boolean::logicalOr)) + .put(Expression.Type.AsString, fromUnary(o -> o != null ? o.toString() : null)) + .put(Expression.Type.Add, fromNumericBinary(add())) + .put(Expression.Type.Sub, fromNumericBinary(subtract())) + .put(Expression.Type.Mul, fromNumericBinary(multiply())) + .put(Expression.Type.Div, fromNumericBinary(divide())) + .put(Expression.Type.Negate, fromNumericUnary(negate())) + .put(Expression.Type.And, fromShortCircuitAnd()) + .put(Expression.Type.Or, fromShortCircuitOr()) .put(Expression.Type.Not, fromUnary(Boolean.FALSE::equals)) - .put(Expression.Type.Equals, fromBinary(Object::equals)) - .put(Expression.Type.GreaterThan, Expressions.fromBinary((a, b) -> a.compareTo(b) > 0)) - .put(Expression.Type.LessThan, Expressions.fromBinary((a, b) -> a.compareTo(b) < 0)) - .put(Expression.Type.IsEmpty, fromUnary(String::isEmpty)) - .put(Expression.Type.Contains, fromBinary(String::contains)) - .put(Expression.Type.StartsWith, Expressions.fromBinary(String::startsWith)) - .put(Expression.Type.EndsWith, fromBinary(String::endsWith)) - .put(Expression.Type.Matches, fromBinary(String::matches)) - .put(Expression.Type.Length, fromUnary(String::length)) - .put(Expression.Type.Concat, Expressions.fromBinary((s1, s2) -> s1 + s2)) - .put(Expression.Type.ToLower, Expressions.fromUnary(String::toLowerCase)) - .put(Expression.Type.ToUpper, Expressions.fromUnary(String::toUpperCase)) - .put(Expression.Type.Trim, fromUnary(String::trim)) - .put(Expression.Type.Count, Expressions.fromUnary(Collection::size)) + .put(Expression.Type.Equals, fromBinary(Objects::equals)) + .put(Expression.Type.GreaterThan, Expressions.fromBinary((a, b) -> a != null && b != null && a.compareTo(b) > 0)) + .put(Expression.Type.LessThan, Expressions.fromBinary((a, b) -> a != null && b != null && a.compareTo(b) < 0)) + .put(Expression.Type.IsEmpty, fromUnary(Strings::isNullOrEmpty)) + .put(Expression.Type.Contains, Expressions.fromBinary(contains())) + .put(Expression.Type.StartsWith, Expressions.fromBinary(startsWith())) + .put(Expression.Type.EndsWith, Expressions.fromBinary(endsWith())) + .put(Expression.Type.Matches, Expressions.fromBinary(matches())) + .put(Expression.Type.Length, Expressions.fromUnary(length())) + .put(Expression.Type.Concat, Expressions.fromBinary(concat())) + .put(Expression.Type.ToLower, Expressions.fromUnary(s -> s != null ? s.toLowerCase() : null)) + .put(Expression.Type.ToUpper, Expressions.fromUnary(s -> s != null ? s.toUpperCase() : null)) + .put(Expression.Type.Trim, Expressions.fromUnary(s -> s != null ? s.trim() : null)) + .put(Expression.Type.Count, Expressions.fromUnary(col -> Optional.ofNullable(col).map(Collection::size).orElse(0))) .put(Expression.Type.Average, notSupported()) .put(Expression.Type.Min, notSupported()) .put(Expression.Type.Max, notSupported()) .put(Expression.Type.Sum, notSupported()) - .put(Expression.Type.SearchText, Expressions.fromBinary((Object obj, String str) -> obj.toString().contains(str))) - .put(Expression.Type.ValueIn, Expressions.fromBinary((Object obj, Collection collection) -> collection.contains(obj))) + .put(Expression.Type.SearchText, Expressions.fromBinary((obj, str) -> obj != null && obj.toString().contains(getStringOrEmpty(str)))) //obj != null and str == null returns true. + .put(Expression.Type.ValueIn, Expressions.fromBinary((Object obj, Collection collection) -> obj != null && collection != null && collection.contains(obj))) + .put(Expression.Type.IsNull, Expressions.fromUnary(Objects::isNull)) .build(); private final static ImmutableMap> operationTypeReducersMap = ImmutableMap.>builder() @@ -157,4 +164,60 @@ protected Function visitArgument(TypeToken argType, Void arg) { return a -> a; } } + + private static BiFunction concat() { + return (s1, s2) -> getStringOrEmpty(s1) + getStringOrEmpty(s2); + } + + private static Function length() { + return s -> Optional.ofNullable(s).map(String::length).orElse(0); + } + + private static Function negate() { + return num -> num != null ? GenericMath.negate(num) : null; + } + + private static BiFunction divide() { + return numbericBinariesWithDefaultNumbers(GenericMath::divide, 0, 1); + } + + private static BiFunction multiply() { + return numbericBinariesWithDefaultNumbers(GenericMath::multiply, 0, 0); + } + + private static BiFunction subtract() { + return numbericBinariesWithDefaultNumbers(GenericMath::subtract, 0, 0); + } + + private static BiFunction add() { + return numbericBinariesWithDefaultNumbers(GenericMath::add, 0, 0); + } + + private static BiFunction startsWith() { + return (s1, s2) -> s1 != null && (s2 == null || s1.startsWith(s2)); + } + + private static BiFunction endsWith() { + return (s1, s2) -> s1 != null && (s2 == null || s1.endsWith(s2)); + } + + private static BiFunction matches() { + return (s1, s2) -> (s1 == null && s2 == null) || (s1 != null && s2 != null && s1.matches(s2)); + } + + private static BiFunction numbericBinariesWithDefaultNumbers(BiFunction func, Number defaultValue1, Number defaultValue2) { + return (n1, n2) -> func.apply(getNumberOrDefault(n1, defaultValue1), getNumberOrDefault(n2, defaultValue2)); + } + + private static Number getNumberOrDefault(Number num, Number defaultValue){ + return Optional.ofNullable(num).orElse(defaultValue); + } + + private static String getStringOrEmpty(String s){ + return Optional.ofNullable(s).orElse(""); + } + + private static BiFunction contains() { + return (s1, s2) -> s1 != null && (s2 == null || s1.contains(s2)); //if s1 == null returns false but then null does not contain null + } } diff --git a/rxrepo-orientdb/src/main/java/com/slimgears/rxrepo/orientdb/OrientDbQueryProviderDecorator.java b/rxrepo-orientdb/src/main/java/com/slimgears/rxrepo/orientdb/OrientDbQueryProviderDecorator.java index c7499efa..3686c33b 100644 --- a/rxrepo-orientdb/src/main/java/com/slimgears/rxrepo/orientdb/OrientDbQueryProviderDecorator.java +++ b/rxrepo-orientdb/src/main/java/com/slimgears/rxrepo/orientdb/OrientDbQueryProviderDecorator.java @@ -76,7 +76,7 @@ private > ObservableTransformer