From 5ae46e1f4b7e9bbf56cc4453ff060fb945ae7aa2 Mon Sep 17 00:00:00 2001 From: michaelpede Date: Fri, 23 Apr 2021 16:25:20 -0700 Subject: [PATCH] $filter $top $skip $count logic implemented eq and ne (case-sensitive) for String values, and eq, ne, gt ,ge , lt, le on the Timestamp field --- .../GenericEntityCollectionProcessor.java | 108 ++++++++++- .../data/meta/FilterExpressionVisitor.java | 172 ++++++++++++++++++ .../security/providers/BasicAuthProvider.java | 5 +- .../providers/BearerAuthProvider.java | 2 +- .../org/reso/service/servlet/RESOservlet.java | 2 +- 5 files changed, 274 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/reso/service/data/meta/FilterExpressionVisitor.java diff --git a/src/main/java/org/reso/service/data/GenericEntityCollectionProcessor.java b/src/main/java/org/reso/service/data/GenericEntityCollectionProcessor.java index e34fa2a..b48bad5 100644 --- a/src/main/java/org/reso/service/data/GenericEntityCollectionProcessor.java +++ b/src/main/java/org/reso/service/data/GenericEntityCollectionProcessor.java @@ -18,7 +18,10 @@ import org.apache.olingo.server.api.serializer.SerializerResult; import org.apache.olingo.server.api.uri.UriInfo; import org.apache.olingo.server.api.uri.UriResource; import org.apache.olingo.server.api.uri.UriResourceEntitySet; +import org.apache.olingo.server.api.uri.queryoption.*; +import org.apache.olingo.server.api.uri.queryoption.expression.Expression; import org.reso.service.data.meta.FieldInfo; +import org.reso.service.data.meta.FilterExpressionVisitor; import org.reso.service.data.meta.ResourceInfo; import org.reso.service.edmprovider.RESOedmProvider; import org.slf4j.Logger; @@ -30,6 +33,7 @@ import java.net.URISyntaxException; import java.sql.*; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; public class GenericEntityCollectionProcessor implements EntityCollectionProcessor @@ -82,9 +86,18 @@ public class GenericEntityCollectionProcessor implements EntityCollectionProcess UriResourceEntitySet uriResourceEntitySet = (UriResourceEntitySet) resourcePaths.get(0); // in our example, the first segment is the EntitySet EdmEntitySet edmEntitySet = uriResourceEntitySet.getEntitySet(); + boolean isCount = false; + CountOption countOption = uriInfo.getCountOption(); + if (countOption != null) { + isCount = countOption.getValue(); + if (isCount){ + LOG.info("Count str:"+countOption.getText() ); + } + } + // 2nd: fetch the data from backend for this requested EntitySetName // it has to be delivered as EntitySet object - EntityCollection entitySet = getData(edmEntitySet); + EntityCollection entitySet = getData(edmEntitySet, uriInfo, isCount); // 3rd: create a serializer based on the requested format (json) try @@ -105,7 +118,19 @@ public class GenericEntityCollectionProcessor implements EntityCollectionProcess ContextURL contextUrl = ContextURL.with().entitySet(edmEntitySet).build(); final String id = request.getRawBaseUri() + "/" + edmEntitySet.getName(); - EntityCollectionSerializerOptions opts = EntityCollectionSerializerOptions.with().id(id).contextURL(contextUrl).build(); + EntityCollectionSerializerOptions opts = null; + if (isCount) // If there's a $count=true in the query string, we need to have a different formatting options. + { + opts = EntityCollectionSerializerOptions.with() + .contextURL(contextUrl) + .id(id) + .count(countOption) + .build(); + } + else + { + opts = EntityCollectionSerializerOptions.with().id(id).contextURL(contextUrl).build(); + } SerializerResult serializerResult = serializer.entityCollection(serviceMetadata, edmEntityType, entitySet, opts); InputStream serializedContent = serializerResult.getContent(); @@ -116,20 +141,85 @@ public class GenericEntityCollectionProcessor implements EntityCollectionProcess } - protected EntityCollection getData(EdmEntitySet edmEntitySet){ + protected EntityCollection getData(EdmEntitySet edmEntitySet, UriInfo uriInfo, boolean isCount) throws ODataApplicationException { ArrayList fields = this.resourceInfo.getFieldList(); - EntityCollection lookupsCollection = new EntityCollection(); + EntityCollection entCollection = new EntityCollection(); - List productList = lookupsCollection.getEntities(); + List productList = entCollection.getEntities(); Map properties = System.getenv(); try { + + FilterOption filter = uriInfo.getFilterOption(); + String sqlCriteria = null; + if (filter!=null) + { + sqlCriteria = filter.getExpression().accept(new FilterExpressionVisitor(this.resourceInfo)); + } + // Statements allow to issue SQL queries to the database Statement statement = connect.createStatement(); // Result set get the result of the SQL query - ResultSet resultSet = statement.executeQuery("select * from "+this.resourceInfo.getTableName()); + String queryString = null; + + // Logic for $count + if (isCount) + { + queryString = "select count(*) AS rowcount from " + this.resourceInfo.getTableName(); + } + else + { + queryString = "select * from " + this.resourceInfo.getTableName(); + } + if (null!=sqlCriteria && sqlCriteria.length()>0) + { + queryString = queryString + " WHERE " + sqlCriteria; + } + + // Logic for $top + TopOption topOption = uriInfo.getTopOption(); + if (topOption != null) { + int topNumber = topOption.getValue(); + if (topNumber >= 0) + { + // Logic for $skip + SkipOption skipOption = uriInfo.getSkipOption(); + if (skipOption != null) + { + int skipNumber = skipOption.getValue(); + if (skipNumber >= 0) + { + queryString = queryString + " LIMIT "+skipNumber+", "+topNumber; + } + else + { + throw new ODataApplicationException("Invalid value for $skip", HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ROOT); + } + } + else + { + queryString = queryString + " LIMIT " + topNumber; + } + } + else + { + throw new ODataApplicationException("Invalid value for $top", HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ROOT); + } + } + + LOG.info("SQL Query: "+queryString); + ResultSet resultSet = statement.executeQuery(queryString); + + // special return logic for $count + if (isCount && resultSet.next()) + { + int size = resultSet.getInt("rowcount"); + LOG.info("Size = "+size); + entCollection.setCount(size); + return entCollection; + } String primaryFieldName = fields.get(0).getFieldName(); @@ -149,7 +239,7 @@ public class GenericEntityCollectionProcessor implements EntityCollectionProcess } else if (field.getType().equals(EdmPrimitiveTypeKind.DateTimeOffset.getFullQualifiedName())) { - value = resultSet.getDate(fieldName); + value = resultSet.getTimestamp(fieldName); } else { @@ -167,10 +257,10 @@ public class GenericEntityCollectionProcessor implements EntityCollectionProcess } catch (Exception e) { LOG.error("Server Error occurred in reading "+this.resourceInfo.getResourceName(), e); - return lookupsCollection; + return entCollection; } - return lookupsCollection; + return entCollection; } private URI createId(String entitySetName, Object id) { diff --git a/src/main/java/org/reso/service/data/meta/FilterExpressionVisitor.java b/src/main/java/org/reso/service/data/meta/FilterExpressionVisitor.java new file mode 100644 index 0000000..5856738 --- /dev/null +++ b/src/main/java/org/reso/service/data/meta/FilterExpressionVisitor.java @@ -0,0 +1,172 @@ +package org.reso.service.data.meta; + +import org.apache.olingo.commons.api.edm.EdmEnumType; +import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind; +import org.apache.olingo.commons.api.edm.EdmType; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.uri.UriResource; +import org.apache.olingo.server.api.uri.UriResourcePrimitiveProperty; +import org.apache.olingo.server.api.uri.queryoption.expression.BinaryOperatorKind; +import org.apache.olingo.server.api.uri.queryoption.expression.Expression; +import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitException; +import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitor; +import org.apache.olingo.server.api.uri.queryoption.expression.Literal; +import org.apache.olingo.server.api.uri.queryoption.expression.Member; +import org.apache.olingo.server.api.uri.queryoption.expression.MethodKind; +import org.apache.olingo.server.api.uri.queryoption.expression.UnaryOperatorKind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.*; + +/** + * $filter + */ +public class FilterExpressionVisitor implements ExpressionVisitor { + private static final Logger LOG = LoggerFactory.getLogger(FilterExpressionVisitor.class); + private static final Map BINARY_OPERATORS = new HashMap() {{ + put(BinaryOperatorKind.ADD, " + "); + put(BinaryOperatorKind.AND, " AND "); + put(BinaryOperatorKind.DIV, " / "); + put(BinaryOperatorKind.EQ, " = "); + put(BinaryOperatorKind.GE, " >= "); + put(BinaryOperatorKind.GT, " > "); + put(BinaryOperatorKind.LE, " <= "); + put(BinaryOperatorKind.LT, " < "); + put(BinaryOperatorKind.MOD, " % "); + put(BinaryOperatorKind.MUL, " * "); + put(BinaryOperatorKind.NE, " <> "); + put(BinaryOperatorKind.OR, " OR "); + put(BinaryOperatorKind.SUB, " - "); + }}; + + private String entityAlias; + private ResourceInfo resourceInfo; + + public FilterExpressionVisitor(ResourceInfo resourceInfo) { + this.entityAlias = resourceInfo.getTableName(); + this.resourceInfo = resourceInfo; + } + + @Override + public String visitBinaryOperator(BinaryOperatorKind operator, String left, String right) + throws ExpressionVisitException, ODataApplicationException { + String strOperator = BINARY_OPERATORS.get(operator); + + if (strOperator == null) { + throw new ODataApplicationException("Unsupported binary operation: " + operator.name(), + operator == BinaryOperatorKind.HAS ? + HttpStatusCode.NOT_IMPLEMENTED.getStatusCode() : + HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ENGLISH); + } + return left + strOperator + right; + } + + @Override + public String visitUnaryOperator(UnaryOperatorKind operator, String operand) + throws ExpressionVisitException, ODataApplicationException { + switch (operator) { + case NOT: + return "NOT " + operand; + case MINUS: + return "-" + operand; + } + throw new ODataApplicationException("Wrong unary operator: " + operator, + HttpStatusCode.BAD_REQUEST.getStatusCode(), Locale.ENGLISH); + } + + @Override + public String visitMethodCall(MethodKind methodCall, List parameters) + throws ExpressionVisitException, ODataApplicationException { + if (parameters.isEmpty() && methodCall.equals(MethodKind.NOW)) { + return "CURRENT_DATE"; + } + String firsEntityParam = parameters.get(0); + switch (methodCall) { + case CONTAINS: + return firsEntityParam + " LIKE '%" + extractFromStringValue(parameters.get(1)) + "%'"; + case STARTSWITH: + return firsEntityParam + " LIKE '" + extractFromStringValue(parameters.get(1)) + "%'"; + case ENDSWITH: + return firsEntityParam + " LIKE '%" + extractFromStringValue(parameters.get(1)); + case DAY: + return "DAY(" + firsEntityParam + ")"; + case MONTH: + return "MONTH(" + firsEntityParam + ")"; + case YEAR: + return "YEAR(" + firsEntityParam + ")"; + } + throw new ODataApplicationException("Method call " + methodCall + " not implemented", + HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH); + } + + @Override + public String visitLiteral(Literal literal) throws ExpressionVisitException, ODataApplicationException { + String literalAsString = literal.getText(); + if (literal.getType() == null) { + literalAsString = "NULL"; + } + if (literal.getType().getFullQualifiedName().equals( EdmPrimitiveTypeKind.DateTimeOffset.getFullQualifiedName() ) ) + { + return "'"+literalAsString+"'"; + } + + return literalAsString; + } + + @Override + public String visitMember(Member member) throws ExpressionVisitException, ODataApplicationException { + List resources = member.getResourcePath().getUriResourceParts(); + + UriResource first = resources.get(0); + + // TODO: Enum and ComplexType; joins + if (resources.size() == 1 && first instanceof UriResourcePrimitiveProperty) { + UriResourcePrimitiveProperty primitiveProperty = (UriResourcePrimitiveProperty) first; + return entityAlias + "." + primitiveProperty.getProperty().getName(); + } else { + throw new ODataApplicationException("Only primitive properties are implemented in filter expressions", + HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH); + } + } + + @Override + public String visitEnum(EdmEnumType type, List enumValues) + throws ExpressionVisitException, ODataApplicationException { + throw new ODataApplicationException("Enums are not implemented", HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), + Locale.ENGLISH); + } + + @Override + public String visitLambdaExpression(String lambdaFunction, String lambdaVariable, Expression expression) + throws ExpressionVisitException, ODataApplicationException { + throw new ODataApplicationException("Lambda expressions are not implemented", + HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH); + } + + @Override + public String visitAlias(String aliasName) throws ExpressionVisitException, ODataApplicationException { + throw new ODataApplicationException("Aliases are not implemented", + HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH); + } + + @Override + public String visitTypeLiteral(EdmType type) throws ExpressionVisitException, ODataApplicationException { + throw new ODataApplicationException("Type literals are not implemented", + HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH); + } + + @Override + public String visitLambdaReference(String variableName) throws ExpressionVisitException, ODataApplicationException { + throw new ODataApplicationException("Lambda references are not implemented", + HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ENGLISH); + } + + private String extractFromStringValue(String val) { + return val.substring(1, val.length() - 1); + } +} \ No newline at end of file diff --git a/src/main/java/org/reso/service/security/providers/BasicAuthProvider.java b/src/main/java/org/reso/service/security/providers/BasicAuthProvider.java index 820688b..8b290fc 100644 --- a/src/main/java/org/reso/service/security/providers/BasicAuthProvider.java +++ b/src/main/java/org/reso/service/security/providers/BasicAuthProvider.java @@ -23,7 +23,7 @@ public class BasicAuthProvider implements Provider private static final Logger LOG = LoggerFactory.getLogger(BasicAuthProvider.class); /** - * A simple BEARER Token Auth with a set token. + * A simple BASIC Auth with static username and password. * @param req The HTTP Request object from the servlet. * @return true if authorized, false otherwise. */ @@ -38,7 +38,6 @@ public class BasicAuthProvider implements Provider if (authResp!=null && authResp.length()>0) { String[] parts = authResp.split(BasicAuthProvider.AUTH_SPACE); - LOG.info("header:"+authResp); if (parts[0].equals(BasicAuthProvider.BASIC_STR) && parts.length==2) { String base64decoded = new String(Base64.getDecoder().decode(parts[1])); @@ -48,8 +47,6 @@ public class BasicAuthProvider implements Provider { String username = parts[0]; String password = parts[1]; - LOG.info("User:"+username); - LOG.info("Pass:"+password); if (username.equals(AUTH_USER) && password.equals(AUTH_PASSWORD)) { diff --git a/src/main/java/org/reso/service/security/providers/BearerAuthProvider.java b/src/main/java/org/reso/service/security/providers/BearerAuthProvider.java index 44586a9..f3a85bb 100644 --- a/src/main/java/org/reso/service/security/providers/BearerAuthProvider.java +++ b/src/main/java/org/reso/service/security/providers/BearerAuthProvider.java @@ -20,7 +20,7 @@ public class BearerAuthProvider implements Provider private static final Logger LOG = LoggerFactory.getLogger(BearerAuthProvider.class); /** - * A simple BASIC Auth with static username and password. Purely for testing purposes. + * A simple BEARER Token Auth with a set token. * @param req The HTTP Request object from the servlet. * @return true if authorized, false otherwise. */ diff --git a/src/main/java/org/reso/service/servlet/RESOservlet.java b/src/main/java/org/reso/service/servlet/RESOservlet.java index a9a90b1..553243e 100644 --- a/src/main/java/org/reso/service/servlet/RESOservlet.java +++ b/src/main/java/org/reso/service/servlet/RESOservlet.java @@ -49,7 +49,7 @@ public class RESOservlet extends HttpServlet } this.validator = new Validator(); -// this.validator.addProvider(new BasicAuthProvider()); +// this.validator.addProvider(new BasicAuthProvider()); // We're using this for the token auth. Only use here for easy browser testing. this.validator.addProvider(new BearerAuthProvider()); String mysqlHost = env.get("SQL_HOST");