├── .gitignore ├── .editorconfig ├── .maven-bintray.xml ├── .travis.yml ├── script └── travis-deploy ├── CHANGELOG.adoc ├── src ├── main │ ├── java │ │ └── cz │ │ │ └── jirutka │ │ │ └── rsql │ │ │ └── parser │ │ │ ├── RSQLParserException.java │ │ │ ├── ast │ │ │ ├── LogicalOperator.java │ │ │ ├── RSQLVisitor.java │ │ │ ├── AbstractNode.java │ │ │ ├── StringUtils.java │ │ │ ├── OrNode.java │ │ │ ├── AndNode.java │ │ │ ├── NoArgRSQLVisitorAdapter.java │ │ │ ├── RSQLOperators.java │ │ │ ├── Assert.java │ │ │ ├── Node.java │ │ │ ├── LogicalNode.java │ │ │ ├── NodesFactory.java │ │ │ ├── ComparisonOperator.java │ │ │ └── ComparisonNode.java │ │ │ ├── UnknownOperatorException.java │ │ │ └── RSQLParser.java │ └── javacc │ │ └── RSQLParser.jj └── test │ └── groovy │ └── cz │ └── jirutka │ └── rsql │ └── parser │ ├── ast │ ├── ComparisonOperatorTest.groovy │ ├── NoArgRSQLVisitorAdapterTest.groovy │ ├── AssertTest.groovy │ ├── LogicalNodeTest.groovy │ ├── ComparisonNodeTest.groovy │ ├── NodesFactoryTest.groovy │ └── NodesTest.groovy │ └── RSQLParserTest.groovy ├── pom.xml └── README.adoc /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.yml] 13 | indent_size = 2 14 | 15 | [script/*] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.maven-bintray.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | jfrog-oss-snapshot-local 9 | jirutka 10 | ${env.BINTRAY_API_KEY} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | jdk: 4 | - openjdk7 5 | - oraclejdk8 6 | env: 7 | global: 8 | - secure: "arKo+t0dCh3i3AaF6Lq2OXez90jz5zLjkSj//66VyvHEDuWbnvqQRubGH8JQLl3wjN/PzIlypWRUMApseOGp/mi0Twwlk6NjW13Tu/5fwmwdzQO1RhCPuJSBuGECrLDAWg1hRYxWtCpfbPCYX8TYrrbHtC5hu4FAYK/59HArmFo=" # BINTRAY_API_KEY 9 | 10 | # Cache local Maven repository. 11 | cache: 12 | directories: 13 | - $HOME/.m2 14 | before_cache: 15 | - rm -Rf $HOME/.m2/repository/cz/jirutka/rsql/rsql-parser 16 | 17 | after_success: 18 | - mvn jacoco:report coveralls:report 19 | - script/travis-deploy 20 | -------------------------------------------------------------------------------- /script/travis-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ts=4: 3 | set -o errexit 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [[ "$TRAVIS_PULL_REQUEST" != 'false' ]]; then 8 | echo 'This is a pull request, skipping deploy.'; exit 0 9 | fi 10 | 11 | if [[ -z "$BINTRAY_API_KEY" ]]; then 12 | echo '$BINTRAY_API_KEY is not set, skipping deploy.'; exit 0 13 | fi 14 | 15 | if [[ "$TRAVIS_BRANCH" != 'master' ]]; then 16 | echo 'This is not the master branch, skipping deploy.'; exit 0 17 | fi 18 | 19 | if [[ "${TRAVIS_BUILD_NUMBER}.2" != "$TRAVIS_JOB_NUMBER" ]]; then 20 | echo 'This is not the build job we are looking for, skipping deploy.'; exit 0 21 | fi 22 | 23 | echo '==> Deploying artifact to JFrog OSS Maven repository' 24 | mvn deploy --settings .maven-bintray.xml -Dgpg.skip=true -DskipTests=true 25 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | :repo-uri: https://github.com/jirutka/rsql-parser 3 | :issue-uri: {repo-uri}/issues 4 | 5 | == 2.1.0 (2014-02-07) 6 | 7 | * Added `\` (backslash) as an escape character inside a quoted argument (requested in {issue-uri}/#7[#7]). 8 | 9 | == 2.0.0 (2014-10-23) 10 | 11 | * Simplified AST; node per operator replaced with just ComparisonNode. 12 | * Simplified support for custom operators; the RSQLNodesFactory is no longer needed. 13 | * The RSQLParserException changed to inherit from the RuntimeException. 14 | * Added withSelector/withArguments/withChildren methods to the AST nodes. 15 | * Tests improved. 16 | 17 | == 2.0.M1 (2014-03-20) 18 | 19 | * JavaCC grammar, all the code and tests completely rewritten. 20 | * Proper AST with enhanced Visitor pattern implemented. 21 | * Introduced support for enhancing parser with custom FIQL-like operators. 22 | 23 | * Added `=in=` and `=out=` operators, arguments group. 24 | * FIQL operators syntax relaxed; any `=[a-z]*=` is parsed as an operator and validation is done in RSQLNodesFactory. 25 | This allows to add custom operators. 26 | * Selector syntax relaxed, it can contain any non-reserved characters now. 27 | * Dropped an alternative notation for equal operator; only pair equal characters (`==`) are allowed! 28 | 29 | == 1.0.2 (2014-01-12) 30 | 31 | * Setup testing on Travis. 32 | * Released in Maven Central. 33 | 34 | == 1.0.1 (2013-03-25) 35 | 36 | * Moved to CVUT Maven repository. 37 | * Relicensed under MIT License. 38 | 39 | == 1.0 (2011-08-29) 40 | 41 | The first public release. 42 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/RSQLParserException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser; 25 | 26 | /** 27 | * A top level exception of RSQL parser that wraps all exceptions occurred in parsing. 28 | */ 29 | public class RSQLParserException extends RuntimeException { 30 | 31 | public RSQLParserException(Throwable cause) { 32 | super(cause); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/LogicalOperator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | public enum LogicalOperator { 27 | 28 | AND (";"), 29 | OR (","); 30 | 31 | private final String symbol; 32 | 33 | private LogicalOperator(String symbol) { 34 | this.symbol = symbol; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return symbol; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/RSQLVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | /** 27 | * An interface for visiting AST nodes of the RSQL. 28 | * 29 | * @param Return type of the visitor's method. 30 | * @param Type of an optional parameter passed to the visitor's method. 31 | */ 32 | public interface RSQLVisitor { 33 | 34 | R visit(AndNode node, A param); 35 | 36 | R visit(OrNode node, A param); 37 | 38 | R visit(ComparisonNode node, A param); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/AbstractNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | public abstract class AbstractNode implements Node { 27 | 28 | /** 29 | * Accepts the visitor, calls its visit() method and returns the result. 30 | * This method just calls {@link #accept(RSQLVisitor, Object)} with 31 | * null as the second argument. 32 | */ 33 | public R accept(RSQLVisitor visitor) { 34 | return accept(visitor, null); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/StringUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import java.util.List; 27 | 28 | abstract class StringUtils { 29 | 30 | public static String join(List list, String glue) { 31 | 32 | StringBuilder line = new StringBuilder(); 33 | for (Object s : list) { 34 | line.append(s).append(glue); 35 | } 36 | return list.isEmpty() ? "" : line.substring(0, line.length() - glue.length()); 37 | } 38 | 39 | public static boolean isBlank(String str) { 40 | return str == null || str.trim().isEmpty(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/OrNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import net.jcip.annotations.Immutable; 27 | 28 | import java.util.List; 29 | 30 | @Immutable 31 | public final class OrNode extends LogicalNode { 32 | 33 | public OrNode(List children) { 34 | super(LogicalOperator.OR, children); 35 | } 36 | 37 | public OrNode withChildren(List children) { 38 | return new OrNode(children); 39 | } 40 | 41 | public R accept(RSQLVisitor visitor, A param) { 42 | return visitor.visit(this, param); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/AndNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import net.jcip.annotations.Immutable; 27 | 28 | import java.util.List; 29 | 30 | @Immutable 31 | public final class AndNode extends LogicalNode { 32 | 33 | public AndNode(List children) { 34 | super(LogicalOperator.AND, children); 35 | } 36 | 37 | public AndNode withChildren(List children) { 38 | return new AndNode(children); 39 | } 40 | 41 | public R accept(RSQLVisitor visitor, A param) { 42 | return visitor.visit(this, param); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/UnknownOperatorException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser; 25 | 26 | /** 27 | * This exception is thrown when unknown/unsupported comparison operator is parsed. 28 | */ 29 | public class UnknownOperatorException extends ParseException { 30 | 31 | private final String operator; 32 | 33 | 34 | public UnknownOperatorException(String operator) { 35 | this(operator, "Unknown operator: " + operator); 36 | } 37 | 38 | public UnknownOperatorException(String operator, String message) { 39 | super(message); 40 | this.operator = operator; 41 | } 42 | 43 | 44 | public String getOperator() { 45 | return operator; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/NoArgRSQLVisitorAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | /** 27 | * An adapter for the {@link RSQLVisitor} interface with a simpler contract that omits the optional 28 | * second argument. 29 | * 30 | * @param Return type of the visitor's method. 31 | */ 32 | public abstract class NoArgRSQLVisitorAdapter implements RSQLVisitor { 33 | 34 | public abstract R visit(AndNode node); 35 | 36 | public abstract R visit(OrNode node); 37 | 38 | public abstract R visit(ComparisonNode node); 39 | 40 | 41 | public R visit(AndNode node, Void param) { 42 | return visit(node); 43 | } 44 | 45 | public R visit(OrNode node, Void param) { 46 | return visit(node); 47 | } 48 | 49 | public R visit(ComparisonNode node, Void param) { 50 | return visit(node); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/ast/ComparisonOperatorTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast 25 | 26 | import spock.lang.Specification 27 | 28 | class ComparisonOperatorTest extends Specification { 29 | 30 | def 'construct with valid symbol'() { 31 | expect: 32 | new ComparisonOperator(sym) 33 | where: 34 | sym << ['=foo=', '==', '!=', '<', '>', '<=', '>='] 35 | } 36 | 37 | def 'throw IllegalArgumentException when given invalid symbol'() { 38 | when: 39 | new ComparisonOperator(sym) 40 | then: 41 | thrown IllegalArgumentException 42 | where: 43 | sym << [null, '', 'foo', '=123=', '=', '=<', '=>', '=!', 'a=b=c'] 44 | } 45 | 46 | def 'equals when contains same symbols'() { 47 | expect: 48 | new ComparisonOperator('=out=', '=notin=') == new ComparisonOperator('=out=', '=notin=', true) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/ast/NoArgRSQLVisitorAdapterTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast 25 | 26 | import spock.lang.Specification 27 | import spock.lang.Unroll 28 | 29 | @Unroll 30 | class NoArgRSQLVisitorAdapterTest extends Specification { 31 | 32 | def 'delegate visit(#className, Void) to visit(#className)'() { 33 | setup: 34 | def node = LogicalNode.isAssignableFrom(nodeClass) ? 35 | nodeClass.newInstance([]) : 36 | nodeClass.newInstance(RSQLOperators.EQUAL, 'sel', ['arg']) 37 | and: 38 | def adapter = Spy(NoArgRSQLVisitorAdapter) { 39 | visit(_) >> null 40 | } 41 | when: 42 | adapter.visit(node, null) 43 | then: 44 | 1 * adapter.visit({ nodeClass.isInstance(it) }) >> null 45 | where: 46 | nodeClass << [AndNode, OrNode, ComparisonNode] 47 | className = nodeClass.simpleName 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/ast/AssertTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast 25 | 26 | import spock.lang.Specification 27 | 28 | class AssertTest extends Specification { 29 | 30 | def 'isTrue'() { 31 | when: 32 | Assert.isTrue(false, 'msg') 33 | then: 34 | thrown IllegalArgumentException 35 | } 36 | 37 | def 'notNull'() { 38 | when: 39 | Assert.notNull(null, 'msg') 40 | then: 41 | thrown IllegalArgumentException 42 | } 43 | 44 | def 'notEmpty'() { 45 | when: 46 | Assert.notEmpty(value, 'msg') 47 | then: 48 | thrown IllegalArgumentException 49 | where: 50 | value << [null, [] as List, [] as Object[]] 51 | } 52 | 53 | def 'notBlank'() { 54 | when: 55 | Assert.notBlank(value, 'msg') 56 | then: 57 | thrown IllegalArgumentException 58 | where: 59 | value << [null, '', ' '] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/RSQLOperators.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import java.util.HashSet; 27 | import java.util.Set; 28 | 29 | import static java.util.Arrays.asList; 30 | 31 | public abstract class RSQLOperators { 32 | 33 | public static final ComparisonOperator 34 | EQUAL = new ComparisonOperator("=="), 35 | NOT_EQUAL = new ComparisonOperator("!="), 36 | GREATER_THAN = new ComparisonOperator("=gt=", ">"), 37 | GREATER_THAN_OR_EQUAL = new ComparisonOperator("=ge=", ">="), 38 | LESS_THAN = new ComparisonOperator("=lt=", "<"), 39 | LESS_THAN_OR_EQUAL = new ComparisonOperator("=le=", "<="), 40 | IN = new ComparisonOperator("=in=", true), 41 | NOT_IN = new ComparisonOperator("=out=", true); 42 | 43 | 44 | public static Set defaultOperators() { 45 | return new HashSet<>(asList(EQUAL, NOT_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, 46 | LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/Assert.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import java.util.Collection; 27 | 28 | abstract class Assert { 29 | 30 | public static void isTrue(boolean expression, String message, Object... args) { 31 | if (!expression) { 32 | throw new IllegalArgumentException(String.format(message, args)); 33 | } 34 | } 35 | 36 | public static void notNull(Object obj, String message, Object... args) { 37 | if (obj == null) { 38 | throw new IllegalArgumentException(String.format(message, args)); 39 | } 40 | } 41 | 42 | public static void notEmpty(Collection col, String message, Object... args) { 43 | if (col == null || col.isEmpty()) { 44 | throw new IllegalArgumentException(String.format(message, args)); 45 | } 46 | } 47 | 48 | public static void notEmpty(Object[] ary, String message, Object... args) { 49 | if (ary == null || ary.length == 0) { 50 | throw new IllegalArgumentException(String.format(message, args)); 51 | } 52 | } 53 | 54 | public static void notBlank(String str, String message, Object... args) { 55 | if (StringUtils.isBlank(str)) { 56 | throw new IllegalArgumentException(String.format(message, args)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/Node.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | /** 27 | * Common interface of the AST nodes. Implementations must be immutable. 28 | */ 29 | public interface Node { 30 | 31 | /** 32 | * Accepts the visitor, calls its visit() method and returns a result. 33 | * 34 | *

Each implementation must implement this methods exactly as listed: 35 | *

{@code
36 |      * public  R accept(RSQLVisitor visitor, A param) {
37 |      *     return visitor.visit(this, param);
38 |      * }
39 |      * }
40 | * 41 | * @param visitor The visitor whose appropriate method will be called. 42 | * @param param An optional parameter to pass to the visitor. 43 | * @param Return type of the visitor's method. 44 | * @param
Type of an optional parameter passed to the visitor's method. 45 | * @return An object returned by the visitor (may be null). 46 | */ 47 | R accept(RSQLVisitor visitor, A param); 48 | 49 | /** 50 | * Accepts the visitor, calls its visit() method and returns the result. 51 | * 52 | * This method should just call {@link #accept(RSQLVisitor, Object)} with 53 | * null as the second argument. 54 | */ 55 | R accept(RSQLVisitor visitor); 56 | } 57 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/ast/LogicalNodeTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast 25 | 26 | import spock.lang.Specification 27 | import spock.lang.Unroll 28 | 29 | import static cz.jirutka.rsql.parser.ast.RSQLOperators.EQUAL 30 | 31 | @Unroll 32 | abstract class LogicalNodeTest extends Specification { 33 | 34 | abstract LogicalNode newNode(List children) 35 | 36 | 37 | def 'should be immutable'() { 38 | given: 39 | def child1 = new ComparisonNode(EQUAL, 'foo', ['bar']) 40 | def child2 = new AndNode([]) 41 | def children = [child1, child2] 42 | def node = newNode(children) 43 | 44 | when: 45 | def it = node.iterator() 46 | it.next() 47 | it.remove() 48 | then: 49 | thrown UnsupportedOperationException 50 | 51 | expect: "withChildren() returns copy and doesn't change original node" 52 | node.withChildren([child1]) == newNode([child1]) 53 | node == newNode(children) 54 | 55 | when: 'modify original list of children given to node' 56 | children << new OrNode([child1]) 57 | then: "node's children remains unchanged" 58 | node.children == [child1, child2] 59 | } 60 | } 61 | 62 | class AndNodeTest extends LogicalNodeTest { 63 | LogicalNode newNode(List children) { 64 | new AndNode(children) 65 | } 66 | } 67 | 68 | class OrNodeTest extends LogicalNodeTest { 69 | LogicalNode newNode(List children) { 70 | new OrNode(children) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/ast/ComparisonNodeTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast 25 | 26 | import spock.lang.Specification 27 | 28 | import static cz.jirutka.rsql.parser.ast.RSQLOperators.* 29 | 30 | class ComparisonNodeTest extends Specification { 31 | 32 | def 'throw exception when given multiple arguments for single-argument operator'() { 33 | when: 34 | new ComparisonNode(operator, 'sel', ['arg1', 'arg2']) 35 | then: 36 | thrown IllegalArgumentException 37 | where: 38 | operator << defaultOperators().findAll{ !it.multiValue } 39 | } 40 | 41 | def 'should be immutable'() { 42 | given: 43 | def args = ['thriller', 'sci-fi'] 44 | def node = new ComparisonNode(IN, 'genres', args) 45 | 46 | when: "modify list of node's arguments" 47 | node.getArguments() << 'horror' 48 | then: "node's arguments remain unchanged" 49 | node.getArguments() == args 50 | 51 | expect: "withX returns copy and doesn't change original node" 52 | node.withOperator(NOT_IN) == new ComparisonNode(NOT_IN, 'genres', args) 53 | node.withSelector('foo') == new ComparisonNode(IN, 'foo', args) 54 | node.withArguments(['foo']) == new ComparisonNode(IN, 'genres', ['foo']) 55 | node == new ComparisonNode(IN, 'genres', args) 56 | 57 | when: 'modify original list of arguments given to node' 58 | args << 'horror' 59 | then: "node's arguments remains unchanged" 60 | node.getArguments() == ['thriller', 'sci-fi'] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/ast/NodesFactoryTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast 25 | 26 | import cz.jirutka.rsql.parser.UnknownOperatorException 27 | import spock.lang.Specification 28 | import spock.lang.Unroll 29 | 30 | import static cz.jirutka.rsql.parser.ast.LogicalOperator.AND 31 | import static cz.jirutka.rsql.parser.ast.LogicalOperator.OR 32 | import static cz.jirutka.rsql.parser.ast.RSQLOperators.EQUAL 33 | import static cz.jirutka.rsql.parser.ast.RSQLOperators.GREATER_THAN 34 | 35 | class NodesFactoryTest extends Specification { 36 | 37 | def factory = new NodesFactory([EQUAL, GREATER_THAN] as Set) 38 | 39 | 40 | @Unroll 41 | def 'create #className for logical operator: #operator'() { 42 | when: 43 | def actual = factory.createLogicalNode(operator, []) 44 | then: 45 | actual.class == expected 46 | where: 47 | operator | expected 48 | AND | AndNode 49 | OR | OrNode 50 | 51 | className = expected.simpleName 52 | } 53 | 54 | def 'create ComparisonNode when given supported operator token'() { 55 | when: 56 | def node = factory.createComparisonNode(opToken, 'doctor', ['who?']) 57 | then: 58 | node.operator == expected 59 | node.selector == 'doctor' 60 | node.arguments == ['who?'] 61 | where: 62 | opToken | expected 63 | '==' | EQUAL 64 | '=gt=' | GREATER_THAN 65 | '>' | GREATER_THAN 66 | } 67 | 68 | def 'throw UnknownOperatorException when given unsupported operator token'() { 69 | when: 70 | factory.createComparisonNode('=lt=', 'sel', ['arg']) 71 | then: 72 | thrown UnknownOperatorException 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/ast/NodesTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast 25 | 26 | import cz.jirutka.rsql.parser.RSQLParser 27 | import spock.lang.Specification 28 | 29 | import static cz.jirutka.rsql.parser.ast.RSQLOperators.EQUAL 30 | 31 | class NodesTest extends Specification { 32 | 33 | def 'nodes should define toString method'() { 34 | setup: 35 | def rootNode = new RSQLParser().parse(input) 36 | expect: 37 | rootNode.toString() == output 38 | where: 39 | input | output 40 | 'genres=in=(sci-fi,action)' | "genres=in=('sci-fi','action')" 41 | 'name=="Kill Bill";year=gt=2003' | "(name=='Kill Bill';year=gt='2003')" 42 | 'a<=1;b!=2;c>3' | "(a=le='1';b!='2';c=gt='3')" 43 | 'a=gt=1,b==2;c!=3,d=lt=4' | "(a=gt='1',(b=='2';c!='3'),d=lt='4')" 44 | } 45 | 46 | def 'nodes should accept visitor'() { 47 | setup: 48 | def visitor = Mock(RSQLVisitor) 49 | def param = Stub(Object) 50 | def expected = Stub(Object) 51 | 52 | when: 'accept method with parameter' 53 | def result = node.accept(visitor, param) 54 | then: 55 | 1 * visitor.visit({ it.class == node.class }, param) >> expected 56 | 0 * visitor._ 57 | and: 58 | result == expected 59 | 60 | when: 'accept method without parameter' 61 | result = node.accept(visitor) 62 | then: 63 | 1 * visitor.visit({ it.class == node.class }, null) >> expected 64 | 0 * visitor._ 65 | and: 66 | result == expected 67 | where: 68 | node << [new AndNode([]), new OrNode([]), new ComparisonNode(EQUAL, 'x', ['y'])] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/LogicalNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Iterator; 28 | import java.util.List; 29 | 30 | import static cz.jirutka.rsql.parser.ast.StringUtils.join; 31 | import static java.util.Collections.unmodifiableList; 32 | 33 | /** 34 | * Superclass of all logical nodes that represents a logical operation that connects 35 | * children nodes. 36 | */ 37 | public abstract class LogicalNode extends AbstractNode implements Iterable { 38 | 39 | private final List children; 40 | 41 | private final LogicalOperator operator; 42 | 43 | 44 | /** 45 | * @param operator Must not be null. 46 | * @param children Children nodes, i.e. operands; must not be null. 47 | */ 48 | protected LogicalNode(LogicalOperator operator, List children) { 49 | assert operator != null : "operator must not be null"; 50 | assert children != null : "children must not be null"; 51 | 52 | this.operator = operator; 53 | this.children = unmodifiableList(new ArrayList<>(children)); 54 | } 55 | 56 | 57 | /** 58 | * Returns a copy of this node with the specified children nodes. 59 | */ 60 | public abstract LogicalNode withChildren(List children); 61 | 62 | 63 | /** 64 | * Iterate over children nodes. The underlying collection is unmodifiable! 65 | */ 66 | public Iterator iterator() { 67 | return children.iterator(); 68 | } 69 | 70 | public LogicalOperator getOperator() { 71 | return operator; 72 | } 73 | 74 | /** 75 | * Returns a copy of the children nodes. 76 | */ 77 | public List getChildren() { 78 | return new ArrayList<>(children); 79 | } 80 | 81 | 82 | @Override 83 | public String toString() { 84 | return "(" + join(children, operator.toString()) + ")"; 85 | } 86 | 87 | @Override 88 | public boolean equals(Object o) { 89 | if (this == o) return true; 90 | if (!(o instanceof LogicalNode)) return false; 91 | LogicalNode nodes = (LogicalNode) o; 92 | 93 | return children.equals(nodes.children) 94 | && operator == nodes.operator; 95 | } 96 | 97 | @Override 98 | public int hashCode() { 99 | int result = children.hashCode(); 100 | result = 31 * result + operator.hashCode(); 101 | return result; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/NodesFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import cz.jirutka.rsql.parser.UnknownOperatorException; 27 | import net.jcip.annotations.Immutable; 28 | 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.Set; 33 | 34 | /** 35 | * Factory that creates {@link Node} instances for the parser. 36 | */ 37 | @Immutable 38 | public class NodesFactory { 39 | 40 | private final Map comparisonOperators; 41 | 42 | 43 | public NodesFactory(Set operators) { 44 | 45 | comparisonOperators = new HashMap<>(operators.size()); 46 | for (ComparisonOperator op : operators) { 47 | for (String sym : op.getSymbols()) { 48 | comparisonOperators.put(sym, op); 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Creates a specific {@link LogicalNode} instance for the specified operator and with the 55 | * given children nodes. 56 | * 57 | * @param operator The logical operator to create a node for. 58 | * @param children Children nodes, i.e. operands. 59 | * @return A subclass of the {@link LogicalNode} according to the specified operator. 60 | */ 61 | public LogicalNode createLogicalNode(LogicalOperator operator, List children) { 62 | switch (operator) { 63 | case AND : return new AndNode(children); 64 | case OR : return new OrNode(children); 65 | 66 | // this normally can't happen 67 | default : throw new IllegalStateException("Unknown operator: " + operator); 68 | } 69 | } 70 | 71 | /** 72 | * Creates a {@link ComparisonNode} instance with the given parameters. 73 | * 74 | * @param operatorToken A textual representation of the comparison operator to be found in the 75 | * set of supported {@linkplain ComparisonOperator operators}. 76 | * @param selector The selector that specifies the left side of the comparison. 77 | * @param arguments A list of arguments that specifies the right side of the comparison. 78 | * 79 | * @throws UnknownOperatorException If no operator for the specified operator token exists. 80 | */ 81 | public ComparisonNode createComparisonNode( 82 | String operatorToken, String selector, List arguments) throws UnknownOperatorException { 83 | 84 | ComparisonOperator op = comparisonOperators.get(operatorToken); 85 | if (op != null) { 86 | return new ComparisonNode(op, selector, arguments); 87 | } else { 88 | throw new UnknownOperatorException(operatorToken); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/ComparisonOperator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import net.jcip.annotations.Immutable; 27 | 28 | import java.util.regex.Pattern; 29 | 30 | import static cz.jirutka.rsql.parser.ast.StringUtils.isBlank; 31 | 32 | @Immutable 33 | public final class ComparisonOperator { 34 | 35 | private static final Pattern SYMBOL_PATTERN = Pattern.compile("=[a-zA-Z]*=|[><]=?|!="); 36 | 37 | private final String[] symbols; 38 | 39 | private final boolean multiValue; 40 | 41 | 42 | /** 43 | * @param symbols Textual representation of this operator (e.g. =gt=); the first item 44 | * is primary representation, any others are alternatives. Must match 45 | * =[a-zA-Z]*=|[><]=?|!=. 46 | * @param multiValue Whether this operator may be used with multiple arguments. This is then 47 | * validated in {@link NodesFactory}. 48 | * 49 | * @throws IllegalArgumentException If the {@code symbols} is either null, empty, 50 | * or contain illegal symbols. 51 | */ 52 | public ComparisonOperator(String[] symbols, boolean multiValue) { 53 | Assert.notEmpty(symbols, "symbols must not be null or empty"); 54 | for (String sym : symbols) { 55 | Assert.isTrue(isValidOperatorSymbol(sym), "symbol must match: %s", SYMBOL_PATTERN); 56 | } 57 | this.multiValue = multiValue; 58 | this.symbols = symbols.clone(); 59 | } 60 | 61 | /** 62 | * @see #ComparisonOperator(String[], boolean) 63 | */ 64 | public ComparisonOperator(String symbol, boolean multiValue) { 65 | this(new String[]{symbol}, multiValue); 66 | } 67 | 68 | /** 69 | * @see #ComparisonOperator(String[], boolean) 70 | */ 71 | public ComparisonOperator(String symbol, String altSymbol, boolean multiValue) { 72 | this(new String[]{symbol, altSymbol}, multiValue); 73 | } 74 | 75 | /** 76 | * @see #ComparisonOperator(String[], boolean) 77 | */ 78 | public ComparisonOperator(String... symbols) { 79 | this(symbols, false); 80 | } 81 | 82 | 83 | /** 84 | * Returns the primary representation of this operator. 85 | */ 86 | public String getSymbol() { 87 | return symbols[0]; 88 | } 89 | 90 | /** 91 | * Returns all representations of this operator. The first item is always the primary 92 | * representation. 93 | */ 94 | public String[] getSymbols() { 95 | return symbols.clone(); 96 | } 97 | 98 | /** 99 | * Whether this operator may be used with multiple arguments. 100 | */ 101 | public boolean isMultiValue() { 102 | return multiValue; 103 | } 104 | 105 | 106 | /** 107 | * Whether the given string can represent an operator. 108 | * Note: Allowed symbols are limited by the RSQL syntax (i.e. parser). 109 | */ 110 | private boolean isValidOperatorSymbol(String str) { 111 | return !isBlank(str) && SYMBOL_PATTERN.matcher(str).matches(); 112 | } 113 | 114 | 115 | @Override 116 | public String toString() { 117 | return getSymbol(); 118 | } 119 | 120 | @Override 121 | public boolean equals(Object o) { 122 | if (this == o) return true; 123 | if (!(o instanceof ComparisonOperator)) return false; 124 | 125 | ComparisonOperator that = (ComparisonOperator) o; 126 | return getSymbol().equals(that.getSymbol()); 127 | } 128 | 129 | @Override 130 | public int hashCode() { 131 | return getSymbol().hashCode(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 4.0.0 6 | 7 | 8 | cz.jirutka.maven 9 | groovy-parent 10 | 1.2.0 11 | 12 | 13 | 14 | 15 | 16 | cz.jirutka.rsql 17 | rsql-parser 18 | 2.1.1-SNAPSHOT 19 | jar 20 | 21 | RSQL-parser 22 | Parser of RSQL / FIQL (query language for RESTful APIs) written in JavaCC. 23 | http://github.com/jirutka/rsql-parser 24 | 2011 25 | 26 | 27 | 28 | Jakub Jirutka 29 | jakub@jirutka.cz 30 | CTU in Prague 31 | http://www.cvut.cz 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | MIT 41 | http://opensource.org/licenses/MIT 42 | 43 | 44 | 45 | 46 | http://github.com/jirutka/rsql-parser 47 | scm:git:git@github.com:jirutka/rsql-parser.git 48 | 49 | 50 | 51 | travis 52 | https://travis-ci.org/jirutka/rsql-parser 53 | 54 | 55 | 56 | github 57 | https://github.com/jirutka/rsql-parser/issues 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | net.jcip 67 | jcip-annotations 68 | 1.0 69 | provided 70 | 71 | 72 | 73 | org.spockframework 74 | spock-core 75 | test 76 | 77 | 78 | 79 | cglib 80 | cglib-nodep 81 | 3.1 82 | test 83 | 84 | 85 | 86 | org.codehaus.groovy 87 | groovy 88 | test 89 | 90 | 91 | 92 | 93 | org.slf4j 94 | slf4j-api 95 | ${slf4j.version} 96 | provided 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.codehaus.mojo 108 | javacc-maven-plugin 109 | 2.6 110 | 111 | 112 | javacc 113 | 114 | javacc 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 125 | 126 | org.jacoco 127 | jacoco-maven-plugin 128 | 129 | 130 | **/Parse*.* 131 | **/SimpleCharStream.* 132 | **/Token.* 133 | **/TokenMgrError.* 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/RSQLParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2016 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser; 25 | 26 | import cz.jirutka.rsql.parser.ast.ComparisonOperator; 27 | import cz.jirutka.rsql.parser.ast.Node; 28 | import cz.jirutka.rsql.parser.ast.NodesFactory; 29 | import cz.jirutka.rsql.parser.ast.RSQLOperators; 30 | import net.jcip.annotations.Immutable; 31 | 32 | import java.io.ByteArrayInputStream; 33 | import java.io.InputStream; 34 | import java.nio.charset.Charset; 35 | import java.util.Set; 36 | 37 | /** 38 | * Parser of the RSQL (RESTful Service Query Language). 39 | * 40 | *

RSQL is a query language for parametrized filtering of entries in RESTful APIs. It's a 41 | * superset of the FIQL 42 | * (Feed Item Query Language), so it can be used for parsing FIQL as well.

43 | * 44 | *

Grammar in EBNF notation: 45 | *

{@code
 46 |  * input          = or, EOF;
 47 |  * or             = and, { ( "," | " or " ) , and };
 48 |  * and            = constraint, { ( ";" | " and " ), constraint };
 49 |  * constraint     = ( group | comparison );
 50 |  * group          = "(", or, ")";
 51 |  *
 52 |  * comparison     = selector, comparator, arguments;
 53 |  * selector       = unreserved-str;
 54 |  *
 55 |  * comparator     = comp-fiql | comp-alt;
 56 |  * comp-fiql      = ( ( "=", { ALPHA } ) | "!" ), "=";
 57 |  * comp-alt       = ( ">" | "<" ), [ "=" ];
 58 |  *
 59 |  * arguments      = ( "(", value, { "," , value }, ")" ) | value;
 60 |  * value          = unreserved-str | double-quoted | single-quoted;
 61 |  *
 62 |  * unreserved-str = unreserved, { unreserved }
 63 |  * single-quoted  = "'", { ( escaped | all-chars - ( "'" | "\" ) ) }, "'";
 64 |  * double-quoted  = '"', { ( escaped | all-chars - ( '"' | "\" ) ) }, '"';
 65 |  *
 66 |  * reserved       = '"' | "'" | "(" | ")" | ";" | "," | "=" | "!" | "~" | "<" | ">" | " ";
 67 |  * unreserved     = all-chars - reserved;
 68 |  * escaped        = "\", all-chars;
 69 |  * all-chars      = ? all unicode characters ?;
 70 |  * }
71 | * 72 | * @version 2.1 73 | */ 74 | @Immutable 75 | public final class RSQLParser { 76 | 77 | private static final Charset ENCODING = Charset.forName("UTF-8"); 78 | 79 | private final NodesFactory nodesFactory; 80 | 81 | 82 | /** 83 | * Creates a new instance of {@code RSQLParser} with the default set of comparison operators. 84 | */ 85 | public RSQLParser() { 86 | this.nodesFactory = new NodesFactory(RSQLOperators.defaultOperators()); 87 | } 88 | 89 | /** 90 | * Creates a new instance of {@code RSQLParser} that supports only the specified comparison 91 | * operators. 92 | * 93 | * @param operators A set of supported comparison operators. Must not be null or empty. 94 | */ 95 | public RSQLParser(Set operators) { 96 | if (operators == null || operators.isEmpty()) { 97 | throw new IllegalArgumentException("operators must not be null or empty"); 98 | } 99 | this.nodesFactory = new NodesFactory(operators); 100 | } 101 | 102 | /** 103 | * Parses the RSQL expression and returns AST. 104 | * 105 | * @param query The query expression to parse. 106 | * @return A root of the parsed AST. 107 | * 108 | * @throws RSQLParserException If some exception occurred during parsing, i.e. the 109 | * {@code query} is syntactically invalid. 110 | * @throws IllegalArgumentException If the {@code query} is null. 111 | */ 112 | public Node parse(String query) throws RSQLParserException { 113 | if (query == null) { 114 | throw new IllegalArgumentException("query must not be null"); 115 | } 116 | InputStream is = new ByteArrayInputStream(query.getBytes(ENCODING)); 117 | Parser parser = new Parser(is, ENCODING.name(), nodesFactory); 118 | 119 | try { 120 | return parser.Input(); 121 | 122 | } catch (Exception | TokenMgrError ex) { 123 | throw new RSQLParserException(ex); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/cz/jirutka/rsql/parser/ast/ComparisonNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser.ast; 25 | 26 | import net.jcip.annotations.Immutable; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | import static cz.jirutka.rsql.parser.ast.StringUtils.join; 32 | 33 | /** 34 | * This node represents a comparison with operator, selector and arguments, 35 | * e.g. name=in=(Jimmy,James). 36 | */ 37 | @Immutable 38 | public final class ComparisonNode extends AbstractNode { 39 | 40 | private final ComparisonOperator operator; 41 | 42 | private final String selector; 43 | 44 | private final List arguments; 45 | 46 | 47 | /** 48 | * @param operator Must not be null. 49 | * @param selector Must not be null or blank. 50 | * @param arguments Must not be null or empty. If the operator is not 51 | * {@link ComparisonOperator#isMultiValue() multiValue}, then it must contain exactly 52 | * one argument. 53 | * 54 | * @throws IllegalArgumentException If one of the conditions specified above it not met. 55 | */ 56 | public ComparisonNode(ComparisonOperator operator, String selector, List arguments) { 57 | Assert.notNull(operator, "operator must not be null"); 58 | Assert.notBlank(selector, "selector must not be blank"); 59 | Assert.notEmpty(arguments, "arguments list must not be empty"); 60 | Assert.isTrue(operator.isMultiValue() || arguments.size() == 1, 61 | "operator %s expects single argument, but multiple values given", operator); 62 | 63 | this.operator = operator; 64 | this.selector = selector; 65 | this.arguments = new ArrayList<>(arguments); 66 | } 67 | 68 | 69 | public R accept(RSQLVisitor visitor, A param) { 70 | return visitor.visit(this, param); 71 | } 72 | 73 | public ComparisonOperator getOperator() { 74 | return operator; 75 | } 76 | 77 | /** 78 | * Returns a copy of this node with the specified operator. 79 | * 80 | * @param newOperator Must not be null. 81 | */ 82 | public ComparisonNode withOperator(ComparisonOperator newOperator) { 83 | return new ComparisonNode(newOperator, selector, arguments); 84 | } 85 | 86 | public String getSelector() { 87 | return selector; 88 | } 89 | 90 | /** 91 | * Returns a copy of this node with the specified selector. 92 | * 93 | * @param newSelector Must not be null or blank. 94 | */ 95 | public ComparisonNode withSelector(String newSelector) { 96 | return new ComparisonNode(operator, newSelector, arguments); 97 | } 98 | 99 | /** 100 | * Returns a copy of the arguments list. It's guaranteed that it contains at least one item. 101 | * When the operator is not {@link ComparisonOperator#isMultiValue() multiValue}, then it 102 | * contains exactly one argument. 103 | */ 104 | public List getArguments() { 105 | return new ArrayList<>(arguments); 106 | } 107 | 108 | /** 109 | * Returns a copy of this node with the specified arguments. 110 | * 111 | * @param newArguments Must not be null or empty. If the operator is not 112 | * {@link ComparisonOperator#isMultiValue() multiValue}, then it must contain exactly 113 | * one argument. 114 | */ 115 | public ComparisonNode withArguments(List newArguments) { 116 | return new ComparisonNode(operator, selector, newArguments); 117 | } 118 | 119 | 120 | @Override 121 | public String toString() { 122 | String args = arguments.size() > 1 123 | ? "('" + join(arguments, "','") + "')" 124 | : "'" + arguments.get(0) + "'"; 125 | return selector + operator + args; 126 | } 127 | 128 | @Override 129 | public boolean equals(Object o) { 130 | if (this == o) return true; 131 | if (!(o instanceof ComparisonNode)) return false; 132 | ComparisonNode that = (ComparisonNode) o; 133 | 134 | return arguments.equals(that.arguments) 135 | && operator.equals(that.operator) 136 | && selector.equals(that.selector); 137 | } 138 | 139 | @Override 140 | public int hashCode() { 141 | int result = selector.hashCode(); 142 | result = 31 * result + arguments.hashCode(); 143 | result = 31 * result + operator.hashCode(); 144 | return result; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/javacc/RSQLParser.jj: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2016 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | options { 26 | LOOKAHEAD = 1; 27 | CHOICE_AMBIGUITY_CHECK = 3; 28 | OTHER_AMBIGUITY_CHECK = 2; 29 | STATIC = false; 30 | DEBUG_PARSER = false; 31 | DEBUG_LOOKAHEAD = false; 32 | DEBUG_TOKEN_MANAGER = false; 33 | UNICODE_INPUT = true; 34 | SUPPORT_CLASS_VISIBILITY_PUBLIC = false; 35 | } 36 | 37 | PARSER_BEGIN(Parser) 38 | 39 | package cz.jirutka.rsql.parser; 40 | 41 | import cz.jirutka.rsql.parser.ast.*; 42 | import java.io.InputStream; 43 | import java.util.ArrayList; 44 | import java.util.Arrays; 45 | import java.util.List; 46 | 47 | final class Parser { 48 | 49 | private NodesFactory factory; 50 | 51 | public Parser(InputStream stream, String encoding, NodesFactory factory) { 52 | this(stream, encoding); 53 | this.factory = factory; 54 | } 55 | 56 | private String unescape(String s) { 57 | if (s.indexOf('\\') < 0) { 58 | return s; 59 | } 60 | final StringBuilder sb = new StringBuilder(s.length()); 61 | 62 | for (int i = 0; i < s.length(); i++) { 63 | if (s.charAt(i) == '\\') { 64 | i++; 65 | } 66 | if (i < s.length()) { 67 | sb.append(s.charAt(i)); 68 | } 69 | } 70 | return sb.toString(); 71 | } 72 | } 73 | 74 | PARSER_END(Parser) 75 | 76 | 77 | SKIP : { 78 | " " | "\t" 79 | } 80 | 81 | TOKEN : { 82 | < #ALPHA : ["a"-"z", "A"-"Z"] > 83 | | < #ESCAPED_CHAR : "\\" ~[] > 84 | } 85 | 86 | TOKEN : { 87 | < UNRESERVED_STR : ( ~["\"", "'", "(", ")", ";", ",", "=", "<", ">", "!", "~", " "] )+ > 88 | | < SINGLE_QUOTED_STR : ( "'" ( | ~["'", "\\"] )* "'" ) > 89 | | < DOUBLE_QUOTED_STR : ( "\"" ( | ~["\"", "\\"] )* "\"" ) > 90 | } 91 | 92 | TOKEN : { 93 | < AND : ( ";" | " and ") > 94 | | < OR : ( "," | " or " ) > 95 | | < LPAREN : "(" > 96 | | < RPAREN : ")" > 97 | | < COMP_FIQL : ( ( "=" ()* ) | "!" ) "=" > 98 | | < COMP_ALT : ( ">" | "<" ) ( "=" )? > 99 | } 100 | 101 | 102 | Node Input(): 103 | { 104 | final Node node; 105 | } 106 | { 107 | node = Or() 108 | { 109 | return node; 110 | } 111 | } 112 | 113 | Node Or(): 114 | { 115 | final List nodes = new ArrayList(3); 116 | Node node; 117 | } 118 | { 119 | node = And() { nodes.add(node); } 120 | ( 121 | node = And() { nodes.add(node); } 122 | )* 123 | { 124 | return nodes.size() != 1 ? factory.createLogicalNode(LogicalOperator.OR, nodes) : nodes.get(0); 125 | } 126 | } 127 | 128 | Node And(): 129 | { 130 | final List nodes = new ArrayList(3); 131 | Node node; 132 | } 133 | { 134 | node = Constraint() { nodes.add(node); } 135 | ( 136 | node = Constraint() { nodes.add(node); } 137 | )* 138 | { 139 | return nodes.size() != 1 ? factory.createLogicalNode(LogicalOperator.AND, nodes) : nodes.get(0); 140 | } 141 | } 142 | 143 | Node Constraint(): 144 | { 145 | final Node node; 146 | } 147 | { 148 | ( node = Group() | node = Comparison() ) 149 | { 150 | return node; 151 | } 152 | } 153 | 154 | Node Group(): 155 | { 156 | final Node node; 157 | } 158 | { 159 | node = Or() 160 | { 161 | return node; 162 | } 163 | } 164 | 165 | ComparisonNode Comparison(): 166 | { 167 | final String sel; 168 | final String op; 169 | final List args; 170 | } 171 | { 172 | ( sel = Selector() op = Operator() args = Arguments() ) 173 | { 174 | return factory.createComparisonNode(op, sel, args); 175 | } 176 | } 177 | 178 | String Selector(): {} 179 | { 180 | token = 181 | { 182 | return token.image; 183 | } 184 | } 185 | 186 | String Operator(): {} 187 | { 188 | ( token = | token = ) 189 | { 190 | return token.image; 191 | } 192 | } 193 | 194 | List Arguments(): 195 | { 196 | final Object value; 197 | } 198 | { 199 | ( value = CommaSepArguments() ) { return (List) value; } 200 | | 201 | value = Argument() { return Arrays.asList((String) value); } 202 | } 203 | 204 | List CommaSepArguments(): 205 | { 206 | final List list = new ArrayList(3); 207 | String arg; 208 | } 209 | { 210 | arg = Argument() { list.add(arg); } 211 | ( 212 | 213 | arg = Argument() { list.add(arg); } 214 | )* 215 | { 216 | return list; 217 | } 218 | } 219 | 220 | String Argument(): {} 221 | { 222 | token = { return token.image; } 223 | | 224 | ( token = | token = ) 225 | { 226 | return unescape(token.image.substring(1, token.image.length() -1)); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = RSQL / FIQL parser 2 | Jakub Jirutka 3 | :name: rsql-parser 4 | :version: 2.1.0 5 | :mvn-group: cz.jirutka.rsql 6 | :gh-name: jirutka/{name} 7 | :gh-branch: master 8 | :src-base: link:src/main/java/cz/jirutka/rsql/parser 9 | 10 | ifdef::env-github[] 11 | image:https://travis-ci.org/{gh-name}.svg?branch={gh-branch}["Build Status", link="https://travis-ci.org/{gh-name}"] 12 | image:https://coveralls.io/repos/{gh-name}/badge.svg?branch={gh-branch}&service=github["Coverage Status", link="https://coveralls.io/github/{gh-name}?branch={gh-branch}"] 13 | image:https://api.codacy.com/project/badge/grade/bd2168ab0e424e028ad6df8ff886d81a["Codacy code quality", link="https://www.codacy.com/app/{gh-name}"] 14 | image:https://maven-badges.herokuapp.com/maven-central/{mvn-group}/{name}/badge.svg["Maven Central", link="https://maven-badges.herokuapp.com/maven-central/{mvn-group}/{name}"] 15 | endif::env-github[] 16 | 17 | RSQL is a query language for parametrized filtering of entries in RESTful APIs. 18 | It’s based on http://tools.ietf.org/html/draft-nottingham-atompub-fiql-00[FIQL] (Feed Item Query Language) – an URI-friendly syntax for expressing filters across the entries in an Atom Feed. 19 | FIQL is great for use in URI; there are no unsafe characters, so URL encoding is not required. 20 | On the other side, FIQL’s syntax is not very intuitive and URL encoding isn’t always that big deal, so RSQL also provides a friendlier syntax for logical operators and some of the comparison operators. 21 | 22 | For example, you can query your resource like this: `/movies?query=name=="Kill Bill";year=gt=2003` or `/movies?query=director.lastName==Nolan and year>=2000`. 23 | See <> below. 24 | 25 | This is a complete and thoroughly tested parser for RSQL written in https://javacc.github.io/javacc/[JavaCC] and Java. 26 | Since RSQL is a superset of the FIQL, it can be used for parsing FIQL as well. 27 | 28 | 29 | == Related libraries 30 | 31 | RSQL-parser can be used with: 32 | 33 | * https://github.com/perplexhub/rsql-jpa-specification[rsql-jpa-specification] to convert RSQL into Spring Data JPA Specification and QueryDSL Predicate, 34 | * https://manosbatsis.github.io/vaultaire/plugins/rsql-support/[Vaultaire] depends on rsql-parser for URL-friendly queries of https://www.corda.net/[Corda] Vault states, 35 | * https://github.com/tennaito/rsql-jpa[rsql-jpa] to convert RSQL into JPA2 CriteriaQuery, 36 | * https://github.com/RutledgePaulV/rsql-mongodb[rsql-mongodb] to convert RSQL into MongoDB query using Spring Data MongoDB, 37 | * https://github.com/RutledgePaulV/q-builders[q-builders] to build (not only) RSQL query in type-safe manner, 38 | * _your own library…_ 39 | 40 | It’s very easy to write a converter for RSQL using its AST. 41 | Take a look at very simple and naive converter to JPA2 in less than 100 lines of code https://gist.github.com/jirutka/42a0f9bfea280b3c5dca[here]. 42 | You may also read a http://www.baeldung.com/rest-api-search-language-rsql-fiql[blog article about RSQL] by https://github.com/eugenp[Eugen Paraschiv]. 43 | 44 | 45 | == Grammar and semantic 46 | 47 | _The following grammar specification is written in EBNF notation (http://www.cl.cam.ac.uk/~mgk25/iso-14977.pdf[ISO 14977])._ 48 | 49 | RSQL expression is composed of one or more comparisons, related to each other with logical operators: 50 | 51 | * Logical AND : `;` or `` and `` 52 | * Logical OR : `,` or `` or `` 53 | 54 | By default, the AND operator takes precedence (i.e. it’s evaluated before any OR operators are). 55 | However, a parenthesized expression can be used to change the precedence, yielding whatever the contained expression yields. 56 | 57 | ---- 58 | input = or, EOF; 59 | or = and, { "," , and }; 60 | and = constraint, { ";" , constraint }; 61 | constraint = ( group | comparison ); 62 | group = "(", or, ")"; 63 | ---- 64 | 65 | Comparison is composed of a selector, an operator and an argument. 66 | 67 | ---- 68 | comparison = selector, comparison-op, arguments; 69 | ---- 70 | 71 | Selector identifies a field (or attribute, element, …) of the resource representation to filter by. 72 | It can be any non empty Unicode string that doesn’t contain reserved characters (see below) or a white space. 73 | The specific syntax of the selector is not enforced by this parser. 74 | 75 | ---- 76 | selector = unreserved-str; 77 | ---- 78 | 79 | Comparison operators are in FIQL notation and some of them has an alternative syntax as well: 80 | 81 | * Equal to : `==` 82 | * Not equal to : `!=` 83 | * Less than : `=lt=` or `<` 84 | * Less than or equal to : `=le=` or `\<=` 85 | * Greater than operator : `=gt=` or `>` 86 | * Greater than or equal to : `=ge=` or `>=` 87 | * In : `=in=` 88 | * Not in : `=out=` 89 | 90 | You can also simply extend this parser with your own operators (see the <>). 91 | 92 | ---- 93 | comparison-op = comp-fiql | comp-alt; 94 | comp-fiql = ( ( "=", { ALPHA } ) | "!" ), "="; 95 | comp-alt = ( ">" | "<" ), [ "=" ]; 96 | ---- 97 | 98 | Argument can be a single value, or multiple values in parenthesis separated by comma. 99 | Value that doesn’t contain any reserved character or a white space can be unquoted, other arguments must be enclosed in single or double quotes. 100 | 101 | ---- 102 | arguments = ( "(", value, { "," , value }, ")" ) | value; 103 | value = unreserved-str | double-quoted | single-quoted; 104 | 105 | unreserved-str = unreserved, { unreserved } 106 | single-quoted = "'", { ( escaped | all-chars - ( "'" | "\" ) ) }, "'"; 107 | double-quoted = '"', { ( escaped | all-chars - ( '"' | "\" ) ) }, '"'; 108 | 109 | reserved = '"' | "'" | "(" | ")" | ";" | "," | "=" | "!" | "~" | "<" | ">"; 110 | unreserved = all-chars - reserved - " "; 111 | escaped = "\", all-chars; 112 | all-chars = ? all unicode characters ?; 113 | ---- 114 | 115 | If you need to use both single and double quotes inside a quoted argument, then you must escape one of them using `\` (backslash). 116 | If you want to use `\` literally, then double it as `\\`. 117 | Backslash has a special meaning only inside a quoted argument, not in unquoted argument. 118 | 119 | 120 | == Examples 121 | 122 | Examples of RSQL expressions in both FIQL-like and alternative notation: 123 | 124 | ---- 125 | - name=="Kill Bill";year=gt=2003 126 | - name=="Kill Bill" and year>2003 127 | - genres=in=(sci-fi,action);(director=='Christopher Nolan',actor==*Bale);year=ge=2000 128 | - genres=in=(sci-fi,action) and (director=='Christopher Nolan' or actor==*Bale) and year>=2000 129 | - director.lastName==Nolan;year=ge=2000;year=lt=2010 130 | - director.lastName==Nolan and year>=2000 and year<2010 131 | - genres=in=(sci-fi,action);genres=out=(romance,animated,horror),director==Que*Tarantino 132 | - genres=in=(sci-fi,action) and genres=out=(romance,animated,horror) or director==Que*Tarantino 133 | ---- 134 | 135 | == How to use 136 | 137 | Nodes are http://en.wikipedia.org/wiki/Visitor_pattern[visitable], so to traverse the parsed AST (and convert it to SQL query maybe), you can implement the provided {src-base}/ast/RSQLVisitor.java[RSQLVisitor] interface or simplified {src-base}/ast/NoArgRSQLVisitorAdapter.java[NoArgRSQLVisitorAdapter]. 138 | 139 | [source, java] 140 | ---- 141 | Node rootNode = new RSQLParser().parse("name==RSQL;version=ge=2.0"); 142 | 143 | rootNode.accept(yourShinyVisitor); 144 | ---- 145 | 146 | 147 | == How to add custom operators 148 | 149 | Need more operators? 150 | The parser can be simply enhanced by custom FIQL-like comparison operators, so you can add your own. 151 | 152 | [source, java] 153 | ---- 154 | Set operators = RSQLOperators.defaultOperators(); 155 | operators.add(new ComparisonOperator("=all=", true)); 156 | 157 | Node rootNode = new RSQLParser(operators).parse("genres=all=('thriller','sci-fi')"); 158 | ---- 159 | 160 | == Maven 161 | 162 | Released versions are available in The Central Repository. 163 | Just add this artifact to your project: 164 | 165 | [source, xml, subs="verbatim, attributes"] 166 | ---- 167 | 168 | {mvn-group} 169 | {name} 170 | {version} 171 | 172 | ---- 173 | 174 | However if you want to use the last snapshot version, you have to add the JFrog OSS repository: 175 | 176 | [source, xml] 177 | ---- 178 | 179 | jfrog-oss-snapshot-local 180 | JFrog OSS repository for snapshots 181 | https://oss.jfrog.org/oss-snapshot-local 182 | 183 | true 184 | 185 | 186 | ---- 187 | 188 | 189 | == License 190 | 191 | This project is licensed under http://opensource.org/licenses/MIT[MIT license]. 192 | -------------------------------------------------------------------------------- /src/test/groovy/cz/jirutka/rsql/parser/RSQLParserTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013-2014 Jakub Jirutka . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package cz.jirutka.rsql.parser 25 | 26 | import cz.jirutka.rsql.parser.ast.* 27 | import spock.lang.Specification 28 | import spock.lang.Unroll 29 | 30 | import static cz.jirutka.rsql.parser.ast.RSQLOperators.* 31 | 32 | @Unroll 33 | class RSQLParserTest extends Specification { 34 | 35 | static final RESERVED = ['"', "'", '(', ')', ';', ',', '=', '<', '>', '!', '~', ' '] 36 | 37 | def factory = new NodesFactory(defaultOperators()) 38 | 39 | 40 | def 'throw exception when created with null or empty set of operators'() { 41 | when: 42 | new RSQLParser(operators as Set) 43 | then: 44 | thrown IllegalArgumentException 45 | where: 46 | operators << [null, []] 47 | } 48 | 49 | 50 | def 'throw exception when input is null'() { 51 | when: 52 | parse(null) 53 | then: 54 | thrown IllegalArgumentException 55 | } 56 | 57 | 58 | def 'parse comparison operator: #op'() { 59 | given: 60 | def expected = factory.createComparisonNode(op, 'sel', ['val']) 61 | expect: 62 | parse("sel${op}val") == expected 63 | where: 64 | op << defaultOperators()*.symbols.flatten() 65 | } 66 | 67 | def 'throw exception for deprecated short equal operator: ='() { 68 | when: 69 | parse('sel=val') 70 | then: 71 | thrown RSQLParserException 72 | } 73 | 74 | 75 | def 'parse selector: #input'() { 76 | expect: 77 | parse("${input}==val") == eq(input, 'val') 78 | where: 79 | input << [ 80 | 'allons-y', 'l00k.dot.path', 'look/XML/path', 'n:look/n:xml', 'path.to::Ref', '$doll_r.way' ] 81 | } 82 | 83 | def 'throw exception for selector with reserved char: #input'() { 84 | when: 85 | parse("${input}==val") 86 | then: 87 | thrown RSQLParserException 88 | where: 89 | input << RESERVED.collect{ ["ill${it}", "ill${it}ness"] }.flatten() - ['ill '] 90 | } 91 | 92 | def 'throw exception for empty selector'() { 93 | when: 94 | parse("==val") 95 | then: 96 | thrown RSQLParserException 97 | } 98 | 99 | 100 | def 'parse unquoted argument: #input'() { 101 | given: 102 | def expected = eq('sel', input) 103 | expect: 104 | parse("sel==${input}") == expected 105 | where: 106 | input << [ '«Allons-y»', 'h@llo', '*star*', 'čes*ký', '42', '0.15', '3:15' ] 107 | } 108 | 109 | def 'throw exception for unquoted argument with reserved char: #input'() { 110 | when: 111 | parse("sel==${input}") 112 | then: 113 | thrown RSQLParserException 114 | where: 115 | input << RESERVED.collect{ ["ill${it}", "ill${it}ness"] }.flatten() - ['ill '] 116 | } 117 | 118 | def 'parse quoted argument with any chars: #input'() { 119 | given: 120 | def expected = eq('sel', input[1..-2]) 121 | expect: 122 | parse("sel==${input}") == expected 123 | where: 124 | input << [ '"hi there!"', "'Pěkný den!'", '"Flynn\'s *"', '"o)\'O\'(o"', '"6*7=42"' ] 125 | } 126 | 127 | 128 | def 'parse escaped single quoted argument: #input'() { 129 | expect: 130 | parse("sel==${input}") == eq('sel', parsed) 131 | where: 132 | input | parsed 133 | "'10\\' 15\"'" | "10' 15\"" 134 | "'10\\' 15\\\"'" | "10' 15\"" 135 | "'w\\\\ \\'Flyn\\n\\''" | "w\\ 'Flynn'" 136 | "'\\\\(^_^)/'" | "\\(^_^)/" 137 | } 138 | 139 | def 'parse escaped double quoted argument: #input'() { 140 | expect: 141 | parse("sel==${input}") == eq('sel', parsed) 142 | where: 143 | input | parsed 144 | '"10\' 15\\""' | '10\' 15"' 145 | '"10\\\' 15\\""' | '10\' 15"' 146 | '"w\\\\ \\"Flyn\\n\\""' | 'w\\ "Flynn"' 147 | '"\\\\(^_^)/"' | '\\(^_^)/' 148 | } 149 | 150 | def 'parse arguments group: #input'() { 151 | setup: 'strip quotes' 152 | def values = input.collect { val -> 153 | val[0] in ['"', "'"] ? val[1..-2] : val 154 | } 155 | expect: 156 | parse("sel=in=(${input.join(',')})") == new ComparisonNode(IN, 'sel', values) 157 | where: 158 | input << [ ['chunky', 'bacon', '"ftw!"'], ["'hi!'", '"how\'re you?"'], ['meh'], ['")o("'] ] 159 | } 160 | 161 | 162 | def 'parse logical operator: #op'() { 163 | given: 164 | def expected = factory.createLogicalNode(op, [eq('sel1', 'arg1'), eq('sel2', 'arg2')]) 165 | expect: 166 | parse("sel1==arg1${op.toString()}sel2==arg2") == expected 167 | where: 168 | op << LogicalOperator.values() 169 | } 170 | 171 | def 'parse alternative logical operator: "#alt"'() { 172 | given: 173 | def expected = factory.createLogicalNode(op, [eq('sel1', 'arg1'), eq('sel2', 'arg2')]) 174 | expect: 175 | parse("sel1==arg1${alt}sel2==arg2") == expected 176 | where: 177 | op << LogicalOperator.values() 178 | alt = op == LogicalOperator.AND ? ' and ' : ' or '; 179 | } 180 | 181 | def 'parse queries with default operators priority: #input'() { 182 | expect: 183 | parse(input) == expected 184 | where: 185 | input | expected 186 | 's0==a0;s1==a1;s2==a2' | and(eq('s0','a0'), eq('s1','a1'), eq('s2','a2')) 187 | 's0==a0,s1=out=(a10,a11),s2==a2' | or(eq('s0','a0'), out('s1','a10', 'a11'), eq('s2','a2')) 188 | 's0==a0,s1==a1;s2==a2,s3==a3' | or(eq('s0','a0'), and(eq('s1','a1'), eq('s2','a2')), eq('s3','a3')) 189 | } 190 | 191 | def 'parse queries with parenthesis: #input'() { 192 | expect: 193 | parse(input) == expected 194 | where: 195 | input | expected 196 | '(s0==a0,s1==a1);s2==a2' | and(or(eq('s0','a0'), eq('s1','a1')), eq('s2','a2')) 197 | '(s0==a0,s1=out=(a10,a11));s2==a2,s3==a3'| or(and(or(eq('s0','a0'), out('s1','a10', 'a11')), eq('s2','a2')), eq('s3','a3')) 198 | '((s0==a0,s1==a1);s2==a2,s3==a3);s4==a4' | and(or(and(or(eq('s0','a0'), eq('s1','a1')), eq('s2','a2')), eq('s3','a3')), eq('s4','a4')) 199 | '(s0==a0)' | eq('s0', 'a0') 200 | '((s0==a0));s1==a1' | and(eq('s0', 'a0'), eq('s1','a1')) 201 | } 202 | 203 | def 'throw exception for unclosed parenthesis: #input'() { 204 | when: 205 | parse(input) 206 | then: 207 | thrown RSQLParserException 208 | where: 209 | input << [ '(s0==a0;s1!=a1', 's0==a0)', 's0==a;(s1=in=(b,c),s2!=d' ] 210 | } 211 | 212 | 213 | def 'use parser with custom set of operators'() { 214 | setup: 215 | def allOperator = new ComparisonOperator('=all=', true) 216 | def parser = new RSQLParser([EQUAL, allOperator] as Set) 217 | def expected = and(eq('name', 'TRON'), new ComparisonNode(allOperator, 'genres', ['sci-fi', 'thriller'])) 218 | 219 | expect: 220 | parser.parse('name==TRON;genres=all=(sci-fi,thriller)') == expected 221 | 222 | when: 'unsupported operator used' 223 | parser.parse('name==TRON;year=ge=2010') 224 | then: 225 | def ex = thrown(RSQLParserException) 226 | ex.cause instanceof UnknownOperatorException 227 | } 228 | 229 | 230 | //////// Helpers //////// 231 | 232 | def parse(String rsql) { new RSQLParser().parse(rsql) } 233 | 234 | def and(Node... nodes) { new AndNode(nodes as List) } 235 | def or(Node... nodes) { new OrNode(nodes as List) } 236 | def eq(sel, arg) { new ComparisonNode(EQUAL, sel, [arg as String]) } 237 | def out(sel, ...args) { new ComparisonNode(NOT_IN, sel, args as List) } 238 | } 239 | --------------------------------------------------------------------------------