.
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 |
--------------------------------------------------------------------------------