Skip to content

Commit

Permalink
Added filter expression implementation to support generation of predi…
Browse files Browse the repository at this point in the history
…cates for filtering calendar objects
  • Loading branch information
benfortuna committed Jul 27, 2021
1 parent 5e7e42c commit 1f6d9d5
Show file tree
Hide file tree
Showing 11 changed files with 611 additions and 29 deletions.
175 changes: 175 additions & 0 deletions src/main/java/net/fortuna/ical4j/filter/FilterExpression.java
@@ -0,0 +1,175 @@
package net.fortuna.ical4j.filter;

import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.Property;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class FilterExpression {

private final Map<String, Object> equalToMap = new HashMap<>();

private final Map<String, List<?>> inMap = new HashMap<>();

private final Map<String, Object> greaterThanMap = new HashMap<>();

private final Map<String, Object> greaterThanEqualMap = new HashMap<>();

private final Map<String, Object> lessThanMap = new HashMap<>();

private final Map<String, Object> lessThanEqualMap = new HashMap<>();

private final Map<String, Object> containsMap = new HashMap<>();

private final Set<String> existsSet = new HashSet<>();

private final Set<String> notExistsSet = new HashSet<>();

public FilterExpression equalTo(String name, Object value) {
equalToMap.put(name, value);
return this;
}

public FilterExpression in(String name, List<?> value) {
inMap.put(name, value);
return this;
}

public FilterExpression greaterThan(String name, Object value) {
greaterThanMap.put(name, value);
return this;
}

public FilterExpression greaterThanEqual(String name, Object value) {
greaterThanEqualMap.put(name, value);
return this;
}

public FilterExpression lessThan(String name, Object value) {
lessThanMap.put(name, value);
return this;
}

public FilterExpression lessThanEqual(String name, Object value) {
lessThanEqualMap.put(name, value);
return this;
}

public FilterExpression contains(String name, Object value) {
containsMap.put(name, value);
return this;
}

public FilterExpression exists(String name) {
existsSet.add(name);
return this;
}

public FilterExpression notExists(String name) {
notExistsSet.add(name);
return this;
}

public static FilterExpression parse(String filterExpression) {
FilterExpression expression = new FilterExpression();
Arrays.stream(filterExpression.split("\\s*and\\s*")).forEach(part -> {
if (part.matches("[\\w-]+\\s*>=\\s*\\w+")) {
String[] greaterThanEqual = part.split("\\s*>=\\s*");
expression.greaterThanEqual(greaterThanEqual[0], greaterThanEqual[1]);
} else if (part.matches("[\\w-]+\\s*<=\\s*\\w+")) {
String[] lessThanEqual = part.split("\\s*<=\\s*");
expression.lessThanEqual(lessThanEqual[0], lessThanEqual[1]);
} else if (part.matches("[\\w-]+\\s*=\\s*[^<>=]+")) {
String[] equalTo = part.split("\\s*=\\s*");
expression.equalTo(equalTo[0], equalTo[1]);
} else if (part.matches("[\\w-]+\\s*>\\s*\\w+")) {
String[] greaterThan = part.split("\\s*>\\s*");
expression.greaterThan(greaterThan[0], greaterThan[1]);
} else if (part.matches("[\\w-]+\\s*<\\s*\\w+")) {
String[] lessThan = part.split("\\s*<\\s*");
expression.lessThan(lessThan[0], lessThan[1]);
} else if (part.matches("[\\w-]+\\s+in\\s+\\[[^<>=]+]")) {
String[] in = part.split("\\s*in\\s*");
List<String> items = Arrays.asList(in[1].replaceAll("[\\[\\]]", "")
.split("\\[?\\s*,\\s*]?"));
expression.in(in[0], items);
} else if (part.matches("[\\w-]+\\s+contains\\s+\".+\"")) {
String[] contains = part.split("\\s*contains\\s*");
expression.contains(contains[0], contains[1].replaceAll("^\"?|\"?$", ""));
} else if (part.matches("[\\w-]+\\s+exists")) {
String[] exists = part.split("\\s*exists");
expression.exists(exists[0]);
} else if (part.matches("[\\w-]+\\s+not exists")) {
String[] notExists = part.split("\\s*not exists");
expression.notExists(notExists[0]);
} else {
throw new IllegalArgumentException("Invalid filter expression: " + filterExpression);
}
});
return expression;
}

public static <T> Predicate<T> and(List<Predicate> predicates) {
// TODO Handle case when argument is null or empty or has only one element
return predicates.stream().reduce(t -> true, Predicate::and);
}

public Predicate<Calendar> toCalendarPredicate() {
Predicate<Calendar> p = and(equalToMap.entrySet().stream().map(e -> new PropertyEqualToRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList()));
p = p.and(and(inMap.entrySet().stream().map(e -> new PropertyInRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(greaterThanMap.entrySet().stream().map(e -> new PropertyGreaterThanRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(lessThanMap.entrySet().stream().map(e -> new PropertyLessThanRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(containsMap.entrySet().stream().map(e -> new PropertyContainsRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(existsSet.stream().map(PropertyExistsRule::new)
.collect(Collectors.toList())));
p = p.and(and(notExistsSet.stream().map(prop -> new PropertyExistsRule<>(prop).negate())
.collect(Collectors.toList())));
return p;
}

public Predicate<Component> toComponentPredicate() {
Predicate<Component> p = and(equalToMap.entrySet().stream()
.map(e -> new PropertyEqualToRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList()));
p = p.and(and(inMap.entrySet().stream()
.map(e -> new PropertyInRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(greaterThanMap.entrySet().stream()
.map(e -> new PropertyGreaterThanRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(greaterThanEqualMap.entrySet().stream()
.map(e -> new PropertyGreaterThanRule<>(e.getKey(), e.getValue(), true))
.collect(Collectors.toList())));
p = p.and(and(lessThanMap.entrySet().stream()
.map(e -> new PropertyLessThanRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(lessThanEqualMap.entrySet().stream()
.map(e -> new PropertyLessThanRule<>(e.getKey(), e.getValue(), true))
.collect(Collectors.toList())));
p = p.and(and(containsMap.entrySet().stream()
.map(e -> new PropertyContainsRule<>(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
p = p.and(and(existsSet.stream().map(PropertyExistsRule::new)
.collect(Collectors.toList())));
p = p.and(and(notExistsSet.stream().map(prop -> new PropertyExistsRule<>(prop).negate())
.collect(Collectors.toList())));

return p;
}

public Predicate<Property> toParameterPredicate() {
Predicate<Property> p = and(equalToMap.entrySet().stream()
.map(e -> new ParameterEqualToRule(e.getKey(), e.getValue()))
.collect(Collectors.toList()));

return p;
}
}
70 changes: 70 additions & 0 deletions src/main/java/net/fortuna/ical4j/filter/ParameterEqualToRule.java
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2012, Ben Fortuna
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* o Neither the name of Ben Fortuna nor the names of any other contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.fortuna.ical4j.filter;

import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.ParameterList;
import net.fortuna.ical4j.model.Property;

import java.util.function.Predicate;

/**
* $Id$
*
* Created on 5/02/2006
*
* A rule that matches any component containing the specified property. Note that this rule ignores any parameters
* matching only on the value of the property.
* @author Ben Fortuna
*/
public class ParameterEqualToRule implements Predicate<Property> {

private final String parameterName;

private final Object value;

public ParameterEqualToRule(String parameterName, Object value) {
this.parameterName = parameterName;
this.value = value;
}

@Override
public final boolean test(final Property property) {
final ParameterList parameters = property.getParameters(parameterName);
for (final Parameter p : parameters) {
if (value.equals(p.getValue())) {
return true;
}
}
return false;
}
}
35 changes: 35 additions & 0 deletions src/main/java/net/fortuna/ical4j/filter/PropertyContainsRule.java
@@ -0,0 +1,35 @@
package net.fortuna.ical4j.filter;

import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;

import java.util.function.Predicate;

/**
* Test for a property that "contains" the provided value.
*
* @param <T>
*/
public class PropertyContainsRule<T extends Component> implements Predicate<T> {

private final String propertyName;

private final Object value;

public PropertyContainsRule(String propertyName, Object value) {
this.propertyName = propertyName;
this.value = value;
}

@Override
public boolean test(T t) {
final PropertyList<Property> properties = t.getProperties(propertyName);
for (final Property p : properties) {
if (p.getValue().contains(value.toString())) {
return true;
}
}
return false;
}
}
Expand Up @@ -46,47 +46,37 @@
* matching only on the value of the property.
* @author Ben Fortuna
*/
public class HasPropertyRule<T extends Component> implements Predicate<T> {
public class PropertyEqualToRule<T extends Component> implements Predicate<T> {

private Property property;
private final String propertyName;

private boolean matchEquals;
private final Object value;

/**
* Constructs a new instance with the specified property. Ignores any parameters matching only on the value of the
* property.
* @param property a property instance to check for
*/
public HasPropertyRule(final Property property) {
this(property, false);
public PropertyEqualToRule(final Property property) {
this(property.getName(), property.getValue());
}

/**
* Constructs a new instance with the specified property.
* @param property the property to match
* @param matchEquals if true, matches must contain an identical property (as indicated by
* <code>Property.equals()</code>
*/
public HasPropertyRule(final Property property, final boolean matchEquals) {
this.property = property;
this.matchEquals = matchEquals;
public PropertyEqualToRule(String propertyName, Object value) {
this.propertyName = propertyName;
this.value = value;
}

/**
* {@inheritDoc}
*/
@Override
public final boolean test(final Component component) {
boolean match = false;
final PropertyList<Property> properties = component.getProperties(property.getName());
public final boolean test(final T component) {
final PropertyList<Property> properties = component.getProperties(propertyName);
for (final Property p : properties) {
if (matchEquals && property.equals(p)) {
match = true;
}
else if (property.getValue().equals(p.getValue())) {
match = true;
if (value.equals(p.getValue())) {
return true;
}
}
return match;
return false;
}
}
29 changes: 29 additions & 0 deletions src/main/java/net/fortuna/ical4j/filter/PropertyExistsRule.java
@@ -0,0 +1,29 @@
package net.fortuna.ical4j.filter;

import net.fortuna.ical4j.model.Component;

import java.util.function.Predicate;

/**
* Test for a property matching any values in the provided list. Supports test for missing property types
* (e.g. "DUE", "ORGANIZER", etc.), or missing a property with a specific value (e.g. ROLE=CHAIR, etc.).
*
* @param <T>
*/
public class PropertyExistsRule<T extends Component> implements Predicate<T> {

private final String propertyName;

public PropertyExistsRule(String propertyName) {
this.propertyName = propertyName;
}

@Override
public boolean test(T t) {
String[] prop = propertyName.split(":");
if (prop.length > 1) {
return new PropertyEqualToRule<>(prop[0], prop[1]).test(t);
}
return !t.getProperties(propertyName).isEmpty();
}
}

0 comments on commit 1f6d9d5

Please sign in to comment.