Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8e15c3e
commit 279b69d
Showing
6 changed files
with
385 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
150 changes: 150 additions & 0 deletions
150
src/main/java/com/kartashov/postgis/search/QueryVisitor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> { | ||
|
||
private static final GeometryFactory geometryFactory = new GeometryFactory(); | ||
|
||
private final Map<String, Object> parameters = new HashMap<>(); | ||
|
||
public Map<String, Object> 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<String> elements = ctx.element().stream().map(this::visitElement).collect(Collectors.toList()); | ||
String base = "d." + elements.get(0); | ||
if (elements.size() == 1) { | ||
return base; | ||
} else { | ||
List<String> 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()); | ||
} | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
src/main/java/com/kartashov/postgis/search/SearchService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Device> 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<String, Object> entry : visitor.getParameters().entrySet()) { | ||
queryObject.setParameter(entry.getKey(), entry.getValue()); | ||
} | ||
return queryObject.getResultList(); | ||
} | ||
} |
Oops, something went wrong.