[OLINGO-1526]Support for Compute aggregate method

This commit is contained in:
ramya vasanth 2021-05-11 11:13:57 +05:30
parent 3de463a5b1
commit a3d679aacd
9 changed files with 199 additions and 106 deletions

View File

@ -28,7 +28,7 @@ import org.apache.olingo.server.api.uri.queryoption.expression.Expression;
* Represents an aggregate expression.
* @see Aggregate
*/
public interface AggregateExpression {
public interface AggregateExpression extends Expression {
/** Standard aggregation method. */
public enum StandardMethod { SUM, MIN, MAX, AVERAGE, COUNT_DISTINCT }

View File

@ -23,6 +23,7 @@ import java.util.List;
import org.apache.olingo.commons.api.edm.EdmEnumType;
import org.apache.olingo.commons.api.edm.EdmType;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.uri.queryoption.apply.AggregateExpression;
/**
* Generic interface to define expression visitors with arbitrary return types.
@ -37,7 +38,7 @@ public interface ExpressionVisitor<T> {
* @param left Application return value of left sub tree
* @param right Application return value of right sub tree
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitBinaryOperator(BinaryOperatorKind operator, T left, T right)
@ -48,7 +49,7 @@ public interface ExpressionVisitor<T> {
* @param operator Operator kind
* @param operand return value of sub tree
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitUnaryOperator(UnaryOperatorKind operator, T operand)
@ -71,7 +72,7 @@ public interface ExpressionVisitor<T> {
* @param lambdaVariable Variable name used lambda variable
* @param expression Lambda expression
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitLambdaExpression(String lambdaFunction, String lambdaVariable, Expression expression)
@ -81,7 +82,7 @@ public interface ExpressionVisitor<T> {
* Called for each traversed {@link Literal} expression
* @param literal Literal
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitLiteral(Literal literal) throws ExpressionVisitException, ODataApplicationException;
@ -91,7 +92,7 @@ public interface ExpressionVisitor<T> {
* @param member UriInfoResource object describing the whole path used to access an data value
* (this includes for example the usage of $root and $it inside the URI)
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitMember(Member member) throws ExpressionVisitException, ODataApplicationException;
@ -100,7 +101,7 @@ public interface ExpressionVisitor<T> {
* Called for each traversed {@link Alias} expression
* @param aliasName Name of the alias
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitAlias(String aliasName) throws ExpressionVisitException, ODataApplicationException;
@ -109,7 +110,7 @@ public interface ExpressionVisitor<T> {
* Called for each traversed {@link TypeLiteral} expression
* @param type EdmType
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitTypeLiteral(EdmType type) throws ExpressionVisitException, ODataApplicationException;
@ -118,7 +119,7 @@ public interface ExpressionVisitor<T> {
* Called for each traversed {@link LambdaRef}
* @param variableName Name of the used lambda variable
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitLambdaReference(String variableName) throws ExpressionVisitException, ODataApplicationException;
@ -128,7 +129,7 @@ public interface ExpressionVisitor<T> {
* @param type Type used in the URI before the enumeration values
* @param enumValues List of enumeration values
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitEnum(EdmEnumType type, List<String> enumValues) throws ExpressionVisitException, ODataApplicationException;
@ -139,10 +140,22 @@ public interface ExpressionVisitor<T> {
* @param left Application return value of left sub tree
* @param right Application return value of right sub tree
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occured
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
T visitBinaryOperator(BinaryOperatorKind operator, T left, List<T> right)
throws ExpressionVisitException, ODataApplicationException;
/**
* Called for each traversed {@link AggregateExpression}
* @param aggregateExpr the aggregate expression
* @return Application return value of type T
* @throws ExpressionVisitException Thrown if an exception while traversing occurred
* @throws ODataApplicationException Thrown by the application
*/
default T visitComputeAggregate(AggregateExpression aggregateExpr)
throws ExpressionVisitException, ODataApplicationException {
throw new UnsupportedOperationException();
}
}

View File

@ -23,6 +23,7 @@ package org.apache.olingo.server.api.uri.queryoption.expression;
* For the semantic of these methods please see the ODATA specification for URL conventions
*/
public enum MethodKind {
COMPUTE_AGGREGATE("aggregate"),
CONTAINS("contains"),
STARTSWITH("startswith"),
ENDSWITH("endswith"),

View File

@ -222,79 +222,84 @@ public class ApplyParser {
throws UriParserException, UriValidationException {
AggregateImpl aggregate = new AggregateImpl();
do {
aggregate.addExpression(parseAggregateExpr(referencedType));
aggregate.addExpression(parseAggregateExpr(referencedType, Requirement.REQUIRED));
} while (tokenizer.next(TokenKind.COMMA));
ParserHelper.requireNext(tokenizer, TokenKind.CLOSE);
return aggregate;
}
public AggregateExpression parseAggregateMethodCallExpr(UriTokenizer tokenizer, EdmStructuredType referringType)
throws UriParserException, UriValidationException {
this.tokenizer = tokenizer;
private AggregateExpression parseAggregateExpr(EdmStructuredType referencedType)
return parseAggregateExpr(referringType, Requirement.FORBIDDEN);
}
private AggregateExpression parseAggregateExpr(EdmStructuredType referencedType, Requirement aliasRequired)
throws UriParserException, UriValidationException {
AggregateExpressionImpl aggregateExpression = new AggregateExpressionImpl();
tokenizer.saveState();
AggregateExpressionImpl aggregateExpression = new AggregateExpressionImpl();
tokenizer.saveState();
// First try is checking for a (potentially empty) path prefix and the things that could follow it.
UriInfoImpl uriInfo = new UriInfoImpl();
final String identifierLeft = parsePathPrefix(uriInfo, referencedType);
if (identifierLeft != null) {
customAggregate(referencedType, aggregateExpression, uriInfo);
} else if (tokenizer.next(TokenKind.OPEN)) {
final UriResource lastResourcePart = uriInfo.getLastResourcePart();
if (lastResourcePart == null) {
throw new UriParserSyntaxException("Invalid 'aggregateExpr' syntax.",
UriParserSyntaxException.MessageKeys.SYNTAX);
}
aggregateExpression.setPath(uriInfo);
DynamicStructuredType inlineType = new DynamicStructuredType((EdmStructuredType)
ParserHelper.getTypeInformation((UriResourcePartTyped) lastResourcePart));
aggregateExpression.setInlineAggregateExpression(parseAggregateExpr(inlineType));
ParserHelper.requireNext(tokenizer, TokenKind.CLOSE);
} else if (tokenizer.next(TokenKind.COUNT)) {
uriInfo.addResourcePart(new UriResourceCountImpl());
aggregateExpression.setPath(uriInfo);
final String alias = parseAsAlias(referencedType, true);
aggregateExpression.setAlias(alias);
((DynamicStructuredType) referencedType).addProperty(
createDynamicProperty(alias,
// The OData standard mandates Edm.Decimal (with no decimals), although counts are always integer.
odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Decimal)));
} else {
// No legitimate continuation of a path prefix has been found.
// First try is checking for a (potentially empty) path prefix and the things that could follow it.
UriInfoImpl uriInfo = new UriInfoImpl();
final String identifierLeft = parsePathPrefix(uriInfo, referencedType);
if (identifierLeft != null) {
customAggregate(referencedType, aggregateExpression, uriInfo);
} else if (tokenizer.next(TokenKind.OPEN)) {
final UriResource lastResourcePart = uriInfo.getLastResourcePart();
if (lastResourcePart == null) {
throw new UriParserSyntaxException("Invalid 'aggregateExpr' syntax.",
UriParserSyntaxException.MessageKeys.SYNTAX);
}
aggregateExpression.setPath(uriInfo);
DynamicStructuredType inlineType = new DynamicStructuredType((EdmStructuredType)
ParserHelper.getTypeInformation((UriResourcePartTyped) lastResourcePart));
aggregateExpression.setInlineAggregateExpression(parseAggregateExpr(inlineType, aliasRequired));
ParserHelper.requireNext(tokenizer, TokenKind.CLOSE);
} else if (tokenizer.next(TokenKind.COUNT)) {
uriInfo.addResourcePart(new UriResourceCountImpl());
aggregateExpression.setPath(uriInfo);
final String alias = parseAsAlias(referencedType, aliasRequired);
if (alias != null) {
aggregateExpression.setAlias(alias);
((DynamicStructuredType) referencedType).addProperty(
createDynamicProperty(alias,
// The OData standard mandates Edm.Decimal (with no decimals),
// although counts are always integer.
odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Decimal)));
}
} else {
// No legitimate continuation of a path prefix has been found.
// Second try is checking for a common expression.
tokenizer.returnToSavedState();
final Expression expression = new ExpressionParser(edm, odata)
.parse(tokenizer, referencedType, crossjoinEntitySetNames, aliases);
aggregateExpression.setExpression(expression);
parseAggregateWith(aggregateExpression);
if (aggregateExpression.getStandardMethod() == null && aggregateExpression.getCustomMethod() == null) {
if (tokenizer.next(TokenKind.AsOperator)) {
throw new UriParserSyntaxException("Invalid 'aggregateExpr' syntax.",
UriParserSyntaxException.MessageKeys.SYNTAX);
}
customAggregateNamedAsProperty(referencedType, aggregateExpression, uriInfo);
return aggregateExpression;
}
final String alias = parseAsAlias(referencedType, true);
aggregateExpression.setAlias(alias);
DynamicProperty dynamicProperty = createDynamicProperty(alias,
// Determine the type for standard methods; there is no way to do this for custom methods.
getTypeForAggregateMethod(aggregateExpression.getStandardMethod(),
ExpressionParser.getType(expression)));
if (aggregateExpression.getStandardMethod() == StandardMethod.SUM
|| aggregateExpression.getStandardMethod() == StandardMethod.AVERAGE) {
//by default a property with no precision/scale defaults to a 0 scale
//this does not work for sum/average in general
dynamicProperty.setScale(Integer.MAX_VALUE);
}
((DynamicStructuredType) referencedType).addProperty(
dynamicProperty);
parseAggregateFrom(aggregateExpression, referencedType);
}
// Second try is checking for a common expression.
tokenizer.returnToSavedState();
final Expression expression = new ExpressionParser(edm, odata)
.parse(tokenizer, referencedType, crossjoinEntitySetNames, aliases);
aggregateExpression.setExpression(expression);
parseAggregateWith(aggregateExpression);
if (aggregateExpression.getStandardMethod() == null && aggregateExpression.getCustomMethod() == null) {
if (tokenizer.next(TokenKind.AsOperator)) {
throw new UriParserSyntaxException("Invalid 'aggregateExpr' syntax.",
UriParserSyntaxException.MessageKeys.SYNTAX);
}
customAggregateNamedAsProperty(referencedType, aggregateExpression, uriInfo);
return aggregateExpression;
}
final String alias = parseAsAlias(referencedType, aliasRequired);
if(alias != null) {
aggregateExpression.setAlias(alias);
((DynamicStructuredType) referencedType).addProperty(
createDynamicProperty(alias,
// Determine the type for standard methods; there is no way to do this for custom methods.
getTypeForAggregateMethod(aggregateExpression.getStandardMethod(),
ExpressionParser.getType(expression))));
}
parseAggregateFrom(aggregateExpression, referencedType);
}
return aggregateExpression;
}
return aggregateExpression;
}
private void customAggregate(EdmStructuredType referencedType, AggregateExpressionImpl aggregateExpression,
UriInfoImpl uriInfo) throws UriParserException {
@ -306,7 +311,7 @@ public class ApplyParser {
// allowed and have no type.
uriInfo.addResourcePart(new UriResourcePrimitivePropertyImpl(createDynamicProperty(customAggregate, null)));
aggregateExpression.setPath(uriInfo);
final String alias = parseAsAlias(referencedType, false);
final String alias = parseAsAlias(referencedType, Requirement.OPTIONAL);
if (alias != null) {
aggregateExpression.setAlias(alias);
((DynamicStructuredType) referencedType).addProperty(createDynamicProperty(alias, null));
@ -361,22 +366,31 @@ public class ApplyParser {
return null;
}
}
enum Requirement {
REQUIRED, OPTIONAL, FORBIDDEN
}
private String parseAsAlias(final EdmStructuredType referencedType, final boolean isRequired)
private String parseAsAlias(final EdmStructuredType referencedType, final Requirement requirement)
throws UriParserException {
if (tokenizer.next(TokenKind.AsOperator)) {
ParserHelper.requireNext(tokenizer, TokenKind.ODataIdentifier);
final String name = tokenizer.getText();
if (referencedType.getProperty(name) != null) {
throw new UriParserSemanticException("Alias '" + name + "' is already a property.",
UriParserSemanticException.MessageKeys.IS_PROPERTY, name);
}
return name;
} else if (isRequired) {
throw new UriParserSyntaxException("Expected asAlias not found.", UriParserSyntaxException.MessageKeys.SYNTAX);
}
return null;
}
if (tokenizer.next(TokenKind.AsOperator)) {
if(requirement == Requirement.FORBIDDEN) {
throw new UriParserSyntaxException("Unexpected as alias found.",
UriParserSyntaxException.MessageKeys.SYNTAX);
}
ParserHelper.requireNext(tokenizer, TokenKind.ODataIdentifier);
final String name = tokenizer.getText();
if (referencedType.getProperty(name) != null) {
throw new UriParserSemanticException("Alias '" + name + "' is already a property.",
UriParserSemanticException.MessageKeys.IS_PROPERTY, name);
}
return name;
} else if (requirement == Requirement.REQUIRED) {
throw new UriParserSyntaxException("Expected as alias not found.",
UriParserSyntaxException.MessageKeys.SYNTAX);
}
return null;
}
private void parseAggregateFrom(AggregateExpressionImpl aggregateExpression,
final EdmStructuredType referencedType) throws UriParserException {
@ -403,7 +417,7 @@ public class ApplyParser {
throw new UriParserSemanticException("Compute expressions must return primitive values.",
UriParserSemanticException.MessageKeys.ONLY_FOR_PRIMITIVE_TYPES, "compute");
}
final String alias = parseAsAlias(referencedType, true);
final String alias = parseAsAlias(referencedType, Requirement.REQUIRED);
((DynamicStructuredType) referencedType).addProperty(createDynamicProperty(alias, expressionType));
compute.addExpression(new ComputeExpressionImpl()
.setExpression(expression)

View File

@ -54,6 +54,7 @@ import org.apache.olingo.server.api.uri.UriResourceLambdaVariable;
import org.apache.olingo.server.api.uri.UriResourceNavigation;
import org.apache.olingo.server.api.uri.UriResourcePartTyped;
import org.apache.olingo.server.api.uri.queryoption.AliasQueryOption;
import org.apache.olingo.server.api.uri.queryoption.apply.AggregateExpression;
import org.apache.olingo.server.api.uri.queryoption.expression.Alias;
import org.apache.olingo.server.api.uri.queryoption.expression.Binary;
import org.apache.olingo.server.api.uri.queryoption.expression.BinaryOperatorKind;
@ -123,6 +124,7 @@ public class ExpressionParser {
private static final Map<TokenKind, MethodKind> tokenToMethod;
static {
Map<TokenKind, MethodKind> temp = new EnumMap<>(TokenKind.class);
temp.put(TokenKind.AggregateTrafo, MethodKind.COMPUTE_AGGREGATE);
temp.put(TokenKind.CeilingMethod, MethodKind.CEILING);
temp.put(TokenKind.ConcatMethod, MethodKind.CONCAT);
temp.put(TokenKind.ContainsMethod, MethodKind.CONTAINS);
@ -660,8 +662,16 @@ public class ExpressionParser {
case CAST:
case ISOF:
break;
case COMPUTE_AGGREGATE:
ApplyParser ap = new ApplyParser(edm, odata);
AggregateExpression aggrExpr = ap.parseAggregateMethodCallExpr(tokenizer,
(EdmStructuredType) referringType);
parameters.add(aggrExpr);
}
ParserHelper.requireNext(tokenizer, TokenKind.CLOSE);
return parameters;
}

View File

@ -23,10 +23,13 @@ import java.util.Collections;
import java.util.List;
import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.queryoption.apply.AggregateExpression;
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;
/**
* Represents an aggregate expression.
@ -110,4 +113,10 @@ public class AggregateExpressionImpl implements AggregateExpression {
this.alias = alias;
return this;
}
@Override
public <T> T accept(ExpressionVisitor<T> visitor) throws ExpressionVisitException, ODataApplicationException {
// TODO Auto-generated method stub
return null;
}
}

View File

@ -112,8 +112,11 @@ public class MethodImpl implements Method {
case ISOF:
kind = EdmPrimitiveTypeKind.Boolean;
break;
case COMPUTE_AGGREGATE:
kind = null;
}
return new ODataImpl().createPrimitiveTypeInstance(kind);
return kind == null ? null : new ODataImpl().createPrimitiveTypeInstance(kind);
}
@Override

View File

@ -127,22 +127,53 @@ public class ApplyParserTest {
@Test
public void customAggregate() throws Exception {
parse("ESTwoKeyNav", "aggregate(customAggregate)")
.is(Aggregate.class)
.goAggregate(0)
.noExpression()
.noInlineAggregateExpression()
.goPath().first().isUriPathInfoKind(UriResourceKind.primitiveProperty);
parse("ESTwoKeyNav", "aggregate(customAggregate)").is(Aggregate.class).goAggregate(0).noExpression()
.noInlineAggregateExpression().goPath().first().isUriPathInfoKind(UriResourceKind.primitiveProperty);
}
@Test
public void customAggregateNamedAsProperty() throws Exception {
parse("ESTwoKeyNav", "aggregate(PropertyInt16)")
.is(Aggregate.class)
.goAggregate(0)
.noExpression()
.noInlineAggregateExpression()
.goPath().first().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
parse("ESTwoKeyNav", "aggregate(PropertyInt16)").is(Aggregate.class).goAggregate(0).noExpression()
.noInlineAggregateExpression().goPath().first()
.isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
}
@Test
public void filterByCustomAggregate() throws Exception {
ApplyValidator validator = parse("ESTwoKeyNav", "filter(aggregate(customAggregate) gt 5)");
FilterValidator filter = validator.is(Filter.class).goFilter().isBinary(BinaryOperatorKind.GT);
AggregateValidator aggregate = aggregate(filter.root().left());
aggregate.goPath().first().isUriPathInfoKind(UriResourceKind.primitiveProperty);
filter.root().right().isLiteral("5");
}
@Test
public void filterByCountAggregate() throws Exception {
ApplyValidator validator = parse("ESTwoKeyNav", "filter(aggregate($count) ge 9)");
FilterValidator filter = validator.is(Filter.class).goFilter().isBinary(BinaryOperatorKind.GE);
AggregateValidator aggregate = aggregate(filter.root().left());
aggregate.goPath().first().isCount();
filter.root().right().isLiteral("9");
}
@Test
public void filterByTransformationAggregate() throws Exception {
ApplyValidator validator = parse("ESTwoKeyNav", "filter(aggregate(PropertyInt16 with min) ne 3)");
FilterValidator filter = validator.is(Filter.class).goFilter().isBinary(BinaryOperatorKind.NE);
AggregateValidator aggregate = aggregate(filter.root().left());
aggregate.isStandardMethod(StandardMethod.MIN)
.goExpression().goPath().first().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
filter.root().right().isLiteral("3");
}
@Test
public void filterByCustomAggregateNamedAsProperty() throws Exception {
ApplyValidator validator = parse("ESTwoKeyNav", "filter(aggregate(PropertyString) eq 'Evgeny')");
FilterValidator filter = validator.is(Filter.class).goFilter().isBinary(BinaryOperatorKind.EQ);
AggregateValidator aggregate = aggregate(filter.root().left());
aggregate.goPath().first()
.isPrimitiveProperty("PropertyString", PropertyProvider.nameString, false);
filter.root().right().isLiteral("'Evgeny'");
}
@Test
@ -660,6 +691,14 @@ public class ApplyParserTest {
return previous;
}
}
private AggregateValidator aggregate(FilterValidator filter) {
filter.isMethod(MethodKind.COMPUTE_AGGREGATE, 1);
filter.goParameter(0);
return new AggregateValidator((AggregateExpression) filter.getExpression(), filter);
}
private final class AggregateValidator implements TestValidator {
@ -670,7 +709,7 @@ public class ApplyParserTest {
this.aggregateExpression = aggregateExpression;
this.previous = previous;
}
public AggregateValidator isStandardMethod(final AggregateExpression.StandardMethod method) {
assertNotNull(aggregateExpression);
assertEquals(method, aggregateExpression.getStandardMethod());

View File

@ -91,6 +91,10 @@ public class FilterValidator implements TestValidator {
rootExpression = curExpression = expression;
return this;
}
public Expression getExpression() {
return curExpression;
}
// --- Execution ---
@ -375,7 +379,7 @@ public class FilterValidator implements TestValidator {
assertTrue("Current expression not a member", curExpression instanceof Member);
return this;
}
public FilterValidator isMemberStartType(final FullQualifiedName fullName) {
isMember();
Member member = (Member) curExpression;