From dde0f76ade99931c6fc5084ad4701ea0976425c3 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Thu, 21 Sep 2017 15:27:23 +0300 Subject: [PATCH 01/15] Aggregation queries support #266 --- .../rest/runtime/parser/PathConstants.java | 18 ++- .../rest/runtime/parser/RequestParser.java | 6 +- .../parser/tree/IncludeExcludeProcessor.java | 10 +- .../runtime/parser/tree/IncludeVisitor.java | 39 +++++++ .../runtime/parser/tree/IncludeWorker.java | 92 +++------------ .../runtime/parser/tree/PathProcessor.java | 110 ++++++++++++++++++ .../rest/runtime/parser/tree/PathVisitor.java | 16 +++ .../parser/tree/function/CountProcessor.java | 22 ++++ .../tree/function/FunctionProcessor.java | 56 +++++++++ .../function/FunctionalIncludeVisitor.java | 43 +++++++ 10 files changed, 333 insertions(+), 79 deletions(-) create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeVisitor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathVisitor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionalIncludeVisitor.java diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/PathConstants.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/PathConstants.java index 497ff1ed3..9c2f8d15c 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/PathConstants.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/PathConstants.java @@ -2,7 +2,19 @@ public interface PathConstants { - public static final char DOT = '.'; - public static final String ID_PK_ATTRIBUTE = "id"; - public static final int MAX_PATH_LENGTH = 300; + String DOT = "."; + + /** + * @since 2.10 + */ + String OPEN_PARENTHESIS = "("; + + /** + * @since 2.10 + */ + String CLOSE_PARENTHESIS = ")"; + + String ID_PK_ATTRIBUTE = "id"; + + int MAX_PATH_LENGTH = 300; } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/RequestParser.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/RequestParser.java index ba9576c2c..55df59b27 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/RequestParser.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/RequestParser.java @@ -7,7 +7,8 @@ import com.nhl.link.rest.runtime.parser.filter.IKeyValueExpProcessor; import com.nhl.link.rest.runtime.parser.sort.ISortProcessor; import com.nhl.link.rest.runtime.parser.tree.ITreeProcessor; -import com.nhl.link.rest.runtime.parser.tree.IncludeWorker; +import com.nhl.link.rest.runtime.parser.tree.IncludeVisitor; +import com.nhl.link.rest.runtime.parser.tree.PathProcessor; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.slf4j.Logger; @@ -86,7 +87,8 @@ private void processMapBy(ResourceEntity descriptor, Map descriptor.mapBy(mapBy, attribute.getName()); } else { ResourceEntity mapBy = new ResourceEntity<>(descriptor.getLrEntity()); - IncludeWorker.processIncludePath(mapBy, mapByPath); + // using standard include visitor, because functions don't make sense in the context of mapBy + PathProcessor.processor().processPath(mapBy, mapByPath, IncludeVisitor.visitor()); descriptor.mapBy(mapBy, mapByPath); } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java index 107fe22e2..312aa1fad 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java @@ -5,8 +5,11 @@ import com.nhl.link.rest.runtime.parser.BaseRequestProcessor; import com.nhl.link.rest.runtime.parser.filter.ICayenneExpProcessor; import com.nhl.link.rest.runtime.parser.sort.ISortProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.CountProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.FunctionProcessor; import org.apache.cayenne.di.Inject; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -18,12 +21,17 @@ public class IncludeExcludeProcessor extends BaseRequestProcessor implements ITr private static final String INCLUDE = "include"; private static final String EXCLUDE = "exclude"; + private static final String COUNT_FN = "count"; + private IncludeWorker includeWorker; private ExcludeWorker excludeWorker; public IncludeExcludeProcessor(@Inject IJacksonService jacksonService, @Inject ISortProcessor sortProcessor, @Inject ICayenneExpProcessor expProcessor) { - this.includeWorker = new IncludeWorker(jacksonService, sortProcessor, expProcessor); + Map functionProcessors = new HashMap<>(); + functionProcessors.put(COUNT_FN, new CountProcessor()); + + this.includeWorker = new IncludeWorker(jacksonService, sortProcessor, expProcessor, functionProcessors); this.excludeWorker = new ExcludeWorker(jacksonService); } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeVisitor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeVisitor.java new file mode 100644 index 000000000..2b75dcd42 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeVisitor.java @@ -0,0 +1,39 @@ +package com.nhl.link.rest.runtime.parser.tree; + +import com.nhl.link.rest.LinkRestException; +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.meta.LrRelationship; + +import javax.ws.rs.core.Response.Status; + +public class IncludeVisitor implements PathVisitor { + + private static final IncludeVisitor instance = new IncludeVisitor(); + + public static IncludeVisitor visitor() { + return instance; + } + + @Override + public void visitAttribute(ResourceEntity entity, LrAttribute attribute) { + entity.getAttributes().put(attribute.getName(), attribute); + } + + @Override + public void visitRelationship(ResourceEntity parent, ResourceEntity child, LrRelationship relationship) { + IncludeWorker.applyDefaultIncludes(child); + // Id should be included implicitly + child.includeId(); + } + + @Override + public void visitId(ResourceEntity entity) { + entity.includeId(); + } + + @Override + public void visitFunction(ResourceEntity context, String functionName, String callExpression) { + throw new LinkRestException(Status.BAD_REQUEST, "Functions are not allowed in the current context"); + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java index 8aaf65613..b4a95d55a 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java @@ -4,18 +4,18 @@ import com.nhl.link.rest.LinkRestException; import com.nhl.link.rest.ResourceEntity; import com.nhl.link.rest.meta.LrAttribute; -import com.nhl.link.rest.meta.LrEntity; -import com.nhl.link.rest.meta.LrRelationship; import com.nhl.link.rest.runtime.jackson.IJacksonService; -import com.nhl.link.rest.runtime.parser.PathConstants; import com.nhl.link.rest.runtime.parser.filter.ICayenneExpProcessor; import com.nhl.link.rest.runtime.parser.sort.ISortProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.FunctionProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.FunctionalIncludeVisitor; import org.apache.cayenne.exp.Expression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response.Status; import java.util.List; +import java.util.Map; public class IncludeWorker { @@ -31,11 +31,18 @@ public class IncludeWorker { private IJacksonService jsonParser; private ISortProcessor sortProcessor; private ICayenneExpProcessor expProcessor; + private PathProcessor pathProcessor; + private FunctionalIncludeVisitor functionalIncludeVisitor; - public IncludeWorker(IJacksonService jsonParser, ISortProcessor sortProcessor, ICayenneExpProcessor expProcessor) { + public IncludeWorker(IJacksonService jsonParser, + ISortProcessor sortProcessor, + ICayenneExpProcessor expProcessor, + Map functionProcessors) { this.jsonParser = jsonParser; this.sortProcessor = sortProcessor; this.expProcessor = expProcessor; + this.pathProcessor = PathProcessor.processor(); + this.functionalIncludeVisitor = new FunctionalIncludeVisitor(functionProcessors); } public void process(ResourceEntity resourceEntity, List includes) { @@ -47,11 +54,11 @@ public void process(ResourceEntity resourceEntity, List includes) { JsonNode root = jsonParser.parseJson(include); processIncludeObject(resourceEntity, root); } else { - processIncludePath(resourceEntity, include); + pathProcessor.processPath(resourceEntity, include, functionalIncludeVisitor); } } - processDefaultIncludes(resourceEntity); + applyDefaultIncludes(resourceEntity); } private void processIncludeArray(ResourceEntity resourceEntity, String include) { @@ -64,7 +71,7 @@ private void processIncludeArray(ResourceEntity resourceEntity, String includ if (child.isObject()) { processIncludeObject(resourceEntity, child); } else if (child.isTextual()) { - processIncludePath(resourceEntity, child.asText()); + pathProcessor.processPath(resourceEntity, child.asText(), functionalIncludeVisitor); } else { throw new LinkRestException(Status.BAD_REQUEST, "Bad include spec: " + child); } @@ -84,7 +91,7 @@ private void processIncludeObject(ResourceEntity rootEntity, JsonNode root) { includeEntity = rootEntity; } else { String path = pathNode.asText(); - includeEntity = processIncludePath(rootEntity, path); + includeEntity = pathProcessor.processPath(rootEntity, path, functionalIncludeVisitor); if (includeEntity == null) { throw new LinkRestException(Status.BAD_REQUEST, "Bad include spec, non-relationship 'path' in include object: " + path); @@ -135,8 +142,9 @@ private void processMapBy(ResourceEntity descriptor, String mapByPath) { // either root list, or to-many relationship if (descriptor.getIncoming() == null || descriptor.getIncoming().isToMany()) { - ResourceEntity mapByRoot = new ResourceEntity(descriptor.getLrEntity()); - processIncludePath(mapByRoot, mapByPath); + ResourceEntity mapByRoot = new ResourceEntity<>(descriptor.getLrEntity()); + // using standard include visitor, because functions don't make sense in the context of mapBy + pathProcessor.processPath(mapByRoot, mapByPath, IncludeVisitor.visitor()); descriptor.mapBy(mapByRoot, mapByPath); } else { @@ -144,69 +152,7 @@ private void processMapBy(ResourceEntity descriptor, String mapByPath) { } } - /** - * Records include path, returning null for the path corresponding to an - * attribute, and a child {@link ResourceEntity} for the path corresponding - * to relationship. - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static ResourceEntity processIncludePath(ResourceEntity parent, String path) { - - ExcludeWorker.checkTooLong(path); - - int dot = path.indexOf(PathConstants.DOT); - - if (dot == 0) { - throw new LinkRestException(Status.BAD_REQUEST, "Include starts with dot: " + path); - } - - if (dot == path.length() - 1) { - throw new LinkRestException(Status.BAD_REQUEST, "Include ends with dot: " + path); - } - - String property = dot > 0 ? path.substring(0, dot) : path; - LrEntity lrEntity = parent.getLrEntity(); - LrAttribute attribute = lrEntity.getAttribute(property); - if (attribute != null) { - - if (dot > 0) { - throw new LinkRestException(Status.BAD_REQUEST, "Invalid include path: " + path); - } - - parent.getAttributes().put(property, attribute); - return null; - } - - LrRelationship relationship = lrEntity.getRelationship(property); - if (relationship != null) { - - ResourceEntity childEntity = parent.getChild(property); - if (childEntity == null) { - LrEntity targetType = relationship.getTargetEntity(); - childEntity = new ResourceEntity(targetType, relationship); - parent.getChildren().put(property, childEntity); - } - - if (dot > 0) { - return processIncludePath(childEntity, path.substring(dot + 1)); - } else { - processDefaultIncludes(childEntity); - // Id should be included implicitly - childEntity.includeId(); - return childEntity; - } - } - - // this is root entity id and it's included explicitly - if (property.equals(PathConstants.ID_PK_ATTRIBUTE)) { - parent.includeId(); - return null; - } - - throw new LinkRestException(Status.BAD_REQUEST, "Invalid include path: " + path); - } - - private static void processDefaultIncludes(ResourceEntity resourceEntity) { + static void applyDefaultIncludes(ResourceEntity resourceEntity) { // either there are no includes (taking into account Id) or all includes // are relationships if (!resourceEntity.isIdIncluded() && resourceEntity.getAttributes().isEmpty()) { diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java new file mode 100644 index 000000000..2b7e41dcd --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java @@ -0,0 +1,110 @@ +package com.nhl.link.rest.runtime.parser.tree; + +import com.nhl.link.rest.LinkRestException; +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.meta.LrEntity; +import com.nhl.link.rest.meta.LrRelationship; +import com.nhl.link.rest.runtime.parser.PathConstants; + +import javax.ws.rs.core.Response; +import java.util.Objects; + +public class PathProcessor { + + private static final PathProcessor instance = new PathProcessor(); + + public static PathProcessor processor() { + return instance; + } + + /** + * Records include path, returning null for the path corresponding to an + * attribute, and a child {@link ResourceEntity} for the path corresponding + * to relationship. + */ + public ResourceEntity processPath(ResourceEntity root, String path, PathVisitor visitor) { + ExcludeWorker.checkTooLong(path); + + int dot = path.indexOf(PathConstants.DOT); + checkDotIndex(path, dot); + + String property = dot > 0 ? path.substring(0, dot) : path; + LrEntity lrEntity = root.getLrEntity(); + + // first we must check if the path is a relationship + LrRelationship relationship = lrEntity.getRelationship(property); + if (relationship != null) { + ResourceEntity childEntity = root.getChild(property); + if (childEntity == null) { + LrEntity targetType = relationship.getTargetEntity(); + childEntity = new ResourceEntity<>(targetType, relationship); + root.getChildren().put(property, childEntity); + } + + if (dot > 0) { + return processPath(childEntity, path.substring(dot + 1), visitor); + } else { + visitor.visitRelationship(root, childEntity, relationship); + return childEntity; + } + } + + // if the path is not a relationship, then we must check it does not contain separators (dots) + // TODO: this also effectively restricts attribute and function names, that contain dots, + // but it's still possible to add attributes with such malformed names programmatically + // Might be nice to add some additional checks, where needed + if (dot > 0) { + throw new LinkRestException(Response.Status.BAD_REQUEST, "Path contains dots, but is not a relationship: " + path); + } + + LrAttribute attribute = lrEntity.getAttribute(property); + if (attribute != null) { + visitor.visitAttribute(root, attribute); + return null; + } + + // this is root entity id and it's included explicitly + if (property.equals(PathConstants.ID_PK_ATTRIBUTE)) { + visitor.visitId(root); + return null; + } + + String functionName = readFunctionName(property); + if (functionName != null) { + visitor.visitFunction(root, functionName, property.substring(functionName.length())); + return null; + } + + throw new LinkRestException(Response.Status.BAD_REQUEST, "Invalid include path: " + path); + } + + private static void checkDotIndex(String path, int dot) { + if (dot == 0) { + throw new LinkRestException(Response.Status.BAD_REQUEST, "Path starts with dot: " + path); + } + if (dot == path.length() - 1) { + throw new LinkRestException(Response.Status.BAD_REQUEST, "Path ends with dot: " + path); + } + } + + /** + * @param property Function call expression (e.g. {@code count()}) + * @return Function name or null, if {@code property} is not a function call expression + */ + private static String readFunctionName(String property) { + if (Objects.requireNonNull(property, "Missing property").isEmpty()) { + return null; + } + + int parenthesis = property.indexOf(PathConstants.OPEN_PARENTHESIS); + if (parenthesis == 0) { + throw new LinkRestException(Response.Status.BAD_REQUEST, + "Function call expression starts with parenthesis (missing function name): " + property); + } else if (parenthesis < 0) { + return null; + } + + return property.substring(0, parenthesis); + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathVisitor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathVisitor.java new file mode 100644 index 000000000..b1d869edd --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathVisitor.java @@ -0,0 +1,16 @@ +package com.nhl.link.rest.runtime.parser.tree; + +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.meta.LrRelationship; + +public interface PathVisitor { + + void visitAttribute(ResourceEntity entity, LrAttribute attribute); + + void visitRelationship(ResourceEntity parent, ResourceEntity child, LrRelationship relationship); + + void visitId(ResourceEntity entity); + + void visitFunction(ResourceEntity context, String functionName, String callExpression); +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java new file mode 100644 index 000000000..9558ac521 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java @@ -0,0 +1,22 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; + +public class CountProcessor implements FunctionProcessor { + + @Override + public void applyWithoutArguments(ResourceEntity context) { + + } + + @Override + public void apply(ResourceEntity context, LrAttribute attribute) { + + } + + @Override + public void apply(ResourceEntity context) { + + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java new file mode 100644 index 000000000..17e426735 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java @@ -0,0 +1,56 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.LinkRestException; +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.meta.LrRelationship; +import com.nhl.link.rest.runtime.parser.PathConstants; +import com.nhl.link.rest.runtime.parser.tree.PathProcessor; +import com.nhl.link.rest.runtime.parser.tree.PathVisitor; + +import javax.ws.rs.core.Response.Status; +import java.util.Objects; + +public interface FunctionProcessor { + + default void processCallExpression(ResourceEntity context, String expression) { + Objects.requireNonNull(expression, "Missing expression"); + if (!expression.startsWith(PathConstants.OPEN_PARENTHESIS) || !expression.endsWith(PathConstants.CLOSE_PARENTHESIS)) { + throw new LinkRestException(Status.BAD_REQUEST, "Expression must start and end with parentheses: " + expression); + } + String arguments = expression.substring(1, expression.length() - 1); + if (arguments.isEmpty()) { + applyWithoutArguments(context); + } else { + PathVisitor visitor = new PathVisitor() { + @Override + public void visitAttribute(ResourceEntity entity, LrAttribute attribute) { + apply(entity, attribute); + } + + @Override + public void visitRelationship(ResourceEntity parent, ResourceEntity child, LrRelationship relationship) { + apply(child); + } + + @Override + public void visitId(ResourceEntity entity) { + apply(entity); + } + + @Override + public void visitFunction(ResourceEntity context, String functionName, String callExpression) { + throw new LinkRestException(Status.BAD_REQUEST, "Nested functions are not allowed"); + } + }; + + PathProcessor.processor().processPath(context, arguments, visitor); + } + } + + void applyWithoutArguments(ResourceEntity context); + + void apply(ResourceEntity context, LrAttribute attribute); + + void apply(ResourceEntity context); +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionalIncludeVisitor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionalIncludeVisitor.java new file mode 100644 index 000000000..4d73b5119 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionalIncludeVisitor.java @@ -0,0 +1,43 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.LinkRestException; +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.runtime.parser.tree.IncludeVisitor; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.HashMap; +import java.util.Map; + +public class FunctionalIncludeVisitor extends IncludeVisitor { + + private final Map functionProcessors; + + public FunctionalIncludeVisitor(Map functionProcessors) { + // sanity check + if (functionProcessors.isEmpty()) { + throw new LinkRestException(Status.INTERNAL_SERVER_ERROR, "No function processors provided"); + } + this.functionProcessors = withNormalizedNames(functionProcessors); + } + + /** + * Converts all keys to lower case + */ + private Map withNormalizedNames(Map functionProcessors) { + Map result = new HashMap<>(); + functionProcessors.forEach((k, v) -> result.put(k.toLowerCase(), v)); + return result; + } + + @Override + public void visitFunction(ResourceEntity context, String functionName, String callExpression) { + // case-insensitive names + functionName = functionName.toLowerCase(); + FunctionProcessor functionProcessor = functionProcessors.get(functionName); + if (functionProcessor == null) { + throw new LinkRestException(Response.Status.BAD_REQUEST, "Unknown function: " + functionName); + } + functionProcessor.processCallExpression(context, callExpression); + } +} From 1d27ec089e3b457820d30e04807d6fb591e8fce0 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Thu, 21 Sep 2017 15:28:54 +0300 Subject: [PATCH 02/15] Aggregation queries support #266 --- .../rest/runtime/parser/tree/function/CountProcessor.java | 7 +------ .../runtime/parser/tree/function/FunctionProcessor.java | 4 +--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java index 9558ac521..1d5671953 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java @@ -6,7 +6,7 @@ public class CountProcessor implements FunctionProcessor { @Override - public void applyWithoutArguments(ResourceEntity context) { + public void apply(ResourceEntity context) { } @@ -14,9 +14,4 @@ public void applyWithoutArguments(ResourceEntity context) { public void apply(ResourceEntity context, LrAttribute attribute) { } - - @Override - public void apply(ResourceEntity context) { - - } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java index 17e426735..9f75f6fd1 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java @@ -20,7 +20,7 @@ default void processCallExpression(ResourceEntity context, String expression) } String arguments = expression.substring(1, expression.length() - 1); if (arguments.isEmpty()) { - applyWithoutArguments(context); + apply(context); } else { PathVisitor visitor = new PathVisitor() { @Override @@ -48,8 +48,6 @@ public void visitFunction(ResourceEntity context, String functionName, String } } - void applyWithoutArguments(ResourceEntity context); - void apply(ResourceEntity context, LrAttribute attribute); void apply(ResourceEntity context); From ec0c0155d78c2d01002f36d0b372e11a4a439eb6 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Thu, 21 Sep 2017 17:48:13 +0300 Subject: [PATCH 03/15] Aggregation queries support #266 --- .../select/CayenneAssembleQueryStage.java | 68 ++------- .../select/CayenneFetchDataStage.java | 4 +- .../processor/select/QueryBuilder.java | 139 ++++++++++++++++++ .../processor/select/SelectContext.java | 8 +- .../link/rest/it/GET_EncoderFilters_IT.java | 5 +- .../select/CayenneAssembleQueryStageTest.java | 25 ++-- 6 files changed, 173 insertions(+), 76 deletions(-) create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index f9c60c621..06efda48b 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -1,11 +1,6 @@ package com.nhl.link.rest.runtime.cayenne.processor.select; -import com.nhl.link.rest.LinkRestException; -import com.nhl.link.rest.LrObjectId; import com.nhl.link.rest.ResourceEntity; -import com.nhl.link.rest.meta.LrAttribute; -import com.nhl.link.rest.meta.LrEntity; -import com.nhl.link.rest.meta.LrPersistentAttribute; import com.nhl.link.rest.meta.LrPersistentEntity; import com.nhl.link.rest.processor.Processor; import com.nhl.link.rest.processor.ProcessorOutcome; @@ -13,15 +8,11 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; -import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; -import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.query.Select; -import javax.ws.rs.core.Response; -import java.util.ArrayList; -import java.util.Collection; import java.util.Map; /** @@ -45,30 +36,30 @@ protected void doExecute(SelectContext context) { context.setSelect(buildQuery(context)); } - SelectQuery buildQuery(SelectContext context) { + Select buildQuery(SelectContext context) { ResourceEntity entity = context.getEntity(); - SelectQuery query = basicSelect(context); + QueryBuilder query = new QueryBuilder<>(context); if (!entity.isFiltered()) { int limit = context.getEntity().getFetchLimit(); if (limit > 0) { - query.setPageSize(limit); + query.pageSize(limit); } } if (context.getParent() != null) { Expression qualifier = context.getParent().qualifier(entityResolver); - query.andQualifier(qualifier); + query.qualifier(qualifier); } if (entity.getQualifier() != null) { - query.andQualifier(entity.getQualifier()); + query.qualifier(entity.getQualifier()); } for (Ordering o : entity.getOrderings()) { - query.addOrdering(o); + query.ordering(o); } if (!entity.getChildren().isEmpty()) { @@ -83,51 +74,14 @@ SelectQuery buildQuery(SelectContext context) { } appendPrefetches(root, entity, prefetchSemantics); - query.setPrefetchTree(root); + query.prefetch(root); } - return query; + return query.buildQuery(); } - SelectQuery basicSelect(SelectContext context) { - - // selecting by ID overrides any explicit SelectQuery... - if (context.isById()) { - - Class root = context.getType(); - SelectQuery query = new SelectQuery<>(root); - query.andQualifier(buildIdQualifer(context.getEntity().getLrEntity(), context.getId())); - return query; - } - - return context.getSelect() != null ? context.getSelect() : new SelectQuery<>(context.getType()); - } - - private Expression buildIdQualifer(LrEntity entity, LrObjectId id) { - - Collection idAttributes = entity.getIds(); - if (idAttributes.size() != id.size()) { - throw new LinkRestException(Response.Status.BAD_REQUEST, - "Wrong ID size: expected " + idAttributes.size() + ", got: " + id.size()); - } - - Collection qualifiers = new ArrayList<>(); - for (LrAttribute idAttribute : idAttributes) { - Object idValue = id.get(idAttribute.getName()); - if (idValue == null) { - throw new LinkRestException(Response.Status.BAD_REQUEST, - "Failed to build a Cayenne qualifier for entity " + entity.getName() - + ": one of the entity's ID parts is missing in this ID: " + idAttribute.getName()); - } - if (idAttribute instanceof LrPersistentAttribute) { - qualifiers.add(ExpressionFactory.matchDbExp( - ((LrPersistentAttribute) idAttribute).getColumnName(), idValue)); - } else { - // can be non-persistent attribute if assembled from @LrId by LrEntityBuilder - qualifiers.add(ExpressionFactory.matchDbExp(idAttribute.getName(), idValue)); - } - } - return ExpressionFactory.and(qualifiers); + Select basicSelect(SelectContext context) { + return new QueryBuilder<>(context).buildQuery(); } private void appendPrefetches(PrefetchTreeNode root, ResourceEntity entity, int prefetchSemantics) { diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java index 235950ffc..0bc86733c 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java @@ -7,7 +7,7 @@ import com.nhl.link.rest.runtime.cayenne.ICayennePersister; import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Inject; -import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.query.Select; import javax.ws.rs.core.Response; import java.util.List; @@ -34,7 +34,7 @@ public ProcessorOutcome execute(SelectContext context) { } protected void doExecute(SelectContext context) { - SelectQuery select = context.getSelect(); + Select select = context.getSelect(); List objects = persister.sharedContext().select(select); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java new file mode 100644 index 000000000..e97165914 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java @@ -0,0 +1,139 @@ +package com.nhl.link.rest.runtime.cayenne.processor.select; + +import com.nhl.link.rest.LinkRestException; +import com.nhl.link.rest.LrObjectId; +import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.meta.LrEntity; +import com.nhl.link.rest.meta.LrPersistentAttribute; +import com.nhl.link.rest.runtime.processor.select.SelectContext; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.ColumnSelect; +import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.query.Ordering; +import org.apache.cayenne.query.PrefetchTreeNode; +import org.apache.cayenne.query.Select; +import org.apache.cayenne.query.SelectQuery; + +import javax.ws.rs.core.Response.Status; +import java.util.ArrayList; +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class QueryBuilder { + + private Select query; + + private Consumer pageSizeSetter; + private Consumer qualifierSetter; + private Consumer orderingSetter; + private Consumer prefetchSetter; + + public QueryBuilder(SelectContext context) { + // selecting by ID overrides any explicit SelectQuery... + if (context.isById()) { + Class root = context.getType(); + SelectQuery query = new SelectQuery<>(root); + query.andQualifier(buildIdQualifer(context.getEntity().getLrEntity(), context.getId())); + this.query = query; + + } else if (context.getSelect() != null) { + this.query = context.getSelect(); + + } else { + this.query = new SelectQuery<>(context.getType()); + } + + initSetters(this.query.getClass()); + } + + private Expression buildIdQualifer(LrEntity entity, LrObjectId id) { + + Collection idAttributes = entity.getIds(); + if (idAttributes.size() != id.size()) { + throw new LinkRestException(Status.BAD_REQUEST, + "Wrong ID size: expected " + idAttributes.size() + ", got: " + id.size()); + } + + Collection qualifiers = new ArrayList<>(); + for (LrAttribute idAttribute : idAttributes) { + Object idValue = id.get(idAttribute.getName()); + if (idValue == null) { + throw new LinkRestException(Status.BAD_REQUEST, + "Failed to build a Cayenne qualifier for entity " + entity.getName() + + ": one of the entity's ID parts is missing in this ID: " + idAttribute.getName()); + } + if (idAttribute instanceof LrPersistentAttribute) { + qualifiers.add(ExpressionFactory.matchDbExp( + ((LrPersistentAttribute) idAttribute).getColumnName(), idValue)); + } else { + // can be non-persistent attribute if assembled from @LrId by LrEntityBuilder + qualifiers.add(ExpressionFactory.matchDbExp(idAttribute.getName(), idValue)); + } + } + return ExpressionFactory.and(qualifiers); + } + + private void updateSetters(Class queryType) { + if (query != null && query.getClass().isAssignableFrom(queryType)) { + return; + } + initSetters(queryType); + } + + private void initSetters(Class queryType) { + // important: not using method references, because query can be null + if (SelectQuery.class.isAssignableFrom(queryType)) { + pageSizeSetter = pageSize -> ((SelectQuery) query).setPageSize(pageSize); + qualifierSetter = exp -> ((SelectQuery) query).andQualifier(exp); + orderingSetter = ordering -> ((SelectQuery) query).addOrdering(ordering); + prefetchSetter = prefetch -> ((SelectQuery) query).setPrefetchTree(prefetch); + + } else if (ObjectSelect.class.isAssignableFrom(queryType)) { + pageSizeSetter = pageSize -> updateQuery(() -> ((ObjectSelect) query).pageSize(pageSize)); + qualifierSetter = exp -> updateQuery(() -> ((ObjectSelect) query).where(exp)); + orderingSetter = ordering -> updateQuery(() -> ((ObjectSelect) query).orderBy(ordering)); + prefetchSetter = prefetch -> updateQuery(() -> ((ObjectSelect) query).prefetch(prefetch)); + + } else if (ColumnSelect.class.isAssignableFrom(queryType)) { + pageSizeSetter = pageSize -> query = ((ColumnSelect) query).pageSize(pageSize); + qualifierSetter = exp -> query = ((ColumnSelect) query).where(exp); + orderingSetter = ordering -> query = ((ColumnSelect) query).orderBy(ordering); + prefetchSetter = prefetch -> query = ((ColumnSelect) query).prefetch(prefetch); + + } else { + throw new LinkRestException(Status.INTERNAL_SERVER_ERROR, "Unknown query type: " + queryType.getName()); + } + } + + private > void updateQuery(Supplier updater) { + E updatedQuery = updater.get(); + updateSetters(updatedQuery.getClass()); + query = updatedQuery; + } + + public QueryBuilder pageSize(int pageSize) { + pageSizeSetter.accept(pageSize); + return this; + } + + public QueryBuilder qualifier(Expression expression) { + qualifierSetter.accept(expression); + return this; + } + + public QueryBuilder ordering(Ordering ordering) { + orderingSetter.accept(ordering); + return this; + } + + public QueryBuilder prefetch(PrefetchTreeNode prefetch) { + prefetchSetter.accept(prefetch); + return this; + } + + public Select buildQuery() { + return query; + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java index 6e8426f82..8bde0c8ee 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java @@ -11,7 +11,7 @@ import com.nhl.link.rest.constraints.Constraint; import com.nhl.link.rest.encoder.Encoder; import com.nhl.link.rest.processor.BaseProcessingContext; -import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.query.Select; import javax.ws.rs.core.UriInfo; import java.util.Collections; @@ -39,7 +39,7 @@ public class SelectContext extends BaseProcessingContext { private List objects; // TODO: deprecate dependency on Cayenne in generic code - private SelectQuery select; + private Select select; public SelectContext(Class type) { super(type); @@ -141,12 +141,12 @@ public void setConstraint(Constraint constraint) { } // TODO: deprecate dependency on Cayenne in generic code - public SelectQuery getSelect() { + public Select getSelect() { return select; } // TODO: deprecate dependency on Cayenne in generic code - public void setSelect(SelectQuery select) { + public void setSelect(Select select) { this.select = select; } diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java index 7bc00ca46..53d62e84e 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java @@ -17,6 +17,7 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.Cayenne; import org.apache.cayenne.query.SQLTemplate; +import org.apache.cayenne.query.SelectQuery; import org.junit.Test; import javax.ws.rs.GET; @@ -209,7 +210,7 @@ public DataResponse get_WithPaginationStage(@Context UriInfo uriInfo) { .stage(SelectStage.APPLY_SERVER_PARAMS, c -> RESOURCE_ENTITY_IS_FILTERED = c.getEntity().isFiltered()) .stage(SelectStage.ASSEMBLE_QUERY, - c -> QUERY_PAGE_SIZE = c.getSelect().getPageSize()) + c -> QUERY_PAGE_SIZE = ((SelectQuery)c.getSelect()).getPageSize()) .get(); } } @@ -239,7 +240,7 @@ public ProcessingStage, T> queryAssembled( SelectContext context, ProcessingStage, T> next) { - QUERY_PAGE_SIZE = context.getSelect().getPageSize(); + QUERY_PAGE_SIZE = ((SelectQuery)context.getSelect()).getPageSize(); return next; } } diff --git a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java index c74fd06e8..e3fc22393 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java +++ b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java @@ -16,7 +16,10 @@ import org.junit.Test; import org.mockito.Mockito; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; public class CayenneAssembleQueryStageTest extends TestWithCayenneMapping { @@ -43,7 +46,7 @@ public void testBuildQuery_Ordering() { context.setSelect(query); context.setEntity(resourceEntity); - SelectQuery amended = makeQueryStage.buildQuery(context); + SelectQuery amended = (SelectQuery) makeQueryStage.buildQuery(context); assertSame(query, amended); assertEquals(2, amended.getOrderings().size()); assertSame(o1, amended.getOrderings().get(0)); @@ -64,7 +67,7 @@ public void testBuildQuery_Prefetches() { context.setEntity(resultFilter); context.setSelect(query); - SelectQuery amended = makeQueryStage.buildQuery(context); + SelectQuery amended = (SelectQuery) makeQueryStage.buildQuery(context); assertSame(query, amended); PrefetchTreeNode rootPrefetch = amended.getPrefetchTree(); @@ -85,7 +88,7 @@ public void testBuildQuery_Pagination() { SelectContext c = new SelectContext(E1.class); c.setEntity(resourceEntity); - SelectQuery q1 = makeQueryStage.buildQuery(c); + SelectQuery q1 = (SelectQuery) makeQueryStage.buildQuery(c); assertEquals("Pagination in the query for paginated request is expected", 10, q1.getPageSize()); assertEquals(0, q1.getFetchOffset()); @@ -94,7 +97,7 @@ public void testBuildQuery_Pagination() { resourceEntity.setFetchLimit(0); resourceEntity.setFetchOffset(0); - SelectQuery q2 = makeQueryStage.buildQuery(c); + SelectQuery q2 = (SelectQuery) makeQueryStage.buildQuery(c); assertEquals(0, q2.getPageSize()); assertEquals(0, q2.getFetchOffset()); assertEquals(0, q2.getFetchLimit()); @@ -102,7 +105,7 @@ public void testBuildQuery_Pagination() { resourceEntity.setFetchLimit(0); resourceEntity.setFetchOffset(5); - SelectQuery q3 = makeQueryStage.buildQuery(c); + SelectQuery q3 = (SelectQuery) makeQueryStage.buildQuery(c); assertEquals(0, q3.getPageSize()); assertEquals(0, q3.getFetchOffset()); assertEquals(0, q3.getFetchLimit()); @@ -118,17 +121,17 @@ public void testBuildQuery_Qualifier() { SelectContext c1 = new SelectContext<>(E1.class); c1.setEntity(resourceEntity); - SelectQuery query = makeQueryStage.buildQuery(c1); + SelectQuery query = (SelectQuery) makeQueryStage.buildQuery(c1); assertEquals(extraQualifier, query.getQualifier()); - SelectQuery query2 = new SelectQuery(E1.class); + SelectQuery query2 = new SelectQuery<>(E1.class); query2.setQualifier(E1.NAME.in("a", "b")); SelectContext c2 = new SelectContext<>(E1.class); c2.setSelect(query2); c2.setEntity(resourceEntity); - SelectQuery query2Amended = makeQueryStage.buildQuery(c2); + SelectQuery query2Amended = (SelectQuery) makeQueryStage.buildQuery(c2); assertEquals(E1.NAME.in("a", "b").andExp(E1.NAME.eq("X")), query2Amended.getQualifier()); } @@ -139,7 +142,7 @@ public void testById() { c.setId(1); c.setEntity(getResourceEntity(E1.class)); - SelectQuery s1 = makeQueryStage.basicSelect(c); + SelectQuery s1 = (SelectQuery) makeQueryStage.basicSelect(c); assertNotNull(s1); assertSame(E1.class, s1.getRoot()); } @@ -153,7 +156,7 @@ public void testById_WithQuery() { c.setSelect(select); c.setEntity(getResourceEntity(E1.class)); - SelectQuery s2 = makeQueryStage.basicSelect(c); + SelectQuery s2 = (SelectQuery) makeQueryStage.basicSelect(c); assertNotNull(s2); assertNotSame(select, s2); assertSame(E1.class, s2.getRoot()); From d7120a87141db46495a4bdab669db1a736fe34ac Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Fri, 22 Sep 2017 14:49:58 +0300 Subject: [PATCH 04/15] Aggregation queries support #266 --- .../com/nhl/link/rest/AggregationType.java | 12 +++ .../com/nhl/link/rest/ResourceEntity.java | 36 +++++++++ .../select/CayenneAssembleQueryStage.java | 53 +++++++++++++ .../processor/select/QueryBuilder.java | 60 +++++++++++--- .../parser/tree/IncludeExcludeProcessor.java | 3 + .../tree/function/AverageProcessor.java | 21 +++++ .../parser/tree/function/CountProcessor.java | 7 +- .../tree/function/FunctionProcessor.java | 6 +- .../link/rest/it/GET_EncoderFilters_IT.java | 10 +-- .../nhl/link/rest/it/GET_IT_Aggregate.java | 79 +++++++++++++++++++ .../select/CayenneAssembleQueryStageTest.java | 64 +++++++-------- 11 files changed, 301 insertions(+), 50 deletions(-) create mode 100644 link-rest/src/main/java/com/nhl/link/rest/AggregationType.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java create mode 100644 link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java diff --git a/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java b/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java new file mode 100644 index 000000000..ca485207d --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java @@ -0,0 +1,12 @@ +package com.nhl.link.rest; + +public enum AggregationType { + + AVERAGE, + + SUM, + + MIN, + + MAX +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java b/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java index 148a356cc..d0c13b772 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java +++ b/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java @@ -26,11 +26,14 @@ public class ResourceEntity { private boolean idIncluded; + private boolean countIncluded; private LrEntity lrEntity; private Map attributes; private Collection defaultProperties; + private Map> aggregatedAttributes; + private String applicationBase; private String mapByPath; private ResourceEntity mapBy; @@ -47,6 +50,7 @@ public ResourceEntity(LrEntity lrEntity) { this.idIncluded = false; this.attributes = new HashMap<>(); this.defaultProperties = new HashSet<>(); + this.aggregatedAttributes = new HashMap<>(); this.children = new HashMap<>(); this.orderings = new ArrayList<>(2); this.extraProperties = new HashMap<>(); @@ -237,4 +241,36 @@ public boolean isFiltered() { public void setFiltered(boolean filtered) { this.filtered = filtered; } + + public void includeCount() { + this.countIncluded = true; + } + + public boolean isCountIncluded() { + return countIncluded; + } + + public Collection getAggregatedAttributes(AggregationType aggregationType) { + return aggregatedAttributes.computeIfAbsent(aggregationType, it -> new ArrayList<>()); + } + + public boolean isAggregate() { + if (countIncluded) { + return true; + } + + for (Collection attributes : aggregatedAttributes.values()) { + if (attributes.size() > 0) { + return true; + } + } + + for (ResourceEntity child : children.values()) { + if (child.isAggregate()) { + return true; + } + } + + return false; + } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index 06efda48b..ebce2fb94 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -1,5 +1,7 @@ package com.nhl.link.rest.runtime.cayenne.processor.select; +import com.nhl.link.rest.AggregationType; +import com.nhl.link.rest.LinkRestException; import com.nhl.link.rest.ResourceEntity; import com.nhl.link.rest.meta.LrPersistentEntity; import com.nhl.link.rest.processor.Processor; @@ -8,11 +10,13 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.Property; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; import org.apache.cayenne.query.Select; +import javax.ws.rs.core.Response.Status; import java.util.Map; /** @@ -42,6 +46,10 @@ Select buildQuery(SelectContext context) { QueryBuilder query = new QueryBuilder<>(context); + if (entity.isAggregate()) { + appendAggregateColumns(entity, query, null); + } + if (!entity.isFiltered()) { int limit = context.getEntity().getFetchLimit(); if (limit > 0) { @@ -80,6 +88,51 @@ Select buildQuery(SelectContext context) { return query.buildQuery(); } + private void appendAggregateColumns(ResourceEntity entity, QueryBuilder query, Property context) { + if (entity.isCountIncluded()) { + query.count(); + } + + entity.getAttributes().values().stream().filter(a -> !entity.isDefault(a.getName())).forEach(attribute -> { + Property property = createProperty(context, attribute.getName(), attribute.getType()); + query.column(property); + }); + + for (AggregationType aggregationType : AggregationType.values()) { + entity.getAggregatedAttributes(aggregationType).forEach(attribute -> { + Property property = createProperty(context, attribute.getName(), attribute.getType()); + switch (aggregationType) { + case AVERAGE: { + query.avg(property); + break; + } + default: { + throw new LinkRestException(Status.INTERNAL_SERVER_ERROR, + "Unsupported aggregation type: " + aggregationType.name()); + } + } + }); + } + + entity.getChildren().forEach((relationshipName, child) -> { + Property relationship = createProperty(context, relationshipName, child.getType()); + appendAggregateColumns(child, query, relationship); + + if (child.isCountIncluded()) { + query.count(relationship); + } + }); + } + + @SuppressWarnings("unchecked") + private static Property createProperty(Property context, String name, Class type) { + Property property = Property.create(name, (Class) type); + if (context != null) { + property = context.dot(property); + } + return property; + } + Select basicSelect(SelectContext context) { return new QueryBuilder<>(context).buildQuery(); } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java index e97165914..f6ccb57d9 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java @@ -8,6 +8,7 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.exp.Property; import org.apache.cayenne.query.ColumnSelect; import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.query.Ordering; @@ -29,20 +30,23 @@ public class QueryBuilder { private Consumer qualifierSetter; private Consumer orderingSetter; private Consumer prefetchSetter; + private Runnable rootCountSetter; + private Consumer> countSetter; + private Consumer> avgSetter; + private Consumer> columnAdder; public QueryBuilder(SelectContext context) { // selecting by ID overrides any explicit SelectQuery... if (context.isById()) { Class root = context.getType(); - SelectQuery query = new SelectQuery<>(root); - query.andQualifier(buildIdQualifer(context.getEntity().getLrEntity(), context.getId())); - this.query = query; + this.query = ObjectSelect.query(root) + .where((buildIdQualifer(context.getEntity().getLrEntity(), context.getId()))); } else if (context.getSelect() != null) { this.query = context.getSelect(); } else { - this.query = new SelectQuery<>(context.getType()); + this.query = ObjectSelect.query(context.getType()); } initSetters(this.query.getClass()); @@ -89,28 +93,44 @@ private void initSetters(Class queryType) { qualifierSetter = exp -> ((SelectQuery) query).andQualifier(exp); orderingSetter = ordering -> ((SelectQuery) query).addOrdering(ordering); prefetchSetter = prefetch -> ((SelectQuery) query).setPrefetchTree(prefetch); + rootCountSetter = () -> {throw new IllegalStateException("count() is not applicable to SelectQuery");}; + countSetter = property -> {throw new IllegalStateException("count() is not applicable to SelectQuery");}; + avgSetter = property -> {throw new IllegalStateException("avg() is not applicable to SelectQuery");}; + columnAdder = it -> {}; // ignore individual columns } else if (ObjectSelect.class.isAssignableFrom(queryType)) { pageSizeSetter = pageSize -> updateQuery(() -> ((ObjectSelect) query).pageSize(pageSize)); qualifierSetter = exp -> updateQuery(() -> ((ObjectSelect) query).where(exp)); orderingSetter = ordering -> updateQuery(() -> ((ObjectSelect) query).orderBy(ordering)); prefetchSetter = prefetch -> updateQuery(() -> ((ObjectSelect) query).prefetch(prefetch)); + rootCountSetter = () -> updateQuery(() -> ((ObjectSelect) query).count()); + countSetter = property -> updateQuery(() -> ((ObjectSelect) query).count(property)); + avgSetter = property -> updateQuery(() -> ((ObjectSelect) query).avg(property)); + columnAdder = property -> updateQuery(() -> ((ObjectSelect) query).columns(property)); } else if (ColumnSelect.class.isAssignableFrom(queryType)) { - pageSizeSetter = pageSize -> query = ((ColumnSelect) query).pageSize(pageSize); - qualifierSetter = exp -> query = ((ColumnSelect) query).where(exp); - orderingSetter = ordering -> query = ((ColumnSelect) query).orderBy(ordering); - prefetchSetter = prefetch -> query = ((ColumnSelect) query).prefetch(prefetch); + pageSizeSetter = pageSize -> updateQuery(() -> ((ColumnSelect) query).pageSize(pageSize)); + qualifierSetter = exp -> updateQuery(() -> ((ColumnSelect) query).where(exp)); + orderingSetter = ordering -> updateQuery(() -> ((ColumnSelect) query).orderBy(ordering)); + prefetchSetter = prefetch -> updateQuery(() -> ((ColumnSelect) query).prefetch(prefetch)); + rootCountSetter = () -> updateQuery(() -> ((ColumnSelect) query).count()); + countSetter = property -> updateQuery(() -> ((ColumnSelect) query).count(property)); + avgSetter = property -> updateQuery(() -> ((ColumnSelect) query).avg(property)); + columnAdder = property -> updateQuery(() -> ((ColumnSelect) query).columns(property)); } else { throw new LinkRestException(Status.INTERNAL_SERVER_ERROR, "Unknown query type: " + queryType.getName()); } } - private > void updateQuery(Supplier updater) { + // ObjectSelect and ColumnSelect aggregate builder methods + // return ColumnSelect or ColumnSelect or ColumnSelect, so we need to manually erase the type... + // and the type does not matter in this context anyway + @SuppressWarnings("unchecked") + private > void updateQuery(Supplier updater) { E updatedQuery = updater.get(); updateSetters(updatedQuery.getClass()); - query = updatedQuery; + query = (Select) updatedQuery; } public QueryBuilder pageSize(int pageSize) { @@ -133,6 +153,26 @@ public QueryBuilder prefetch(PrefetchTreeNode prefetch) { return this; } + public QueryBuilder count() { + rootCountSetter.run(); + return this; + } + + public QueryBuilder count(Property property) { + countSetter.accept(property); + return this; + } + + public QueryBuilder avg(Property property) { + avgSetter.accept(property); + return this; + } + + public QueryBuilder column(Property property) { + columnAdder.accept(property); + return this; + } + public Select buildQuery() { return query; } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java index 312aa1fad..f06680c23 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java @@ -5,6 +5,7 @@ import com.nhl.link.rest.runtime.parser.BaseRequestProcessor; import com.nhl.link.rest.runtime.parser.filter.ICayenneExpProcessor; import com.nhl.link.rest.runtime.parser.sort.ISortProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.AverageProcessor; import com.nhl.link.rest.runtime.parser.tree.function.CountProcessor; import com.nhl.link.rest.runtime.parser.tree.function.FunctionProcessor; import org.apache.cayenne.di.Inject; @@ -22,6 +23,7 @@ public class IncludeExcludeProcessor extends BaseRequestProcessor implements ITr private static final String EXCLUDE = "exclude"; private static final String COUNT_FN = "count"; + private static final String AVERAGE_FN = "avg"; private IncludeWorker includeWorker; private ExcludeWorker excludeWorker; @@ -30,6 +32,7 @@ public IncludeExcludeProcessor(@Inject IJacksonService jacksonService, @Inject I @Inject ICayenneExpProcessor expProcessor) { Map functionProcessors = new HashMap<>(); functionProcessors.put(COUNT_FN, new CountProcessor()); + functionProcessors.put(AVERAGE_FN, new AverageProcessor()); this.includeWorker = new IncludeWorker(jacksonService, sortProcessor, expProcessor, functionProcessors); this.excludeWorker = new ExcludeWorker(jacksonService); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java new file mode 100644 index 000000000..1268b16b3 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java @@ -0,0 +1,21 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.AggregationType; +import com.nhl.link.rest.LinkRestException; +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; + +import javax.ws.rs.core.Response.Status; + +public class AverageProcessor implements FunctionProcessor { + + @Override + public void apply(ResourceEntity context) { + throw new LinkRestException(Status.BAD_REQUEST, "Function is not applicable in this context"); + } + + @Override + public void apply(ResourceEntity context, LrAttribute attribute) { + context.getAggregatedAttributes(AggregationType.AVERAGE).add(attribute); + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java index 1d5671953..c612584ab 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/CountProcessor.java @@ -1,17 +1,20 @@ package com.nhl.link.rest.runtime.parser.tree.function; +import com.nhl.link.rest.LinkRestException; import com.nhl.link.rest.ResourceEntity; import com.nhl.link.rest.meta.LrAttribute; +import javax.ws.rs.core.Response; + public class CountProcessor implements FunctionProcessor { @Override public void apply(ResourceEntity context) { - + context.includeCount(); } @Override public void apply(ResourceEntity context, LrAttribute attribute) { - + throw new LinkRestException(Response.Status.BAD_REQUEST, "Function is not applicable in this context"); } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java index 9f75f6fd1..d34549929 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/FunctionProcessor.java @@ -35,6 +35,8 @@ public void visitRelationship(ResourceEntity parent, ResourceEntity child, @Override public void visitId(ResourceEntity entity) { + // TODO: some functions might need to know, that the argument is ID... like avg(id) + // but in the context of LR the utility of such usage is questionable apply(entity); } @@ -48,7 +50,7 @@ public void visitFunction(ResourceEntity context, String functionName, String } } - void apply(ResourceEntity context, LrAttribute attribute); - void apply(ResourceEntity context); + + void apply(ResourceEntity context, LrAttribute attribute); } diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java index 53d62e84e..a05f92e4d 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java @@ -16,8 +16,8 @@ import com.nhl.link.rest.runtime.LinkRestBuilder; import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.Cayenne; +import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.query.SQLTemplate; -import org.apache.cayenne.query.SelectQuery; import org.junit.Test; import javax.ws.rs.GET; @@ -110,7 +110,7 @@ public void testFilteredPagination3() { public void testFilteredPagination4_Listeners() { CayennePaginationListener.RESOURCE_ENTITY_IS_FILTERED = false; - CayennePaginationListener.QUERY_PAGE_SIZE = 0; + CayennePaginationListener.QUERY_PAGE_SIZE = 1; target("/e4/pagination_listener") .queryParam("include", "id") @@ -128,7 +128,7 @@ public void testFilteredPagination4_Listeners() { public void testFilteredPagination4_CustomStage() { Resource.RESOURCE_ENTITY_IS_FILTERED = false; - Resource.QUERY_PAGE_SIZE = 0; + Resource.QUERY_PAGE_SIZE = 1; target("/e4/pagination_stage") .queryParam("include", "id") @@ -210,7 +210,7 @@ public DataResponse get_WithPaginationStage(@Context UriInfo uriInfo) { .stage(SelectStage.APPLY_SERVER_PARAMS, c -> RESOURCE_ENTITY_IS_FILTERED = c.getEntity().isFiltered()) .stage(SelectStage.ASSEMBLE_QUERY, - c -> QUERY_PAGE_SIZE = ((SelectQuery)c.getSelect()).getPageSize()) + c -> QUERY_PAGE_SIZE = ((ObjectSelect)c.getSelect()).getPageSize()) .get(); } } @@ -240,7 +240,7 @@ public ProcessingStage, T> queryAssembled( SelectContext context, ProcessingStage, T> next) { - QUERY_PAGE_SIZE = ((SelectQuery)context.getSelect()).getPageSize(); + QUERY_PAGE_SIZE = ((ObjectSelect)context.getSelect()).getPageSize(); return next; } } diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java new file mode 100644 index 000000000..d8566724f --- /dev/null +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java @@ -0,0 +1,79 @@ +package com.nhl.link.rest.it; + +import com.nhl.link.rest.DataResponse; +import com.nhl.link.rest.LinkRest; +import com.nhl.link.rest.it.fixture.JerseyTestOnDerby; +import com.nhl.link.rest.it.fixture.cayenne.E20; +import com.nhl.link.rest.it.fixture.cayenne.E3; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +public class GET_IT_Aggregate extends JerseyTestOnDerby { + + @Override + protected void doAddResources(FeatureContext context) { + context.register(Resource.class); + } + + @Test + public void test_Select_AggregationOnRootEntity() { + + insert("e2", "id, name", "1, 'xxx'"); + insert("e2", "id, name", "2, 'yyy'"); + + Response response = target("/e3") + .queryParam("include", "count()") + .queryParam("include", "name") + .request() + .get(); + + onSuccess(response).bodyEquals(2, "{...TODO...}"); + } + + @Test + public void test_Select_AggregationOnRootEntity_GroupByRelated() { + + insert("e21", "id, name", "1, 'xxx'"); + insert("e21", "id, name", "2, 'yyy'"); + insert("e20", "id, e21_id, age, name", "1, 1, 10, 'aaa'"); + insert("e20", "id, e21_id, age, name", "2, 1, 20, 'bbb'"); + insert("e20", "id, e21_id, age, name", "3, 2, 5, 'ccc'"); + + Response response = target("/e20") + .queryParam("include", "avg(age)") + .queryParam("include", "e21.name") + .request() + .get(); + + onSuccess(response).bodyEquals(2, "{\"id\":8,\"e2\":{\"id\":1}}", "{\"id\":9,\"e2\":{\"id\":1}}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e3") + public DataResponse getE3(@Context UriInfo uriInfo) { + return LinkRest.service(config).select(E3.class).uri(uriInfo).get(); + } + + @GET + @Path("e20") + public DataResponse getE20(@Context UriInfo uriInfo) { + return LinkRest.service(config).select(E20.class).uri(uriInfo).get(); + } + } +} diff --git a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java index e3fc22393..427df294e 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java +++ b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java @@ -9,17 +9,20 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import com.nhl.link.rest.unit.TestWithCayenneMapping; import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; -import org.apache.cayenne.query.SelectQuery; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import java.util.Arrays; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; public class CayenneAssembleQueryStageTest extends TestWithCayenneMapping { @@ -36,26 +39,25 @@ public void testBuildQuery_Ordering() { Ordering o1 = E1.NAME.asc(); Ordering o2 = E1.NAME.desc(); - SelectQuery query = new SelectQuery(E1.class); - query.addOrdering(o1); + ObjectSelect query = ObjectSelect.query(E1.class); + query.orderBy(o1); ResourceEntity resourceEntity = getResourceEntity(E1.class); resourceEntity.getOrderings().add(o2); - SelectContext context = new SelectContext(E1.class); + SelectContext context = new SelectContext<>(E1.class); context.setSelect(query); context.setEntity(resourceEntity); - SelectQuery amended = (SelectQuery) makeQueryStage.buildQuery(context); + ObjectSelect amended = (ObjectSelect) makeQueryStage.buildQuery(context); assertSame(query, amended); assertEquals(2, amended.getOrderings().size()); - assertSame(o1, amended.getOrderings().get(0)); - assertSame(o2, amended.getOrderings().get(1)); + assertTrue(amended.getOrderings().containsAll(Arrays.asList(o1, o2))); } @Test public void testBuildQuery_Prefetches() { - SelectQuery query = new SelectQuery(E2.class); + ObjectSelect query = ObjectSelect.query(E2.class); ResourceEntity resultFilter = getResourceEntity(E2.class); LrRelationship incoming = resultFilter.getLrEntity().getRelationship(E2.E3S.getName()); @@ -63,13 +65,13 @@ public void testBuildQuery_Prefetches() { LrPersistentEntity target = Mockito.mock(LrPersistentEntity.class); resultFilter.getChildren().put(E2.E3S.getName(), new ResourceEntity(target, incoming)); - SelectContext context = new SelectContext(E2.class); + SelectContext context = new SelectContext<>(E2.class); context.setEntity(resultFilter); context.setSelect(query); - SelectQuery amended = (SelectQuery) makeQueryStage.buildQuery(context); + ObjectSelect amended = (ObjectSelect) makeQueryStage.buildQuery(context); assertSame(query, amended); - PrefetchTreeNode rootPrefetch = amended.getPrefetchTree(); + PrefetchTreeNode rootPrefetch = amended.getPrefetches(); assertNotNull(rootPrefetch); assertEquals(1, rootPrefetch.getChildren().size()); @@ -88,27 +90,27 @@ public void testBuildQuery_Pagination() { SelectContext c = new SelectContext(E1.class); c.setEntity(resourceEntity); - SelectQuery q1 = (SelectQuery) makeQueryStage.buildQuery(c); + ObjectSelect q1 = (ObjectSelect) makeQueryStage.buildQuery(c); assertEquals("Pagination in the query for paginated request is expected", 10, q1.getPageSize()); - assertEquals(0, q1.getFetchOffset()); - assertEquals(0, q1.getFetchLimit()); + assertEquals(0, q1.getOffset()); + assertEquals(0, q1.getLimit()); resourceEntity.setFetchLimit(0); resourceEntity.setFetchOffset(0); - SelectQuery q2 = (SelectQuery) makeQueryStage.buildQuery(c); + ObjectSelect q2 = (ObjectSelect) makeQueryStage.buildQuery(c); assertEquals(0, q2.getPageSize()); - assertEquals(0, q2.getFetchOffset()); - assertEquals(0, q2.getFetchLimit()); + assertEquals(0, q2.getOffset()); + assertEquals(0, q2.getLimit()); resourceEntity.setFetchLimit(0); resourceEntity.setFetchOffset(5); - SelectQuery q3 = (SelectQuery) makeQueryStage.buildQuery(c); + ObjectSelect q3 = (ObjectSelect) makeQueryStage.buildQuery(c); assertEquals(0, q3.getPageSize()); - assertEquals(0, q3.getFetchOffset()); - assertEquals(0, q3.getFetchLimit()); + assertEquals(0, q3.getOffset()); + assertEquals(0, q3.getLimit()); } @Test @@ -121,18 +123,18 @@ public void testBuildQuery_Qualifier() { SelectContext c1 = new SelectContext<>(E1.class); c1.setEntity(resourceEntity); - SelectQuery query = (SelectQuery) makeQueryStage.buildQuery(c1); - assertEquals(extraQualifier, query.getQualifier()); + ObjectSelect query = (ObjectSelect) makeQueryStage.buildQuery(c1); + assertEquals(extraQualifier, query.getWhere()); - SelectQuery query2 = new SelectQuery<>(E1.class); - query2.setQualifier(E1.NAME.in("a", "b")); + ObjectSelect query2 = ObjectSelect.query(E1.class); + query2.where(E1.NAME.in("a", "b")); SelectContext c2 = new SelectContext<>(E1.class); c2.setSelect(query2); c2.setEntity(resourceEntity); - SelectQuery query2Amended = (SelectQuery) makeQueryStage.buildQuery(c2); - assertEquals(E1.NAME.in("a", "b").andExp(E1.NAME.eq("X")), query2Amended.getQualifier()); + ObjectSelect query2Amended = (ObjectSelect) makeQueryStage.buildQuery(c2); + assertEquals(E1.NAME.in("a", "b").andExp(E1.NAME.eq("X")), query2Amended.getWhere()); } @Test @@ -142,23 +144,23 @@ public void testById() { c.setId(1); c.setEntity(getResourceEntity(E1.class)); - SelectQuery s1 = (SelectQuery) makeQueryStage.basicSelect(c); + ObjectSelect s1 = (ObjectSelect) makeQueryStage.basicSelect(c); assertNotNull(s1); - assertSame(E1.class, s1.getRoot()); + assertSame(E1.class, s1.getEntityType()); } @Test public void testById_WithQuery() { - SelectQuery select = new SelectQuery(E1.class); + ObjectSelect select = ObjectSelect.query(E1.class); SelectContext c = new SelectContext<>(E1.class); c.setId(1); c.setSelect(select); c.setEntity(getResourceEntity(E1.class)); - SelectQuery s2 = (SelectQuery) makeQueryStage.basicSelect(c); + ObjectSelect s2 = (ObjectSelect) makeQueryStage.basicSelect(c); assertNotNull(s2); assertNotSame(select, s2); - assertSame(E1.class, s2.getRoot()); + assertSame(E1.class, s2.getEntityType()); } } From 1641f3ef17150731b798f3af0677b6c7ce20409a Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Fri, 22 Sep 2017 15:18:09 +0300 Subject: [PATCH 05/15] Aggregation queries support #266 --- .../com/nhl/link/rest/AggregationType.java | 4 +- .../select/CayenneAssembleQueryStage.java | 22 ++++ .../processor/select/QueryBuilder.java | 28 +++++ .../parser/tree/IncludeExcludeProcessor.java | 9 ++ .../AggregateByAttributeProcessor.java | 27 +++++ .../tree/function/AverageProcessor.java | 17 +-- .../tree/function/MaximumProcessor.java | 10 ++ .../tree/function/MinimumProcessor.java | 10 ++ .../parser/tree/function/SumProcessor.java | 10 ++ .../nhl/link/rest/it/GET_IT_Aggregate.java | 102 +++++++++++++++++- 10 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AggregateByAttributeProcessor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MaximumProcessor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MinimumProcessor.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/SumProcessor.java diff --git a/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java b/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java index ca485207d..9aca9ff6c 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java +++ b/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java @@ -6,7 +6,7 @@ public enum AggregationType { SUM, - MIN, + MINIMUM, - MAX + MAXIMUM } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index ebce2fb94..671873543 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -88,6 +88,7 @@ Select buildQuery(SelectContext context) { return query.buildQuery(); } + @SuppressWarnings("unchecked") private void appendAggregateColumns(ResourceEntity entity, QueryBuilder query, Property context) { if (entity.isCountIncluded()) { query.count(); @@ -106,6 +107,18 @@ private void appendAggregateColumns(ResourceEntity entity, QueryBuilder createProperty(Property context, String name, Clas return property; } + @SuppressWarnings("unchecked") + private static Property castProperty(Property property, Class type) { + if (!type.isAssignableFrom(property.getType())) { + throw new LinkRestException(Status.BAD_REQUEST, + "Property '" + property.getName() + "' can not be cast to Property<" + type.getSimpleName() + ">"); + } + return (Property) property; + } + Select basicSelect(SelectContext context) { return new QueryBuilder<>(context).buildQuery(); } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java index f6ccb57d9..1d01e94e6 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java @@ -33,6 +33,9 @@ public class QueryBuilder { private Runnable rootCountSetter; private Consumer> countSetter; private Consumer> avgSetter; + private Consumer> sumSetter; + private Consumer> minSetter; + private Consumer> maxSetter; private Consumer> columnAdder; public QueryBuilder(SelectContext context) { @@ -88,6 +91,7 @@ private void updateSetters(Class queryType) { private void initSetters(Class queryType) { // important: not using method references, because query can be null + // TODO: SelectQuery can be removed if (SelectQuery.class.isAssignableFrom(queryType)) { pageSizeSetter = pageSize -> ((SelectQuery) query).setPageSize(pageSize); qualifierSetter = exp -> ((SelectQuery) query).andQualifier(exp); @@ -96,6 +100,9 @@ private void initSetters(Class queryType) { rootCountSetter = () -> {throw new IllegalStateException("count() is not applicable to SelectQuery");}; countSetter = property -> {throw new IllegalStateException("count() is not applicable to SelectQuery");}; avgSetter = property -> {throw new IllegalStateException("avg() is not applicable to SelectQuery");}; + sumSetter = property -> {throw new IllegalStateException("sum() is not applicable to SelectQuery");}; + minSetter = property -> {throw new IllegalStateException("min() is not applicable to SelectQuery");}; + maxSetter = property -> {throw new IllegalStateException("max() is not applicable to SelectQuery");}; columnAdder = it -> {}; // ignore individual columns } else if (ObjectSelect.class.isAssignableFrom(queryType)) { @@ -106,6 +113,9 @@ private void initSetters(Class queryType) { rootCountSetter = () -> updateQuery(() -> ((ObjectSelect) query).count()); countSetter = property -> updateQuery(() -> ((ObjectSelect) query).count(property)); avgSetter = property -> updateQuery(() -> ((ObjectSelect) query).avg(property)); + sumSetter = property -> updateQuery(() -> ((ObjectSelect) query).sum(property)); + minSetter = property -> updateQuery(() -> ((ObjectSelect) query).min(property)); + maxSetter = property -> updateQuery(() -> ((ObjectSelect) query).max(property)); columnAdder = property -> updateQuery(() -> ((ObjectSelect) query).columns(property)); } else if (ColumnSelect.class.isAssignableFrom(queryType)) { @@ -116,6 +126,9 @@ private void initSetters(Class queryType) { rootCountSetter = () -> updateQuery(() -> ((ColumnSelect) query).count()); countSetter = property -> updateQuery(() -> ((ColumnSelect) query).count(property)); avgSetter = property -> updateQuery(() -> ((ColumnSelect) query).avg(property)); + sumSetter = property -> updateQuery(() -> ((ColumnSelect) query).sum(property)); + minSetter = property -> updateQuery(() -> ((ColumnSelect) query).min(property)); + maxSetter = property -> updateQuery(() -> ((ColumnSelect) query).max(property)); columnAdder = property -> updateQuery(() -> ((ColumnSelect) query).columns(property)); } else { @@ -168,6 +181,21 @@ public QueryBuilder avg(Property property) { return this; } + public QueryBuilder sum(Property property) { + sumSetter.accept(property); + return this; + } + + public QueryBuilder min(Property property) { + minSetter.accept(property); + return this; + } + + public QueryBuilder max(Property property) { + maxSetter.accept(property); + return this; + } + public QueryBuilder column(Property property) { columnAdder.accept(property); return this; diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java index f06680c23..edde3d49a 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeExcludeProcessor.java @@ -8,6 +8,9 @@ import com.nhl.link.rest.runtime.parser.tree.function.AverageProcessor; import com.nhl.link.rest.runtime.parser.tree.function.CountProcessor; import com.nhl.link.rest.runtime.parser.tree.function.FunctionProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.MaximumProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.MinimumProcessor; +import com.nhl.link.rest.runtime.parser.tree.function.SumProcessor; import org.apache.cayenne.di.Inject; import java.util.HashMap; @@ -24,6 +27,9 @@ public class IncludeExcludeProcessor extends BaseRequestProcessor implements ITr private static final String COUNT_FN = "count"; private static final String AVERAGE_FN = "avg"; + private static final String SUM_FN = "sum"; + private static final String MINIMUM_FN = "min"; + private static final String MAXIMUM_FN = "max"; private IncludeWorker includeWorker; private ExcludeWorker excludeWorker; @@ -33,6 +39,9 @@ public IncludeExcludeProcessor(@Inject IJacksonService jacksonService, @Inject I Map functionProcessors = new HashMap<>(); functionProcessors.put(COUNT_FN, new CountProcessor()); functionProcessors.put(AVERAGE_FN, new AverageProcessor()); + functionProcessors.put(SUM_FN, new SumProcessor()); + functionProcessors.put(MINIMUM_FN, new MinimumProcessor()); + functionProcessors.put(MAXIMUM_FN, new MaximumProcessor()); this.includeWorker = new IncludeWorker(jacksonService, sortProcessor, expProcessor, functionProcessors); this.excludeWorker = new ExcludeWorker(jacksonService); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AggregateByAttributeProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AggregateByAttributeProcessor.java new file mode 100644 index 000000000..a19a1f3a2 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AggregateByAttributeProcessor.java @@ -0,0 +1,27 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.AggregationType; +import com.nhl.link.rest.LinkRestException; +import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; + +import javax.ws.rs.core.Response.Status; + +public class AggregateByAttributeProcessor implements FunctionProcessor { + + private final AggregationType aggregationType; + + public AggregateByAttributeProcessor(AggregationType aggregationType) { + this.aggregationType = aggregationType; + } + + @Override + public void apply(ResourceEntity context) { + throw new LinkRestException(Status.BAD_REQUEST, "Function is not applicable in this context"); + } + + @Override + public void apply(ResourceEntity context, LrAttribute attribute) { + context.getAggregatedAttributes(aggregationType).add(attribute); + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java index 1268b16b3..1dfc0d190 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/AverageProcessor.java @@ -1,21 +1,10 @@ package com.nhl.link.rest.runtime.parser.tree.function; import com.nhl.link.rest.AggregationType; -import com.nhl.link.rest.LinkRestException; -import com.nhl.link.rest.ResourceEntity; -import com.nhl.link.rest.meta.LrAttribute; -import javax.ws.rs.core.Response.Status; +public class AverageProcessor extends AggregateByAttributeProcessor { -public class AverageProcessor implements FunctionProcessor { - - @Override - public void apply(ResourceEntity context) { - throw new LinkRestException(Status.BAD_REQUEST, "Function is not applicable in this context"); - } - - @Override - public void apply(ResourceEntity context, LrAttribute attribute) { - context.getAggregatedAttributes(AggregationType.AVERAGE).add(attribute); + public AverageProcessor() { + super(AggregationType.AVERAGE); } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MaximumProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MaximumProcessor.java new file mode 100644 index 000000000..c87323155 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MaximumProcessor.java @@ -0,0 +1,10 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.AggregationType; + +public class MaximumProcessor extends AggregateByAttributeProcessor { + + public MaximumProcessor() { + super(AggregationType.MAXIMUM); + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MinimumProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MinimumProcessor.java new file mode 100644 index 000000000..895c82e67 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/MinimumProcessor.java @@ -0,0 +1,10 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.AggregationType; + +public class MinimumProcessor extends AggregateByAttributeProcessor { + + public MinimumProcessor() { + super(AggregationType.MINIMUM); + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/SumProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/SumProcessor.java new file mode 100644 index 000000000..9d3420dfa --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/function/SumProcessor.java @@ -0,0 +1,10 @@ +package com.nhl.link.rest.runtime.parser.tree.function; + +import com.nhl.link.rest.AggregationType; + +public class SumProcessor extends AggregateByAttributeProcessor { + + public SumProcessor() { + super(AggregationType.SUM); + } +} diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java index d8566724f..142889d08 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java @@ -4,6 +4,7 @@ import com.nhl.link.rest.LinkRest; import com.nhl.link.rest.it.fixture.JerseyTestOnDerby; import com.nhl.link.rest.it.fixture.cayenne.E20; +import com.nhl.link.rest.it.fixture.cayenne.E21; import com.nhl.link.rest.it.fixture.cayenne.E3; import org.junit.Test; @@ -24,7 +25,18 @@ protected void doAddResources(FeatureContext context) { context.register(Resource.class); } - @Test + /** + # Aggregation on the root entity (employee) + # ?include=count()&include=lastName + + data: + - count() : 10 + lastName: Smith + - count() : 1 + lastName: Adamchik + total: 2 + */ +// @Test public void test_Select_AggregationOnRootEntity() { insert("e2", "id, name", "1, 'xxx'"); @@ -39,7 +51,20 @@ public void test_Select_AggregationOnRootEntity() { onSuccess(response).bodyEquals(2, "{...TODO...}"); } - @Test + /** + # Aggregation on the root entity (employee) , including related property + # ?include=avg(salary)&include=department.name + + data: + - avg(salary) : 10000 + department: + name: accounting + - avg(salary) : 20000 + department: + name: it + total: 2 + */ +// @Test public void test_Select_AggregationOnRootEntity_GroupByRelated() { insert("e21", "id, name", "1, 'xxx'"); @@ -54,7 +79,72 @@ public void test_Select_AggregationOnRootEntity_GroupByRelated() { .request() .get(); - onSuccess(response).bodyEquals(2, "{\"id\":8,\"e2\":{\"id\":1}}", "{\"id\":9,\"e2\":{\"id\":1}}"); + onSuccess(response).bodyEquals(2, "{...TODO...}"); + } + + /** + # Aggregation on a related entity (root is department) + # ?include=employees.avg(salary)&include=name + + data: + - name: accounting + "@aggregated:employees": + avg(salary) : 10000 + - name: it + "@aggregated:employees": + employees.avg(salary) : 20000 + total: 2 + */ +// @Test + public void test_Select_AggregationOnRelatedEntity_GroupByRoot() { + + insert("e21", "id, name", "1, 'xxx'"); + insert("e21", "id, name", "2, 'yyy'"); + insert("e20", "id, e21_id, age, name", "1, 1, 10, 'aaa'"); + insert("e20", "id, e21_id, age, name", "2, 1, 20, 'bbb'"); + insert("e20", "id, e21_id, age, name", "3, 2, 5, 'ccc'"); + + Response response = target("/e21") + .queryParam("include", "e20s.sum(age)") + .queryParam("include", "name") + .request() + .get(); + + onSuccess(response).bodyEquals(2, "{...TODO...}"); + } + + /** + # Aggregation on a related entity, grouping by property from that entity (root is department) + # ?include=employees.avg(salary)&include=employees.lastName&include=name + + data: + - name: accounting + "@aggregated:employees": + - avg(salary) : 10000 + lastName: Smith + - avg(salary) : 20000 + lastName: Doe + - name: it + ... + total: 2 + */ +// @Test + public void test_Select_AggregationOnRelatedEntity_GroupByBoth() { + + insert("e21", "id, name", "1, 'xxx'"); + insert("e21", "id, name", "2, 'yyy'"); + insert("e20", "id, e21_id, age, name", "1, 1, 10, 'aaa'"); + insert("e20", "id, e21_id, age, name", "2, 1, 20, 'bbb'"); + insert("e20", "id, e21_id, age, name", "3, 2, 5, 'ccc'"); + + Response response = target("/e21") + .queryParam("include", "e20s.sum(age)") + .queryParam("include", "e20s.name") + .queryParam("include", "name") + .request() + .get(); + + onSuccess(response).bodyEquals(2, "{...TODO...}"); } @Path("") @@ -75,5 +165,11 @@ public DataResponse getE3(@Context UriInfo uriInfo) { public DataResponse getE20(@Context UriInfo uriInfo) { return LinkRest.service(config).select(E20.class).uri(uriInfo).get(); } + + @GET + @Path("e21") + public DataResponse getE21(@Context UriInfo uriInfo) { + return LinkRest.service(config).select(E21.class).uri(uriInfo).get(); + } } } From cd6bd081b6a8fd51d6d54c0fdecfa456d05e8327 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Mon, 25 Sep 2017 17:25:29 +0300 Subject: [PATCH 06/15] Aggregation queries support #266 (back to using SelectQuery) --- .../com/nhl/link/rest/ResourceEntity.java | 6 - .../select/CayenneAssembleQueryStage.java | 103 ++++++++------ .../processor/select/QueryBuilder.java | 134 ++++-------------- .../processor/select/SelectContext.java | 8 +- .../link/rest/it/GET_EncoderFilters_IT.java | 5 +- .../select/CayenneAssembleQueryStageTest.java | 56 ++++---- 6 files changed, 122 insertions(+), 190 deletions(-) diff --git a/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java b/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java index d0c13b772..c28f6b2f1 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java +++ b/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java @@ -265,12 +265,6 @@ public boolean isAggregate() { } } - for (ResourceEntity child : children.values()) { - if (child.isAggregate()) { - return true; - } - } - return false; } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index 671873543..8f33f31bf 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -15,6 +15,7 @@ import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; import org.apache.cayenne.query.Select; +import org.apache.cayenne.query.SelectQuery; import javax.ws.rs.core.Response.Status; import java.util.Map; @@ -40,14 +41,14 @@ protected void doExecute(SelectContext context) { context.setSelect(buildQuery(context)); } - Select buildQuery(SelectContext context) { + SelectQuery buildQuery(SelectContext context) { ResourceEntity entity = context.getEntity(); QueryBuilder query = new QueryBuilder<>(context); - if (entity.isAggregate()) { - appendAggregateColumns(entity, query, null); + if (appendAggregateColumns(entity, query, null)) { + appendGroupByColumns(entity, query, null); } if (!entity.isFiltered()) { @@ -89,51 +90,69 @@ Select buildQuery(SelectContext context) { } @SuppressWarnings("unchecked") - private void appendAggregateColumns(ResourceEntity entity, QueryBuilder query, Property context) { + private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilder query, Property context) { + boolean shouldAppendGroupByColumns = false; + if (entity.isCountIncluded()) { - query.count(); + shouldAppendGroupByColumns = true; + if (context == null) { + query.count(); + } else { + query.count(context); + } + } + + if (entity.isAggregate()) { + shouldAppendGroupByColumns = true; + + for (AggregationType aggregationType : AggregationType.values()) { + entity.getAggregatedAttributes(aggregationType).forEach(attribute -> { + Property property = createProperty(context, attribute.getName(), attribute.getType()); + switch (aggregationType) { + case AVERAGE: { + query.avg(property); + break; + } + case SUM: { + query.sum(castProperty(property, Number.class)); + break; + } + case MINIMUM: { + query.min(property); + break; + } + case MAXIMUM: { + query.max(property); + break; + } + default: { + throw new LinkRestException(Status.INTERNAL_SERVER_ERROR, + "Unsupported aggregation type: " + aggregationType.name()); + } + } + }); + } } + for (Map.Entry> e : entity.getChildren().entrySet()) { + String relationshipName = e.getKey(); + ResourceEntity child = e.getValue(); + Property relationship = createProperty(context, relationshipName, child.getType()); + shouldAppendGroupByColumns = shouldAppendGroupByColumns || appendAggregateColumns(child, query, relationship); + } + + return shouldAppendGroupByColumns; + } + + private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context) { entity.getAttributes().values().stream().filter(a -> !entity.isDefault(a.getName())).forEach(attribute -> { Property property = createProperty(context, attribute.getName(), attribute.getType()); query.column(property); }); - for (AggregationType aggregationType : AggregationType.values()) { - entity.getAggregatedAttributes(aggregationType).forEach(attribute -> { - Property property = createProperty(context, attribute.getName(), attribute.getType()); - switch (aggregationType) { - case AVERAGE: { - query.avg(property); - break; - } - case SUM: { - query.sum(castProperty(property, Number.class)); - break; - } - case MINIMUM: { - query.min(property); - break; - } - case MAXIMUM: { - query.max(property); - break; - } - default: { - throw new LinkRestException(Status.INTERNAL_SERVER_ERROR, - "Unsupported aggregation type: " + aggregationType.name()); - } - } - }); - } - entity.getChildren().forEach((relationshipName, child) -> { Property relationship = createProperty(context, relationshipName, child.getType()); - appendAggregateColumns(child, query, relationship); - - if (child.isCountIncluded()) { - query.count(relationship); - } + appendGroupByColumns(child, query, relationship); }); } @@ -155,10 +174,6 @@ private static Property castProperty(Property property, Class type) return (Property) property; } - Select basicSelect(SelectContext context) { - return new QueryBuilder<>(context).buildQuery(); - } - private void appendPrefetches(PrefetchTreeNode root, ResourceEntity entity, int prefetchSemantics) { for (Map.Entry> e : entity.getChildren().entrySet()) { @@ -180,4 +195,8 @@ private void appendPrefetches(PrefetchTreeNode root, ResourceEntity entity, i appendPrefetches(root, entity.getMapBy(), prefetchSemantics); } } + + SelectQuery basicSelect(SelectContext context) { + return new QueryBuilder<>(context).buildQuery(); + } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java index 1d01e94e6..c166ca99b 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java @@ -9,54 +9,34 @@ import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.exp.Property; -import org.apache.cayenne.query.ColumnSelect; -import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; -import org.apache.cayenne.query.Select; import org.apache.cayenne.query.SelectQuery; import javax.ws.rs.core.Response.Status; import java.util.ArrayList; import java.util.Collection; -import java.util.function.Consumer; -import java.util.function.Supplier; public class QueryBuilder { - private Select query; - - private Consumer pageSizeSetter; - private Consumer qualifierSetter; - private Consumer orderingSetter; - private Consumer prefetchSetter; - private Runnable rootCountSetter; - private Consumer> countSetter; - private Consumer> avgSetter; - private Consumer> sumSetter; - private Consumer> minSetter; - private Consumer> maxSetter; - private Consumer> columnAdder; + private SelectQuery query; public QueryBuilder(SelectContext context) { + Class root = context.getType(); + // use existing query or create a new one + SelectQuery query = context.getSelect(); // selecting by ID overrides any explicit SelectQuery... - if (context.isById()) { - Class root = context.getType(); - this.query = ObjectSelect.query(root) - .where((buildIdQualifer(context.getEntity().getLrEntity(), context.getId()))); - - } else if (context.getSelect() != null) { - this.query = context.getSelect(); - - } else { - this.query = ObjectSelect.query(context.getType()); + if (query == null || context.isById()) { + query = new SelectQuery<>(root); + query.setColumns(new ArrayList<>()); + if (context.isById()) { + query.setQualifier((buildIdQualifer(context.getEntity().getLrEntity(), context.getId()))); + } } - - initSetters(this.query.getClass()); + this.query = query; } private Expression buildIdQualifer(LrEntity entity, LrObjectId id) { - Collection idAttributes = entity.getIds(); if (idAttributes.size() != id.size()) { throw new LinkRestException(Status.BAD_REQUEST, @@ -82,126 +62,66 @@ private Expression buildIdQualifer(LrEntity entity, LrObjectId id) { return ExpressionFactory.and(qualifiers); } - private void updateSetters(Class queryType) { - if (query != null && query.getClass().isAssignableFrom(queryType)) { - return; - } - initSetters(queryType); - } - - private void initSetters(Class queryType) { - // important: not using method references, because query can be null - // TODO: SelectQuery can be removed - if (SelectQuery.class.isAssignableFrom(queryType)) { - pageSizeSetter = pageSize -> ((SelectQuery) query).setPageSize(pageSize); - qualifierSetter = exp -> ((SelectQuery) query).andQualifier(exp); - orderingSetter = ordering -> ((SelectQuery) query).addOrdering(ordering); - prefetchSetter = prefetch -> ((SelectQuery) query).setPrefetchTree(prefetch); - rootCountSetter = () -> {throw new IllegalStateException("count() is not applicable to SelectQuery");}; - countSetter = property -> {throw new IllegalStateException("count() is not applicable to SelectQuery");}; - avgSetter = property -> {throw new IllegalStateException("avg() is not applicable to SelectQuery");}; - sumSetter = property -> {throw new IllegalStateException("sum() is not applicable to SelectQuery");}; - minSetter = property -> {throw new IllegalStateException("min() is not applicable to SelectQuery");}; - maxSetter = property -> {throw new IllegalStateException("max() is not applicable to SelectQuery");}; - columnAdder = it -> {}; // ignore individual columns - - } else if (ObjectSelect.class.isAssignableFrom(queryType)) { - pageSizeSetter = pageSize -> updateQuery(() -> ((ObjectSelect) query).pageSize(pageSize)); - qualifierSetter = exp -> updateQuery(() -> ((ObjectSelect) query).where(exp)); - orderingSetter = ordering -> updateQuery(() -> ((ObjectSelect) query).orderBy(ordering)); - prefetchSetter = prefetch -> updateQuery(() -> ((ObjectSelect) query).prefetch(prefetch)); - rootCountSetter = () -> updateQuery(() -> ((ObjectSelect) query).count()); - countSetter = property -> updateQuery(() -> ((ObjectSelect) query).count(property)); - avgSetter = property -> updateQuery(() -> ((ObjectSelect) query).avg(property)); - sumSetter = property -> updateQuery(() -> ((ObjectSelect) query).sum(property)); - minSetter = property -> updateQuery(() -> ((ObjectSelect) query).min(property)); - maxSetter = property -> updateQuery(() -> ((ObjectSelect) query).max(property)); - columnAdder = property -> updateQuery(() -> ((ObjectSelect) query).columns(property)); - - } else if (ColumnSelect.class.isAssignableFrom(queryType)) { - pageSizeSetter = pageSize -> updateQuery(() -> ((ColumnSelect) query).pageSize(pageSize)); - qualifierSetter = exp -> updateQuery(() -> ((ColumnSelect) query).where(exp)); - orderingSetter = ordering -> updateQuery(() -> ((ColumnSelect) query).orderBy(ordering)); - prefetchSetter = prefetch -> updateQuery(() -> ((ColumnSelect) query).prefetch(prefetch)); - rootCountSetter = () -> updateQuery(() -> ((ColumnSelect) query).count()); - countSetter = property -> updateQuery(() -> ((ColumnSelect) query).count(property)); - avgSetter = property -> updateQuery(() -> ((ColumnSelect) query).avg(property)); - sumSetter = property -> updateQuery(() -> ((ColumnSelect) query).sum(property)); - minSetter = property -> updateQuery(() -> ((ColumnSelect) query).min(property)); - maxSetter = property -> updateQuery(() -> ((ColumnSelect) query).max(property)); - columnAdder = property -> updateQuery(() -> ((ColumnSelect) query).columns(property)); - - } else { - throw new LinkRestException(Status.INTERNAL_SERVER_ERROR, "Unknown query type: " + queryType.getName()); - } - } - - // ObjectSelect and ColumnSelect aggregate builder methods - // return ColumnSelect or ColumnSelect or ColumnSelect, so we need to manually erase the type... - // and the type does not matter in this context anyway - @SuppressWarnings("unchecked") - private > void updateQuery(Supplier updater) { - E updatedQuery = updater.get(); - updateSetters(updatedQuery.getClass()); - query = (Select) updatedQuery; - } - public QueryBuilder pageSize(int pageSize) { - pageSizeSetter.accept(pageSize); + query.setPageSize(pageSize); return this; } public QueryBuilder qualifier(Expression expression) { - qualifierSetter.accept(expression); + query.andQualifier(expression); return this; } public QueryBuilder ordering(Ordering ordering) { - orderingSetter.accept(ordering); + query.addOrdering(ordering); return this; } public QueryBuilder prefetch(PrefetchTreeNode prefetch) { - prefetchSetter.accept(prefetch); + query.addPrefetch(prefetch); return this; } public QueryBuilder count() { - rootCountSetter.run(); + column(Property.COUNT); return this; } public QueryBuilder count(Property property) { - countSetter.accept(property); + column(property.count()); return this; } + @SuppressWarnings("unchecked") public QueryBuilder avg(Property property) { - avgSetter.accept(property); + column(property.avg()); return this; } + @SuppressWarnings("unchecked") public QueryBuilder sum(Property property) { - sumSetter.accept(property); + column(property.sum()); return this; } + @SuppressWarnings("unchecked") public QueryBuilder min(Property property) { - minSetter.accept(property); + column(property.min()); return this; } + @SuppressWarnings("unchecked") public QueryBuilder max(Property property) { - maxSetter.accept(property); + column(property.max()); return this; } public QueryBuilder column(Property property) { - columnAdder.accept(property); + query.getColumns().add(property); return this; } - public Select buildQuery() { + public SelectQuery buildQuery() { return query; } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java index 8bde0c8ee..6e8426f82 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/SelectContext.java @@ -11,7 +11,7 @@ import com.nhl.link.rest.constraints.Constraint; import com.nhl.link.rest.encoder.Encoder; import com.nhl.link.rest.processor.BaseProcessingContext; -import org.apache.cayenne.query.Select; +import org.apache.cayenne.query.SelectQuery; import javax.ws.rs.core.UriInfo; import java.util.Collections; @@ -39,7 +39,7 @@ public class SelectContext extends BaseProcessingContext { private List objects; // TODO: deprecate dependency on Cayenne in generic code - private Select select; + private SelectQuery select; public SelectContext(Class type) { super(type); @@ -141,12 +141,12 @@ public void setConstraint(Constraint constraint) { } // TODO: deprecate dependency on Cayenne in generic code - public Select getSelect() { + public SelectQuery getSelect() { return select; } // TODO: deprecate dependency on Cayenne in generic code - public void setSelect(Select select) { + public void setSelect(SelectQuery select) { this.select = select; } diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java index a05f92e4d..555a6949e 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_EncoderFilters_IT.java @@ -16,7 +16,6 @@ import com.nhl.link.rest.runtime.LinkRestBuilder; import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.Cayenne; -import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.query.SQLTemplate; import org.junit.Test; @@ -210,7 +209,7 @@ public DataResponse get_WithPaginationStage(@Context UriInfo uriInfo) { .stage(SelectStage.APPLY_SERVER_PARAMS, c -> RESOURCE_ENTITY_IS_FILTERED = c.getEntity().isFiltered()) .stage(SelectStage.ASSEMBLE_QUERY, - c -> QUERY_PAGE_SIZE = ((ObjectSelect)c.getSelect()).getPageSize()) + c -> QUERY_PAGE_SIZE = c.getSelect().getPageSize()) .get(); } } @@ -240,7 +239,7 @@ public ProcessingStage, T> queryAssembled( SelectContext context, ProcessingStage, T> next) { - QUERY_PAGE_SIZE = ((ObjectSelect)context.getSelect()).getPageSize(); + QUERY_PAGE_SIZE = context.getSelect().getPageSize(); return next; } } diff --git a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java index 427df294e..364e5afe7 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java +++ b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java @@ -9,7 +9,7 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import com.nhl.link.rest.unit.TestWithCayenneMapping; import org.apache.cayenne.exp.Expression; -import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.query.SelectQuery; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; import org.junit.Before; @@ -39,8 +39,8 @@ public void testBuildQuery_Ordering() { Ordering o1 = E1.NAME.asc(); Ordering o2 = E1.NAME.desc(); - ObjectSelect query = ObjectSelect.query(E1.class); - query.orderBy(o1); + SelectQuery query = SelectQuery.query(E1.class); + query.addOrdering(o1); ResourceEntity resourceEntity = getResourceEntity(E1.class); resourceEntity.getOrderings().add(o2); @@ -49,7 +49,7 @@ public void testBuildQuery_Ordering() { context.setSelect(query); context.setEntity(resourceEntity); - ObjectSelect amended = (ObjectSelect) makeQueryStage.buildQuery(context); + SelectQuery amended = makeQueryStage.buildQuery(context); assertSame(query, amended); assertEquals(2, amended.getOrderings().size()); assertTrue(amended.getOrderings().containsAll(Arrays.asList(o1, o2))); @@ -57,7 +57,7 @@ public void testBuildQuery_Ordering() { @Test public void testBuildQuery_Prefetches() { - ObjectSelect query = ObjectSelect.query(E2.class); + SelectQuery query = SelectQuery.query(E2.class); ResourceEntity resultFilter = getResourceEntity(E2.class); LrRelationship incoming = resultFilter.getLrEntity().getRelationship(E2.E3S.getName()); @@ -69,9 +69,9 @@ public void testBuildQuery_Prefetches() { context.setEntity(resultFilter); context.setSelect(query); - ObjectSelect amended = (ObjectSelect) makeQueryStage.buildQuery(context); + SelectQuery amended = makeQueryStage.buildQuery(context); assertSame(query, amended); - PrefetchTreeNode rootPrefetch = amended.getPrefetches(); + PrefetchTreeNode rootPrefetch = amended.getPrefetchTree(); assertNotNull(rootPrefetch); assertEquals(1, rootPrefetch.getChildren().size()); @@ -87,30 +87,30 @@ public void testBuildQuery_Pagination() { resourceEntity.setFetchLimit(10); resourceEntity.setFetchOffset(0); - SelectContext c = new SelectContext(E1.class); + SelectContext c = new SelectContext<>(E1.class); c.setEntity(resourceEntity); - ObjectSelect q1 = (ObjectSelect) makeQueryStage.buildQuery(c); + SelectQuery q1 = makeQueryStage.buildQuery(c); assertEquals("Pagination in the query for paginated request is expected", 10, q1.getPageSize()); - assertEquals(0, q1.getOffset()); - assertEquals(0, q1.getLimit()); + assertEquals(0, q1.getFetchOffset()); + assertEquals(0, q1.getFetchLimit()); resourceEntity.setFetchLimit(0); resourceEntity.setFetchOffset(0); - ObjectSelect q2 = (ObjectSelect) makeQueryStage.buildQuery(c); + SelectQuery q2 = makeQueryStage.buildQuery(c); assertEquals(0, q2.getPageSize()); - assertEquals(0, q2.getOffset()); - assertEquals(0, q2.getLimit()); + assertEquals(0, q2.getFetchOffset()); + assertEquals(0, q2.getFetchLimit()); resourceEntity.setFetchLimit(0); resourceEntity.setFetchOffset(5); - ObjectSelect q3 = (ObjectSelect) makeQueryStage.buildQuery(c); + SelectQuery q3 = makeQueryStage.buildQuery(c); assertEquals(0, q3.getPageSize()); - assertEquals(0, q3.getOffset()); - assertEquals(0, q3.getLimit()); + assertEquals(0, q3.getFetchOffset()); + assertEquals(0, q3.getFetchLimit()); } @Test @@ -123,18 +123,18 @@ public void testBuildQuery_Qualifier() { SelectContext c1 = new SelectContext<>(E1.class); c1.setEntity(resourceEntity); - ObjectSelect query = (ObjectSelect) makeQueryStage.buildQuery(c1); - assertEquals(extraQualifier, query.getWhere()); + SelectQuery query = makeQueryStage.buildQuery(c1); + assertEquals(extraQualifier, query.getQualifier()); - ObjectSelect query2 = ObjectSelect.query(E1.class); - query2.where(E1.NAME.in("a", "b")); + SelectQuery query2 = SelectQuery.query(E1.class); + query2.setQualifier(E1.NAME.in("a", "b")); SelectContext c2 = new SelectContext<>(E1.class); c2.setSelect(query2); c2.setEntity(resourceEntity); - ObjectSelect query2Amended = (ObjectSelect) makeQueryStage.buildQuery(c2); - assertEquals(E1.NAME.in("a", "b").andExp(E1.NAME.eq("X")), query2Amended.getWhere()); + SelectQuery query2Amended = makeQueryStage.buildQuery(c2); + assertEquals(E1.NAME.in("a", "b").andExp(E1.NAME.eq("X")), query2Amended.getQualifier()); } @Test @@ -144,23 +144,23 @@ public void testById() { c.setId(1); c.setEntity(getResourceEntity(E1.class)); - ObjectSelect s1 = (ObjectSelect) makeQueryStage.basicSelect(c); + SelectQuery s1 = makeQueryStage.basicSelect(c); assertNotNull(s1); - assertSame(E1.class, s1.getEntityType()); + assertSame(E1.class, s1.getRoot()); } @Test public void testById_WithQuery() { - ObjectSelect select = ObjectSelect.query(E1.class); + SelectQuery select = SelectQuery.query(E1.class); SelectContext c = new SelectContext<>(E1.class); c.setId(1); c.setSelect(select); c.setEntity(getResourceEntity(E1.class)); - ObjectSelect s2 = (ObjectSelect) makeQueryStage.basicSelect(c); + SelectQuery s2 = makeQueryStage.basicSelect(c); assertNotNull(s2); assertNotSame(select, s2); - assertSame(E1.class, s2.getEntityType()); + assertSame(E1.class, s2.getRoot()); } } From ee47e428a7c9e5f96fc970724428640f1fb94a48 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Tue, 26 Sep 2017 14:22:59 +0300 Subject: [PATCH 07/15] Aggregation queries support #266 (new test case) --- .../select/CayenneAssembleQueryStage.java | 27 +++++++++--- .../select/CayenneFetchDataStage.java | 4 +- .../rest/runtime/encoder/EncoderService.java | 6 +-- .../nhl/link/rest/it/GET_IT_Aggregate.java | 42 +++++++++++++++++-- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index 8f33f31bf..73f36be79 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -3,22 +3,25 @@ import com.nhl.link.rest.AggregationType; import com.nhl.link.rest.LinkRestException; import com.nhl.link.rest.ResourceEntity; +import com.nhl.link.rest.meta.LrAttribute; import com.nhl.link.rest.meta.LrPersistentEntity; import com.nhl.link.rest.processor.Processor; import com.nhl.link.rest.processor.ProcessorOutcome; import com.nhl.link.rest.runtime.cayenne.ICayennePersister; import com.nhl.link.rest.runtime.processor.select.SelectContext; +import org.apache.cayenne.Persistent; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.Property; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; -import org.apache.cayenne.query.Select; import org.apache.cayenne.query.SelectQuery; import javax.ws.rs.core.Response.Status; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * @since 2.7 @@ -144,11 +147,25 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde return shouldAppendGroupByColumns; } + @SuppressWarnings("unchecked") private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context) { - entity.getAttributes().values().stream().filter(a -> !entity.isDefault(a.getName())).forEach(attribute -> { - Property property = createProperty(context, attribute.getName(), attribute.getType()); - query.column(property); - }); + List groupByAttributes = entity.getAttributes().values().stream() + .filter(a -> !entity.isDefault(a.getName())) + .collect(Collectors.toList()); + + if (groupByAttributes.isEmpty()) { + if (context == null) { + // root entity + query.column(Property.createSelf((Class) entity.getType())); // does this imply grouping by ID or all attributes? + } else { + // TODO: group by the single- or multi-column PK + } + } else { + groupByAttributes.forEach(attribute -> { + Property property = createProperty(context, attribute.getName(), attribute.getType()); + query.column(property); + }); + } entity.getChildren().forEach((relationshipName, child) -> { Property relationship = createProperty(context, relationshipName, child.getType()); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java index 0bc86733c..235950ffc 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java @@ -7,7 +7,7 @@ import com.nhl.link.rest.runtime.cayenne.ICayennePersister; import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Inject; -import org.apache.cayenne.query.Select; +import org.apache.cayenne.query.SelectQuery; import javax.ws.rs.core.Response; import java.util.List; @@ -34,7 +34,7 @@ public ProcessorOutcome execute(SelectContext context) { } protected void doExecute(SelectContext context) { - Select select = context.getSelect(); + SelectQuery select = context.getSelect(); List objects = persister.sharedContext().select(select); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java index c3438025d..ec3932105 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java @@ -117,7 +117,7 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { // ensure we sort property encoders alphabetically for cleaner JSON // output - Map attributeEncoders = new TreeMap(); + Map attributeEncoders = new TreeMap<>(); for (LrAttribute attribute : resourceEntity.getAttributes().values()) { EntityProperty property = attributeEncoderFactory.getAttributeProperty(resourceEntity.getLrEntity(), @@ -125,7 +125,7 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { attributeEncoders.put(attribute.getName(), property); } - Map relationshipEncoders = new TreeMap(); + Map relationshipEncoders = new TreeMap<>(); for (Entry> e : resourceEntity.getChildren().entrySet()) { LrRelationship relationship = resourceEntity.getLrEntity().getRelationship(e.getKey()); @@ -137,7 +137,7 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { relationshipEncoders.put(e.getKey(), property); } - Map extraEncoders = new TreeMap(); + Map extraEncoders = new TreeMap<>(); extraEncoders.putAll(resourceEntity.getExtraProperties()); diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java index 142889d08..ff409a945 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java @@ -115,15 +115,15 @@ public void test_Select_AggregationOnRelatedEntity_GroupByRoot() { /** # Aggregation on a related entity, grouping by property from that entity (root is department) - # ?include=employees.avg(salary)&include=employees.lastName&include=name + # ?include=employees.avg(salary)&include=employees.lastName data: - name: accounting "@aggregated:employees": - avg(salary) : 10000 - lastName: Smith + lastName: Smith - avg(salary) : 20000 - lastName: Doe + lastName: Doe - name: it ... total: 2 @@ -147,6 +147,42 @@ public void test_Select_AggregationOnRelatedEntity_GroupByBoth() { onSuccess(response).bodyEquals(2, "{...TODO...}"); } + /** + # Aggregation on a related entity, grouping by property from that entity (root is department) + # ?include=employees.avg(salary)&include=employees.lastName&include=name + + data: + - id: ... + name: accounting + ... + "@aggregated:employees": + - avg(salary) : 10000 + lastName: Smith + - avg(salary) : 20000 + lastName: Doe + - id: ... + name: it + ... + total: 2 + */ +// @Test + public void test_Select_AggregationOnRelatedEntity_GroupRelated_IncludeRoot() { + + insert("e21", "id, name", "1, 'xxx'"); + insert("e21", "id, name", "2, 'yyy'"); + insert("e20", "id, e21_id, age, name", "1, 1, 10, 'aaa'"); + insert("e20", "id, e21_id, age, name", "2, 1, 20, 'bbb'"); + insert("e20", "id, e21_id, age, name", "3, 2, 5, 'ccc'"); + + Response response = target("/e21") + .queryParam("include", "e20s.sum(age)") + .queryParam("include", "e20s.name") + .request() + .get(); + + onSuccess(response).bodyEquals(2, "{...TODO...}"); + } + @Path("") @Produces(MediaType.APPLICATION_JSON) public static class Resource { From bf981d110333c8329ef9b046d96adee80db69637 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Wed, 27 Sep 2017 12:56:50 +0300 Subject: [PATCH 08/15] Aggregation queries support #266 (3 test cases passing) --- .../com/nhl/link/rest/ResourceEntity.java | 15 +- .../select/CayenneAssembleQueryStage.java | 185 +++++++++++++++--- .../select/CayenneFetchDataStage.java | 13 +- .../processor/select/QueryBuilder.java | 25 +++ .../rest/runtime/encoder/EncoderService.java | 36 +++- .../runtime/parser/tree/PathProcessor.java | 17 +- .../select/ApplyServerParamsStage.java | 12 +- .../nhl/link/rest/it/GET_IT_Aggregate.java | 26 +-- 8 files changed, 277 insertions(+), 52 deletions(-) diff --git a/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java b/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java index c28f6b2f1..e649bf882 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java +++ b/link-rest/src/main/java/com/nhl/link/rest/ResourceEntity.java @@ -11,6 +11,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; /** @@ -32,12 +33,13 @@ public class ResourceEntity { private Map attributes; private Collection defaultProperties; - private Map> aggregatedAttributes; + private Map> aggregatedAttributes; private String applicationBase; private String mapByPath; private ResourceEntity mapBy; private Map> children; + private Map> aggregateChildren; private LrRelationship incoming; private Collection orderings; private Expression qualifier; @@ -52,6 +54,7 @@ public ResourceEntity(LrEntity lrEntity) { this.defaultProperties = new HashSet<>(); this.aggregatedAttributes = new HashMap<>(); this.children = new HashMap<>(); + this.aggregateChildren = new HashMap<>(); this.orderings = new ArrayList<>(2); this.extraProperties = new HashMap<>(); this.lrEntity = lrEntity; @@ -73,6 +76,10 @@ public LrRelationship getIncoming() { return incoming; } + public void setIncoming(LrRelationship incoming) { + this.incoming = incoming; + } + public Expression getQualifier() { return qualifier; } @@ -131,6 +138,10 @@ public ResourceEntity getChild(String name) { return children.get(name); } + public Map> getAggregateChildren() { + return aggregateChildren; + } + public Map getExtraProperties() { return extraProperties; } @@ -250,7 +261,7 @@ public boolean isCountIncluded() { return countIncluded; } - public Collection getAggregatedAttributes(AggregationType aggregationType) { + public List getAggregatedAttributes(AggregationType aggregationType) { return aggregatedAttributes.computeIfAbsent(aggregationType, it -> new ArrayList<>()); } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index 73f36be79..941c6b20c 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -4,24 +4,28 @@ import com.nhl.link.rest.LinkRestException; import com.nhl.link.rest.ResourceEntity; import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.meta.LrEntity; import com.nhl.link.rest.meta.LrPersistentEntity; +import com.nhl.link.rest.meta.LrRelationship; import com.nhl.link.rest.processor.Processor; import com.nhl.link.rest.processor.ProcessorOutcome; +import com.nhl.link.rest.property.DataObjectPropertyReader; +import com.nhl.link.rest.property.PropertyReader; import com.nhl.link.rest.runtime.cayenne.ICayennePersister; import com.nhl.link.rest.runtime.processor.select.SelectContext; -import org.apache.cayenne.Persistent; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.Property; +import org.apache.cayenne.exp.parser.ASTCount; +import org.apache.cayenne.exp.parser.ASTPath; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; import org.apache.cayenne.query.SelectQuery; import javax.ws.rs.core.Response.Status; -import java.util.List; +import java.util.ListIterator; import java.util.Map; -import java.util.stream.Collectors; /** * @since 2.7 @@ -51,6 +55,7 @@ SelectQuery buildQuery(SelectContext context) { QueryBuilder query = new QueryBuilder<>(context); if (appendAggregateColumns(entity, query, null)) { + entity.excludeId(); // TODO: remove appendGroupByColumns(entity, query, null); } @@ -98,18 +103,15 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde if (entity.isCountIncluded()) { shouldAppendGroupByColumns = true; - if (context == null) { - query.count(); - } else { - query.count(context); - } } if (entity.isAggregate()) { shouldAppendGroupByColumns = true; for (AggregationType aggregationType : AggregationType.values()) { - entity.getAggregatedAttributes(aggregationType).forEach(attribute -> { + ListIterator iter = entity.getAggregatedAttributes(aggregationType).listIterator(); + while (iter.hasNext()) { + LrAttribute attribute = iter.next(); Property property = createProperty(context, attribute.getName(), attribute.getType()); switch (aggregationType) { case AVERAGE: { @@ -133,11 +135,20 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde "Unsupported aggregation type: " + aggregationType.name()); } } - }); + + iter.set(columnAttribute(attribute, query.columnCount() - 1)); + } } } - for (Map.Entry> e : entity.getChildren().entrySet()) { +// for (Map.Entry> e : entity.getAggregateChildren().entrySet()) { +// String relationshipName = e.getKey(); +// ResourceEntity child = e.getValue(); +// Property relationship = createProperty(context, relationshipName, child.getType()); +// shouldAppendGroupByColumns = shouldAppendGroupByColumns || appendAggregateColumns(child, query, relationship); +// } + + for (Map.Entry> e : entity.getAggregateChildren().entrySet()) { String relationshipName = e.getKey(); ResourceEntity child = e.getValue(); Property relationship = createProperty(context, relationshipName, child.getType()); @@ -149,30 +160,62 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde @SuppressWarnings("unchecked") private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context) { - List groupByAttributes = entity.getAttributes().values().stream() - .filter(a -> !entity.isDefault(a.getName())) - .collect(Collectors.toList()); + boolean shouldIncludeSelf = true; + + for (Map.Entry e : entity.getAttributes().entrySet()) { + LrAttribute attribute = e.getValue(); + if (entity.isDefault(attribute.getName())) { + continue; + } + + Property property = createProperty(context, attribute.getName(), attribute.getType()); + query.column(property); + + e.setValue(columnAttribute(attribute, query.columnCount() - 1)); + shouldIncludeSelf = false; + } + + // do this after all attributes have been added, because we'll add one more fictional attribute for encoding purposes + if (entity.isCountIncluded()) { + if (context == null) { + query.count(); + } else { + query.count(context); + } + entity.getAttributes().put("count()", columnAttribute(COUNT_ATTRIBUTE, query.columnCount() - 1)); + shouldIncludeSelf = false; + } - if (groupByAttributes.isEmpty()) { + if (shouldIncludeSelf) { + // no groupBy columns were explicitly included if (context == null) { // root entity - query.column(Property.createSelf((Class) entity.getType())); // does this imply grouping by ID or all attributes? + query.includeSelf(); + swapPropertyReadersToSelf(entity); } else { - // TODO: group by the single- or multi-column PK + // TODO: group by the single- or multi-column PK? or Cayenne takes care of that? } - } else { - groupByAttributes.forEach(attribute -> { - Property property = createProperty(context, attribute.getName(), attribute.getType()); - query.column(property); - }); } - entity.getChildren().forEach((relationshipName, child) -> { + entity.getAggregateChildren().forEach((relationshipName, child) -> { Property relationship = createProperty(context, relationshipName, child.getType()); appendGroupByColumns(child, query, relationship); }); } + private static void swapPropertyReadersToSelf(ResourceEntity entity) { + for (Map.Entry e : entity.getAttributes().entrySet()) { + e.setValue(selfAttribute(e.getValue())); + } + + entity.getAggregateChildren().values().forEach(child -> { + LrRelationship incoming = child.getIncoming(); + if (incoming != null) { + child.setIncoming(selfRelationship(incoming)); + } + }); + } + @SuppressWarnings("unchecked") private static Property createProperty(Property context, String name, Class type) { Property property = Property.create(name, (Class) type); @@ -191,6 +234,102 @@ private static Property castProperty(Property property, Class type) return (Property) property; } + private static LrAttribute columnAttribute(LrAttribute attribute, int columnIndex) { + PropertyReader reader = PropertyReader.forValueProducer((Object[] row) -> row[columnIndex]); + return decoratedAttribute(attribute, reader); + } + + private static LrAttribute selfAttribute(LrAttribute attribute) { + PropertyReader delegate = (attribute.getPropertyReader() == null) ? + DataObjectPropertyReader.reader() : attribute.getPropertyReader(); + PropertyReader reader = (root, name) -> { + Object[] row = (Object[]) root; + return delegate.value(row[0], name); + }; + return decoratedAttribute(attribute, reader); + } + + private static LrRelationship selfRelationship(LrRelationship relationship) { + PropertyReader delegate = (relationship.getPropertyReader() == null) ? + DataObjectPropertyReader.reader() : relationship.getPropertyReader(); + PropertyReader reader = (root, name) -> { + Object[] row = (Object[]) root; + name = name.substring("@aggregated:".length()); + return delegate.value(row[0], name); + }; + return decoratedRelationship(relationship, reader); + } + + private static LrAttribute COUNT_ATTRIBUTE = new LrAttribute() { + @Override + public String getName() { + return "count()"; + } + + @Override + public Class getType() { + return Long.class; + } + + @Override + public ASTPath getPathExp() { + return null; + } + + @Override + public PropertyReader getPropertyReader() { + return null; + } + }; + + private static LrAttribute decoratedAttribute(LrAttribute delegate, PropertyReader reader) { + return new LrAttribute() { + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public Class getType() { + return delegate.getType(); + } + + @Override + public ASTPath getPathExp() { + return delegate.getPathExp(); + } + + @Override + public PropertyReader getPropertyReader() { + return reader; + } + }; + } + + private static LrRelationship decoratedRelationship(LrRelationship delegate, PropertyReader reader) { + return new LrRelationship() { + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public LrEntity getTargetEntity() { + return delegate.getTargetEntity(); + } + + @Override + public boolean isToMany() { + return delegate.isToMany(); + } + + @Override + public PropertyReader getPropertyReader() { + return reader; + } + }; + } + private void appendPrefetches(PrefetchTreeNode root, ResourceEntity entity, int prefetchSemantics) { for (Map.Entry> e : entity.getChildren().entrySet()) { diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java index 235950ffc..cf487980d 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java @@ -5,6 +5,7 @@ import com.nhl.link.rest.processor.Processor; import com.nhl.link.rest.processor.ProcessorOutcome; import com.nhl.link.rest.runtime.cayenne.ICayennePersister; +import com.nhl.link.rest.runtime.encoder.IEncoderService; import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Inject; import org.apache.cayenne.query.SelectQuery; @@ -18,13 +19,16 @@ public class CayenneFetchDataStage implements Processor> { private ICayennePersister persister; + private IEncoderService encoderService; - public CayenneFetchDataStage(@Inject ICayennePersister persister) { + public CayenneFetchDataStage(@Inject ICayennePersister persister, + @Inject IEncoderService encoderService) { // Store persister, don't extract ObjectContext from it right away. // Such deferred initialization may help building POJO pipelines. this.persister = persister; + this.encoderService = encoderService; } @Override @@ -51,5 +55,12 @@ protected void doExecute(SelectContext context) { } } context.setObjects(objects); + + // make sure we create the encoder, even if we end up with an empty + // list, as we need to encode the totals + + if (context.getEncoder() == null) { + context.setEncoder(encoderService.dataEncoder(context.getEntity())); + } } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java index c166ca99b..179ed2e77 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java @@ -6,6 +6,7 @@ import com.nhl.link.rest.meta.LrEntity; import com.nhl.link.rest.meta.LrPersistentAttribute; import com.nhl.link.rest.runtime.processor.select.SelectContext; +import org.apache.cayenne.Persistent; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.exp.Property; @@ -15,7 +16,10 @@ import javax.ws.rs.core.Response.Status; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.Objects; public class QueryBuilder { @@ -121,6 +125,27 @@ public QueryBuilder column(Property property) { return this; } + @SuppressWarnings("unchecked") + public QueryBuilder includeSelf() { + Property self = Property.createSelf((Class) query.getRoot()); + + List> columns = (List>) query.getColumns(); + if (columns.isEmpty()) { + columns.add(self); + } else { + Property[] array = columns.toArray(new Property[columns.size() + 1]); + System.arraycopy(array, 0, array, 1, columns.size()); + array[0] = self; + query.setColumns(new ArrayList<>(Arrays.asList(array))); + } + + return this; + } + + public int columnCount() { + return Objects.requireNonNull(query.getColumns()).size(); + } + public SelectQuery buildQuery() { return query; } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java index ec3932105..aef8b8bb5 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java @@ -1,5 +1,7 @@ package com.nhl.link.rest.runtime.encoder; +import com.fasterxml.jackson.core.JsonGenerator; +import com.nhl.link.rest.AggregationType; import com.nhl.link.rest.EntityProperty; import com.nhl.link.rest.ResourceEntity; import com.nhl.link.rest.encoder.*; @@ -9,6 +11,7 @@ import com.nhl.link.rest.runtime.semantics.IRelationshipMapper; import org.apache.cayenne.di.Inject; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -125,9 +128,19 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { attributeEncoders.put(attribute.getName(), property); } + for (AggregationType aggregationType : AggregationType.values()) { + resourceEntity.getAggregatedAttributes(aggregationType).forEach(attribute -> { + EntityProperty property = attributeEncoderFactory.getAttributeProperty(resourceEntity.getLrEntity(), + attribute); + String key = aggregationType.name().toLowerCase() + "(" + attribute.getName() + ")"; + attributeEncoders.put(key, property); + }); + } + Map relationshipEncoders = new TreeMap<>(); for (Entry> e : resourceEntity.getChildren().entrySet()) { - LrRelationship relationship = resourceEntity.getLrEntity().getRelationship(e.getKey()); + ResourceEntity child = e.getValue(); + LrRelationship relationship = child.getIncoming(); Encoder encoder = relationship.isToMany() ? nestedToManyEncoder(e.getValue()) : toOneEncoder(e.getValue(), relationship); @@ -141,6 +154,27 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { extraEncoders.putAll(resourceEntity.getExtraProperties()); + for (Entry> e : resourceEntity.getAggregateChildren().entrySet()) { + ResourceEntity child = e.getValue(); + Encoder encoder = entityEncoder(child); + relationshipEncoders.put("@aggregated:" + e.getKey(), new EntityProperty() { + @Override + public void encode(Object root, String propertyName, JsonGenerator out) throws IOException { + encoder.encode("@aggregated:" + e.getKey(), root, out); + } + + @Override + public Object read(Object root, String propertyName) { + throw new UnsupportedOperationException(); + } + + @Override + public int visit(Object object, String propertyName, EncoderVisitor visitor) { + throw new UnsupportedOperationException(); + } + }); + } + EntityProperty idEncoder = resourceEntity.isIdIncluded() ? attributeEncoderFactory.getIdProperty(resourceEntity) : PropertyBuilder.doNothingProperty(); return new EntityEncoder(idEncoder, attributeEncoders, relationshipEncoders, extraEncoders); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java index 2b7e41dcd..9e6a36890 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/PathProcessor.java @@ -35,19 +35,30 @@ public ResourceEntity processPath(ResourceEntity root, String path, PathVi // first we must check if the path is a relationship LrRelationship relationship = lrEntity.getRelationship(property); if (relationship != null) { - ResourceEntity childEntity = root.getChild(property); + ResourceEntity childEntity = root.getChildren().get(property); + if (childEntity == null && dot > 0) { + childEntity = root.getAggregateChildren().get(property); + } + if (childEntity == null) { LrEntity targetType = relationship.getTargetEntity(); childEntity = new ResourceEntity<>(targetType, relationship); root.getChildren().put(property, childEntity); } + ResourceEntity result; if (dot > 0) { - return processPath(childEntity, path.substring(dot + 1), visitor); + result = processPath(childEntity, path.substring(dot + 1), visitor); } else { visitor.visitRelationship(root, childEntity, relationship); - return childEntity; + result = childEntity; + } + + if (childEntity.isAggregate()) { + root.getChildren().remove(property); + root.getAggregateChildren().put(property, childEntity); } + return result; } // if the path is not a relationship, then we must check it does not contain separators (dots) diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java index 9dbe63449..30ba2ca8d 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java @@ -5,7 +5,6 @@ import com.nhl.link.rest.processor.Processor; import com.nhl.link.rest.processor.ProcessorOutcome; import com.nhl.link.rest.runtime.constraints.IConstraintsHandler; -import com.nhl.link.rest.runtime.encoder.IEncoderService; import org.apache.cayenne.di.Inject; import java.util.List; @@ -16,16 +15,14 @@ public class ApplyServerParamsStage implements Processor> { private IConstraintsHandler constraintsHandler; - private IEncoderService encoderService; + private List filters; public ApplyServerParamsStage( @Inject IConstraintsHandler constraintsHandler, - @Inject IEncoderService encoderService, @Inject List filters) { this.constraintsHandler = constraintsHandler; - this.encoderService = encoderService; this.filters = filters; } @@ -51,12 +48,5 @@ protected void doExecute(SelectContext context) { break; } } - - // make sure we create the encoder, even if we end up with an empty - // list, as we need to encode the totals - - if (context.getEncoder() == null) { - context.setEncoder(encoderService.dataEncoder(entity)); - } } } \ No newline at end of file diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java index ff409a945..b9921b203 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java @@ -3,9 +3,9 @@ import com.nhl.link.rest.DataResponse; import com.nhl.link.rest.LinkRest; import com.nhl.link.rest.it.fixture.JerseyTestOnDerby; +import com.nhl.link.rest.it.fixture.cayenne.E2; import com.nhl.link.rest.it.fixture.cayenne.E20; import com.nhl.link.rest.it.fixture.cayenne.E21; -import com.nhl.link.rest.it.fixture.cayenne.E3; import org.junit.Test; import javax.ws.rs.GET; @@ -36,19 +36,20 @@ protected void doAddResources(FeatureContext context) { lastName: Adamchik total: 2 */ -// @Test + @Test public void test_Select_AggregationOnRootEntity() { insert("e2", "id, name", "1, 'xxx'"); insert("e2", "id, name", "2, 'yyy'"); + insert("e2", "id, name", "3, 'yyy'"); - Response response = target("/e3") + Response response = target("/e2") .queryParam("include", "count()") .queryParam("include", "name") .request() .get(); - onSuccess(response).bodyEquals(2, "{...TODO...}"); + onSuccess(response).bodyEquals(2, "{\"count()\":1,\"name\":\"xxx\"},{\"count()\":2,\"name\":\"yyy\"}"); } /** @@ -95,7 +96,7 @@ public void test_Select_AggregationOnRootEntity_GroupByRelated() { employees.avg(salary) : 20000 total: 2 */ -// @Test + @Test public void test_Select_AggregationOnRelatedEntity_GroupByRoot() { insert("e21", "id, name", "1, 'xxx'"); @@ -110,7 +111,8 @@ public void test_Select_AggregationOnRelatedEntity_GroupByRoot() { .request() .get(); - onSuccess(response).bodyEquals(2, "{...TODO...}"); + onSuccess(response).bodyEquals(2, "{\"@aggregated:e20s\":{\"sum(age)\":5},\"name\":\"yyy\"}," + + "{\"@aggregated:e20s\":{\"sum(age)\":30},\"name\":\"xxx\"}"); } /** @@ -128,7 +130,7 @@ public void test_Select_AggregationOnRelatedEntity_GroupByRoot() { ... total: 2 */ -// @Test + @Test public void test_Select_AggregationOnRelatedEntity_GroupByBoth() { insert("e21", "id, name", "1, 'xxx'"); @@ -144,7 +146,9 @@ public void test_Select_AggregationOnRelatedEntity_GroupByBoth() { .request() .get(); - onSuccess(response).bodyEquals(2, "{...TODO...}"); + onSuccess(response).bodyEquals(3, "{\"@aggregated:e20s\":{\"name\":\"ccc\",\"sum(age)\":5},\"name\":\"yyy\"}," + + "{\"@aggregated:e20s\":{\"name\":\"aaa\",\"sum(age)\":10},\"name\":\"xxx\"}," + + "{\"@aggregated:e20s\":{\"name\":\"bbb\",\"sum(age)\":20},\"name\":\"xxx\"}"); } /** @@ -191,9 +195,9 @@ public static class Resource { private Configuration config; @GET - @Path("e3") - public DataResponse getE3(@Context UriInfo uriInfo) { - return LinkRest.service(config).select(E3.class).uri(uriInfo).get(); + @Path("e2") + public DataResponse getE2(@Context UriInfo uriInfo) { + return LinkRest.service(config).select(E2.class).uri(uriInfo).get(); } @GET From 2a3cea9960ed6ce58e9d213173b114ec156f7f90 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Wed, 27 Sep 2017 16:25:26 +0300 Subject: [PATCH 09/15] Aggregation queries support #266 (4 test cases passing) --- .../com/nhl/link/rest/AggregationType.java | 18 ++- .../select/CayenneAssembleQueryStage.java | 148 +++++++++++------- .../rest/runtime/encoder/EncoderService.java | 38 ++++- .../runtime/parser/tree/IncludeWorker.java | 2 +- .../nhl/link/rest/it/GET_IT_Aggregate.java | 17 +- 5 files changed, 150 insertions(+), 73 deletions(-) diff --git a/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java b/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java index 9aca9ff6c..4f5a86dce 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java +++ b/link-rest/src/main/java/com/nhl/link/rest/AggregationType.java @@ -2,11 +2,21 @@ public enum AggregationType { - AVERAGE, + AVERAGE("avg"), - SUM, + SUM("sum"), - MINIMUM, + MINIMUM("min"), - MAXIMUM + MAXIMUM("max"); + + private String functionName; + + AggregationType(String functionName) { + this.functionName = functionName; + } + + public String functionName() { + return functionName; + } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index 941c6b20c..0d834f55c 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -13,10 +13,10 @@ import com.nhl.link.rest.property.PropertyReader; import com.nhl.link.rest.runtime.cayenne.ICayennePersister; import com.nhl.link.rest.runtime.processor.select.SelectContext; +import org.apache.cayenne.DataObject; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.Property; -import org.apache.cayenne.exp.parser.ASTCount; import org.apache.cayenne.exp.parser.ASTPath; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Ordering; @@ -56,7 +56,7 @@ SelectQuery buildQuery(SelectContext context) { if (appendAggregateColumns(entity, query, null)) { entity.excludeId(); // TODO: remove - appendGroupByColumns(entity, query, null); + appendGroupByColumns(entity, query, null, entity.isAggregate()); } if (!entity.isFiltered()) { @@ -141,13 +141,6 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde } } -// for (Map.Entry> e : entity.getAggregateChildren().entrySet()) { -// String relationshipName = e.getKey(); -// ResourceEntity child = e.getValue(); -// Property relationship = createProperty(context, relationshipName, child.getType()); -// shouldAppendGroupByColumns = shouldAppendGroupByColumns || appendAggregateColumns(child, query, relationship); -// } - for (Map.Entry> e : entity.getAggregateChildren().entrySet()) { String relationshipName = e.getKey(); ResourceEntity child = e.getValue(); @@ -159,11 +152,12 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde } @SuppressWarnings("unchecked") - private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context) { - boolean shouldIncludeSelf = true; + private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context, boolean parentIsAggregate) { + boolean shouldIncludeSelf = !parentIsAggregate; for (Map.Entry e : entity.getAttributes().entrySet()) { LrAttribute attribute = e.getValue(); + // related entity attributes will be read from the data object IF the parent is not aggregate if (entity.isDefault(attribute.getName())) { continue; } @@ -191,26 +185,44 @@ private void appendGroupByColumns(ResourceEntity entity, QueryBuilder if (context == null) { // root entity query.includeSelf(); - swapPropertyReadersToSelf(entity); + swapAttributeReadersToSelf(entity); + swapRelationshipReadersToSelf(entity); } else { // TODO: group by the single- or multi-column PK? or Cayenne takes care of that? } } + // this method is called only when there is aggregation, but it's not known in which subtree, so need to track + + entity.getChildren().forEach((relationshipName, child) -> { + Property relationship = createProperty(context, relationshipName, child.getType()); + appendGroupByColumns(child, query, relationship, parentIsAggregate || entity.isAggregate()); + }); + entity.getAggregateChildren().forEach((relationshipName, child) -> { Property relationship = createProperty(context, relationshipName, child.getType()); - appendGroupByColumns(child, query, relationship); + appendGroupByColumns(child, query, relationship, parentIsAggregate || entity.isAggregate()); }); } - private static void swapPropertyReadersToSelf(ResourceEntity entity) { + private static void swapAttributeReadersToSelf(ResourceEntity entity) { for (Map.Entry e : entity.getAttributes().entrySet()) { - e.setValue(selfAttribute(e.getValue())); + if (!(e.getValue() instanceof DecoratedLrAttribute)) { + e.setValue(selfAttribute(e.getValue())); + } } + } + private static void swapRelationshipReadersToSelf(ResourceEntity entity) { + entity.getChildren().values().forEach(child -> { + LrRelationship incoming = child.getIncoming(); + if (incoming != null && !(incoming instanceof DecoratedLrRelationship)) { + child.setIncoming(selfRelationship(incoming)); + } + }); entity.getAggregateChildren().values().forEach(child -> { LrRelationship incoming = child.getIncoming(); - if (incoming != null) { + if (incoming != null && !(incoming instanceof DecoratedLrRelationship)) { child.setIncoming(selfRelationship(incoming)); } }); @@ -235,7 +247,13 @@ private static Property castProperty(Property property, Class type) } private static LrAttribute columnAttribute(LrAttribute attribute, int columnIndex) { - PropertyReader reader = PropertyReader.forValueProducer((Object[] row) -> row[columnIndex]); + PropertyReader reader = PropertyReader.forValueProducer((Object[] row) -> { + if (row[0] instanceof DataObject) { // self has been included + return row[columnIndex + 1]; + } else { + return row[columnIndex]; + } + }); return decoratedAttribute(attribute, reader); } @@ -254,7 +272,7 @@ private static LrRelationship selfRelationship(LrRelationship relationship) { DataObjectPropertyReader.reader() : relationship.getPropertyReader(); PropertyReader reader = (root, name) -> { Object[] row = (Object[]) root; - name = name.substring("@aggregated:".length()); + name = name.replace("@aggregated:", ""); return delegate.value(row[0], name); }; return decoratedRelationship(relationship, reader); @@ -283,51 +301,73 @@ public PropertyReader getPropertyReader() { }; private static LrAttribute decoratedAttribute(LrAttribute delegate, PropertyReader reader) { - return new LrAttribute() { - @Override - public String getName() { - return delegate.getName(); - } + return new DecoratedLrAttribute(delegate, reader); + } - @Override - public Class getType() { - return delegate.getType(); - } + private static class DecoratedLrAttribute implements LrAttribute { - @Override - public ASTPath getPathExp() { - return delegate.getPathExp(); - } + private LrAttribute delegate; + private PropertyReader reader; - @Override - public PropertyReader getPropertyReader() { - return reader; - } - }; + public DecoratedLrAttribute(LrAttribute delegate, PropertyReader reader) { + this.delegate = delegate; + this.reader = reader; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public Class getType() { + return delegate.getType(); + } + + @Override + public ASTPath getPathExp() { + return delegate.getPathExp(); + } + + @Override + public PropertyReader getPropertyReader() { + return reader; + } } private static LrRelationship decoratedRelationship(LrRelationship delegate, PropertyReader reader) { - return new LrRelationship() { - @Override - public String getName() { - return delegate.getName(); - } + return new DecoratedLrRelationship(delegate, reader); + } - @Override - public LrEntity getTargetEntity() { - return delegate.getTargetEntity(); - } + private static class DecoratedLrRelationship implements LrRelationship { - @Override - public boolean isToMany() { - return delegate.isToMany(); - } + private LrRelationship delegate; + private PropertyReader reader; - @Override - public PropertyReader getPropertyReader() { - return reader; - } - }; + public DecoratedLrRelationship(LrRelationship delegate, PropertyReader reader) { + this.delegate = delegate; + this.reader = reader; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public LrEntity getTargetEntity() { + return delegate.getTargetEntity(); + } + + @Override + public boolean isToMany() { + return delegate.isToMany(); + } + + @Override + public PropertyReader getPropertyReader() { + return reader; + } } private void appendPrefetches(PrefetchTreeNode root, ResourceEntity entity, int prefetchSemantics) { diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java index aef8b8bb5..cfb620048 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java @@ -132,7 +132,7 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { resourceEntity.getAggregatedAttributes(aggregationType).forEach(attribute -> { EntityProperty property = attributeEncoderFactory.getAttributeProperty(resourceEntity.getLrEntity(), attribute); - String key = aggregationType.name().toLowerCase() + "(" + attribute.getName() + ")"; + String key = aggregationType.functionName().toLowerCase() + "(" + attribute.getName() + ")"; attributeEncoders.put(key, property); }); } @@ -140,14 +140,36 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { Map relationshipEncoders = new TreeMap<>(); for (Entry> e : resourceEntity.getChildren().entrySet()) { ResourceEntity child = e.getValue(); - LrRelationship relationship = child.getIncoming(); - Encoder encoder = relationship.isToMany() ? nestedToManyEncoder(e.getValue()) - : toOneEncoder(e.getValue(), relationship); - - EntityProperty property = attributeEncoderFactory.getRelationshipProperty(resourceEntity.getLrEntity(), - relationship, encoder); - relationshipEncoders.put(e.getKey(), property); + // TODO: same when the parent's parent is aggregate (need to pass context throughout the hierarchy) + if (resourceEntity.isAggregate()) { + Encoder encoder = entityEncoder(child); + relationshipEncoders.put(e.getKey(), new EntityProperty() { + @Override + public void encode(Object root, String propertyName, JsonGenerator out) throws IOException { + encoder.encode(e.getKey(), root, out); + } + + @Override + public Object read(Object root, String propertyName) { + throw new UnsupportedOperationException(); + } + + @Override + public int visit(Object object, String propertyName, EncoderVisitor visitor) { + throw new UnsupportedOperationException(); + } + }); + } else { + LrRelationship relationship = child.getIncoming(); + + Encoder encoder = relationship.isToMany() ? nestedToManyEncoder(e.getValue()) + : toOneEncoder(e.getValue(), relationship); + + EntityProperty property = attributeEncoderFactory.getRelationshipProperty(resourceEntity.getLrEntity(), + relationship, encoder); + relationshipEncoders.put(e.getKey(), property); + } } Map extraEncoders = new TreeMap<>(); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java index b4a95d55a..a4e811ee2 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/parser/tree/IncludeWorker.java @@ -155,7 +155,7 @@ private void processMapBy(ResourceEntity descriptor, String mapByPath) { static void applyDefaultIncludes(ResourceEntity resourceEntity) { // either there are no includes (taking into account Id) or all includes // are relationships - if (!resourceEntity.isIdIncluded() && resourceEntity.getAttributes().isEmpty()) { + if (!resourceEntity.isIdIncluded() && resourceEntity.getAttributes().isEmpty() && !resourceEntity.isAggregate()) { for (LrAttribute a : resourceEntity.getLrEntity().getAttributes()) { resourceEntity.getAttributes().put(a.getName(), a); diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java index b9921b203..d0e81bd63 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java @@ -46,6 +46,7 @@ public void test_Select_AggregationOnRootEntity() { Response response = target("/e2") .queryParam("include", "count()") .queryParam("include", "name") + .queryParam("sort", "name") .request() .get(); @@ -65,7 +66,7 @@ public void test_Select_AggregationOnRootEntity() { name: it total: 2 */ -// @Test + @Test public void test_Select_AggregationOnRootEntity_GroupByRelated() { insert("e21", "id, name", "1, 'xxx'"); @@ -77,10 +78,12 @@ public void test_Select_AggregationOnRootEntity_GroupByRelated() { Response response = target("/e20") .queryParam("include", "avg(age)") .queryParam("include", "e21.name") + .queryParam("sort", "e21.name") .request() .get(); - onSuccess(response).bodyEquals(2, "{...TODO...}"); + onSuccess(response).bodyEquals(2, "{\"avg(age)\":15,\"e21\":{\"name\":\"xxx\"}}," + + "{\"avg(age)\":5,\"e21\":{\"name\":\"yyy\"}}"); } /** @@ -108,11 +111,12 @@ public void test_Select_AggregationOnRelatedEntity_GroupByRoot() { Response response = target("/e21") .queryParam("include", "e20s.sum(age)") .queryParam("include", "name") + .queryParam("sort", "name") .request() .get(); - onSuccess(response).bodyEquals(2, "{\"@aggregated:e20s\":{\"sum(age)\":5},\"name\":\"yyy\"}," + - "{\"@aggregated:e20s\":{\"sum(age)\":30},\"name\":\"xxx\"}"); + onSuccess(response).bodyEquals(2, "{\"@aggregated:e20s\":{\"sum(age)\":30},\"name\":\"xxx\"}," + + "{\"@aggregated:e20s\":{\"sum(age)\":5},\"name\":\"yyy\"}"); } /** @@ -143,12 +147,13 @@ public void test_Select_AggregationOnRelatedEntity_GroupByBoth() { .queryParam("include", "e20s.sum(age)") .queryParam("include", "e20s.name") .queryParam("include", "name") + .queryParam("sort", "name") .request() .get(); - onSuccess(response).bodyEquals(3, "{\"@aggregated:e20s\":{\"name\":\"ccc\",\"sum(age)\":5},\"name\":\"yyy\"}," + + onSuccess(response).bodyEquals(3, "{\"@aggregated:e20s\":{\"name\":\"bbb\",\"sum(age)\":20},\"name\":\"xxx\"}," + "{\"@aggregated:e20s\":{\"name\":\"aaa\",\"sum(age)\":10},\"name\":\"xxx\"}," + - "{\"@aggregated:e20s\":{\"name\":\"bbb\",\"sum(age)\":20},\"name\":\"xxx\"}"); + "{\"@aggregated:e20s\":{\"name\":\"ccc\",\"sum(age)\":5},\"name\":\"yyy\"}"); } /** From a00b7e26ef0f614d37b206b56de3e99ea0eb6d2b Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Wed, 27 Sep 2017 16:43:41 +0300 Subject: [PATCH 10/15] Aggregation queries support #266 (all test cases passing, except GET_ListenersIT and GET_PojoIT) --- .../java/com/nhl/link/rest/sencha/SenchaEncoderServiceTest.java | 1 + .../src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java | 2 ++ link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java | 2 ++ .../com/nhl/link/rest/runtime/encoder/EncoderServiceTest.java | 2 ++ 4 files changed, 7 insertions(+) diff --git a/link-rest-sencha/src/test/java/com/nhl/link/rest/sencha/SenchaEncoderServiceTest.java b/link-rest-sencha/src/test/java/com/nhl/link/rest/sencha/SenchaEncoderServiceTest.java index 4fa420d88..0c9379f23 100644 --- a/link-rest-sencha/src/test/java/com/nhl/link/rest/sencha/SenchaEncoderServiceTest.java +++ b/link-rest-sencha/src/test/java/com/nhl/link/rest/sencha/SenchaEncoderServiceTest.java @@ -98,6 +98,7 @@ public boolean willEncode(String propertyName, Object object, Encoder delegate) ResourceEntity e2Descriptor = getResourceEntity(E2.class); e2Descriptor.includeId(); + e2Descriptor.setIncoming(metadataService.getLrRelationship(E3.class, E3.E2.getName())); ResourceEntity e3Descriptor = getResourceEntity(E3.class); e3Descriptor.includeId(); diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java index 68f456eb8..584e9208f 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java @@ -10,6 +10,7 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.query.SQLTemplate; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import javax.ws.rs.GET; @@ -76,6 +77,7 @@ public void testPassThroughLisetner() { response1.readEntity(String.class)); } + @Ignore @Test public void testTakeOverListener() { diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java index ab8d01c41..5399aeaec 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java @@ -8,6 +8,7 @@ import com.nhl.link.rest.it.fixture.pojo.model.P4; import com.nhl.link.rest.it.fixture.pojo.model.P6; import com.nhl.link.rest.it.fixture.pojo.model.P8; +import org.junit.Ignore; import org.junit.Test; import javax.ws.rs.GET; @@ -27,6 +28,7 @@ import static org.junit.Assert.assertEquals; +@Ignore public class GET_PojoIT extends JerseyTestOnPojo { @Override diff --git a/link-rest/src/test/java/com/nhl/link/rest/runtime/encoder/EncoderServiceTest.java b/link-rest/src/test/java/com/nhl/link/rest/runtime/encoder/EncoderServiceTest.java index 5ae91b8d7..86596b67a 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/runtime/encoder/EncoderServiceTest.java +++ b/link-rest/src/test/java/com/nhl/link/rest/runtime/encoder/EncoderServiceTest.java @@ -65,6 +65,7 @@ public void testGetRootEncoder_ExcludedAttributes() throws IOException { public void testGetRootEncoder_ExcludedRelationshipAttributes() throws IOException { ResourceEntity e3Descriptor = getResourceEntity(E3.class); e3Descriptor.includeId(); + e3Descriptor.setIncoming(metadataService.getLrRelationship(E2.class, E2.E3S.getName())); appendAttribute(e3Descriptor, E3.NAME, String.class); @@ -197,6 +198,7 @@ public boolean willEncode(String propertyName, Object object, Encoder delegate) ResourceEntity e2Descriptor = getResourceEntity(E2.class); e2Descriptor.includeId(); + e2Descriptor.setIncoming(metadataService.getLrRelationship(E3.class, E3.E2.getName())); ResourceEntity e3Descriptor = getResourceEntity(E3.class); e3Descriptor.includeId(); From a50c35e394886e16e60aa6a214b280658cd70ac1 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Wed, 27 Sep 2017 17:39:31 +0300 Subject: [PATCH 11/15] Aggregation queries support #266 --- .../select/CayenneAssembleQueryStage.java | 51 +++++++++++-------- .../select/CayenneFetchDataStage.java | 13 +---- .../encoder/AttributeEncoderFactory.java | 12 +---- .../select/ApplyServerParamsStage.java | 13 ++++- .../nhl/link/rest/it/GET_IT_Aggregate.java | 17 +++++-- .../com/nhl/link/rest/it/GET_ListenersIT.java | 2 - .../java/com/nhl/link/rest/it/GET_PojoIT.java | 4 -- .../select/CayenneAssembleQueryStageTest.java | 2 +- 8 files changed, 57 insertions(+), 57 deletions(-) diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index 0d834f55c..a74395fec 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -12,6 +12,7 @@ import com.nhl.link.rest.property.DataObjectPropertyReader; import com.nhl.link.rest.property.PropertyReader; import com.nhl.link.rest.runtime.cayenne.ICayennePersister; +import com.nhl.link.rest.runtime.encoder.IEncoderService; import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.DataObject; import org.apache.cayenne.di.Inject; @@ -26,6 +27,7 @@ import javax.ws.rs.core.Response.Status; import java.util.ListIterator; import java.util.Map; +import java.util.Optional; /** * @since 2.7 @@ -33,9 +35,12 @@ public class CayenneAssembleQueryStage implements Processor> { private EntityResolver entityResolver; + private Optional encoderService; - public CayenneAssembleQueryStage(@Inject ICayennePersister persister) { + public CayenneAssembleQueryStage(@Inject ICayennePersister persister, + @Inject IEncoderService encoderService) { this.entityResolver = persister.entityResolver(); + this.encoderService = Optional.ofNullable(encoderService); // can be absent in tests } @Override @@ -46,6 +51,11 @@ public ProcessorOutcome execute(SelectContext context) { protected void doExecute(SelectContext context) { context.setSelect(buildQuery(context)); + + // create a new encoder, based on augmented entity + encoderService.ifPresent(service -> { + context.setEncoder(service.dataEncoder(context.getEntity())); + }); } SelectQuery buildQuery(SelectContext context) { @@ -56,7 +66,12 @@ SelectQuery buildQuery(SelectContext context) { if (appendAggregateColumns(entity, query, null)) { entity.excludeId(); // TODO: remove - appendGroupByColumns(entity, query, null, entity.isAggregate()); + appendGroupByColumns(entity, query, null); + if (!entity.isAggregate() && !hasGroupByColumns(entity)) { + query.includeSelf(); + swapAttributeReadersToSelf(entity); + swapRelationshipReadersToSelf(entity); + } } if (!entity.isFiltered()) { @@ -97,6 +112,16 @@ SelectQuery buildQuery(SelectContext context) { return query.buildQuery(); } + private boolean hasGroupByColumns(ResourceEntity entity) { + for (Map.Entry e : entity.getAttributes().entrySet()) { + LrAttribute attribute = e.getValue(); + if (!entity.isDefault(attribute.getName())) { + return true; + } + } + return false; + } + @SuppressWarnings("unchecked") private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilder query, Property context) { boolean shouldAppendGroupByColumns = false; @@ -152,9 +177,7 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde } @SuppressWarnings("unchecked") - private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context, boolean parentIsAggregate) { - boolean shouldIncludeSelf = !parentIsAggregate; - + private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context) { for (Map.Entry e : entity.getAttributes().entrySet()) { LrAttribute attribute = e.getValue(); // related entity attributes will be read from the data object IF the parent is not aggregate @@ -166,7 +189,6 @@ private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query.column(property); e.setValue(columnAttribute(attribute, query.columnCount() - 1)); - shouldIncludeSelf = false; } // do this after all attributes have been added, because we'll add one more fictional attribute for encoding purposes @@ -177,31 +199,18 @@ private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query.count(context); } entity.getAttributes().put("count()", columnAttribute(COUNT_ATTRIBUTE, query.columnCount() - 1)); - shouldIncludeSelf = false; - } - - if (shouldIncludeSelf) { - // no groupBy columns were explicitly included - if (context == null) { - // root entity - query.includeSelf(); - swapAttributeReadersToSelf(entity); - swapRelationshipReadersToSelf(entity); - } else { - // TODO: group by the single- or multi-column PK? or Cayenne takes care of that? - } } // this method is called only when there is aggregation, but it's not known in which subtree, so need to track entity.getChildren().forEach((relationshipName, child) -> { Property relationship = createProperty(context, relationshipName, child.getType()); - appendGroupByColumns(child, query, relationship, parentIsAggregate || entity.isAggregate()); + appendGroupByColumns(child, query, relationship); }); entity.getAggregateChildren().forEach((relationshipName, child) -> { Property relationship = createProperty(context, relationshipName, child.getType()); - appendGroupByColumns(child, query, relationship, parentIsAggregate || entity.isAggregate()); + appendGroupByColumns(child, query, relationship); }); } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java index cf487980d..235950ffc 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneFetchDataStage.java @@ -5,7 +5,6 @@ import com.nhl.link.rest.processor.Processor; import com.nhl.link.rest.processor.ProcessorOutcome; import com.nhl.link.rest.runtime.cayenne.ICayennePersister; -import com.nhl.link.rest.runtime.encoder.IEncoderService; import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Inject; import org.apache.cayenne.query.SelectQuery; @@ -19,16 +18,13 @@ public class CayenneFetchDataStage implements Processor> { private ICayennePersister persister; - private IEncoderService encoderService; - public CayenneFetchDataStage(@Inject ICayennePersister persister, - @Inject IEncoderService encoderService) { + public CayenneFetchDataStage(@Inject ICayennePersister persister) { // Store persister, don't extract ObjectContext from it right away. // Such deferred initialization may help building POJO pipelines. this.persister = persister; - this.encoderService = encoderService; } @Override @@ -55,12 +51,5 @@ protected void doExecute(SelectContext context) { } } context.setObjects(objects); - - // make sure we create the encoder, even if we end up with an empty - // list, as we need to encode the totals - - if (context.getEncoder() == null) { - context.setEncoder(encoderService.dataEncoder(context.getEntity())); - } } } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java index 1bf4332d5..27d2485c2 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java @@ -46,27 +46,17 @@ public class AttributeEncoderFactory implements IAttributeEncoderFactory { static final Class LOCAL_DATETIME = LocalDateTime.class; // these are explicit overrides for named attributes - private Map attributePropertiesByPath; private Map idPropertiesByEntity; private ConcurrentMap, IdPropertyReader> idPropertyReaders; public AttributeEncoderFactory() { - this.attributePropertiesByPath = new ConcurrentHashMap<>(); this.idPropertiesByEntity = new ConcurrentHashMap<>(); this.idPropertyReaders = new ConcurrentHashMap<>(); } @Override public EntityProperty getAttributeProperty(LrEntity entity, LrAttribute attribute) { - String key = entity.getName() + "." + attribute.getName(); - - EntityProperty property = attributePropertiesByPath.get(key); - if (property == null) { - property = buildAttributeProperty(entity, attribute); - attributePropertiesByPath.put(key, property); - } - - return property; + return buildAttributeProperty(entity, attribute); } @Override diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java index 30ba2ca8d..88056b90d 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/processor/select/ApplyServerParamsStage.java @@ -5,6 +5,7 @@ import com.nhl.link.rest.processor.Processor; import com.nhl.link.rest.processor.ProcessorOutcome; import com.nhl.link.rest.runtime.constraints.IConstraintsHandler; +import com.nhl.link.rest.runtime.encoder.IEncoderService; import org.apache.cayenne.di.Inject; import java.util.List; @@ -15,14 +16,16 @@ public class ApplyServerParamsStage implements Processor> { private IConstraintsHandler constraintsHandler; - + private IEncoderService encoderService; private List filters; public ApplyServerParamsStage( @Inject IConstraintsHandler constraintsHandler, + @Inject IEncoderService encoderService, @Inject List filters) { this.constraintsHandler = constraintsHandler; + this.encoderService = encoderService; this.filters = filters; } @@ -48,5 +51,13 @@ protected void doExecute(SelectContext context) { break; } } + + // make sure we create the default encoder, even if we end up with an empty + // list, as we need to encode the totals; + // the default encoder will also come in handy for Pojos and when the processing chain is terminated by a listener + + if (context.getEncoder() == null) { + context.setEncoder(encoderService.dataEncoder(entity)); + } } } \ No newline at end of file diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java index d0e81bd63..6a80918fd 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java @@ -177,19 +177,26 @@ public void test_Select_AggregationOnRelatedEntity_GroupByBoth() { // @Test public void test_Select_AggregationOnRelatedEntity_GroupRelated_IncludeRoot() { - insert("e21", "id, name", "1, 'xxx'"); - insert("e21", "id, name", "2, 'yyy'"); + insert("e21", "id, name, age, description", "1, 'xxx', 99, 'xxx_desc'"); + insert("e21", "id, name, age, description", "2, 'yyy', 77, 'yyy_desc'"); insert("e20", "id, e21_id, age, name", "1, 1, 10, 'aaa'"); - insert("e20", "id, e21_id, age, name", "2, 1, 20, 'bbb'"); - insert("e20", "id, e21_id, age, name", "3, 2, 5, 'ccc'"); + insert("e20", "id, e21_id, age, name", "2, 1, 20, 'aaa'"); + insert("e20", "id, e21_id, age, name", "3, 2, 5, 'bbb'"); + insert("e20", "id, e21_id, age, name", "4, 2, 15, 'ccc'"); Response response = target("/e21") .queryParam("include", "e20s.sum(age)") .queryParam("include", "e20s.name") + .queryParam("sort", "name") + .queryParam("sort", "e20s.name") .request() .get(); - onSuccess(response).bodyEquals(2, "{...TODO...}"); + // need to map by self (and recursively by related to-one entity in general case) + onSuccess(response).bodyEquals(3, + "{\"@aggregated:e20s\":{\"name\":\"aaa\",\"sum(age)\":30},\"age\":99,\"description\":\"xxx_desc\",\"name\":\"xxx\"}," + + "{\"@aggregated:e20s\":{\"name\":\"ccc\",\"sum(age)\":15},\"age\":77,\"description\":\"yyy_desc\",\"name\":\"yyy\"}," + + "{\"@aggregated:e20s\":{\"name\":\"bbb\",\"sum(age)\":5},\"age\":77,\"description\":\"yyy_desc\",\"name\":\"yyy\"}"); } @Path("") diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java index 584e9208f..68f456eb8 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_ListenersIT.java @@ -10,7 +10,6 @@ import com.nhl.link.rest.runtime.processor.select.SelectContext; import org.apache.cayenne.query.SQLTemplate; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import javax.ws.rs.GET; @@ -77,7 +76,6 @@ public void testPassThroughLisetner() { response1.readEntity(String.class)); } - @Ignore @Test public void testTakeOverListener() { diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java index 5399aeaec..709613a32 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_PojoIT.java @@ -8,27 +8,23 @@ import com.nhl.link.rest.it.fixture.pojo.model.P4; import com.nhl.link.rest.it.fixture.pojo.model.P6; import com.nhl.link.rest.it.fixture.pojo.model.P8; -import org.junit.Ignore; import org.junit.Test; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; -import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Configuration; import javax.ws.rs.core.Context; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; -import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.junit.Assert.assertEquals; -@Ignore public class GET_PojoIT extends JerseyTestOnPojo { @Override diff --git a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java index 364e5afe7..b663ce5bb 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java +++ b/link-rest/src/test/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStageTest.java @@ -30,7 +30,7 @@ public class CayenneAssembleQueryStageTest extends TestWithCayenneMapping { @Before public void before() { - this.makeQueryStage = new CayenneAssembleQueryStage(mockCayennePersister); + this.makeQueryStage = new CayenneAssembleQueryStage(mockCayennePersister, null); } @Test From 38425e598f22494cb0f19577fa158c4401da61a7 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Thu, 28 Sep 2017 16:09:21 +0300 Subject: [PATCH 12/15] Aggregation queries support #266 --- .../select/CayenneAssembleQueryStage.java | 147 ++++-------------- .../processor/select/CountAttribute.java | 34 ++++ .../select/DecoratedLrAttribute.java | 36 +++++ .../select/DecoratedLrRelationship.java | 36 +++++ .../processor/select/QueryBuilder.java | 7 + .../rest/runtime/encoder/EncoderService.java | 95 ++++++----- 6 files changed, 194 insertions(+), 161 deletions(-) create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CountAttribute.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrAttribute.java create mode 100644 link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrRelationship.java diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index a74395fec..61513271c 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -4,7 +4,6 @@ import com.nhl.link.rest.LinkRestException; import com.nhl.link.rest.ResourceEntity; import com.nhl.link.rest.meta.LrAttribute; -import com.nhl.link.rest.meta.LrEntity; import com.nhl.link.rest.meta.LrPersistentEntity; import com.nhl.link.rest.meta.LrRelationship; import com.nhl.link.rest.processor.Processor; @@ -14,11 +13,9 @@ import com.nhl.link.rest.runtime.cayenne.ICayennePersister; import com.nhl.link.rest.runtime.encoder.IEncoderService; import com.nhl.link.rest.runtime.processor.select.SelectContext; -import org.apache.cayenne.DataObject; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.Property; -import org.apache.cayenne.exp.parser.ASTPath; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; @@ -52,7 +49,7 @@ public ProcessorOutcome execute(SelectContext context) { protected void doExecute(SelectContext context) { context.setSelect(buildQuery(context)); - // create a new encoder, based on augmented entity + // create a new encoder, based on augmented entity (possibly overriding the custom encoder, which is bad) encoderService.ifPresent(service -> { context.setEncoder(service.dataEncoder(context.getEntity())); }); @@ -65,12 +62,11 @@ SelectQuery buildQuery(SelectContext context) { QueryBuilder query = new QueryBuilder<>(context); if (appendAggregateColumns(entity, query, null)) { - entity.excludeId(); // TODO: remove appendGroupByColumns(entity, query, null); if (!entity.isAggregate() && !hasGroupByColumns(entity)) { query.includeSelf(); swapAttributeReadersToSelf(entity); - swapRelationshipReadersToSelf(entity); + swapChildrenToSelf(entity); } } @@ -122,14 +118,14 @@ private boolean hasGroupByColumns(ResourceEntity entity) { return false; } + /** + * @return true if some of the resource entities is aggregate, + * which means that all explicit includes should be treated as GROUP BY columns + */ @SuppressWarnings("unchecked") private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilder query, Property context) { boolean shouldAppendGroupByColumns = false; - if (entity.isCountIncluded()) { - shouldAppendGroupByColumns = true; - } - if (entity.isAggregate()) { shouldAppendGroupByColumns = true; @@ -161,7 +157,7 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde } } - iter.set(columnAttribute(attribute, query.columnCount() - 1)); + iter.set(currentColumnAttribute(attribute, query)); } } } @@ -180,15 +176,12 @@ private boolean appendAggregateColumns(ResourceEntity entity, QueryBuilde private void appendGroupByColumns(ResourceEntity entity, QueryBuilder query, Property context) { for (Map.Entry e : entity.getAttributes().entrySet()) { LrAttribute attribute = e.getValue(); - // related entity attributes will be read from the data object IF the parent is not aggregate - if (entity.isDefault(attribute.getName())) { - continue; - } - - Property property = createProperty(context, attribute.getName(), attribute.getType()); - query.column(property); + if (!entity.isDefault(attribute.getName())) { + Property property = createProperty(context, attribute.getName(), attribute.getType()); + query.column(property); - e.setValue(columnAttribute(attribute, query.columnCount() - 1)); + e.setValue(currentColumnAttribute(attribute, query)); + } } // do this after all attributes have been added, because we'll add one more fictional attribute for encoding purposes @@ -198,7 +191,7 @@ private void appendGroupByColumns(ResourceEntity entity, QueryBuilder } else { query.count(context); } - entity.getAttributes().put("count()", columnAttribute(COUNT_ATTRIBUTE, query.columnCount() - 1)); + entity.getAttributes().put("count()", currentColumnAttribute(CountAttribute.instance(), query)); } // this method is called only when there is aggregation, but it's not known in which subtree, so need to track @@ -216,25 +209,24 @@ private void appendGroupByColumns(ResourceEntity entity, QueryBuilder private static void swapAttributeReadersToSelf(ResourceEntity entity) { for (Map.Entry e : entity.getAttributes().entrySet()) { + // prevent double swap, which can happen, depending on when this method is called if (!(e.getValue() instanceof DecoratedLrAttribute)) { e.setValue(selfAttribute(e.getValue())); } } } - private static void swapRelationshipReadersToSelf(ResourceEntity entity) { - entity.getChildren().values().forEach(child -> { - LrRelationship incoming = child.getIncoming(); - if (incoming != null && !(incoming instanceof DecoratedLrRelationship)) { - child.setIncoming(selfRelationship(incoming)); - } - }); - entity.getAggregateChildren().values().forEach(child -> { - LrRelationship incoming = child.getIncoming(); - if (incoming != null && !(incoming instanceof DecoratedLrRelationship)) { - child.setIncoming(selfRelationship(incoming)); - } - }); + private static void swapChildrenToSelf(ResourceEntity entity) { + entity.getChildren().values().forEach(CayenneAssembleQueryStage::swapRelationshipReaderToSelf); + entity.getAggregateChildren().values().forEach(CayenneAssembleQueryStage::swapRelationshipReaderToSelf); + } + + private static void swapRelationshipReaderToSelf(ResourceEntity child) { + LrRelationship incoming = child.getIncoming(); + // prevent double swap, which can happen, depending on when this method is called + if (incoming != null && !(incoming instanceof DecoratedLrRelationship)) { + child.setIncoming(selfRelationship(incoming)); + } } @SuppressWarnings("unchecked") @@ -255,9 +247,10 @@ private static Property castProperty(Property property, Class type) return (Property) property; } - private static LrAttribute columnAttribute(LrAttribute attribute, int columnIndex) { + private static LrAttribute currentColumnAttribute(LrAttribute attribute, QueryBuilder query) { + int columnIndex = query.columnCount() - 1; // use current column PropertyReader reader = PropertyReader.forValueProducer((Object[] row) -> { - if (row[0] instanceof DataObject) { // self has been included + if (query.isSelfIncluded()) { return row[columnIndex + 1]; } else { return row[columnIndex]; @@ -287,98 +280,14 @@ private static LrRelationship selfRelationship(LrRelationship relationship) { return decoratedRelationship(relationship, reader); } - private static LrAttribute COUNT_ATTRIBUTE = new LrAttribute() { - @Override - public String getName() { - return "count()"; - } - - @Override - public Class getType() { - return Long.class; - } - - @Override - public ASTPath getPathExp() { - return null; - } - - @Override - public PropertyReader getPropertyReader() { - return null; - } - }; - private static LrAttribute decoratedAttribute(LrAttribute delegate, PropertyReader reader) { return new DecoratedLrAttribute(delegate, reader); } - private static class DecoratedLrAttribute implements LrAttribute { - - private LrAttribute delegate; - private PropertyReader reader; - - public DecoratedLrAttribute(LrAttribute delegate, PropertyReader reader) { - this.delegate = delegate; - this.reader = reader; - } - - @Override - public String getName() { - return delegate.getName(); - } - - @Override - public Class getType() { - return delegate.getType(); - } - - @Override - public ASTPath getPathExp() { - return delegate.getPathExp(); - } - - @Override - public PropertyReader getPropertyReader() { - return reader; - } - } - private static LrRelationship decoratedRelationship(LrRelationship delegate, PropertyReader reader) { return new DecoratedLrRelationship(delegate, reader); } - private static class DecoratedLrRelationship implements LrRelationship { - - private LrRelationship delegate; - private PropertyReader reader; - - public DecoratedLrRelationship(LrRelationship delegate, PropertyReader reader) { - this.delegate = delegate; - this.reader = reader; - } - - @Override - public String getName() { - return delegate.getName(); - } - - @Override - public LrEntity getTargetEntity() { - return delegate.getTargetEntity(); - } - - @Override - public boolean isToMany() { - return delegate.isToMany(); - } - - @Override - public PropertyReader getPropertyReader() { - return reader; - } - } - private void appendPrefetches(PrefetchTreeNode root, ResourceEntity entity, int prefetchSemantics) { for (Map.Entry> e : entity.getChildren().entrySet()) { diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CountAttribute.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CountAttribute.java new file mode 100644 index 000000000..93c46a91e --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CountAttribute.java @@ -0,0 +1,34 @@ +package com.nhl.link.rest.runtime.cayenne.processor.select; + +import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.property.PropertyReader; +import org.apache.cayenne.exp.parser.ASTPath; + +class CountAttribute implements LrAttribute { + + private static final CountAttribute instance = new CountAttribute(); + + public static CountAttribute instance() { + return instance; + } + + @Override + public String getName() { + return "count()"; + } + + @Override + public Class getType() { + return Long.class; + } + + @Override + public ASTPath getPathExp() { + return null; + } + + @Override + public PropertyReader getPropertyReader() { + return null; + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrAttribute.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrAttribute.java new file mode 100644 index 000000000..b07c2e644 --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrAttribute.java @@ -0,0 +1,36 @@ +package com.nhl.link.rest.runtime.cayenne.processor.select; + +import com.nhl.link.rest.meta.LrAttribute; +import com.nhl.link.rest.property.PropertyReader; +import org.apache.cayenne.exp.parser.ASTPath; + +class DecoratedLrAttribute implements LrAttribute { + + private LrAttribute delegate; + private PropertyReader reader; + + public DecoratedLrAttribute(LrAttribute delegate, PropertyReader reader) { + this.delegate = delegate; + this.reader = reader; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public Class getType() { + return delegate.getType(); + } + + @Override + public ASTPath getPathExp() { + return delegate.getPathExp(); + } + + @Override + public PropertyReader getPropertyReader() { + return reader; + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrRelationship.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrRelationship.java new file mode 100644 index 000000000..f932fb2de --- /dev/null +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/DecoratedLrRelationship.java @@ -0,0 +1,36 @@ +package com.nhl.link.rest.runtime.cayenne.processor.select; + +import com.nhl.link.rest.meta.LrEntity; +import com.nhl.link.rest.meta.LrRelationship; +import com.nhl.link.rest.property.PropertyReader; + +class DecoratedLrRelationship implements LrRelationship { + + private LrRelationship delegate; + private PropertyReader reader; + + public DecoratedLrRelationship(LrRelationship delegate, PropertyReader reader) { + this.delegate = delegate; + this.reader = reader; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public LrEntity getTargetEntity() { + return delegate.getTargetEntity(); + } + + @Override + public boolean isToMany() { + return delegate.isToMany(); + } + + @Override + public PropertyReader getPropertyReader() { + return reader; + } +} diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java index 179ed2e77..d66558711 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/QueryBuilder.java @@ -24,6 +24,7 @@ public class QueryBuilder { private SelectQuery query; + private boolean selfIncluded; public QueryBuilder(SelectContext context) { Class root = context.getType(); @@ -127,6 +128,8 @@ public QueryBuilder column(Property property) { @SuppressWarnings("unchecked") public QueryBuilder includeSelf() { + selfIncluded = true; + Property self = Property.createSelf((Class) query.getRoot()); List> columns = (List>) query.getColumns(); @@ -146,6 +149,10 @@ public int columnCount() { return Objects.requireNonNull(query.getColumns()).size(); } + public boolean isSelfIncluded() { + return selfIncluded; + } + public SelectQuery buildQuery() { return query; } diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java index cfb620048..7399598df 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java @@ -4,7 +4,20 @@ import com.nhl.link.rest.AggregationType; import com.nhl.link.rest.EntityProperty; import com.nhl.link.rest.ResourceEntity; -import com.nhl.link.rest.encoder.*; +import com.nhl.link.rest.encoder.CollectionEncoder; +import com.nhl.link.rest.encoder.DataResponseEncoder; +import com.nhl.link.rest.encoder.Encoder; +import com.nhl.link.rest.encoder.EncoderFilter; +import com.nhl.link.rest.encoder.EncoderVisitor; +import com.nhl.link.rest.encoder.EntityEncoder; +import com.nhl.link.rest.encoder.EntityMetadataEncoder; +import com.nhl.link.rest.encoder.EntityToOneEncoder; +import com.nhl.link.rest.encoder.FilterChainEncoder; +import com.nhl.link.rest.encoder.GenericEncoder; +import com.nhl.link.rest.encoder.ListEncoder; +import com.nhl.link.rest.encoder.MapByEncoder; +import com.nhl.link.rest.encoder.PropertyMetadataEncoder; +import com.nhl.link.rest.encoder.ResourceEncoder; import com.nhl.link.rest.meta.LrAttribute; import com.nhl.link.rest.meta.LrRelationship; import com.nhl.link.rest.property.PropertyBuilder; @@ -132,7 +145,7 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { resourceEntity.getAggregatedAttributes(aggregationType).forEach(attribute -> { EntityProperty property = attributeEncoderFactory.getAttributeProperty(resourceEntity.getLrEntity(), attribute); - String key = aggregationType.functionName().toLowerCase() + "(" + attribute.getName() + ")"; + String key = toFunctionName(aggregationType, attribute.getName()); attributeEncoders.put(key, property); }); } @@ -143,62 +156,31 @@ protected Encoder entityEncoder(ResourceEntity resourceEntity) { // TODO: same when the parent's parent is aggregate (need to pass context throughout the hierarchy) if (resourceEntity.isAggregate()) { - Encoder encoder = entityEncoder(child); - relationshipEncoders.put(e.getKey(), new EntityProperty() { - @Override - public void encode(Object root, String propertyName, JsonGenerator out) throws IOException { - encoder.encode(e.getKey(), root, out); - } - - @Override - public Object read(Object root, String propertyName) { - throw new UnsupportedOperationException(); - } - - @Override - public int visit(Object object, String propertyName, EncoderVisitor visitor) { - throw new UnsupportedOperationException(); - } - }); + String propertyName = e.getKey(); + relationshipEncoders.put(propertyName, new PropertyEncoder(entityEncoder(child), propertyName)); + } else { LrRelationship relationship = child.getIncoming(); - Encoder encoder = relationship.isToMany() ? nestedToManyEncoder(e.getValue()) : toOneEncoder(e.getValue(), relationship); - EntityProperty property = attributeEncoderFactory.getRelationshipProperty(resourceEntity.getLrEntity(), relationship, encoder); relationshipEncoders.put(e.getKey(), property); } } - Map extraEncoders = new TreeMap<>(); - - extraEncoders.putAll(resourceEntity.getExtraProperties()); - for (Entry> e : resourceEntity.getAggregateChildren().entrySet()) { ResourceEntity child = e.getValue(); - Encoder encoder = entityEncoder(child); - relationshipEncoders.put("@aggregated:" + e.getKey(), new EntityProperty() { - @Override - public void encode(Object root, String propertyName, JsonGenerator out) throws IOException { - encoder.encode("@aggregated:" + e.getKey(), root, out); - } - - @Override - public Object read(Object root, String propertyName) { - throw new UnsupportedOperationException(); - } - - @Override - public int visit(Object object, String propertyName, EncoderVisitor visitor) { - throw new UnsupportedOperationException(); - } - }); + String propertyName = "@aggregated:" + e.getKey(); + relationshipEncoders.put(propertyName, new PropertyEncoder(entityEncoder(child), propertyName)); } + Map extraEncoders = new TreeMap<>(); + extraEncoders.putAll(resourceEntity.getExtraProperties()); + EntityProperty idEncoder = resourceEntity.isIdIncluded() ? attributeEncoderFactory.getIdProperty(resourceEntity) : PropertyBuilder.doNothingProperty(); + return new EntityEncoder(idEncoder, attributeEncoders, relationshipEncoders, extraEncoders); } @@ -218,4 +200,33 @@ protected Encoder filteredEncoder(Encoder encoder, ResourceEntity resourceEnt return matchingFilters != null ? new FilterChainEncoder(encoder, matchingFilters) : encoder; } + private static String toFunctionName(AggregationType aggregationType, String attributeName) { + return aggregationType.functionName().toLowerCase() + "(" + attributeName + ")"; + } + + private static class PropertyEncoder implements EntityProperty { + + private Encoder encoder; + private String name; + + public PropertyEncoder(Encoder encoder, String name) { + this.encoder = encoder; + this.name = name; + } + + @Override + public void encode(Object root, String propertyName, JsonGenerator out) throws IOException { + encoder.encode(name, root, out); + } + + @Override + public Object read(Object root, String propertyName) { + throw new UnsupportedOperationException(); + } + + @Override + public int visit(Object object, String propertyName, EncoderVisitor visitor) { + throw new UnsupportedOperationException(); + } + } } From 33d414f4f5223dc27c3c8d05df05194e9325834e Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Fri, 29 Sep 2017 12:45:23 +0300 Subject: [PATCH 13/15] Aggregation queries support #266 --- .../rest/sencha/SenchaEncoderService.java | 10 ++++-- .../select/CayenneAssembleQueryStage.java | 1 - .../rest/runtime/encoder/EncoderService.java | 33 +++++++++++-------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/link-rest-sencha/src/main/java/com/nhl/link/rest/sencha/SenchaEncoderService.java b/link-rest-sencha/src/main/java/com/nhl/link/rest/sencha/SenchaEncoderService.java index fc5566b61..3e8670dce 100644 --- a/link-rest-sencha/src/main/java/com/nhl/link/rest/sencha/SenchaEncoderService.java +++ b/link-rest-sencha/src/main/java/com/nhl/link/rest/sencha/SenchaEncoderService.java @@ -3,7 +3,12 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.nhl.link.rest.EntityProperty; import com.nhl.link.rest.ResourceEntity; -import com.nhl.link.rest.encoder.*; +import com.nhl.link.rest.encoder.CollectionEncoder; +import com.nhl.link.rest.encoder.DataResponseEncoder; +import com.nhl.link.rest.encoder.Encoder; +import com.nhl.link.rest.encoder.EncoderFilter; +import com.nhl.link.rest.encoder.GenericEncoder; +import com.nhl.link.rest.encoder.PropertyMetadataEncoder; import com.nhl.link.rest.meta.LrRelationship; import com.nhl.link.rest.runtime.encoder.EncoderService; import com.nhl.link.rest.runtime.encoder.IAttributeEncoderFactory; @@ -28,8 +33,7 @@ public SenchaEncoderService(@Inject List filters, } @Override - public Encoder dataEncoder(ResourceEntity entity) { - CollectionEncoder resultEncoder = resultEncoder(entity); + protected DataResponseEncoder toDataResponseEncoder(CollectionEncoder resultEncoder) { return new DataResponseEncoder("data", resultEncoder, "total", GenericEncoder.encoder()) { @Override protected void encodeObjectBody(Object object, JsonGenerator out) throws IOException { diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java index 61513271c..12a5590e8 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/cayenne/processor/select/CayenneAssembleQueryStage.java @@ -274,7 +274,6 @@ private static LrRelationship selfRelationship(LrRelationship relationship) { DataObjectPropertyReader.reader() : relationship.getPropertyReader(); PropertyReader reader = (root, name) -> { Object[] row = (Object[]) root; - name = name.replace("@aggregated:", ""); return delegate.value(row[0], name); }; return decoratedRelationship(relationship, reader); diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java index 7399598df..e6e8c74ba 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/EncoderService.java @@ -43,7 +43,8 @@ public class EncoderService implements IEncoderService { public EncoderService(@Inject List filters, @Inject IAttributeEncoderFactory attributeEncoderFactory, - @Inject IStringConverterFactory stringConverterFactory, @Inject IRelationshipMapper relationshipMapper, + @Inject IStringConverterFactory stringConverterFactory, + @Inject IRelationshipMapper relationshipMapper, @Inject Map propertyMetadataEncoders) { this.attributeEncoderFactory = attributeEncoderFactory; this.relationshipMapper = relationshipMapper; @@ -60,20 +61,29 @@ public Encoder metadataEncoder(ResourceEntity entity) { @Override public Encoder dataEncoder(ResourceEntity entity) { - CollectionEncoder resultEncoder = resultEncoder(entity); + return buildDataEncoder(entity, entityEncoder(entity)); + } + + private Encoder buildDataEncoder(ResourceEntity entity, Encoder entityEncoder) { + CollectionEncoder resultEncoder = resultEncoder(entity, entityEncoder); + return toDataResponseEncoder(resultEncoder); + } + + protected DataResponseEncoder toDataResponseEncoder(CollectionEncoder resultEncoder) { return new DataResponseEncoder("data", resultEncoder, "total", GenericEncoder.encoder()); } - protected CollectionEncoder resultEncoder(ResourceEntity entity) { - Encoder elementEncoder = collectionElementEncoder(entity); + protected CollectionEncoder resultEncoder(ResourceEntity entity, Encoder elementEncoder) { boolean isMapBy = entity.getMapBy() != null; // notice that we are not passing either qualifier or ordering to the // encoder, as those are presumably applied at the query level.. (unlike // with #nestedToManyEncoder) - CollectionEncoder encoder = new ListEncoder(elementEncoder).withOffset(entity.getFetchOffset()) - .withLimit(entity.getFetchLimit()).shouldFilter(entity.isFiltered()); + CollectionEncoder encoder = new ListEncoder(filteredEncoder(elementEncoder, entity)) + .withOffset(entity.getFetchOffset()) + .withLimit(entity.getFetchLimit()) + .shouldFilter(entity.isFiltered()); return isMapBy ? new MapByEncoder(entity.getMapByPath(), null, entity.getMapBy(), encoder, stringConverterFactory, attributeEncoderFactory) @@ -82,12 +92,14 @@ protected CollectionEncoder resultEncoder(ResourceEntity entity) { protected Encoder nestedToManyEncoder(ResourceEntity resourceEntity) { - Encoder elementEncoder = collectionElementEncoder(resourceEntity); + Encoder elementEncoder = entityEncoder(resourceEntity); boolean isMapBy = resourceEntity.getMapBy() != null; // if mapBy is involved, apply filters at MapBy level, not inside // sublists... - ListEncoder listEncoder = new ListEncoder(elementEncoder, isMapBy ? null : resourceEntity.getQualifier(), + ListEncoder listEncoder = new ListEncoder( + filteredEncoder(elementEncoder, resourceEntity), + isMapBy ? null : resourceEntity.getQualifier(), resourceEntity.getOrderings()); listEncoder.withOffset(resourceEntity.getFetchOffset()).withLimit(resourceEntity.getFetchLimit()); @@ -100,11 +112,6 @@ protected Encoder nestedToManyEncoder(ResourceEntity resourceEntity) { resourceEntity.getMapBy(), listEncoder, stringConverterFactory, attributeEncoderFactory) : listEncoder; } - protected Encoder collectionElementEncoder(ResourceEntity resourceEntity) { - Encoder encoder = entityEncoder(resourceEntity); - return filteredEncoder(encoder, resourceEntity); - } - protected Encoder toOneEncoder(ResourceEntity resourceEntity, LrRelationship relationship) { // to-one encoder is made of the following decorator layers (from outer From 3d1152c9830c3f5ca39e133b3fb4c535e8928965 Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Wed, 8 Nov 2017 14:15:26 +0300 Subject: [PATCH 14/15] Aggregation queries support #266 (merge master) --- .../{GET_IT_Aggregate.java => GET_AggregateIT.java} | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) rename link-rest/src/test/java/com/nhl/link/rest/it/{GET_IT_Aggregate.java => GET_AggregateIT.java} (94%) diff --git a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java b/link-rest/src/test/java/com/nhl/link/rest/it/GET_AggregateIT.java similarity index 94% rename from link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java rename to link-rest/src/test/java/com/nhl/link/rest/it/GET_AggregateIT.java index 6a80918fd..e2e99d374 100644 --- a/link-rest/src/test/java/com/nhl/link/rest/it/GET_IT_Aggregate.java +++ b/link-rest/src/test/java/com/nhl/link/rest/it/GET_AggregateIT.java @@ -18,7 +18,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -public class GET_IT_Aggregate extends JerseyTestOnDerby { +public class GET_AggregateIT extends JerseyTestOnDerby { @Override protected void doAddResources(FeatureContext context) { @@ -82,7 +82,8 @@ public void test_Select_AggregationOnRootEntity_GroupByRelated() { .request() .get(); - onSuccess(response).bodyEquals(2, "{\"avg(age)\":15,\"e21\":{\"name\":\"xxx\"}}," + + onSuccess(response).bodyEquals(2, + "{\"avg(age)\":15,\"e21\":{\"name\":\"xxx\"}}," + "{\"avg(age)\":5,\"e21\":{\"name\":\"yyy\"}}"); } @@ -115,7 +116,8 @@ public void test_Select_AggregationOnRelatedEntity_GroupByRoot() { .request() .get(); - onSuccess(response).bodyEquals(2, "{\"@aggregated:e20s\":{\"sum(age)\":30},\"name\":\"xxx\"}," + + onSuccess(response).bodyEquals(2, + "{\"@aggregated:e20s\":{\"sum(age)\":30},\"name\":\"xxx\"}," + "{\"@aggregated:e20s\":{\"sum(age)\":5},\"name\":\"yyy\"}"); } @@ -151,7 +153,8 @@ public void test_Select_AggregationOnRelatedEntity_GroupByBoth() { .request() .get(); - onSuccess(response).bodyEquals(3, "{\"@aggregated:e20s\":{\"name\":\"bbb\",\"sum(age)\":20},\"name\":\"xxx\"}," + + onSuccess(response).bodyEquals(3, + "{\"@aggregated:e20s\":{\"name\":\"bbb\",\"sum(age)\":20},\"name\":\"xxx\"}," + "{\"@aggregated:e20s\":{\"name\":\"aaa\",\"sum(age)\":10},\"name\":\"xxx\"}," + "{\"@aggregated:e20s\":{\"name\":\"ccc\",\"sum(age)\":5},\"name\":\"yyy\"}"); } From 6b055ec7afd832cebf79294c08e9ff0ed7b8d78a Mon Sep 17 00:00:00 2001 From: Andrei Tomashpolskiy Date: Wed, 8 Nov 2017 15:21:24 +0300 Subject: [PATCH 15/15] Aggregation queries support #266 (merge master) --- .../encoder/AttributeEncoderFactory.java | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java index c69543954..725749f84 100644 --- a/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java +++ b/link-rest/src/main/java/com/nhl/link/rest/runtime/encoder/AttributeEncoderFactory.java @@ -3,13 +3,6 @@ import com.nhl.link.rest.EntityProperty; import com.nhl.link.rest.ResourceEntity; import com.nhl.link.rest.encoder.Encoder; -import com.nhl.link.rest.encoder.GenericEncoder; -import com.nhl.link.rest.encoder.ISODateEncoder; -import com.nhl.link.rest.encoder.ISODateTimeEncoder; -import com.nhl.link.rest.encoder.ISOLocalDateEncoder; -import com.nhl.link.rest.encoder.ISOLocalDateTimeEncoder; -import com.nhl.link.rest.encoder.ISOLocalTimeEncoder; -import com.nhl.link.rest.encoder.ISOTimeEncoder; import com.nhl.link.rest.encoder.IdEncoder; import com.nhl.link.rest.meta.LrAttribute; import com.nhl.link.rest.meta.LrEntity; @@ -66,15 +59,9 @@ public AttributeEncoderFactory(Map, Encoder> knownEncoders, @Override public EntityProperty getAttributeProperty(LrEntity entity, LrAttribute attribute) { - String key = entity.getName() + "." + attribute.getName(); - - EntityProperty property = attributePropertiesByPath.get(key); - if (property == null) { - property = buildAttributeProperty(entity, attribute); - attributePropertiesByPath.put(key, property); - } - - return property; + // can't cache encoders for attributes, because we're using ad-hoc "decorated" attributes for aggregation purposes + // e.g. see com.nhl.link.rest.runtime.cayenne.processor.select.CayenneAssembleQueryStage#currentColumnAttribute() + return buildAttributeProperty(entity, attribute); } @Override