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

  • 876