Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Use jparsec for parsing filter expression strings
  • Loading branch information
benfortuna committed Aug 15, 2021
1 parent 0956bef commit e2bca04
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 21 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Expand Up @@ -51,6 +51,9 @@ dependencies {
// optional support for non-Gregorian chronologies..
implementation "org.threeten:threeten-extra:$threetenExtraVersion", optional

// optional groovy DSL for calendar builder..
implementation "org.jparsec:jparsec:$jparsecVersion", optional

// optional groovy DSL for calendar builder..
implementation "org.codehaus.groovy:groovy:$groovyVersion", optional
implementation "org.codehaus.groovy:groovy-dateutil:$groovyVersion", optional
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Expand Up @@ -4,6 +4,7 @@ commonsLangVersion = 3.8.1
commonsCollectionsVersion = 4.1
threetenExtraVersion = 1.5.0
joolVersion = 0.9.14
jparsecVersion = 3.1

groovyVersion = 3.0.7
bndVersion = 5.1.1
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/net/fortuna/ical4j/filter/AbstractFilter.java
Expand Up @@ -190,9 +190,10 @@ protected List<Comparable<Parameter>> parameters(BinaryExpression expression) {
return literal.stream().map(l -> parameter(specification.getName(), l)).collect(Collectors.toList());
}

protected Parameter parameter(String name) {
protected Parameter parameter(FilterSpec.Attribute a) {
try {
return new ParameterBuilder(new DefaultParameterFactorySupplier().get()).name(name).build();
return new ParameterBuilder(new DefaultParameterFactorySupplier().get())
.name(a.getName()).value(a.getValue()).build();
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
Expand Down
109 changes: 94 additions & 15 deletions src/main/java/net/fortuna/ical4j/filter/FilterExpressionParser.java
@@ -1,6 +1,12 @@
package net.fortuna.ical4j.filter;

import net.fortuna.ical4j.filter.FilterExpression.Op;
import net.fortuna.ical4j.filter.expression.BinaryExpression;
import net.fortuna.ical4j.filter.expression.NumberExpression;
import net.fortuna.ical4j.filter.expression.SpecificationExpression;
import net.fortuna.ical4j.filter.expression.StringExpression;
import net.fortuna.ical4j.model.TemporalAmountAdapter;
import org.jparsec.*;

import java.time.*;
import java.time.temporal.Temporal;
Expand All @@ -18,6 +24,65 @@
*/
public class FilterExpressionParser {

private static final String[] OPERATORS = {
">", "<", "=", ">=", "<=", "<>", ".", "(", ")", "[", "]", ":", ","
};

private static final String[] KEYWORDS = {
"by", "order", "asc", "desc",
"and", "or", "not", "in", "exists", "between", "is", "null", "like",
"contains", "matches"
};

private static final String[] FUNCTION_NAMES = {
"now", "startOfDay", "endOfDay", "startOfWeek", "endOfWeek", "startOfMonth", "endOfMonth",
"startOfYear", "endOfYear", "startOfWeek", "endOfWeek", "startOfMonth", "endOfMonth",
};

private static final Terminals TERMS = Terminals.operators(OPERATORS)
.words(Scanners.IDENTIFIER).caseInsensitiveKeywords(Arrays.asList(KEYWORDS))
.keywords(FUNCTION_NAMES).build();

private static final Parser<?> TOKENIZER = Parsers.or(
Terminals.IntegerLiteral.TOKENIZER, Terminals.StringLiteral.SINGLE_QUOTE_TOKENIZER,
TERMS.tokenizer());

static final Parser<String> ATTRIBUTE_PARSER = Parsers.sequence(Terminals.Identifier.PARSER,
term(":"), Terminals.Identifier.PARSER);

static final Parser<List<String>> ATTRIBUTE_LIST_PARSER = ATTRIBUTE_PARSER
.between(term("["), term("]")).sepBy(term(","));

static final Parser<NumberExpression> NUMBER = Terminals.IntegerLiteral.PARSER.map(NumberExpression::new);

static final Parser<StringExpression> STRING = Terminals.StringLiteral.PARSER.map(StringExpression::new);

static final Parser<SpecificationExpression> NAME = Parsers.sequence(
Terminals.Identifier.PARSER, ATTRIBUTE_LIST_PARSER, (name, attr) -> new SpecificationExpression(name))
.or(Terminals.Identifier.PARSER.map(SpecificationExpression::new));
// static final Parser<SpecificationExpression> NAME = Terminals.Identifier.PARSER
// .map(SpecificationExpression::new);

static final Parser<Void> IGNORED = Parsers.or(
Scanners.JAVA_LINE_COMMENT,
Scanners.JAVA_BLOCK_COMMENT,
Scanners.WHITESPACES).skipMany();

static final Parser<List<String>> COLLECTION_PARSER = Terminals.StringLiteral.PARSER
.between(term("("), term(")")).sepBy(term(",")).from(TOKENIZER, IGNORED);

// static final Parser<StringExpression> STRING = Terminals.StringLiteral.SINGLE_QUOTE_TOKENIZER.map(StringExpression::new);

// static final Parser<TemporalExpression> TEMPORAL = Terminals.StringLiteral.PARSER.map(TemporalExpression::new);

static Parser<?> term(String... names) {
return TERMS.token(names);
}

static <T> Parser<T> op(String name, T value) {
return term(name).retn(value);
}

private static final Map<String, Function<String, ?>> FUNCTIONS = new HashMap<>();
static {
FUNCTIONS.put("now", (Function<String, Temporal>) s -> {
Expand Down Expand Up @@ -88,51 +153,65 @@ public class FilterExpressionParser {
}

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

private Object resolveValue(String valueString) {
private <T> T resolveValue(String valueString) {
if (valueString.matches("\\w+\\(.*\\)")
&& FUNCTIONS.containsKey(valueString.replaceAll("\\(.*\\)", ""))) {
return FUNCTIONS.get(valueString.replaceAll("\\(.*\\)", ""))
return (T) FUNCTIONS.get(valueString.replaceAll("\\(.*\\)", ""))
.apply(valueString.split("\\(|\\)")[1]);
} else if (valueString.matches("\\d+")) {
return (T) Integer.valueOf(valueString);
} else {
return valueString;
return (T) valueString;
}
}

public static Parser<FilterExpression> newInstance() {
Parser.Reference<FilterExpression> ref = Parser.newReference();
Parser<FilterExpression> unit = ref.lazy().between(term("("), term(")"))
.or(NUMBER).or(NAME).or(STRING); //.or(TEMPORAL);
Parser<FilterExpression> parser = new OperatorTable<FilterExpression>()
.infixl(op("=", (l, r) -> new BinaryExpression(l, Op.equalTo, r)), 10)
.infixl(op("<>", (l, r) -> new BinaryExpression(l, Op.notEqualTo, r)), 10)
.build(unit);
ref.set(parser);
return parser.from(TOKENIZER, IGNORED);
}
}
48 changes: 44 additions & 4 deletions src/main/java/net/fortuna/ical4j/filter/FilterSpec.java
Expand Up @@ -35,6 +35,7 @@

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class FilterSpec {
Expand All @@ -43,14 +44,14 @@ public class FilterSpec {

private final Optional<String> value;

private final List<String> attributes;
private final List<Attribute> attributes;

public FilterSpec(String spec) {
this(spec, Collections.emptyList());
}

public FilterSpec(String spec, List<String> attributes) {
this.name = spec.split(":")[0];
public FilterSpec(String spec, List<Attribute> attributes) {
this.name = spec.split(":")[0].replace("_", "-");
this.value = Optional.ofNullable(spec.split(":").length > 1 ? spec.split(":")[1] : null);
this.attributes = attributes;
}
Expand All @@ -63,7 +64,46 @@ public Optional<String> getValue() {
return value;
}

public List<String> getAttributes() {
public List<Attribute> getAttributes() {
return attributes;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FilterSpec that = (FilterSpec) o;
return name.equals(that.name) && Objects.equals(value, that.value) && Objects.equals(attributes, that.attributes);
}

@Override
public int hashCode() {
return Objects.hash(name, value, attributes);
}

public static class Attribute {

private String name;

private String value;

public Attribute(String name, String value) {
this.name = name;
this.value = value;
}

public String getName() {
return name;
}

public String getValue() {
return value;
}

public static Attribute parse(String string) {
String name = string.split(":")[0];
String value = string.contains(":") ? string.split(":")[1] : null;
return new Attribute(name, value);
}
}
}
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2004-2021, 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.filter.expression.NumberExpression
import net.fortuna.ical4j.filter.expression.SpecificationExpression
import org.jparsec.Parser
import spock.lang.Specification

class FilterExpressionParserTest extends Specification {

def 'test expression parsing'() {
given: 'an expression parser instance'
Parser parser = FilterExpressionParser.newInstance()

expect: 'a parsed result'
parser.parse(expression) == expectedResult

where:
expression | expectedResult
'1' | new NumberExpression('1')
'due' | new SpecificationExpression('due')
"due = 12" | FilterExpression.equalTo('due', 12)
"related_to = '1234-1234-1234'" | FilterExpression.equalTo("related_to", '1234-1234-1234')
"related_to[rel_type:SIBLING] = '1234-1234-1234'" | FilterExpression.equalTo("related_to", '1234-1234-1234')
"attendee[role:CHAIR] = '1234-1234-1234'" | FilterExpression.equalTo("attendee", '1234-1234-1234')
}
}

0 comments on commit e2bca04

Please sign in to comment.