diff --git a/commons/src/main/java/org/restheart/utils/BsonUtils.java b/commons/src/main/java/org/restheart/utils/BsonUtils.java index 9fc8ae59a..e964f1655 100644 --- a/commons/src/main/java/org/restheart/utils/BsonUtils.java +++ b/commons/src/main/java/org/restheart/utils/BsonUtils.java @@ -285,6 +285,22 @@ public static Optional get(BsonDocument doc, String path) { } } + /** + * + * @param ctx the JxPathContext build from a BsonDocument + * @param path the path of the field, can use the dot notation + * @return + */ + public static Optional get(JXPathContext ctx, String path) { + final String xpath = dotNotationToXPath(path); + + try { + return Optional.of((BsonValue) ctx.getValue(xpath)); + } catch(Throwable t) { + return Optional.empty(); + } + } + /** * converts the dot notation syntax to xpath * a.b.c -> /a/b/c diff --git a/graphql/src/main/java/org/restheart/graphql/models/GraphQLApp.java b/graphql/src/main/java/org/restheart/graphql/models/GraphQLApp.java index 616437461..b3262820b 100644 --- a/graphql/src/main/java/org/restheart/graphql/models/GraphQLApp.java +++ b/graphql/src/main/java/org/restheart/graphql/models/GraphQLApp.java @@ -172,8 +172,9 @@ public GraphQLApp build() throws IllegalStateException { final Optional> match; if (obj instanceof BsonValue value) { + final var ex = ExchangeWithBsonValue.exchange(value); match = unionMapping.entrySet().stream() - .filter(p -> p.getValue().resolve(ExchangeWithBsonValue.exchange(value))) + .filter(p -> p.getValue().resolve(ex)) .findFirst(); } else { // predicates can only resolve on BsonValues @@ -199,8 +200,9 @@ public GraphQLApp build() throws IllegalStateException { final Optional> match; if (obj instanceof BsonValue value) { + final var ex = ExchangeWithBsonValue.exchange(value); match = interfaceMapping.entrySet().stream() - .filter(p -> p.getValue().resolve(ExchangeWithBsonValue.exchange(value))) + .filter(p -> p.getValue().resolve(ex)) .findFirst(); } else { // predicates can resolve on BsonValues diff --git a/graphql/src/main/java/org/restheart/graphql/predicates/ExchangeWithBsonValue.java b/graphql/src/main/java/org/restheart/graphql/predicates/ExchangeWithBsonValue.java index c9b69fa02..b7663c80f 100644 --- a/graphql/src/main/java/org/restheart/graphql/predicates/ExchangeWithBsonValue.java +++ b/graphql/src/main/java/org/restheart/graphql/predicates/ExchangeWithBsonValue.java @@ -21,6 +21,7 @@ package org.restheart.graphql.predicates; +import org.apache.commons.jxpath.JXPathContext; import org.bson.BsonValue; import io.undertow.server.HttpServerExchange; @@ -44,5 +45,27 @@ public static BsonValue value(HttpServerExchange exchange) { return exchange.getAttachment(DOC_KEY); } + /** + * To enhance the performance of XPath expression evaluations, this method caches the + * JXPathContext object. This cached context facilitates quicker lookups and reduces the + * overhead associated with creating new contexts for each query. + * + * For more information, refer to {@link org.restheart.utils.BsonUtils#get(JXPathContext docCtx, String path)} + * + * @param exchange The exchange containing the BsonValue from which the JXPathContext is constructed. + * @return The JXPathContext built from the BsonValue attached to the exchange. + */ + public static JXPathContext jxPathCtx(HttpServerExchange exchange) { + var ctx = exchange.getAttachment(JX_PATH_CTX_KEY); + + if (ctx == null) { + ctx = JXPathContext.newContext(value(exchange)); + exchange.putAttachment(JX_PATH_CTX_KEY, ctx); + } + + return ctx; + } + private static final AttachmentKey DOC_KEY = AttachmentKey.create(BsonValue.class); + private static final AttachmentKey JX_PATH_CTX_KEY = AttachmentKey.create(JXPathContext.class); } diff --git a/graphql/src/main/java/org/restheart/graphql/predicates/FieldEqPredicate.java b/graphql/src/main/java/org/restheart/graphql/predicates/FieldEqPredicate.java index 1aa677615..56194bdc5 100644 --- a/graphql/src/main/java/org/restheart/graphql/predicates/FieldEqPredicate.java +++ b/graphql/src/main/java/org/restheart/graphql/predicates/FieldEqPredicate.java @@ -25,7 +25,7 @@ import java.util.Map; import java.util.Set; -import org.bson.BsonDocument; +import org.apache.commons.jxpath.JXPathContext; import org.bson.BsonValue; import org.restheart.utils.BsonUtils; @@ -35,7 +35,7 @@ /** * a predicate that resolve to true if the request contains the specified keys */ -public class FieldEqPredicate implements PredicateOverBsonValue { +public class FieldEqPredicate implements PredicateOverJxPathCtx { private final String key; private final BsonValue value; @@ -49,15 +49,11 @@ public FieldEqPredicate(String key, String value) { } @Override - public boolean resolve(BsonValue value) { - if (value instanceof BsonDocument doc) { - var _v = BsonUtils.get(doc, key); + public boolean resolve(JXPathContext ctx) { + var _v = BsonUtils.get(ctx, key); - if (_v.isPresent()) { - return this.value.equals(_v.get()); - } else { - return false; - } + if (_v.isPresent()) { + return this.value.equals(_v.get()); } else { return false; } diff --git a/graphql/src/main/java/org/restheart/graphql/predicates/FieldExists.java b/graphql/src/main/java/org/restheart/graphql/predicates/FieldExists.java index 561218452..390797769 100644 --- a/graphql/src/main/java/org/restheart/graphql/predicates/FieldExists.java +++ b/graphql/src/main/java/org/restheart/graphql/predicates/FieldExists.java @@ -24,8 +24,7 @@ import java.util.Map; import java.util.Set; -import org.bson.BsonDocument; -import org.bson.BsonValue; +import org.apache.commons.jxpath.JXPathContext; import org.restheart.utils.BsonUtils; import com.google.common.collect.Sets; @@ -36,7 +35,7 @@ /** * a predicate that resolve to true if the request contains the specified keys */ -public class FieldExists implements PredicateOverBsonValue { +public class FieldExists implements PredicateOverJxPathCtx { private final Set fields; public FieldExists(String[] fields) { @@ -48,12 +47,8 @@ public FieldExists(String[] fields) { } @Override - public boolean resolve(BsonValue value) { - if (value instanceof BsonDocument doc) { - return this.fields.stream().allMatch(f -> BsonUtils.get(doc, f).isPresent()); - } else { - return false; - } + public boolean resolve(JXPathContext ctx) { + return this.fields.stream().allMatch(f -> BsonUtils.get(ctx, f).isPresent()); } public static class Builder implements PredicateBuilder { diff --git a/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverBsonValue.java b/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverBsonValue.java index 6344c3270..3bb1ed35c 100644 --- a/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverBsonValue.java +++ b/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverBsonValue.java @@ -22,10 +22,12 @@ package org.restheart.graphql.predicates; import org.bson.BsonValue; + import io.undertow.predicate.Predicate; import io.undertow.server.HttpServerExchange; public interface PredicateOverBsonValue extends Predicate { + @Override default boolean resolve(HttpServerExchange exchage) { return resolve(ExchangeWithBsonValue.value(exchage)); } diff --git a/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverJxPathCtx.java b/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverJxPathCtx.java index 25ce48ea7..a95cb9cfc 100644 --- a/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverJxPathCtx.java +++ b/graphql/src/main/java/org/restheart/graphql/predicates/PredicateOverJxPathCtx.java @@ -29,8 +29,7 @@ public interface PredicateOverJxPathCtx extends Predicate { @Override default boolean resolve(HttpServerExchange exchage) { - //return resolve(ExchangeWithBsonValue.jxPathCtx(exchage)); - return true; + return resolve(ExchangeWithBsonValue.jxPathCtx(exchage)); } boolean resolve(JXPathContext doc); diff --git a/graphql/src/test/java/org/restheart/graphql/predicates/PredicatesTest.java b/graphql/src/test/java/org/restheart/graphql/predicates/PredicatesTest.java index 4e22dfc50..436108645 100644 --- a/graphql/src/test/java/org/restheart/graphql/predicates/PredicatesTest.java +++ b/graphql/src/test/java/org/restheart/graphql/predicates/PredicatesTest.java @@ -21,30 +21,33 @@ package org.restheart.graphql.predicates; +import org.apache.commons.jxpath.JXPathContext; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; import static org.restheart.utils.BsonUtils.document; -import org.junit.jupiter.api.Test; import io.undertow.predicate.Predicates; public class PredicatesTest { @Test public void testNullVsAbsent() { var doc = document().put("bar", 1).get(); - var p = (PredicateOverBsonValue) Predicates.parse("field-exists(bar)"); - var np = (PredicateOverBsonValue) Predicates.parse("field-exists(foo)"); + var ctx = JXPathContext.newContext(doc); + var p = (PredicateOverJxPathCtx) Predicates.parse("field-exists(bar)"); + var np = (PredicateOverJxPathCtx) Predicates.parse("field-exists(foo)"); - assertTrue(p.resolve(doc)); - assertFalse(np.resolve(doc)); + assertTrue(p.resolve(ctx)); + assertFalse(np.resolve(ctx)); var nestedDoc = document().put("bar", document().put("foo", 1)).get(); + var nestedDocCtx = JXPathContext.newContext(nestedDoc); - var _p = (PredicateOverBsonValue) Predicates.parse("field-exists(bar.foo)"); - var _np = (PredicateOverBsonValue) Predicates.parse("field-exists(bar.not)"); + var _p = (PredicateOverJxPathCtx) Predicates.parse("field-exists(bar.foo)"); + var _np = (PredicateOverJxPathCtx) Predicates.parse("field-exists(bar.not)"); - assertTrue(_p.resolve(nestedDoc)); - assertFalse(_np.resolve(nestedDoc)); + assertTrue(_p.resolve(nestedDocCtx)); + assertFalse(_np.resolve(nestedDocCtx)); } @Test