RESTful Service Query Language (RSQL) is a language and a library designed for searching entries in RESTful services.
This library provides convertor of RSQL expression to Hibernate’s Criteria Query (object representation of HQL), which is translated to SQL query. RSQL was originally created for KOSapi - RESTful web services for IS at the Czech Technical University in Prague.
Feel free to contribute!
RSQL-hibernate consists of five main parts - RSQLHibernateFactory, RSQL2CriteriaConverter, set of Criterion Builders, ArgumentParser and Mapper.
RSQL2HibernateFactory is a factory for creating preconfigured instances of the RSQL2CriteriaConverter.
RSQL2CriteriaConverter is the main client interface that provides methods for creating Hibernate’s DetachedCriteria from an input RSQL expression, or appending to given Criteria instance (i.e. combine static Criteria and user’s RSQL query). Firstly, the expression is parsed through the RSQL-parser. The resulting tree is traversed, Criterions for logical operators are created, and comparisons (constraints) are delegated to one of the Criterion Builders. Criterion Builders are arranged in the stack, which is searched for the builder that is able to handle the given comparison. You can simply add your custom builders.
Criterion Builders are responsible for creating Criterion from a given comparison. Here comes the juicy part: Before creating a Criterion, the Builder has to match a selector (typically a name of an XML element) with a particular entity’s property and convert an argument to the property type (via given ArgumentParser). RSQL-hibernate provides four builders:
- DefaultCriterionBuilder - Default implementation, simply creates Criterion for a basic property (not association).
- IdentifierCriterionBuilder - Creates Criterion for a property representing an association, and an argument containing ID of the associated entity.
- NaturalIdCriterionBuilder - Creates Criterion for a property representing an association, and an argument containing NaturalID of the associated entity.
- AssociationsCriterionBuilder - Handles association “dereference”. That means you can specify constraints upon related entities by navigating associations using dot-notation. For example, we have entity Course with property department, which is ManyToOne association, and entity Department with basic property name. Then we can use
department.name==KSI
to find all courses related to the department KSI. Builder implicitly creates JOIN (i.e. association alias) for every associated entity. You can also set upper limit of JOINs that can be generated.
If you need a custom builder, simply extend AbstractCriterionBuilder (or any of the previously mentioned) and override methods you want.
ArgumentParser is used for parsing arguments from RSQL query according to a class type of the target entity’s property. When an argument cannot be parsed as a required type (i.e. is not in suitable format), then it throws an exception with useful information for user what’s wrong with his query. Provided DefaultArgumentParser supports String, Integer, Long, Float, Boolean, Enum and Date. If neither one of them match, it tries to invoke valueOf(String s) method via reflection on the type’s class. If you need support for more types, simply implement your own ArgumentParser.
Mapper translates a selector to entity’s property name. Translation is done before delegating to Criterion Builder or setting orderBy property. Mapper is useful when you don’t have 1:1 names mapping between selectors and entity properties, i.e. identifier used in RSQL expression doesn’t exactly match name of the corresponding entity’s property. You can use provided SimpleMapper with maps of names mapping per entity or implement your own special Mapper. For example, I have one that maps selectors of multilingual elements according to request’s Accept-Language.
Example of basic usage with only provided builders, default ArgumentParser and without selectors remapping:
// what we need from Hibernate
SessionFactory sessionFactory;
Session session;
// setup factory
RSQL2HibernateFactory factory = RSQL2HibernateFactory.getInstance();
factory.setSessionFactory(sessionFactory);
// create converter (may be done e.g. in Spring’s context)
RSQL2CriteriaConverter converter = factory.createConverter();
// parse RSQL and create detached criteria for specified entity class
DetachedCriteria detached = converter.createCriteria("name==web*;credits>=5", Course.class);
// connect it with current Hibernate Session
Criteria criteria = detached.getExecutableCriteria(session);
// execute query and get result
List<Course> result = criteria.list();
You can also specify ordering:
// ascending order by property "name"
converter.createCriteria("name==web*;credits>=5", "name", true, Course.class);
or add what you like to Criteria:
detached.setFetchMode("department", FetchMode.JOIN);
criteria.setMaxResults(50);
Combine your Criteria query and RSQL query from user:
// specify your static query
Criteria criteria = session.createCriteria(Course.class, "c")
.createCriteria("courseInProgrammes")
.add(Restrictions.eq("programme", programme))
.addOrder(Order.asc("c.name"));
// merge user’s RSQL query with given criteria
converter.extendCriteria("name==web*;department.code==12345", Course.class, criteria)
// execute query and get result
List<Course> result = criteria.list();
…or with provided Criteria decorator:
Criteria criteria = session.createCriteria(Course.class, "c");
List<Course> result = new RSQLCriteriaDecorator(criteria)
.createCriteria("courseInProgrammes")
.add(Restrictions.eq("programme", programme))
.addOrder(Order.asc("c.name"))
.setRSQLConverter(converter)
.mergeRSQLQuery("name==web*;department.code==12345")
.list();
Add MySpecialCriterionBuilder and provided ones to all RSQL2CriteriaConverter instances:
List<AbstractCriterionBuilder> builders = new ArrayList(4);
builders.add(new MySpecialCriterionBuilder());
builders.add(new AssociationsCriterionBuilder());
builders.add(new NaturalIdCriterionBuilder());
builders.add(new IdentifierCriterionBuilder());
builders.add(new DefaultCriterionBuilder());
factory.setCriterionBuilders(builders);
or only to particular converter:
converter.pushCriterionBuilder(new MySpecialCriterionBuilder());
When some selector doesn’t match name of its entity’s property, you can use SimpleMapper:
Map<String, String> map = new HashMap<String, String>();
SimpleMapper mapper = new SimpleMapper();
// selector -> property
map.put("name", "nameEn");
// mapping for entity Course.class
mapper.addMapping(Course.class, map);
factory.setMapper(mapper);
Do you like Spring Framework and it’s XML configuration?
<bean id="rsqlConverter" class="cz.jirutka.rsql.hibernate.RSQL2CriteriaConverterImpl">
<constructor-arg name="sessionFactory" ref="sessionFactory" />
<property name="associationsLimit" value="3" />
<property name="argumentParser" ref="rsqlArgumentParser" />
<property name="mapper" ref="rsqlMapper" />
<property name="criterionBuilders">
<list value-type="cz.jirutka.rsql.hibernate.AbstractCriterionBuilder">
<bean class="cz.jirutka.rsql.hibernate.AssociationsCriterionBuilder" />
<bean class="cz.jirutka.rsql.hibernate.NaturalIdCriterionBuilder" />
<bean class="cz.jirutka.rsql.hibernate.IdentifierCriterionBuilder" />
<!-- Default must be the last one. -->
<bean class="cz.jirutka.rsql.hibernate.DefaultCriterionBuilder" />
</list>
</property>
</bean>
<bean id="rsqlArgumentParser" class="cz.jirutka.rsql.hibernate.DefaultArgumentParser" />
<bean id="rsqlMapper" class="cz.jirutka.rsql.hibernate.SimpleMapper" />
RSQL syntax is described on RSQL-parser’s project page. There’s only one addition described below.
For comparing string arguments with Equals or Not Equals, you can use wildcards *
and _
. If the argument begins or ends with an asterisk character *
, it acts as a wild card, matching any characters preceding or following (respectively) that position. If the argument also contains an underscore character _
, it acts as a wildcard, matching exactly one character. It corresponds to the percentage, respectively underscore wildcard of the LIKE condition in SQL.
I guess that some practical example will come handy. Below is a truncated output from my RESTful service KOSapi.
<atom:feed xml:lang="en" xml:base="https://kosapi.fit.cvut.cz/api/3/">
...
<atom:entry>
<atom:title>Web Services and Middleware</atom:title>
<atom:id>https://kosapi.fit.cvut.cz/api/3/courses/MI-MDW</atom:id>
...
<atom:content xsi:type="kos:course" atom:type="xml">
<code>MI-MDW</code>
<completion>CREDIT_EXAM</completion>
<credits>4</credits>
<name>Web Services and Middleware</name>
<season>WINTER</season>
<department xlink:href="units/18102">Department of Software Engineering</department>
...
</atom:content>
</atom:entry>
...
</atom:feed>
Now some real examples of RSQL queries.
finds courses which...
- /courses?query=code==MI-MDW - code matches MI-MDW
- /courses?query=name==*services* - name contains "services"
- /courses?query=name=='web services*' - name begins with "web services"
- /courses?query=credits>3 - is for more than 3 credits
- /courses?query=name==*web*;season==WINTER;(completion==CLFD_CREDIT,completion==CREDIT_EXAM) - name contains "web" and season is "WINTER" and completion is CLFD_CREDIT or CREDIT_EXAM
- /courses?query=department==18102 - is related with department 18102 (this is actually ManyToOne association and NaturalID)
- /courses?query=department.name==*engineering - is guaranteed by the department that name ends to "engineering"
- /courses?query=name==*services*&orderBy=name&maxResults=50 - name constains "services", order by name and limit output to maximum 50 results
<dependency>
<groupId>cz.jirutka.rsql</groupId>
<artifactId>rsql-hibernate</artifactId>
<version>1.1.3</version>
</dependency>
<repository>
<id>cvut-local-repos</id>
<name>CVUT Repository Local</name>
<url>https://repository.fit.cvut.cz/maven/local-repos/</url>
</repository>
This project is licensed under MIT license.