From 279b69d80783ad437018f1a689a6c6a620fbce9a Mon Sep 17 00:00:00 2001 From: Vasily Kartashov Date: Sun, 31 Jan 2016 16:11:21 +1100 Subject: [PATCH] query language and parser added --- pom.xml | 36 ++++- src/main/antlr4/Query.g4 | 79 +++++++++ .../kartashov/postgis/entities/Status.java | 5 + .../postgis/search/QueryVisitor.java | 150 ++++++++++++++++++ .../postgis/search/SearchService.java | 58 +++++++ .../kartashov/postgis/search/SearchTest.java | 60 +++++++ 6 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 src/main/antlr4/Query.g4 create mode 100644 src/main/java/com/kartashov/postgis/search/QueryVisitor.java create mode 100644 src/main/java/com/kartashov/postgis/search/SearchService.java create mode 100644 src/test/java/com/kartashov/postgis/search/SearchTest.java diff --git a/pom.xml b/pom.xml index 892e542..ee2daa8 100644 --- a/pom.xml +++ b/pom.xml @@ -37,12 +37,22 @@ hibernate-spatial ${hibernate.version} - + + org.antlr + antlr4-runtime + ${antlr.version} + + + org.jscience + jscience + 4.3.1 + 1.8 5.0.7.Final + 4.5.1 @@ -58,20 +68,40 @@ never + + org.antlr + antlr4-maven-plugin + ${antlr.version} + + true + + ${project.build.directory}/generated-sources/com/kartashov/postgis/antlr + + + + + + antlr4 + + + + spring-releases - Spring Releases https://repo.spring.io/libs-release org.jboss.repository.releases - JBoss Maven Release Repository https://repository.jboss.org/nexus/content/repositories/releases + + central + https://repo1.maven.org/maven2 + diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 new file mode 100644 index 0000000..2b53ef6 --- /dev/null +++ b/src/main/antlr4/Query.g4 @@ -0,0 +1,79 @@ +grammar Query; + +@header { + package com.kartashov.postgis.antlr; +} + +AND : ',' | 'and' ; +OR : 'or' ; +INT : '-'? [0-9]+ ; +DOUBLE : '-'? [0-9]+'.'[0-9]+ ; +WITHIN : 'within' ; +FROM : 'from' ; +ID : [a-zA-Z_][a-zA-Z_0-9]* ; +STRING : '"' (~["])* '"' | '\'' (~['])* '\'' + { + String s = getText(); + setText(s.substring(1, s.length() - 1)); + } + ; +EQ : '=' '=' ? ; +LE : '<=' ; +GE : '>=' ; +NE : '!=' ; +LT : '<' ; +GT : '>' ; +SEP : '.' ; +WS : [ \t\r\n]+ -> skip ; + +query : expression ; + +expression + : expression AND expression # AndExpression + | expression OR expression # OrExpression + | predicate # PredicateExpression + | '(' expression ')' # BracketExpression + ; + +reference : element (SEP element)* ; + +element : ID ; + +predicate + : reference WITHIN amount FROM location # LocationPredicate + | reference operator term # OperatorPredicate + ; + +location : '(' latitude ',' longitude ')' ; + +latitude : DOUBLE ; +longitude : DOUBLE ; + +term + : reference + | value + | amount + ; + +operator + : LE + | GE + | NE + | LT + | GT + | EQ + ; + +amount : value unit ; + +value + : INT # IntegerValue + | DOUBLE # DoubleValue + | STRING # StringValue + | ID # StringValue + ; + +unit : + | '%' + | ID + ; diff --git a/src/main/java/com/kartashov/postgis/entities/Status.java b/src/main/java/com/kartashov/postgis/entities/Status.java index 9aab214..91cdd3d 100644 --- a/src/main/java/com/kartashov/postgis/entities/Status.java +++ b/src/main/java/com/kartashov/postgis/entities/Status.java @@ -21,4 +21,9 @@ public String getLifeCycle() { public void setLifeCycle(String lifeCycle) { this.lifeCycle = lifeCycle; } + + @Override + public String toString() { + return "{ stateOfCharge: " + stateOfCharge + ", lifeCycle: " + lifeCycle + " }"; + } } diff --git a/src/main/java/com/kartashov/postgis/search/QueryVisitor.java b/src/main/java/com/kartashov/postgis/search/QueryVisitor.java new file mode 100644 index 0000000..829cc56 --- /dev/null +++ b/src/main/java/com/kartashov/postgis/search/QueryVisitor.java @@ -0,0 +1,150 @@ +package com.kartashov.postgis.search; + +import com.kartashov.postgis.antlr.QueryBaseVisitor; +import com.kartashov.postgis.antlr.QueryParser; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.Point; +import org.jscience.physics.amount.Amount; + +import javax.measure.quantity.Length; +import javax.measure.unit.SI; +import javax.measure.unit.Unit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class QueryVisitor extends QueryBaseVisitor { + + private static final GeometryFactory geometryFactory = new GeometryFactory(); + + private final Map parameters = new HashMap<>(); + + public Map getParameters() { + return parameters; + } + + private String addParameter(Object value) { + String name = "var" + parameters.size(); + parameters.put(name, value); + return name; + } + + @Override + public String visitQuery(QueryParser.QueryContext ctx) { + return "SELECT d FROM Device AS d WHERE " + visit(ctx.expression()); + } + + @Override + public String visitBracketExpression(QueryParser.BracketExpressionContext ctx) { + return visit(ctx.expression()); + } + + @Override + public String visitAndExpression(QueryParser.AndExpressionContext ctx) { + return visit(ctx.expression(0)) + " AND " + visit(ctx.expression(1)); + } + + @Override + public String visitPredicateExpression(QueryParser.PredicateExpressionContext ctx) { + return visit(ctx.predicate()); + } + + @Override + public String visitOrExpression(QueryParser.OrExpressionContext ctx) { + return "(" + visit(ctx.expression(0)) + " OR " + visit(ctx.expression(1)) + ")"; + } + + @Override + public String visitOperator(QueryParser.OperatorContext ctx) { + return ctx.getText(); + } + + @Override + public String visitIntegerValue(QueryParser.IntegerValueContext ctx) { + return addParameter(Integer.valueOf(ctx.getText())); + } + + @Override + public String visitDoubleValue(QueryParser.DoubleValueContext ctx) { + return addParameter(Double.valueOf(ctx.getText())); + } + + @Override + public String visitStringValue(QueryParser.StringValueContext ctx) { + return addParameter(ctx.getText()); + } + + @Override + public String visitAmount(QueryParser.AmountContext ctx) { + Amount amount = Amount.valueOf(ctx.getText()); + @SuppressWarnings("unchecked") + double value = amount.doubleValue((Unit) amount.getUnit().getStandardUnit()); + return addParameter(value); + } + + @Override + public String visitUnit(QueryParser.UnitContext ctx) { + return ctx.getText(); + } + + @Override + public String visitElement(QueryParser.ElementContext ctx) { + return ctx.getText(); + } + + @Override + public String visitOperatorPredicate(QueryParser.OperatorPredicateContext ctx) { + String operator = visit(ctx.operator()); + String value = visit(ctx.term()); + String reference = visitReference(ctx.reference(), parameters.get(value).getClass()); + return reference + " " + operator + " :" + value; + } + + public String visitReference(QueryParser.ReferenceContext ctx, Class type) { + List elements = ctx.element().stream().map(this::visitElement).collect(Collectors.toList()); + String base = "d." + elements.get(0); + if (elements.size() == 1) { + return base; + } else { + List tail = elements.subList(1, elements.size()); + String extract = "extract(" + base + ", '" + String.join("', '", tail) + "')"; + if (type == Integer.class) { + return "CAST(" + extract + " integer)"; + } else if (type == Double.class) { + return "CAST(" + extract + " double)"; + } else { + return extract; + } + } + } + + @Override + public String visitLocationPredicate(QueryParser.LocationPredicateContext ctx) { + String reference = visit(ctx.reference()); + String location = visit(ctx.location()); + String distance = visit(ctx.amount()); + return "distance(" + reference + ", :" + location + ") <= :" + distance; + } + + @Override + public String visitLocation(QueryParser.LocationContext ctx) { + double latitude = Double.valueOf(ctx.latitude().getText()); + double longitude = Double.valueOf(ctx.longitude().getText()); + Point point = geometryFactory.createPoint(new Coordinate(latitude, longitude)); + point.setSRID(4326); + return addParameter(point); + } + + @Override + public String visitTerm(QueryParser.TermContext ctx) { + if (ctx.amount() != null) { + return visit(ctx.amount()); + } else if (ctx.value() != null) { + return visit(ctx.value()); + } else { + return visit(ctx.reference()); + } + } +} diff --git a/src/main/java/com/kartashov/postgis/search/SearchService.java b/src/main/java/com/kartashov/postgis/search/SearchService.java new file mode 100644 index 0000000..cfe7d69 --- /dev/null +++ b/src/main/java/com/kartashov/postgis/search/SearchService.java @@ -0,0 +1,58 @@ +package com.kartashov.postgis.search; + +import com.kartashov.postgis.antlr.QueryLexer; +import com.kartashov.postgis.antlr.QueryParser; +import com.kartashov.postgis.entities.Device; +import org.antlr.v4.runtime.ANTLRInputStream; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +@Service +@Transactional +public class SearchService { + + private static final Logger logger = LoggerFactory.getLogger(SearchService.class); + + @Autowired + private EntityManager entityManager; + + @SuppressWarnings("unchecked") + public List search(String query) throws IOException { + + logger.info("Parsing search query {}", query); + + ANTLRInputStream input = new ANTLRInputStream( + new ByteArrayInputStream(query.getBytes(StandardCharsets.UTF_8))); + QueryLexer lexer = new QueryLexer(input); + CommonTokenStream tokens = new CommonTokenStream(lexer); + + QueryParser parser = new QueryParser(tokens); + ParseTree tree = parser.query(); + + logger.info("Expression tree: {}", tree.toStringTree(parser)); + + QueryVisitor visitor = new QueryVisitor(); + String jpqlQuery = visitor.visit(tree); + + logger.info("Resulting JPQL query:\n{}", jpqlQuery); + + Query queryObject = entityManager.createQuery(jpqlQuery); + for (Map.Entry entry : visitor.getParameters().entrySet()) { + queryObject.setParameter(entry.getKey(), entry.getValue()); + } + return queryObject.getResultList(); + } +} diff --git a/src/test/java/com/kartashov/postgis/search/SearchTest.java b/src/test/java/com/kartashov/postgis/search/SearchTest.java new file mode 100644 index 0000000..d0f574e --- /dev/null +++ b/src/test/java/com/kartashov/postgis/search/SearchTest.java @@ -0,0 +1,60 @@ +package com.kartashov.postgis.search; + +import com.kartashov.postgis.Application; +import com.kartashov.postgis.entities.Device; +import com.kartashov.postgis.repositories.DeviceRepository; +import com.vividsolutions.jts.geom.Point; +import com.vividsolutions.jts.io.ParseException; +import com.vividsolutions.jts.io.WKTReader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.IOException; +import java.util.List; + +import static junit.framework.TestCase.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(Application.class) +public class SearchTest { + + @Autowired + private DeviceRepository deviceRepository; + + @Autowired + private SearchService searchService; + + @Before + public void setUp() throws ParseException { + Device device1 = new Device(); + device1.setId("de-001"); + Point point1 = (Point) new WKTReader().read("POINT(-37.814 144.963)"); + point1.setSRID(4326); + device1.setLocation(point1); + device1.getStatus().setStateOfCharge(0.2); + device1.getStatus().setLifeCycle("ready"); + deviceRepository.save(device1); + + Device device2 = new Device(); + device2.setId("de-002"); + Point point2 = (Point) new WKTReader().read("POINT(-37.814 144.963)"); + point2.setSRID(4326); + device2.setLocation(point2); + device1.getStatus().setStateOfCharge(0.03); + deviceRepository.save(device2); + } + + @Test + public void testSearch() throws IOException { + List devices = searchService.search("location within 10 km from (-37.814, 144.963) and status.stateOfCharge < 10%"); + + System.out.println(devices); + + assertEquals(1, devices.size()); + assertEquals("de-002", devices.get(0).getId()); + } +}