Kotlin Paris Meetup
Michael Mollard
Architech Developer @ Sipios
Spring Search
Implement a language parser in kotlin
Key points
- Spring Search - A story
- Language processing
- Kotlin integration
The Story
I want either a blue Tesla or a car under 30,000€
(brand:Tesla AND color:blue) OR price<30000
GET /cars?brand=Tesla&color=bleu
GET /cars?maxPrice=30000
GET /cars?search=
(brand:Tesla%20AND%20color:blue)%20OR%20price<30000
Before
Our IDEA
Language
A Set of Words
{AND OR : < identifier value}
(brand:Tesla AND color:blue) OR price<30000
A Set of construction pattern
- S -> S AND S
- S -> S OR S
- S -> ( S )
- S -> identifier : value
- S -> identifier < value
Parsing
Programming language
LPAREN '('
IDENTIFIER 'brand'
EQ ':'
IDENTIFIER 'Tesla'
AND 'AND'
IDENTIFIER 'color'
EQ ':'
IDENTIFIER 'blue'
RPAREN ')'
OR 'OR'
IDENTIFIER 'price'
LT '<'
NUMBER '100000'
(query
(query
(query
(query
(criteria
(key brand) (op :) (value Tesla)
)
)
AND
(query
(criteria
(key color) (op :) (value blue)
)
)
)
)
OR
(query
(criteria
(key price) (op <) (value 100000)
)
)
)
ANother Tool for Language Recognition
Kotlin Integration
CODE GENERATION
plugins {
antlr
}
plugins {
antlr
}
tasks.withType<KotlinCompile> {
dependsOn(tasks.generateGrammarSource)
}
tasks.withType<AntlrTask> {
outputDirectory =
File("${project.buildDir}/generated-src/antlr/main/com/sipios/springsearch")
arguments = arguments + listOf("-visitor", "-package", "com.sipios.springsearch")
}
~/query master $ ./gradlew bootJar taskTree
Picked up _JAVA_OPTIONS: -Dlog.level=INFO -Dappendar=Console
> Task :taskTree
------------------------------------------------------------
Root project
------------------------------------------------------------
:bootJar
+--- :classes
| +--- :compileJava
| | +--- :compileKotlin
| | | \--- :generateGrammarSource
| | \--- :generateGrammarSource
| \--- :processResources
\--- :compileKotlin
\--- :generateGrammarSource
https://github.com/dorongold/gradle-task-tree
Using ANTLR
The Grammar
grammar Query;
input
: query EOF
;
query
: left=query logicalOp=(AND | OR) right=query #opQuery
| LPAREN query RPAREN #priorityQuery
| criteria #atomQuery
;
criteria
: key op value
;
key
: IDENTIFIER
;
value
: IDENTIFIER
| STRING
| ENCODED_STRING
| NUMBER
| BOOL
;
op
: EQ
| GT
| LT
| NOT_EQ
;
BOOL
: 'true'
| 'false'
;
STRING
: '"' DoubleStringCharacter* '"'
| '\'' SingleStringCharacter* '\''
;
(brand:Tesla AND color:blue) OR price<30000
The VISITOR
class QueryVisitorImpl<T>: QueryBaseVisitor<Specification<T>>() {
private val ValueRegExp = Regex(pattern = "^(\\*?)([^\\p{Space}]+?)(\\*?)$")
override fun visitOpQuery(ctx: QueryParser.OpQueryContext): Specification<T> {
val left = visit(ctx.left)
val right = visit(ctx.right)
val op = ctx?.logicalOp.text
return when(op) {
"AND" -> left.and(right)
"OR" -> left.or(right)
else -> throw Exception("Wrong Operator")
}
}
override fun visitPriorityQuery(ctx: QueryParser.PriorityQueryContext): Specification<T> {
return visit(ctx.query())
}
override fun visitAtomQuery(ctx: QueryParser.AtomQueryContext): Specification<T> {
return visit(ctx.criteria())
}
override fun visitInput(ctx: QueryParser.InputContext): Specification<T> {
return visit(ctx.query())
}
override fun visitCriteria(ctx: QueryParser.CriteriaContext): Specification<T> {
val key = ctx.key().text
val op = ctx.op().text
var value = ctx.value().text
val matchResult = this.ValueRegExp.find(value!!)
val criteria = SearchCriteria(key, op, matchResult!!.groups[1]!!.value, matchResult.groups[2]!!.value, matchResult.groups[3]!!.value)
return SpecificationImpl(criteria);
}
DEMO
Links
Kotlin Meetup
By Michael Mollard
Kotlin Meetup
- 857