blob: 2dcdb65554d35d9d64e09900b6a5baa2d5868f98 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.filter;
import java.util.Set;
import java.util.List;
import java.util.Collection;
import java.util.function.Function;
import java.util.function.BiFunction;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.math.FunctionProperty;
// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.sis.test.TestCase;
import static org.apache.sis.test.Assertions.assertSerializedEquals;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.LogicalOperator;
/**
* Tests {@link LogicalFilter} implementations.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
public final class LogicalFilterTest extends TestCase {
/**
* The factory to use for creating the objects to test.
*/
private final FilterFactory<Feature,Object,?> factory;
/**
* Creates a new test case.
*/
public LogicalFilterTest() {
factory = DefaultFilterFactory.forFeatures();
}
/**
* Tests creation of "And" expression from the factory.
* Also tests an evaluation from literal values and serialization.
*/
@Test
public void testAnd() {
create(factory::and, factory::and, false);
}
/**
* Tests creation of "Or" expression from the factory.
* Also tests an evaluation from literal values and serialization.
*/
@Test
public void testOr() {
create(factory::or, factory::or, true);
}
/**
* Tests creation of "Not" expression from the factory.
* Also tests an evaluation from literal values and serialization.
*/
@Test
public void testNot() {
final var literal = factory.literal("text");
final var operand = factory.isNull(literal);
final var filter = factory.not(operand);
assertArrayEquals(new Filter<?>[] {operand}, filter.getOperands().toArray());
assertTrue(filter.test(null));
assertSerializedEquals(filter);
assertFalse(isVolatile(filter));
}
/**
* Verifies that {@link Filter#negate()} is overridden.
*/
@Test
public void testNegate() {
final var literal = factory.literal("text");
final var operand = factory.isNull(literal);
assertInstanceOf(LogicalFilter.Not.class, operand.negate());
}
/**
* Tests a filter having a volatile expression.
*/
@Test
public void testVolatile() {
final var literal = new LeafExpression.Literal<Feature,Object>("test") {
@Override public Set<FunctionProperty> properties() {
return Set.of(FunctionProperty.VOLATILE);
}
};
final var operand = factory.isNull(literal);
final var filter = factory.not(operand);
assertTrue(isVolatile(filter));
}
/**
* Implementation of {@link #testAnd()} and {@link #testOr()}.
*
* @param binary the function creating a logical operator from two operands.
* @param anyArity the function creating a logical operator from an arbitrary number of operands.
* @param expected expected evaluation result.
*/
private void create(final BiFunction<Filter<Feature>, Filter<Feature>, LogicalOperator<Feature>> binary,
final Function<Collection<Filter<Feature>>, LogicalOperator<Feature>> anyArity,
final boolean expected)
{
final var f1 = factory.isNull(factory.literal("text"));
final var f2 = factory.isNull(factory.literal(null));
RuntimeException e;
e = assertThrows(NullPointerException.class, () -> binary.apply(null, null),
"Creation with a null operand shall raise an exception.");
assertNotNull(e);
e = assertThrows(NullPointerException.class, () -> binary.apply(f1, null),
"Creation with a null operand shall raise an exception.");
assertNotNull(e);
e = assertThrows(NullPointerException.class, () -> binary.apply(null, f2),
"Creation with a null operand shall raise an exception.");
assertNotNull(e);
e = assertThrows(IllegalArgumentException.class, () -> anyArity.apply(Set.of(f1)),
"Creation with less then two operands shall raise an exception.");
assertNotNull(e);
/*
* Test construction, evaluation and serialization.
*/
var filter = binary.apply(f1, f2);
assertArrayEquals(new Filter<?>[] {f1, f2}, filter.getOperands().toArray());
assertEquals(expected, filter.test(null));
assertSerializedEquals(filter);
assertFalse(isVolatile(filter));
/*
* Same test, using the constructor accepting any number of operands.
*/
filter = anyArity.apply(List.of(f1, f2, f1));
assertArrayEquals(new Filter<?>[] {f1, f2, f1}, filter.getOperands().toArray());
assertEquals(expected, filter.test(null));
assertSerializedEquals(filter);
assertFalse(isVolatile(filter));
/*
* Test the `Predicate` methods, which should be overridden by `Optimization.OnFilter`.
*/
assertInstanceOf(Optimization.OnFilter.class, f1.and(f2));
assertInstanceOf(Optimization.OnFilter.class, f1.or(f2));
assertInstanceOf(Optimization.OnFilter.class, f1.negate());
}
/**
* Tests evaluation using a feature instance.
*/
@Test
public void testEvaluateOnFeature() {
final var builder = new FeatureTypeBuilder();
builder.addAttribute(String.class).setName("attNull");
builder.addAttribute(String.class).setName("attNotNull");
final var feature = builder.setName("Test").build().newInstance();
feature.setPropertyValue("attNotNull", "a value");
final var filterTrue = factory.isNull(factory.property("attNull", String.class));
final var filterFalse = factory.isNull(factory.property("attNotNull", String.class));
assertTrue (factory.and(filterTrue, filterTrue ).test(feature));
assertFalse(factory.and(filterFalse, filterTrue ).test(feature));
assertFalse(factory.and(filterTrue, filterFalse).test(feature));
assertFalse(factory.and(filterFalse, filterFalse).test(feature));
assertTrue (factory.or (filterTrue, filterTrue ).test(feature));
assertTrue (factory.or (filterFalse, filterTrue ).test(feature));
assertTrue (factory.or (filterTrue, filterFalse).test(feature));
assertFalse(factory.or (filterFalse, filterFalse).test(feature));
assertFalse(factory.not(filterTrue ).test(feature));
assertTrue (factory.not(filterFalse).test(feature));
/*
* Test the `Predicate` methods, which should be overridden by `Optimization.OnFilter`.
*/
var predicate = filterTrue.and(filterFalse);
assertInstanceOf(Optimization.OnFilter.class, predicate);
assertFalse(predicate.test(feature));
predicate = filterTrue.or(filterFalse);
assertInstanceOf(Optimization.OnFilter.class, predicate);
assertTrue(predicate.test(feature));
}
/**
* Tests {@link Optimization} applied on logical filters.
*/
@Test
public void testOptimization() {
final var f1 = factory.isNull(factory.literal("text")); // False
final var f2 = factory.isNull(factory.literal(null)); // True
final var f3 = factory.isNull(factory.property("*")); // Indeterminate
optimize(factory.and(f1, f2), Filter.exclude());
optimize(factory.or (f1, f2), Filter.include());
optimize(factory.and(f3, factory.not(f3)), Filter.exclude());
optimize(factory.or (f3, factory.not(f3)), Filter.include());
}
/**
* Verifies an optimization which is expected to evaluate immediately.
*/
private static void optimize(final Filter<Feature> original, final Filter<Feature> expected) {
final Filter<?> optimized = new Optimization().apply(original);
assertNotSame(original, optimized, "Expected a new optimized filter.");
assertSame(optimized, new Optimization().apply(optimized), "Second optimization should have no effect.");
assertSame(expected, optimized, "Expression should have been evaluated now.");
}
/**
* Tests {@link Optimization} applied on logical filters when the {@code FeatureType} is known.
*/
@Test
public void testFeatureOptimization() {
final String attribute = "population";
final var builder = new FeatureTypeBuilder();
builder.addAttribute(String.class).setName(attribute);
final var feature = builder.setName("Test").build();
final var instance = feature.newInstance();
instance.setPropertyValue("population", "1000");
/*
* Prepare an expression which divide the population value by 5.
*/
final var expression = factory.divide(factory.property(attribute, Integer.class), factory.literal(5));
final Optimization optimization = new Optimization();
assertSame(expression, optimization.apply(expression)); // No optimization.
assertEquals(200, expression.apply(instance).intValue());
/*
* Notify the optimizer that property values will be of `String` type.
* The optimizer should compute an `ObjectConverter` in advance.
*/
optimization.setFeatureType(feature);
final var optimized = optimization.apply(expression);
assertEquals(200, expression.apply(instance).intValue());
assertNotSame(expression, optimized);
final var property = assertInstanceOf(PropertyValue.class, optimized.getParameters().get(0));
assertEquals(String.class, property.getSourceClass());
assertEquals(Number.class, property.getValueClass());
}
/**
* Returns {@code true} if the given filter is declared volatile.
*/
private static boolean isVolatile(final Filter<?> filter) {
return Optimization.properties(filter).equals(Set.of(FunctionProperty.VOLATILE));
}
}