Skip to content

Commit 75c4014

Browse files
Merge pull request #1936 from marklogic/MLE-27881
MLE-27881 Add cts.param support to Java Client API
2 parents 2af48b9 + ea92190 commit 75c4014

6 files changed

Lines changed: 189 additions & 4 deletions

File tree

.github/copilot-instructions.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Project Guidelines
2+
3+
## Architecture
4+
- `marklogic-client-api`: core Java client library for interacting with the MarkLogic REST API.
5+
- `ml-development-tools`: Gradle plugin and generators for Data Services endpoint proxies/tests.
6+
- `marklogic-client-api-functionaltests`: functional/regression-style tests, split into fragile/fast/slow groups.
7+
- `test-app`: ml-gradle deployment project that provisions test infrastructure in MarkLogic.
8+
- `examples`: supporting code used by tests and usage examples.
9+
10+
## Build And Test
11+
- Prefer Gradle from the repo root.
12+
- Quick compile verification: `./gradlew clean build -x test`
13+
- Core module tests: `./gradlew marklogic-client-api:test`
14+
- Plugin tests (includes generated tests workflow): `./gradlew ml-development-tools:test`
15+
- Functional tests must run in this order to reduce flakiness:
16+
1. `./gradlew marklogic-client-api-functionaltests:runFragileTests`
17+
2. `./gradlew marklogic-client-api-functionaltests:runFastFunctionalTests`
18+
3. `./gradlew marklogic-client-api-functionaltests:runSlowFunctionalTests`
19+
20+
## Test Environment
21+
- Java 17+ is required for current releases.
22+
- Most tests require a running MarkLogic instance and deployed test resources.
23+
- Typical setup sequence:
24+
1. `docker compose up -d --build`
25+
2. `./gradlew -i mlWaitTillReady`
26+
3. `./gradlew -i mlDeploy`
27+
4. Run module tests
28+
- Override local MarkLogic connection settings via `gradle-local.properties` (`mlHost`, `mlPassword`).
29+
30+
## Quality Controls
31+
- Treat compile warnings as failures: project builds enforce `-Xlint:unchecked`, `-Xlint:deprecation`, and `-Werror`.
32+
- Keep dependency security constraints intact (e.g. forced/excluded dependencies for CVE mitigation in Gradle files).
33+
- When adding or changing first-party Java/Kotlin code, run security scanning steps used by this workspace workflow before finalizing changes.
34+
- Do not relax quality gates (tests/compilation) to make a change pass; fix the underlying issue.
35+
36+
## Code Generation And Automation
37+
- Data Services proxy generation is automated; use `generateEndpointProxies` instead of hand-writing proxy classes.
38+
- `ml-development-tools` test automation uses `generateTests` and `fixMjsModulesForMarkLogic12` before `test`.
39+
- Generated sources commonly include an "IMPORTANT: Do not edit" header. Regenerate from source declarations instead of editing generated output directly.
40+
- For changes affecting generation logic, validate both generator behavior and generated artifact compilation/tests.
41+
42+
## Conventions For Changes
43+
- Keep edits scoped to the target module; avoid cross-module churn unless required.
44+
- Prefer existing patterns in nearby code over introducing new abstractions.
45+
- For test-related fixes, document whether behavior changes impact unit tests, functional tests, or deployment setup.
46+
47+
## Docs To Link (Do Not Duplicate)
48+
- `README.md`: product overview, dependency usage, Java compatibility.
49+
- `CONTRIBUTING.md`: local build/test workflow and MarkLogic test setup.
50+
- `ml-development-tools/src/test/example-project/README.md`: plugin-focused usage/testing notes.

marklogic-client-api/src/main/java/com/marklogic/client/expression/CtsExpr.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44

55
package com.marklogic.client.expression;
@@ -38,6 +38,7 @@
3838
// 2023-10-24 Exception: Manual changes have been made to this to expose the string constructors for cts.point and
3939
// cts.polygon. These changes can be removed once optic-defs.json in the xdmp repository is updated to define these
4040
// constructors.
41+
// 2026-04-15 Exception: Manual changes have been made to expose cts:param prior to generator support.
4142

4243
/**
4344
* Builds expressions to call functions in the cts server library for a row
@@ -2545,6 +2546,26 @@ public interface CtsExpr {
25452546
* @return a server expression with the <a href="{@docRoot}/doc-files/types/cts_query.html">cts:query</a> server data type
25462547
*/
25472548
public CtsQueryExpr orQuery(ServerExpression queries, XsStringSeqVal options);
2549+
/**
2550+
* Returns a parameter placeholder for a cts expression.
2551+
*
2552+
* <p>
2553+
* Provides a client interface to the <a href="http://docs.marklogic.com/cts:param" target="mlserverdoc">cts:param</a> server function.
2554+
* @param name The parameter name. (of <a href="{@docRoot}/doc-files/types/xs_string.html">xs:string</a>)
2555+
* @return a server expression with the <a href="{@docRoot}/doc-files/types/xs_anyAtomicType.html">xs:anyAtomicType</a> server data type
2556+
* @since 8.2.0
2557+
*/
2558+
public ServerExpression param(String name);
2559+
/**
2560+
* Returns a parameter placeholder for a cts expression.
2561+
*
2562+
* <p>
2563+
* Provides a client interface to the <a href="http://docs.marklogic.com/cts:param" target="mlserverdoc">cts:param</a> server function.
2564+
* @param name The parameter name. (of <a href="{@docRoot}/doc-files/types/xs_string.html">xs:string</a>)
2565+
* @return a server expression with the <a href="{@docRoot}/doc-files/types/xs_anyAtomicType.html">xs:anyAtomicType</a> server data type
2566+
* @since 8.2.0
2567+
*/
2568+
public ServerExpression param(XsStringVal name);
25482569
/**
25492570
* Returns the part of speech for a cts:token, if any.
25502571
*

marklogic-client-api/src/main/java/com/marklogic/client/impl/BaseTypeImpl.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44
package com.marklogic.client.impl;
55

@@ -150,7 +150,7 @@ static class ServerExpressionListImpl extends BaseListImpl<BaseArgImpl> implemen
150150
}
151151
static class ServerExpressionCallImpl extends BaseCallImpl<BaseArgImpl> implements ServerExpression {
152152
ServerExpressionCallImpl(String fnPrefix, String fnName, Object[] fnArgs) {
153-
super(fnPrefix, fnName, convertList(fnArgs));
153+
super(fnPrefix, fnName, convertList(validateNoOpticParamInCtsCall(fnPrefix, fnName, fnArgs)));
154154
}
155155
}
156156

@@ -394,6 +394,46 @@ static private void astifyObject(StringBuilder strb, Object value) {
394394
}
395395
}
396396

397+
static private Object[] validateNoOpticParamInCtsCall(String fnPrefix, String fnName, Object[] fnArgs) {
398+
if (!"cts".equals(fnPrefix) || fnArgs == null) {
399+
return fnArgs;
400+
}
401+
if (containsPlanParam(fnArgs)) {
402+
throw new IllegalArgumentException(
403+
"Cannot pass op:param() to cts:" + fnName + "(). Use cts:param() for cts namespace expressions."
404+
);
405+
}
406+
return fnArgs;
407+
}
408+
409+
static private boolean containsPlanParam(Object value) {
410+
if (value == null) {
411+
return false;
412+
}
413+
if (value instanceof PlanParamExpr) {
414+
return true;
415+
}
416+
if (value instanceof Object[]) {
417+
for (Object item : (Object[]) value) {
418+
if (containsPlanParam(item)) {
419+
return true;
420+
}
421+
}
422+
return false;
423+
}
424+
if (value instanceof BaseListImpl) {
425+
return containsPlanParam(((BaseListImpl<?>) value).getArgsImpl());
426+
}
427+
if (value instanceof BaseMapImpl) {
428+
return containsPlanParam(((BaseMapImpl) value).getMap().values().toArray());
429+
}
430+
if (value instanceof java.util.Map<?, ?>) {
431+
java.util.Map<?, ?> mapValue = (java.util.Map<?, ?>) value;
432+
return containsPlanParam(mapValue.keySet().toArray()) || containsPlanParam(mapValue.values().toArray());
433+
}
434+
return false;
435+
}
436+
397437
static BaseArgImpl[] convertList(Object[] items) {
398438
return convertList(items, BaseArgImpl.class);
399439
}

marklogic-client-api/src/main/java/com/marklogic/client/impl/CtsExprImpl.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44

55
package com.marklogic.client.impl;
@@ -41,6 +41,7 @@
4141
// 2023-10-24 Exception: Manual changes have been made to this to expose the string constructors for cts.point and
4242
// cts.polygon. These changes can be removed once optic-defs.json in the xdmp repository is updated to define these
4343
// constructors.
44+
// 2026-04-15 Exception: Manual changes have been made to expose cts:param prior to generator support.
4445

4546
class CtsExprImpl implements CtsExpr {
4647

@@ -1684,6 +1685,24 @@ public CtsQueryExpr orQuery(ServerExpression queries, XsStringSeqVal options) {
16841685
}
16851686

16861687

1688+
@Override
1689+
public ServerExpression param(String name) {
1690+
if (name == null) {
1691+
throw new IllegalArgumentException("name parameter for param() cannot be null");
1692+
}
1693+
return param(new XsValueImpl.StringValImpl(name));
1694+
}
1695+
1696+
1697+
@Override
1698+
public ServerExpression param(XsStringVal name) {
1699+
if (name == null) {
1700+
throw new IllegalArgumentException("name parameter for param() cannot be null");
1701+
}
1702+
return new XsExprImpl.AnyAtomicTypeCallImpl("cts", "param", new Object[]{ name });
1703+
}
1704+
1705+
16871706
@Override
16881707
public ServerExpression partOfSpeech(ServerExpression token) {
16891708
if (token == null) {

marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderSubImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,7 @@ public PlanPrefixer prefixer(String base) {
694694
public PlanParamExpr param(String name) {
695695
return new PlanParamBase(name);
696696
}
697+
697698
@Override
698699
public PlanParamExpr param(XsStringVal name) {
699700
if (name == null) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
3+
*/
4+
package com.marklogic.client.impl;
5+
6+
import com.fasterxml.jackson.databind.node.ObjectNode;
7+
import com.marklogic.client.expression.PlanBuilder;
8+
import com.marklogic.client.io.JacksonHandle;
9+
import org.junit.jupiter.api.Test;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertThrows;
13+
14+
public class CtsParamExprTest {
15+
16+
@Test
17+
void exportsCtsParamInCollectionQuery() {
18+
PlanBuilderSubImpl p = new PlanBuilderSubImpl();
19+
20+
PlanBuilder.ModifyPlan employeesPlan = p
21+
.fromView("main", "employees")
22+
.select(p.col("EmployeeID"), p.col("FirstName"), p.col("LastName"))
23+
.where(p.cts.collectionQuery(p.cts.param("collection")));
24+
25+
JacksonHandle handle = new JacksonHandle();
26+
employeesPlan.export(handle);
27+
ObjectNode exportNode = (ObjectNode) handle.get();
28+
29+
assertEquals("op", exportNode.path("$optic").path("ns").asText());
30+
assertEquals("operators", exportNode.path("$optic").path("fn").asText());
31+
assertEquals("from-view", exportNode.path("$optic").path("args").get(0).path("fn").asText());
32+
assertEquals("select", exportNode.path("$optic").path("args").get(1).path("fn").asText());
33+
assertEquals("where", exportNode.path("$optic").path("args").get(2).path("fn").asText());
34+
assertEquals("collection-query", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("fn").asText());
35+
assertEquals("param", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("args").get(0).path("fn").asText());
36+
assertEquals("cts", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("args").get(0).path("ns").asText());
37+
assertEquals("collection", exportNode.path("$optic").path("args").get(2).path("args").get(0).path("args").get(0).path("args").get(0).path("args").get(0).asText());
38+
}
39+
40+
@Test
41+
void rejectsOpParamInCtsNamespace() {
42+
PlanBuilderSubImpl p = new PlanBuilderSubImpl();
43+
44+
IllegalArgumentException ex = assertThrows(
45+
IllegalArgumentException.class,
46+
() -> p.cts.collectionQuery(p.param("collection"))
47+
);
48+
49+
assertEquals(
50+
"Cannot pass op:param() to cts:collection-query(). Use cts:param() for cts namespace expressions.",
51+
ex.getMessage()
52+
);
53+
}
54+
}

0 commit comments

Comments
 (0)