diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpressionVisitor.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpressionVisitor.java index f0720c906..129f4d79b 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpressionVisitor.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpressionVisitor.java @@ -287,6 +287,7 @@ public Expression visit(ExpNamedParameter node, Expression data) { @Override public Expression visit(ExpPath node, Expression parent) { ASTPath path = PathOps.parsePath((String) node.jjtGetValue()); + path.setPathAliases(node.getPathAliases()); return process(node, parent, path); } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java index 791a405c6..da239f6fa 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java @@ -20,6 +20,14 @@ public void parseNamedParams() { assertEquals(ExpressionFactory.exp("a = 'x'"), e); } + @Test + public void parsePathAliases() { + Expression e = parser.parse(Exp.parse("a.b.c#p1.d#p2")); + Expression expected = ExpressionFactory.exp("a.b.c#p1.d#p2"); + assertEquals(expected, e); + assertEquals(expected.getPathAliases(), e.getPathAliases()); + } + @Test public void parsePositionalParams() { Expression e = parser.parse(Exp.parse("a = $a").positionalParams("x")); diff --git a/agrest-engine/src/main/java/io/agrest/exp/parser/ExpPath.java b/agrest-engine/src/main/java/io/agrest/exp/parser/ExpPath.java index e90939e8f..953cf2081 100644 --- a/agrest-engine/src/main/java/io/agrest/exp/parser/ExpPath.java +++ b/agrest-engine/src/main/java/io/agrest/exp/parser/ExpPath.java @@ -2,8 +2,16 @@ /* JavaCCOptions:MULTI=true,NODE_USES_PARSER=false,VISITOR=true,TRACK_TOKENS=false,NODE_PREFIX=Exp,NODE_EXTENDS=,NODE_FACTORY=,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ package io.agrest.exp.parser; +import io.agrest.exp.AgExpressionException; + +import java.util.Collections; +import java.util.Map; + public class ExpPath extends SimpleNode { + + protected Map pathAliases = Collections.emptyMap(); + public ExpPath(int id) { super(id); } @@ -16,6 +24,11 @@ public ExpPath() { super(AgExpressionParserTreeConstants.JJTPATH); } + public ExpPath(String path) { + this(); + setPath(path); + } + public String getPath() { return (String)jjtGetValue(); } @@ -24,6 +37,20 @@ public void setPath(String path) { jjtSetValue(path); } + public Map getPathAliases() { + return pathAliases; + } + + public void setPathAliases(Map pathAliases) { + this.pathAliases = pathAliases; + } + + @Override + public void jjtSetValue(Object value) { + super.jjtSetValue(value); + syncAliases(); + } + /** Accept the visitor. **/ public T jjtAccept(AgExpressionParserVisitor visitor, T data) { @@ -31,6 +58,14 @@ public T jjtAccept(AgExpressionParserVisitor visitor, T data) { visitor.visit(this, data); } + protected void syncAliases() { + try { + ParsingUtils.processPathAliases(this); + } catch (ParseException e) { + throw new AgExpressionException(e); + } + } + @Override protected ExpPath shallowCopy() { ExpPath copy = new ExpPath(id); diff --git a/agrest-engine/src/main/java/io/agrest/exp/parser/ParsingUtils.java b/agrest-engine/src/main/java/io/agrest/exp/parser/ParsingUtils.java new file mode 100644 index 000000000..99c436f6f --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/exp/parser/ParsingUtils.java @@ -0,0 +1,36 @@ +package io.agrest.exp.parser; + +import java.util.HashMap; +import java.util.Map; + +public final class ParsingUtils { + + private ParsingUtils() { + } + + static void processPathAliases(ExpPath pathExp) throws ParseException { + String path = pathExp.getPath(); + if (!path.contains("#")) { + return; + } + + String[] pathSegments = path.split("\\."); + Map aliasMap = new HashMap<>(); + for (int i = 0; i < pathSegments.length; i++) { + if (pathSegments[i].contains("#")) { + String[] splitSegment = pathSegments[i].split("#"); + if (splitSegment[1].endsWith("+")) { + splitSegment[0] += '+'; + splitSegment[1] = splitSegment[1].substring(0, splitSegment[1].length() - 1); + } + String previousAlias = aliasMap.putIfAbsent(splitSegment[1], splitSegment[0]); + if (previousAlias != null && !previousAlias.equals(splitSegment[0])) { + throw new ParseException("Can't add the same alias to different path segments."); + } + pathSegments[i] = splitSegment[1]; + } + } + pathExp.setPath(String.join(".", pathSegments)); + pathExp.setPathAliases(aliasMap); + } +} diff --git a/agrest-engine/src/main/java/io/agrest/protocol/Exp.java b/agrest-engine/src/main/java/io/agrest/protocol/Exp.java index 944908ff2..ce3c3630c 100644 --- a/agrest-engine/src/main/java/io/agrest/protocol/Exp.java +++ b/agrest-engine/src/main/java/io/agrest/protocol/Exp.java @@ -54,9 +54,7 @@ static Exp withPositionalParams(String template, Object... params) { * @since 5.0 */ static Exp path(String path) { - ExpPath pathExp = new ExpPath(); - pathExp.jjtSetValue(Objects.requireNonNull(path)); - return pathExp; + return new ExpPath(Objects.requireNonNull(path)); } /** diff --git a/agrest-engine/src/test/java/io/agrest/exp/parser/ExpPathTest.java b/agrest-engine/src/test/java/io/agrest/exp/parser/ExpPathTest.java index 2c3faaa0c..b5d4ac6d7 100644 --- a/agrest-engine/src/test/java/io/agrest/exp/parser/ExpPathTest.java +++ b/agrest-engine/src/test/java/io/agrest/exp/parser/ExpPathTest.java @@ -1,8 +1,13 @@ package io.agrest.exp.parser; import io.agrest.AgException; +import io.agrest.exp.AgExpressionException; import io.agrest.protocol.Exp; +import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.aggregator.ArgumentsAggregator; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; @@ -63,6 +68,23 @@ public void parsedToString(String expString, String expected) { assertEquals(expected, Exp.parse(expString).toString()); } + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "a.b.c.d|a.b.c.d", + "a.b+.c+.d|a.b+.c+.d", + "a.b.c#p1.d#p2|a.b.p1.p2|p1-c|p2-d", + "a.b+.c#p1+.d#p2|a.b+.p1.p2|p1-c+|p2-d", + }) + public void pathAliases(String expString, String expectedPath, @AggregateWith(VarargsAggregator.class) String... expectedAliases) { + ExpPath expPath = new ExpPath(expString); + assertEquals(expectedPath, expPath.getPath()); + assertEquals(expectedAliases.length, expPath.getPathAliases().size()); + for (String expectedAlias : expectedAliases) { + String[] aliasMapping = expectedAlias.split("-"); + assertEquals(aliasMapping[1], expPath.getPathAliases().get(aliasMapping[0])); + } + } + @ParameterizedTest @ValueSource(strings = { "0a", @@ -78,4 +100,22 @@ public void parsedToString(String expString, String expected) { public void parseInvalidGrammar(String expString) { assertThrows(AgException.class, () -> Exp.parse(expString)); } + + @ParameterizedTest + @ValueSource(strings = { + "a.b.c#p1.d#p1" + }) + public void parseInvalidAliases(String expString) { + assertThrows(AgExpressionException.class, () -> Exp.path(expString)); + } + + static class VarargsAggregator implements ArgumentsAggregator { + @Override + public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) { + return accessor.toList().stream() + .skip(context.getIndex()) + .map(String::valueOf) + .toArray(String[]::new); + } + } }