From a89d34fb9d824fbb09b61e11f27fa56eb51aa18a Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 29 Jul 2025 09:41:08 -0400 Subject: [PATCH 01/33] Bumped to 7.3-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index eddce958e..43995279a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.marklogic -version=7.2.0 +version=7.3-SNAPSHOT describedName=MarkLogic Java Client API publishUrl=file:../marklogic-java/releases From c95e51ff06f78533b80e87dd3fd4a3bf214cc3c2 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 25 Aug 2025 09:53:23 -0400 Subject: [PATCH 02/33] MLE-12345 Polaris fixes Not sure how it's reporting on files under ".kotlin", so hopefully the gitignore addition will prevent that. --- .gitignore | 2 ++ docker-compose.yaml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 1bf7c14e2..bfa99a5f5 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ ml-development-tools/src/test/java/com/marklogic/client/test/dbfunction/generate .vscode docker/ + +.kotlin diff --git a/docker-compose.yaml b/docker-compose.yaml index 4a851837e..569f62b3f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,10 @@ services: - MARKLOGIC_INIT=true - MARKLOGIC_ADMIN_USERNAME=admin - MARKLOGIC_ADMIN_PASSWORD=admin + # The NET_RAW capability allows a process to create raw sockets. Polaris does not like that. + # This setting removes the NET_RAW capability from the container. + cap_drop: + - NET_RAW volumes: - ${MARKLOGIC_LOGS_VOLUME}:/var/opt/MarkLogic/Logs ports: From 7c377fdbd29f3ecb97bec2ebe35507c0e6b99e37 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 22 Jul 2025 15:24:12 -0400 Subject: [PATCH 03/33] MLE-23032 Bumping to Java 17 and OkHttp 5 --- Jenkinsfile | 11 ++++------- build.gradle | 6 +++--- gradle.properties | 2 +- .../build.gradle | 4 ++-- marklogic-client-api/build.gradle | 10 +++++----- .../OAuthAuthenticationConfigurerTest.java | 17 ++++++++++------- .../TokenAuthenticationInterceptorTest.java | 18 +++++++++++++----- ml-development-tools/build.gradle | 4 ++-- 8 files changed, 40 insertions(+), 32 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 98e6d169d..aca93c979 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,14 +1,10 @@ @Library('shared-libraries') _ def getJava(){ - if(env.JAVA_VERSION=="JAVA17"){ - return "/home/builder/java/jdk-17.0.2" - }else if(env.JAVA_VERSION=="JAVA11"){ - return "/home/builder/java/jdk-11.0.2" - }else if(env.JAVA_VERSION=="JAVA21"){ + if (env.JAVA_VERSION == "JAVA21") { return "/home/builder/java/jdk-21.0.1" - }else{ - return "/home/builder/java/openjdk-1.8.0-262" + } else { + return "/home/builder/java/jdk-17.0.2" } } @@ -26,6 +22,7 @@ def setupDockerMarkLogic(String image){ MARKLOGIC_IMAGE='''+image+''' MARKLOGIC_LOGS_VOLUME=marklogicLogs docker compose up -d --build echo "Waiting for MarkLogic server to initialize." sleep 60s + export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH ./gradlew mlTestConnections diff --git a/build.gradle b/build.gradle index f5d11837e..5af224625 100644 --- a/build.gradle +++ b/build.gradle @@ -23,10 +23,10 @@ subprojects { options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] } - // To ensure that the Java Client continues to support Java 8, both source and target compatibility are set to 1.8. java { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } configurations { diff --git a/gradle.properties b/gradle.properties index 43995279a..17da5b520 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.marklogic -version=7.3-SNAPSHOT +version=8.0-SNAPSHOT describedName=MarkLogic Java Client API publishUrl=file:../marklogic-java/releases diff --git a/marklogic-client-api-functionaltests/build.gradle b/marklogic-client-api-functionaltests/build.gradle index c12d6085f..489a1d077 100755 --- a/marklogic-client-api-functionaltests/build.gradle +++ b/marklogic-client-api-functionaltests/build.gradle @@ -19,7 +19,7 @@ dependencies { testImplementation 'org.skyscreamer:jsonassert:1.5.3' testImplementation 'org.slf4j:slf4j-api:2.0.17' testImplementation 'commons-io:commons-io:2.17.0' - testImplementation 'com.squareup.okhttp3:okhttp:4.12.0' + testImplementation 'com.squareup.okhttp3:okhttp:5.1.0' testImplementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" testImplementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" testImplementation "org.jdom:jdom2:2.0.6.1" @@ -32,7 +32,7 @@ dependencies { exclude module: "commons-lang3" } - testImplementation 'ch.qos.logback:logback-classic:1.3.15' + testImplementation 'ch.qos.logback:logback-classic:1.5.18' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testImplementation 'org.xmlunit:xmlunit-legacy:2.10.0' diff --git a/marklogic-client-api/build.gradle b/marklogic-client-api/build.gradle index b2bfdd46d..7bb2b8ffe 100644 --- a/marklogic-client-api/build.gradle +++ b/marklogic-client-api/build.gradle @@ -18,9 +18,9 @@ dependencies { api "jakarta.xml.bind:jakarta.xml.bind-api:3.0.1" implementation "org.glassfish.jaxb:jaxb-runtime:3.0.2" - implementation 'com.squareup.okhttp3:okhttp:4.12.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' - implementation 'io.github.rburgst:okhttp-digest:2.7' + implementation 'com.squareup.okhttp3:okhttp:5.1.0' + implementation 'com.squareup.okhttp3:logging-interceptor:5.1.0' + implementation 'io.github.rburgst:okhttp-digest:3.1.1' // We tried upgrading to the org.eclipse.angus:angus-mail dependency, but we ran into significant performance issues // with using the Java Client eval call in our Spark connector. Example - an eval() call for getting 50k URIs would @@ -57,10 +57,10 @@ dependencies { // Starting with mockito 5.x, Java 11 is required, so sticking with 4.x as we have to support Java 8. testImplementation "org.mockito:mockito-core:4.11.0" testImplementation "org.mockito:mockito-inline:4.11.0" - testImplementation "com.squareup.okhttp3:mockwebserver:4.12.0" + testImplementation "com.squareup.okhttp3:mockwebserver3:5.1.0" testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jacksonVersion}" - testImplementation 'ch.qos.logback:logback-classic:1.3.15' + testImplementation 'ch.qos.logback:logback-classic:1.5.18' // Using this to avoid a schema validation issue with the regular xercesImpl testImplementation 'org.opengis.cite.xerces:xercesImpl-xsd11:2.12-beta-r1667115' diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/OAuthAuthenticationConfigurerTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/OAuthAuthenticationConfigurerTest.java index b6bee7c30..c8a2ddd8a 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/OAuthAuthenticationConfigurerTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/OAuthAuthenticationConfigurerTest.java @@ -4,20 +4,23 @@ package com.marklogic.client.impl.okhttp; import com.marklogic.client.DatabaseClientFactory; +import mockwebserver3.MockWebServer; import okhttp3.Request; -import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -public class OAuthAuthenticationConfigurerTest { +class OAuthAuthenticationConfigurerTest { @Test - void test() { - DatabaseClientFactory.OAuthContext authContext = new DatabaseClientFactory.OAuthContext("abc123"); - Request request = new Request.Builder().url(new MockWebServer().url("/url-doesnt-matter")).build(); + void test() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + Request request = new Request.Builder().url(server.url("/url-doesnt-matter")).build(); - Request authenticatedRequest = new OAuthAuthenticationConfigurer().makeAuthenticatedRequest(request, authContext); - assertEquals("Bearer abc123", authenticatedRequest.header("Authorization")); + DatabaseClientFactory.OAuthContext authContext = new DatabaseClientFactory.OAuthContext("abc123"); + Request authenticatedRequest = new OAuthAuthenticationConfigurer().makeAuthenticatedRequest(request, authContext); + assertEquals("Bearer abc123", authenticatedRequest.header("Authorization")); + } } } diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/TokenAuthenticationInterceptorTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/TokenAuthenticationInterceptorTest.java index 2f1496497..b1529fb2c 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/TokenAuthenticationInterceptorTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/TokenAuthenticationInterceptorTest.java @@ -4,10 +4,11 @@ package com.marklogic.client.impl.okhttp; import com.marklogic.client.ext.helper.LoggingObject; +import mockwebserver3.MockResponse; +import mockwebserver3.MockWebServer; import okhttp3.OkHttpClient; import okhttp3.Request; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -23,15 +24,17 @@ * Uses OkHttp's MockWebServer to completely mock a MarkLogic instance so that we can control what response codes are * returned and processed by TokenAuthenticationInterceptor. */ -public class TokenAuthenticationInterceptorTest extends LoggingObject { +class TokenAuthenticationInterceptorTest extends LoggingObject { private MockWebServer mockWebServer; private FakeTokenGenerator fakeTokenGenerator; private OkHttpClient okHttpClient; @BeforeEach - void beforeEach() { + void beforeEach() throws IOException { mockWebServer = new MockWebServer(); + mockWebServer.start(); + fakeTokenGenerator = new FakeTokenGenerator(); ProgressDataCloudAuthenticationConfigurer.TokenAuthenticationInterceptor interceptor = @@ -43,6 +46,11 @@ void beforeEach() { okHttpClient = new OkHttpClient.Builder().addInterceptor(interceptor).build(); } + @AfterEach + void tearDown() { + mockWebServer.close(); + } + @Test void receive401() { enqueueResponseCodes(200, 200, 401, 200); @@ -110,7 +118,7 @@ void multipleThreads() throws Exception { */ private void enqueueResponseCodes(int... codes) { for (int code : codes) { - mockWebServer.enqueue(new MockResponse().setResponseCode(code)); + mockWebServer.enqueue(new MockResponse.Builder().code(code).build()); } } diff --git a/ml-development-tools/build.gradle b/ml-development-tools/build.gradle index 84c83e245..82dfc7513 100644 --- a/ml-development-tools/build.gradle +++ b/ml-development-tools/build.gradle @@ -71,10 +71,10 @@ publishing { } compileKotlin { - kotlinOptions.jvmTarget = '1.8' + kotlinOptions.jvmTarget = '17' } compileTestKotlin { - kotlinOptions.jvmTarget = '1.8' + kotlinOptions.jvmTarget = '17' } tasks.register("generateTests", JavaExec) { From f3ab5402302c45abe7ee430925ac3510dc27d56c Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Fri, 29 Aug 2025 13:57:45 -0400 Subject: [PATCH 04/33] MLE-23032 Fixing Jenkins build for Java 21 --- gradle.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gradle.properties b/gradle.properties index 17da5b520..8d8fe5986 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,3 +18,8 @@ testUseReverseProxyServer=false cloudHost= cloudKey= cloudBasePath= + +# See https://docs.gradle.org/current/userguide/toolchains.html#sec:custom_loc for information +# on custom toolchain locations in Gradle. Adding these to try to make Jenkins happy. +org.gradle.java.installations.fromEnv=JAVA_HOME_DIR +org.gradle.java.installations.paths=/home/builder/java/jdk-17.0.2,/home/builder/java/jdk-21.0.1 From 75819de7e8b2f450b4b9479dd9393352dd0bc719 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Fri, 29 Aug 2025 14:16:37 -0400 Subject: [PATCH 05/33] MLE-23230 Trying out retry interceptor This is a prototype; we don't want to apply it automatically. Intent for now is to see if this helps avoid connection errors during the regression piplines in Jenkins. --- .../client/impl/okhttp/OkHttpUtil.java | 9 +- .../client/impl/okhttp/RetryInterceptor.java | 87 +++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java index b14da8ada..93a273db8 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java @@ -7,11 +7,7 @@ import com.marklogic.client.impl.HTTPKerberosAuthInterceptor; import com.marklogic.client.impl.HTTPSamlAuthInterceptor; import com.marklogic.client.impl.SSLUtil; -import okhttp3.ConnectionPool; -import okhttp3.CookieJar; -import okhttp3.Dns; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; +import okhttp3.*; import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; @@ -82,6 +78,9 @@ public static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseC OkHttpUtil.configureSocketFactory(clientBuilder, sslContext, trustManager); OkHttpUtil.configureHostnameVerifier(clientBuilder, sslVerifier); + // Trying this out for all calls initially to see how the regression test piplines do. + clientBuilder.addInterceptor(new RetryInterceptor(3, 1000, 2, 8000)); + return clientBuilder; } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java new file mode 100644 index 000000000..2f54430a8 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.impl.okhttp; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +/** + * OkHttp interceptor that retries requests on certain connection failures, + * which can be helpful when MarkLogic is temporarily unavailable during restarts. + */ +class RetryInterceptor implements Interceptor { + + private final static Logger logger = org.slf4j.LoggerFactory.getLogger(RetryInterceptor.class); + + private final int maxRetries; + private final long initialDelayMs; + private final double backoffMultiplier; + private final long maxDelayMs; + + RetryInterceptor(int maxRetries, long initialDelayMs, double backoffMultiplier, long maxDelayMs) { + this.maxRetries = maxRetries; + this.initialDelayMs = initialDelayMs; + this.backoffMultiplier = backoffMultiplier; + this.maxDelayMs = maxDelayMs; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + IOException lastException = null; + + for (int attempt = 0; attempt <= maxRetries; attempt++) { + try { + return chain.proceed(request); + } catch (IOException e) { + lastException = e; + + if (attempt == maxRetries || !isRetryableException(e)) { + logger.warn("Not retryable: {}; {}", e.getClass(), e.getMessage()); + throw e; + } + + long delay = calculateDelay(attempt); + logger.warn("Request to {} failed (attempt {}/{}): {}. Retrying in {}ms", + request.url(), attempt + 1, maxRetries, e.getMessage(), delay); + + sleep(delay); + } + } + + throw lastException; + } + + private boolean isRetryableException(IOException e) { + return e instanceof ConnectException || + e instanceof SocketTimeoutException || + e instanceof UnknownHostException || + (e.getMessage() != null && ( + e.getMessage().contains("Failed to connect") || + e.getMessage().contains("unexpected end of stream") || + e.getMessage().contains("Connection reset") || + e.getMessage().contains("Read timed out") + )); + } + + private long calculateDelay(int attempt) { + long delay = (long) (initialDelayMs * Math.pow(backoffMultiplier, attempt)); + return Math.min(delay, maxDelayMs); + } + + private void sleep(long delay) { + try { + Thread.sleep(delay); + } catch (InterruptedException ie) { + logger.warn("Ignoring InterruptedException while sleeping for retry delay: {}", ie.getMessage()); + } + } +} From bf6f5c14f062aefe60389121e5b43d359a210a30 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Wed, 3 Sep 2025 14:56:22 -0400 Subject: [PATCH 06/33] MLE-23230 Tweaking retry interceptor --- .../com/marklogic/client/impl/okhttp/RetryInterceptor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java index 2f54430a8..eeaaf0294 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java @@ -68,7 +68,8 @@ private boolean isRetryableException(IOException e) { e.getMessage().contains("Failed to connect") || e.getMessage().contains("unexpected end of stream") || e.getMessage().contains("Connection reset") || - e.getMessage().contains("Read timed out") + e.getMessage().contains("Read timed out") || + e.getMessage().contains("Broken pipe") )); } From 9e70443920c0059e096ceb9ec53a5c2f62c11a09 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Fri, 19 Sep 2025 15:19:45 -0400 Subject: [PATCH 07/33] MLE-23230 Applying RetryInterceptor in test plumbing This avoids hardcoding it in the actual client and let's us still see the results for the tests. Also fixed a warning from Polaris about the interceptor. --- .../client/functionaltest/ConnectedRESTQA.java | 8 ++++++++ .../com/marklogic/client/impl/okhttp/OkHttpUtil.java | 3 --- .../marklogic/client/impl/okhttp/RetryInterceptor.java | 10 ++++------ .../test/java/com/marklogic/client/test/Common.java | 9 +++++++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java index 40c6e17d5..52f1d743f 100644 --- a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java +++ b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java @@ -15,7 +15,9 @@ import com.marklogic.client.DatabaseClientFactory; import com.marklogic.client.FailedRequestException; import com.marklogic.client.admin.ServerConfigurationManager; +import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; import com.marklogic.client.impl.SSLUtil; +import com.marklogic.client.impl.okhttp.RetryInterceptor; import com.marklogic.client.io.DocumentMetadataHandle; import com.marklogic.client.io.DocumentMetadataHandle.Capability; import com.marklogic.client.query.QueryManager; @@ -45,6 +47,12 @@ public abstract class ConnectedRESTQA { + static { + DatabaseClientFactory.removeConfigurators(); + DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) client -> + client.addInterceptor(new RetryInterceptor(3, 1000, 2, 8000))); + } + private static Properties testProperties = null; private static String authType; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java index 93a273db8..3dff7a53d 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java @@ -78,9 +78,6 @@ public static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseC OkHttpUtil.configureSocketFactory(clientBuilder, sslContext, trustManager); OkHttpUtil.configureHostnameVerifier(clientBuilder, sslVerifier); - // Trying this out for all calls initially to see how the regression test piplines do. - clientBuilder.addInterceptor(new RetryInterceptor(3, 1000, 2, 8000)); - return clientBuilder; } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java index eeaaf0294..dd6879a53 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java @@ -17,7 +17,7 @@ * OkHttp interceptor that retries requests on certain connection failures, * which can be helpful when MarkLogic is temporarily unavailable during restarts. */ -class RetryInterceptor implements Interceptor { +public class RetryInterceptor implements Interceptor { private final static Logger logger = org.slf4j.LoggerFactory.getLogger(RetryInterceptor.class); @@ -26,7 +26,7 @@ class RetryInterceptor implements Interceptor { private final double backoffMultiplier; private final long maxDelayMs; - RetryInterceptor(int maxRetries, long initialDelayMs, double backoffMultiplier, long maxDelayMs) { + public RetryInterceptor(int maxRetries, long initialDelayMs, double backoffMultiplier, long maxDelayMs) { this.maxRetries = maxRetries; this.initialDelayMs = initialDelayMs; this.backoffMultiplier = backoffMultiplier; @@ -36,14 +36,11 @@ class RetryInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); - IOException lastException = null; for (int attempt = 0; attempt <= maxRetries; attempt++) { try { return chain.proceed(request); } catch (IOException e) { - lastException = e; - if (attempt == maxRetries || !isRetryableException(e)) { logger.warn("Not retryable: {}; {}", e.getClass(), e.getMessage()); throw e; @@ -57,7 +54,8 @@ public Response intercept(Chain chain) throws IOException { } } - throw lastException; + // This should never be reached due to loop logic, but is required for compilation. + throw new IllegalStateException("Unexpected end of retry loop"); } private boolean isRetryableException(IOException e) { diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java index bf2d51a47..9b11eee9d 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java @@ -8,9 +8,12 @@ import com.marklogic.client.DatabaseClient; import com.marklogic.client.DatabaseClientBuilder; import com.marklogic.client.DatabaseClientFactory; +import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; +import com.marklogic.client.impl.okhttp.RetryInterceptor; import com.marklogic.client.io.DocumentMetadataHandle; import com.marklogic.mgmt.ManageClient; import com.marklogic.mgmt.ManageConfig; +import okhttp3.OkHttpClient; import org.springframework.util.FileCopyUtils; import org.w3c.dom.DOMException; import org.w3c.dom.Document; @@ -29,6 +32,12 @@ public class Common { + static { + DatabaseClientFactory.removeConfigurators(); + DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) client -> + client.addInterceptor(new RetryInterceptor(3, 1000, 2, 8000))); + } + final public static String USER = "rest-writer"; final public static String PASS = "x"; final public static String REST_ADMIN_USER = "rest-admin"; From 38e2e7b6e96e65a2f5282f23edfb592c0b545270 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 22 Sep 2025 12:43:10 -0400 Subject: [PATCH 08/33] MLE-24405 Cleaned up RESTServices API This internal API had several unused methods. In addition, application of the client configurators happens in OkHttpServices now. That removes as much OkHttp stuff as possible from DatabaseClientFactory. This will greatly simplify shifting to the JDK HTTP client some time in the future. --- .../client/DatabaseClientFactory.java | 37 +--- .../OkHttpClientBuilderFactory.java | 4 +- .../client/impl/DatabaseClientImpl.java | 2 - .../marklogic/client/impl/OkHttpServices.java | 171 +++++------------- .../marklogic/client/impl/RESTServices.java | 56 ++---- .../client/impl/okhttp/OkHttpUtil.java | 10 +- 6 files changed, 81 insertions(+), 199 deletions(-) diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java index 8406d7213..10bb1c69c 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java @@ -7,7 +7,6 @@ import com.marklogic.client.impl.*; import com.marklogic.client.io.marker.ContentHandle; import com.marklogic.client.io.marker.ContentHandleFactory; -import okhttp3.OkHttpClient; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; @@ -31,7 +30,7 @@ */ public class DatabaseClientFactory { - static private List> clientConfigurators = Collections.synchronizedList(new ArrayList<>()); + static private List clientConfigurators = Collections.synchronizedList(new ArrayList<>()); static private HandleFactoryRegistry handleRegistry = HandleFactoryRegistryImpl.newDefault(); @@ -1329,7 +1328,6 @@ static public DatabaseClient newClient(String host, int port, String database, static public DatabaseClient newClient(String host, int port, String basePath, String database, SecurityContext securityContext, DatabaseClient.ConnectionType connectionType) { - RESTServices services = new OkHttpServices(); // As of 6.1.0, the following optimization is made as it's guaranteed that if the user is connecting to a // Progress Data Cloud instance, then port 443 will be used. Every path for constructing a DatabaseClient goes through // this method, ensuring that this optimization will always be applied, and thus freeing the user from having to @@ -1337,25 +1335,10 @@ static public DatabaseClient newClient(String host, int port, String basePath, S if (securityContext instanceof MarkLogicCloudAuthContext || securityContext instanceof ProgressDataCloudAuthContext) { port = 443; } - services.connect(host, port, basePath, database, securityContext); - - if (clientConfigurators != null) { - clientConfigurators.forEach(configurator -> { - if (configurator instanceof OkHttpClientConfigurator) { - OkHttpClient okHttpClient = (OkHttpClient) services.getClientImplementation(); - Objects.requireNonNull(okHttpClient); - OkHttpClient.Builder clientBuilder = okHttpClient.newBuilder(); - ((OkHttpClientConfigurator) configurator).configure(clientBuilder); - ((OkHttpServices) services).setClientImplementation(clientBuilder.build()); - } else { - throw new IllegalArgumentException("A ClientConfigurator must implement OkHttpClientConfigurator"); - } - }); - } - DatabaseClientImpl client = new DatabaseClientImpl( - services, host, port, basePath, database, securityContext, connectionType - ); + OkHttpServices.ConnectionConfig config = new OkHttpServices.ConnectionConfig(host, port, basePath, database, securityContext, clientConfigurators); + RESTServices services = new OkHttpServices(config); + DatabaseClientImpl client = new DatabaseClientImpl(services, host, port, basePath, database, securityContext, connectionType); client.setHandleRegistry(getHandleRegistry().copy()); return client; } @@ -1397,13 +1380,13 @@ static public void registerDefaultHandles() { * @param configurator the listener for configuring the communication library */ static public void addConfigurator(ClientConfigurator configurator) { - if (!OkHttpClientConfigurator.class.isInstance(configurator)) { - throw new IllegalArgumentException( - "Configurator must implement OkHttpClientConfigurator" - ); - } + if (!OkHttpClientConfigurator.class.isInstance(configurator)) { + throw new IllegalArgumentException( + "Configurator must implement OkHttpClientConfigurator" + ); + } - clientConfigurators.add(configurator); + clientConfigurators.add((OkHttpClientConfigurator) configurator); } /** diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java b/marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java index fcd0c022f..995a5392b 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java @@ -7,6 +7,8 @@ import com.marklogic.client.impl.okhttp.OkHttpUtil; import okhttp3.OkHttpClient; +import java.util.ArrayList; + /** * Exposes the mechanism for constructing an {@code OkHttpClient.Builder} in the same fashion as when a * {@code DatabaseClient} is constructed. Primarily intended for reuse in the ml-app-deployer library. If the @@ -17,6 +19,6 @@ public interface OkHttpClientBuilderFactory { static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseClientFactory.SecurityContext securityContext) { - return OkHttpUtil.newOkHttpClientBuilder(host, securityContext); + return OkHttpUtil.newOkHttpClientBuilder(host, securityContext, new ArrayList<>()); } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientImpl.java index a61b7df47..7a144b2dd 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientImpl.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientImpl.java @@ -60,8 +60,6 @@ public DatabaseClientImpl(RESTServices services, String host, int port, String b this.database = database; this.securityContext = securityContext; this.connectionType = connectionType; - - services.setDatabaseClient(this); } public long getServerVersion() { diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java index dd8a81bc0..29017c2c2 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java @@ -18,6 +18,7 @@ import com.marklogic.client.document.DocumentManager.Metadata; import com.marklogic.client.eval.EvalResult; import com.marklogic.client.eval.EvalResultIterator; +import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; import com.marklogic.client.impl.okhttp.HttpUrlBuilder; import com.marklogic.client.impl.okhttp.OkHttpUtil; import com.marklogic.client.impl.okhttp.PartIterator; @@ -74,10 +75,10 @@ public class OkHttpServices implements RESTServices { static final private Logger logger = LoggerFactory.getLogger(OkHttpServices.class); - static final public String OKHTTP_LOGGINGINTERCEPTOR_LEVEL = "com.marklogic.client.okhttp.httplogginginterceptor.level"; - static final public String OKHTTP_LOGGINGINTERCEPTOR_OUTPUT = "com.marklogic.client.okhttp.httplogginginterceptor.output"; + private static final String OKHTTP_LOGGINGINTERCEPTOR_LEVEL = "com.marklogic.client.okhttp.httplogginginterceptor.level"; + private static final String OKHTTP_LOGGINGINTERCEPTOR_OUTPUT = "com.marklogic.client.okhttp.httplogginginterceptor.output"; - static final private String DOCUMENT_URI_PREFIX = "/documents?uri="; + private static final String DOCUMENT_URI_PREFIX = "/documents?uri="; static final private int DELAY_FLOOR = 125; static final private int DELAY_CEILING = 2000; @@ -88,10 +89,14 @@ public class OkHttpServices implements RESTServices { private final static MediaType URLENCODED_MIME_TYPE = MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"); private final static String UTF8_ID = StandardCharsets.UTF_8.toString(); - private DatabaseClient databaseClient; private String database = null; private HttpUrl baseUri; - private OkHttpClient client; + + // This should really be final, but given the history of this class and the former "connect()" method that meant + // the client was created in the constructor, this is being kept as non-final so it can be assigned a value of null + // on release. + private OkHttpClient okHttpClient; + private boolean released = false; private final Random randRetry = new Random(); @@ -114,25 +119,16 @@ static protected class ThreadState { private final ThreadLocal threadState = ThreadLocal.withInitial(() -> new ThreadState(checkFirstRequest)); - public OkHttpServices() { + public record ConnectionConfig(String host, int port, String basePath, String database, + SecurityContext securityContext, List clientConfigurators) { + } + + public OkHttpServices(ConnectionConfig connectionConfig) { retryStatus.add(STATUS_BAD_GATEWAY); retryStatus.add(STATUS_SERVICE_UNAVAILABLE); retryStatus.add(STATUS_GATEWAY_TIMEOUT); - } - @Override - public Set getRetryStatus() { - return retryStatus; - } - - @Override - public int getMaxDelay() { - return maxDelay; - } - - @Override - public void setMaxDelay(int maxDelay) { - this.maxDelay = maxDelay; + this.okHttpClient = connect(connectionConfig); } private FailedRequest extractErrorFields(Response response) { @@ -176,18 +172,19 @@ private FailedRequest extractErrorFields(Response response) { } } - @Override - public void connect(String host, int port, String basePath, String database, SecurityContext securityContext) { - if (host == null) + private OkHttpClient connect(ConnectionConfig config) { + if (config.host == null) { throw new IllegalArgumentException("No host provided"); - if (securityContext == null) + } + if (config.securityContext == null) { throw new IllegalArgumentException("No security context provided"); + } - this.checkFirstRequest = securityContext instanceof DigestAuthContext; - this.database = database; - this.baseUri = HttpUrlBuilder.newBaseUrl(host, port, basePath, securityContext.getSSLContext()); + this.checkFirstRequest = config.securityContext instanceof DigestAuthContext; + this.database = config.database; + this.baseUri = HttpUrlBuilder.newBaseUrl(config.host, config.port, config.basePath, config.securityContext.getSSLContext()); - OkHttpClient.Builder clientBuilder = OkHttpUtil.newOkHttpClientBuilder(host, securityContext); + OkHttpClient.Builder clientBuilder = OkHttpUtil.newOkHttpClientBuilder(config.host, config.securityContext, config.clientConfigurators); Properties props = System.getProperties(); if (props.containsKey(OKHTTP_LOGGINGINTERCEPTOR_LEVEL)) { @@ -195,15 +192,12 @@ public void connect(String host, int port, String basePath, String database, Sec } this.configureDelayAndRetry(props); - this.client = clientBuilder.build(); + return clientBuilder.build(); } /** * Based on the given properties, add a network interceptor to the given OkHttpClient.Builder to log HTTP * traffic. - * - * @param clientBuilder - * @param props */ private void configureOkHttpLogging(OkHttpClient.Builder clientBuilder, Properties props) { final boolean useLogger = "LOGGER".equalsIgnoreCase(props.getProperty(OKHTTP_LOGGINGINTERCEPTOR_OUTPUT)); @@ -244,40 +238,21 @@ private void configureDelayAndRetry(Properties props) { } } - @Override - public DatabaseClient getDatabaseClient() { - return databaseClient; - } - - @Override - public void setDatabaseClient(DatabaseClient client) { - this.databaseClient = client; - } - - private OkHttpClient getConnection() { - if (client != null) { - return client; - } else if (released) { - throw new IllegalStateException( - "You cannot use this connected object anymore--connection has already been released"); - } else { - throw new MarkLogicInternalException("Cannot proceed--connection is null for unknown reason"); - } - } - @Override public void release() { - if (client == null) return; + if (released || okHttpClient == null) { + return; + } try { released = true; - client.dispatcher().executorService().shutdownNow(); + okHttpClient.dispatcher().executorService().shutdownNow(); } finally { try { - if (client.cache() != null) client.cache().close(); + if (okHttpClient.cache() != null) okHttpClient.cache().close(); } catch (IOException e) { throw new MarkLogicIOException(e); } finally { - client = null; + okHttpClient = null; logger.debug("Releasing connection"); } } @@ -491,8 +466,13 @@ private Response sendRequestOnce(Request.Builder requestBldr) { } private Response sendRequestOnce(Request request) { + if (released) { + throw new IllegalStateException( + "You cannot use this connected object anymore--connection has already been released"); + } + try { - return getConnection().newCall(request).execute(); + return okHttpClient.newCall(request).execute(); } catch (IOException e) { if (e instanceof SSLException) { String message = e.getMessage(); @@ -2591,25 +2571,6 @@ public Response apply(Request.Builder funcBuilder) { return (reqlog != null) ? reqlog.copyContent(entity) : entity; } - @Override - public void postValue(RequestLogger reqlog, String type, String key, - String mimetype, Object value) - throws ResourceNotResendableException, ForbiddenUserException, FailedRequestException { - logger.debug("Posting {}/{}", type, key); - - putPostValueImpl(reqlog, "post", type, key, null, mimetype, value, STATUS_CREATED); - } - - @Override - public void postValue(RequestLogger reqlog, String type, String key, - RequestParameters extraParams) - throws ResourceNotResendableException, ForbiddenUserException, FailedRequestException { - logger.debug("Posting {}/{}", type, key); - - putPostValueImpl(reqlog, "post", type, key, extraParams, null, null, STATUS_NO_CONTENT); - } - - @Override public void putValue(RequestLogger reqlog, String type, String key, String mimetype, Object value) @@ -2795,42 +2756,6 @@ public Response apply(Request.Builder funcBuilder) { logRequest(reqlog, "deleted %s value with %s key", type, key); } - @Override - public void deleteValues(RequestLogger reqlog, String type) - throws ForbiddenUserException, FailedRequestException { - logger.debug("Deleting {}", type); - - Request.Builder requestBldr = setupRequest(type, null); - requestBldr = addTelemetryAgentId(requestBldr); - - Function doDeleteFunction = new Function() { - public Response apply(Request.Builder funcBuilder) { - return sendRequestOnce(funcBuilder.delete().build()); - } - }; - Response response = sendRequestWithRetry(requestBldr, doDeleteFunction, null); - int status = response.code(); - if (status == STATUS_FORBIDDEN) { - throw new ForbiddenUserException("User is not allowed to delete " - + type, extractErrorFields(response)); - } - if (status != STATUS_NO_CONTENT) { - throw new FailedRequestException("delete failed: " - + getReasonPhrase(response), extractErrorFields(response)); - } - closeResponse(response); - - logRequest(reqlog, "deleted %s values", type); - } - - @Override - public R getSystemSchema(RequestLogger reqlog, String schemaName, R output) - throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { - RequestParameters params = new RequestParameters(); - params.add("system", schemaName); - return getResource(reqlog, "internal/schemas", null, params, output); - } - @Override public R uris(RequestLogger reqlog, String method, SearchQueryDefinition qdef, Boolean filtered, long start, String afterUri, long pageLength, String forestName, R output @@ -3352,7 +3277,7 @@ public R postResou } @Override - public R postBulkDocuments( + public void postBulkDocuments( RequestLogger reqlog, DocumentWriteSet writeSet, ServerTransform transform, Transaction transaction, Format defaultFormat, R output, String temporalCollection, String extraContentDispositionParams) @@ -3411,7 +3336,7 @@ public R postBulkDocuments( transform.merge(params); } if (temporalCollection != null) params.add("temporal-collection", temporalCollection); - return postResource(reqlog, "documents", transaction, params, + postResource(reqlog, "documents", transaction, params, (AbstractWriteHandle[]) writeHandles.toArray(new AbstractWriteHandle[0]), (RequestParameters[]) headerList.toArray(new RequestParameters[0]), output); @@ -4843,12 +4768,7 @@ public T getContentAs(Class as) { @Override public OkHttpClient getClientImplementation() { - if (client == null) return null; - return client; - } - - public void setClientImplementation(OkHttpClient client) { - this.client = client; + return okHttpClient; } @Override @@ -5153,12 +5073,12 @@ public R getGraphUris(RequestLogger reqlog, R out } @Override - public R readGraph(RequestLogger reqlog, String uri, R output, + public void readGraph(RequestLogger reqlog, String uri, R output, Transaction transaction) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { RequestParameters params = new RequestParameters(); addGraphUriParam(params, uri); - return getResource(reqlog, "graphs", transaction, params, output); + getResource(reqlog, "graphs", transaction, params, output); } @Override @@ -5235,12 +5155,11 @@ public void mergePermissions(RequestLogger reqlog, String uri, } @Override - public Object deleteGraph(RequestLogger reqlog, String uri, Transaction transaction) + public void deleteGraph(RequestLogger reqlog, String uri, Transaction transaction) throws ForbiddenUserException, FailedRequestException { RequestParameters params = new RequestParameters(); addGraphUriParam(params, uri); - return deleteResource(reqlog, "graphs", transaction, params, null); - + deleteResource(reqlog, "graphs", transaction, params, null); } @Override diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/RESTServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/RESTServices.java index 7750361c6..0f643daa7 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/RESTServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/RESTServices.java @@ -3,34 +3,15 @@ */ package com.marklogic.client.impl; -import java.io.InputStream; -import java.io.Reader; -import java.util.Calendar; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import com.marklogic.client.DatabaseClient; -import com.marklogic.client.DatabaseClientFactory.SecurityContext; -import com.marklogic.client.FailedRequestException; -import com.marklogic.client.ForbiddenUserException; -import com.marklogic.client.ResourceNotFoundException; -import com.marklogic.client.ResourceNotResendableException; -import com.marklogic.client.SessionState; -import com.marklogic.client.Transaction; +import com.marklogic.client.DatabaseClient.ConnectionResult; +import com.marklogic.client.*; import com.marklogic.client.bitemporal.TemporalDescriptor; import com.marklogic.client.bitemporal.TemporalDocumentManager.ProtectionLevel; -import com.marklogic.client.document.DocumentDescriptor; +import com.marklogic.client.document.*; import com.marklogic.client.document.DocumentManager.Metadata; -import com.marklogic.client.document.DocumentPage; -import com.marklogic.client.document.DocumentUriTemplate; -import com.marklogic.client.document.DocumentWriteSet; -import com.marklogic.client.document.ServerTransform; import com.marklogic.client.eval.EvalResultIterator; import com.marklogic.client.extensions.ResourceServices.ServiceResult; import com.marklogic.client.extensions.ResourceServices.ServiceResultIterator; -import com.marklogic.client.DatabaseClient.ConnectionResult; import com.marklogic.client.io.BytesHandle; import com.marklogic.client.io.Format; import com.marklogic.client.io.InputStreamHandle; @@ -44,6 +25,14 @@ import com.marklogic.client.util.RequestLogger; import com.marklogic.client.util.RequestParameters; +import java.io.InputStream; +import java.io.Reader; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + public interface RESTServices { String AUTHORIZATION_TYPE_SAML = "SAML"; @@ -78,7 +67,6 @@ public interface RESTServices { String MIMETYPE_APPLICATION_JSON = "application/json"; String MIMETYPE_APPLICATION_XML = "application/xml"; String MIMETYPE_MULTIPART_MIXED = "multipart/mixed"; - String MIMETYPE_MULTIPART_FORM = "multipart/form-data"; int STATUS_OK = 200; int STATUS_CREATED = 201; @@ -98,13 +86,6 @@ public interface RESTServices { String MAX_DELAY_PROP = "com.marklogic.client.maximumRetrySeconds"; String MIN_RETRY_PROP = "com.marklogic.client.minimumRetries"; - Set getRetryStatus(); - int getMaxDelay(); - void setMaxDelay(int maxDelay); - - void connect(String host, int port, String basePath, String database, SecurityContext securityContext); - DatabaseClient getDatabaseClient(); - void setDatabaseClient(DatabaseClient client); void release(); TemporalDescriptor deleteDocument(RequestLogger logger, DocumentDescriptor desc, Transaction transaction, @@ -129,7 +110,7 @@ DocumentPage getBulkDocuments(RequestLogger logger, long serverTimestamp, Search RequestParameters extraParams, String forestName) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; - T postBulkDocuments(RequestLogger logger, DocumentWriteSet writeSet, + void postBulkDocuments(RequestLogger logger, DocumentWriteSet writeSet, ServerTransform transform, Transaction transaction, Format defaultFormat, T output, String temporalCollection, String extraContentDispositionParams) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; @@ -188,10 +169,6 @@ T getValues(RequestLogger logger, String type, String mimetype, Class as) T getValues(RequestLogger reqlog, String type, RequestParameters extraParams, String mimetype, Class as) throws ForbiddenUserException, FailedRequestException; - void postValue(RequestLogger logger, String type, String key, String mimetype, Object value) - throws ResourceNotResendableException, ForbiddenUserException, FailedRequestException; - void postValue(RequestLogger reqlog, String type, String key, RequestParameters extraParams) - throws ResourceNotResendableException, ForbiddenUserException, FailedRequestException; void putValue(RequestLogger logger, String type, String key, String mimetype, Object value) throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, @@ -202,11 +179,6 @@ void putValue(RequestLogger logger, String type, String key, RequestParameters e FailedRequestException; void deleteValue(RequestLogger logger, String type, String key) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; - void deleteValues(RequestLogger logger, String type) - throws ForbiddenUserException, FailedRequestException; - - R getSystemSchema(RequestLogger reqlog, String schemaName, R output) - throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; R uris(RequestLogger reqlog, String method, SearchQueryDefinition qdef, Boolean filtered, long start, String afterUri, long pageLength, String forestName, R output) @@ -335,7 +307,7 @@ public boolean isExpected(int status) { R getGraphUris(RequestLogger reqlog, R output) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; - R readGraph(RequestLogger reqlog, String uri, R output, + void readGraph(RequestLogger reqlog, String uri, R output, Transaction transaction) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; void writeGraph(RequestLogger reqlog, String uri, @@ -343,7 +315,7 @@ void writeGraph(RequestLogger reqlog, String uri, throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; void writeGraphs(RequestLogger reqlog, AbstractWriteHandle input, Transaction transaction) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException; - Object deleteGraph(RequestLogger requestLogger, String uri, + void deleteGraph(RequestLogger requestLogger, String uri, Transaction transaction) throws ForbiddenUserException, FailedRequestException; void deleteGraphs(RequestLogger requestLogger, Transaction transaction) diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java index 3dff7a53d..6a018cad2 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java @@ -4,8 +4,10 @@ package com.marklogic.client.impl.okhttp; import com.marklogic.client.DatabaseClientFactory; +import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; import com.marklogic.client.impl.HTTPKerberosAuthInterceptor; import com.marklogic.client.impl.HTTPSamlAuthInterceptor; +import com.marklogic.client.impl.OkHttpServices; import com.marklogic.client.impl.SSLUtil; import okhttp3.*; @@ -21,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -35,7 +38,8 @@ public abstract class OkHttpUtil { final private static ConnectionPool connectionPool = new ConnectionPool(); @SuppressWarnings({"unchecked", "deprecation"}) - public static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseClientFactory.SecurityContext securityContext) { + public static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseClientFactory.SecurityContext securityContext, + List clientConfigurators) { OkHttpClient.Builder clientBuilder = OkHttpUtil.newClientBuilder(); AuthenticationConfigurer authenticationConfigurer = null; @@ -78,6 +82,10 @@ public static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseC OkHttpUtil.configureSocketFactory(clientBuilder, sslContext, trustManager); OkHttpUtil.configureHostnameVerifier(clientBuilder, sslVerifier); + if (clientConfigurators != null) { + clientConfigurators.forEach(configurator -> configurator.configure(clientBuilder)); + } + return clientBuilder; } From b56710a517403dca50c1da00179059e82b918a1f Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Tue, 26 Aug 2025 17:09:31 -0700 Subject: [PATCH 09/33] PDP-536: Adding copyright check --- .copyrightconfig | 14 ++++++++++++++ .github/workflows/pr-workflow.yaml | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .copyrightconfig diff --git a/.copyrightconfig b/.copyrightconfig new file mode 100644 index 000000000..6a060caea --- /dev/null +++ b/.copyrightconfig @@ -0,0 +1,14 @@ +# COPYRIGHT VALIDATION CONFIG +# --------------------------------- +# Required start year (keep fixed; end year auto-updates in check output) +startyear: 2010 + +# Optional exclusions list (comma-separated). Leave commented if none. +# Rules: +# - Relative paths (no leading ./) +# - Simple * wildcard only (no recursive **) +# - Use sparingly (third_party, generated, binary assets) +# - Dotfiles already skipped automatically +# Enable by removing the leading '# ' from the next line and editing values. +# filesexcluded: third_party/*, docs/generated/*.md, assets/*.png, scripts/temp_*.py, vendor/lib.js +filesexcluded: .github/*, README.md, Jenkinsfile, gradle/*, docker-compose.yml, *.gradle, gradle.properties, gradlew, gradlew.bat \ No newline at end of file diff --git a/.github/workflows/pr-workflow.yaml b/.github/workflows/pr-workflow.yaml index f2a31ab99..d11ced4a0 100644 --- a/.github/workflows/pr-workflow.yaml +++ b/.github/workflows/pr-workflow.yaml @@ -1,4 +1,4 @@ -name: 🏷️ JIRA ID Validator +name: PR Workflow on: # Using pull_request_target instead of pull_request to handle PRs from forks @@ -14,3 +14,10 @@ jobs: with: # Pass the PR title from the event context pr-title: ${{ github.event.pull_request.title }} + copyright-validation: + name: © Validate Copyright Headers + uses: marklogic/pr-workflows/.github/workflows/copyright-check.yml@main + permissions: + contents: read + pull-requests: write + issues: write \ No newline at end of file From cc05b57668b794bb0a7a5fbe18b35629bedc2f80 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 30 Sep 2025 09:24:50 -0400 Subject: [PATCH 10/33] MLE-24405 Refactor: Moved some OkHttp-specific classes Trying to get as many okhttp3 imports into one package as possible, with the notable exception for now of OkHttpServices. --- .../main/java/com/marklogic/client/impl/FailedRequest.java | 1 - .../impl/{ => okhttp}/HTTPKerberosAuthInterceptor.java | 3 ++- .../client/impl/{ => okhttp}/HTTPSamlAuthInterceptor.java | 5 +++-- .../java/com/marklogic/client/impl/okhttp/OkHttpUtil.java | 4 ---- 4 files changed, 5 insertions(+), 8 deletions(-) rename marklogic-client-api/src/main/java/com/marklogic/client/impl/{ => okhttp}/HTTPKerberosAuthInterceptor.java (99%) rename marklogic-client-api/src/main/java/com/marklogic/client/impl/{ => okhttp}/HTTPSamlAuthInterceptor.java (97%) diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/FailedRequest.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/FailedRequest.java index 5b0d2cb5a..00ca7c3be 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/FailedRequest.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/FailedRequest.java @@ -11,7 +11,6 @@ import javax.xml.parsers.ParserConfigurationException; import com.marklogic.client.io.Format; -import okhttp3.MediaType; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/HTTPKerberosAuthInterceptor.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/HTTPKerberosAuthInterceptor.java similarity index 99% rename from marklogic-client-api/src/main/java/com/marklogic/client/impl/HTTPKerberosAuthInterceptor.java rename to marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/HTTPKerberosAuthInterceptor.java index 44f4de8f7..15ac1059b 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/HTTPKerberosAuthInterceptor.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/HTTPKerberosAuthInterceptor.java @@ -1,7 +1,7 @@ /* * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ -package com.marklogic.client.impl; +package com.marklogic.client.impl.okhttp; import java.io.IOException; import java.util.Map; @@ -20,6 +20,7 @@ import javax.security.auth.login.Configuration; import javax.security.auth.kerberos.KerberosTicket; +import com.marklogic.client.impl.SSLUtil; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/HTTPSamlAuthInterceptor.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/HTTPSamlAuthInterceptor.java similarity index 97% rename from marklogic-client-api/src/main/java/com/marklogic/client/impl/HTTPSamlAuthInterceptor.java rename to marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/HTTPSamlAuthInterceptor.java index 0a58e9324..9a856306f 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/HTTPSamlAuthInterceptor.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/HTTPSamlAuthInterceptor.java @@ -2,11 +2,12 @@ * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ -package com.marklogic.client.impl; +package com.marklogic.client.impl.okhttp; import com.marklogic.client.DatabaseClientFactory.SAMLAuthContext.AuthorizerCallback; import com.marklogic.client.DatabaseClientFactory.SAMLAuthContext.ExpiringSAMLAuth; import com.marklogic.client.DatabaseClientFactory.SAMLAuthContext.RenewerCallback; +import com.marklogic.client.impl.RESTServices; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; @@ -55,7 +56,7 @@ public Response intercept(Chain chain) throws IOException { Request authenticatedRequest = chain.request().newBuilder() .header(RESTServices.HEADER_AUTHORIZATION, buildSamlHeader()) .build(); - + return chain.proceed(authenticatedRequest); } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java index 6a018cad2..9d0f384ae 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java @@ -5,9 +5,6 @@ import com.marklogic.client.DatabaseClientFactory; import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; -import com.marklogic.client.impl.HTTPKerberosAuthInterceptor; -import com.marklogic.client.impl.HTTPSamlAuthInterceptor; -import com.marklogic.client.impl.OkHttpServices; import com.marklogic.client.impl.SSLUtil; import okhttp3.*; @@ -23,7 +20,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.concurrent.TimeUnit; /** From 9ee3b91a50b26ccc43b38feed9d93d8e51811b72 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 30 Sep 2025 09:40:07 -0400 Subject: [PATCH 11/33] MLE-24405 Making ClientCookie a real public class This was already in the public API via the Transaction interface, but it was in the "impl" package. I also can't determine any reason why it was reconstructing an OkHttp Cookie and then holding onto it. The Cookie class is immutable - but ClientCookie was already being given the immutable values that it needed. So no idea why ClientCookie was then rebuilding an OkHttp Cookie. It no longer does that - it's just a simple bean now, holding onto the values of interest that were extracted from an OkHttp Cookie. --- .../com/marklogic/client/ClientCookie.java | 58 ++++++++++++++ .../com/marklogic/client/Transaction.java | 1 - .../marklogic/client/impl/ClientCookie.java | 73 ----------------- .../marklogic/client/impl/OkHttpServices.java | 11 ++- .../client/impl/SessionStateImpl.java | 78 +++++++++---------- .../client/impl/TransactionImpl.java | 14 +--- 6 files changed, 109 insertions(+), 126 deletions(-) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/ClientCookie.java delete mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/ClientCookie.java diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/ClientCookie.java b/marklogic-client-api/src/main/java/com/marklogic/client/ClientCookie.java new file mode 100644 index 000000000..810b4bbb4 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/ClientCookie.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client; + +import java.util.concurrent.TimeUnit; + +/** + * ClientCookie is a wrapper around the Cookie implementation so that the + * underlying implementation can be changed. + * + */ +public class ClientCookie { + + private final String name; + private final String value; + private final long expiresAt; + private final String domain; + private final String path; + private final boolean secure; + + public ClientCookie(String name, String value, long expiresAt, String domain, String path, boolean secure) { + this.name = name; + this.value = value; + this.expiresAt = expiresAt; + this.domain = domain; + this.path = path; + this.secure = secure; + } + + public boolean isSecure() { + return secure; + } + + public String getPath() { + return path; + } + + public String getDomain() { + return domain; + } + + public long expiresAt() { + return expiresAt; + } + + public String getName() { + return name; + } + + public int getMaxAge() { + return (int) TimeUnit.MILLISECONDS.toSeconds(expiresAt - System.currentTimeMillis()); + } + + public String getValue() { + return value; + } +} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/Transaction.java b/marklogic-client-api/src/main/java/com/marklogic/client/Transaction.java index 578743845..d98071bef 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/Transaction.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/Transaction.java @@ -3,7 +3,6 @@ */ package com.marklogic.client; -import com.marklogic.client.impl.ClientCookie; import com.marklogic.client.io.marker.StructureReadHandle; import java.util.List; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/ClientCookie.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/ClientCookie.java deleted file mode 100644 index 48735bd98..000000000 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/ClientCookie.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. - */ -package com.marklogic.client.impl; - -import java.util.concurrent.TimeUnit; - -import okhttp3.Cookie; -import okhttp3.HttpUrl; - -/** - * ClientCookie is a wrapper around the Cookie implementation so that the - * underlying implementation can be changed. - * - */ -public class ClientCookie { - Cookie cookie; - - ClientCookie(String name, String value, long expiresAt, String domain, String path, - boolean secure) { - Cookie.Builder cookieBldr = new Cookie.Builder() - .domain(domain) - .path(path) - .name(name) - .value(value) - .expiresAt(expiresAt); - if ( secure == true ) cookieBldr = cookieBldr.secure(); - this.cookie = cookieBldr.build(); - } - - public ClientCookie(ClientCookie cookie) { - this(cookie.getName(), cookie.getValue(), cookie.expiresAt(), cookie.getDomain(), cookie.getPath(), - cookie.isSecure()); - } - - public boolean isSecure() { - return cookie.secure(); - } - - public String getPath() { - return cookie.path(); - } - - public String getDomain() { - return cookie.domain(); - } - - public long expiresAt() { - return cookie.expiresAt(); - } - - public String getName() { - return cookie.name(); - } - - public int getMaxAge() { - return (int) TimeUnit.MILLISECONDS.toSeconds(cookie.expiresAt() - System.currentTimeMillis()); - } - public String getValue() { - return cookie.value(); - } - - public static ClientCookie parse(HttpUrl url, String setCookie) { - Cookie cookie = Cookie.parse(url, setCookie); - if(cookie == null) throw new IllegalStateException(setCookie + "is not a well-formed cookie"); - return new ClientCookie(cookie.name(), cookie.value(), cookie.expiresAt(), cookie.domain(), cookie.path(), - cookie.secure()); - } - - public String toString() { - return cookie.toString(); - } -} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java index 29017c2c2..4c8ecf904 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java @@ -131,6 +131,13 @@ public OkHttpServices(ConnectionConfig connectionConfig) { this.okHttpClient = connect(connectionConfig); } + private static ClientCookie parseClientCookie(HttpUrl url, String setCookieHeaderValue) { + Cookie cookie = Cookie.parse(url, setCookieHeaderValue); + if(cookie == null) throw new IllegalStateException(setCookieHeaderValue + " is not a well-formed cookie"); + return new ClientCookie(cookie.name(), cookie.value(), cookie.expiresAt(), cookie.domain(), cookie.path(), + cookie.secure()); + } + private FailedRequest extractErrorFields(Response response) { if (response == null) return null; try { @@ -1501,7 +1508,7 @@ public Response apply(Request.Builder funcBuilder) { String location = response.headers().get("Location"); List cookies = new ArrayList<>(); for (String setCookie : response.headers(HEADER_SET_COOKIE)) { - ClientCookie cookie = ClientCookie.parse(requestBldr.build().url(), setCookie); + ClientCookie cookie = parseClientCookie(requestBldr.build().url(), setCookie); cookies.add(cookie); } closeResponse(response); @@ -5599,7 +5606,7 @@ private void executeRequest(CallResponseImpl responseImpl) { if (session != null) { List cookies = new ArrayList<>(); for (String setCookie : response.headers(HEADER_SET_COOKIE)) { - ClientCookie cookie = ClientCookie.parse(requestBldr.build().url(), setCookie); + ClientCookie cookie = parseClientCookie(requestBldr.build().url(), setCookie); cookies.add(cookie); } ((SessionStateImpl) session).setCookies(cookies); diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/SessionStateImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/SessionStateImpl.java index 53d5defe9..1c27cccfb 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/SessionStateImpl.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/SessionStateImpl.java @@ -3,6 +3,7 @@ */ package com.marklogic.client.impl; +import com.marklogic.client.ClientCookie; import com.marklogic.client.SessionState; import java.util.ArrayList; @@ -12,43 +13,42 @@ import java.util.concurrent.atomic.AtomicBoolean; public class SessionStateImpl implements SessionState { - private List cookies; - private String sessionId; - private AtomicBoolean setCreatedTimestamp; - private Calendar created; - - public SessionStateImpl() { - sessionId = Long.toUnsignedString(ThreadLocalRandom.current().nextLong(), 16); - cookies = new ArrayList<>(); - setCreatedTimestamp = new AtomicBoolean(false); - } - - @Override - public String getSessionId() { - return sessionId; - } - - List getCookies() { - return cookies; - } - - void setCookies(List cookies) { - if ( cookies != null ) { - if(setCreatedTimestamp.compareAndSet(false, true)) { - for (ClientCookie cookie : cookies) { - // Drop the SessionId cookie received from the server. We add it every - // time we make a request with a SessionState object passed - if(cookie.getName().equalsIgnoreCase("SessionId")) continue; - // make a clone to ensure we're not holding on to any resources - // related to an HTTP connection that need to be released - this.cookies.add(new ClientCookie(cookie)); - } - created = Calendar.getInstance(); - } - } - } - - Calendar getCreatedTimestamp() { - return created; - } + + private List cookies; + private String sessionId; + private AtomicBoolean setCreatedTimestamp; + private Calendar created; + + public SessionStateImpl() { + sessionId = Long.toUnsignedString(ThreadLocalRandom.current().nextLong(), 16); + cookies = new ArrayList<>(); + setCreatedTimestamp = new AtomicBoolean(false); + } + + @Override + public String getSessionId() { + return sessionId; + } + + List getCookies() { + return cookies; + } + + void setCookies(List cookies) { + if (cookies != null) { + if (setCreatedTimestamp.compareAndSet(false, true)) { + for (ClientCookie cookie : cookies) { + // Drop the SessionId cookie received from the server. We add it every + // time we make a request with a SessionState object passed + if (cookie.getName().equalsIgnoreCase("SessionId")) continue; + this.cookies.add(cookie); + } + created = Calendar.getInstance(); + } + } + } + + Calendar getCreatedTimestamp() { + return created; + } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/TransactionImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/TransactionImpl.java index 60d25bd53..150313940 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/TransactionImpl.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/TransactionImpl.java @@ -3,7 +3,7 @@ */ package com.marklogic.client.impl; -import com.marklogic.client.impl.ClientCookie; +import com.marklogic.client.ClientCookie; import com.marklogic.client.FailedRequestException; import com.marklogic.client.ForbiddenUserException; import com.marklogic.client.Transaction; @@ -19,7 +19,7 @@ class TransactionImpl implements Transaction { private RESTServices services; private String transactionId; private String hostId; - // we keep cookies scoped with each tranasaction to work with load balancers + // we keep cookies scoped with each transaction to work with load balancers // that need to keep requests for one transaction on a specific MarkLogic Server host private List cookies = new ArrayList<>(); private Calendar created; @@ -29,9 +29,7 @@ class TransactionImpl implements Transaction { this.transactionId = transactionId; if ( cookies != null ) { for (ClientCookie cookie : cookies) { - // make a clone to ensure we're not holding on to any resources - // related to an HTTP connection that need to be released - this.cookies.add(new ClientCookie(cookie)); + this.cookies.add(cookie); if ( "HostId".equalsIgnoreCase(cookie.getName()) ) { hostId = cookie.getValue(); } @@ -44,9 +42,6 @@ class TransactionImpl implements Transaction { public String getTransactionId() { return transactionId; } - public void setTransactionId(String transactionId) { - this.transactionId = transactionId; - } @Override public List getCookies() { @@ -57,9 +52,6 @@ public List getCookies() { public String getHostId() { return hostId; } - protected void setHostId(String hostId) { - this.hostId = hostId; - } public Calendar getCreatedTimestamp() { return created; From 5eaa9655cacdbcccf79fb2b96082150ff8ab9051 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 30 Sep 2025 10:56:02 -0400 Subject: [PATCH 12/33] MLE-24504 Bumping all dependencies Should make Black Duck happy as well. Not touching the Jakarta APIs yet, going to take care of that in a follow up PR as that needs more testing. --- .copyrightconfig | 2 +- CONTRIBUTING.md | 6 +---- examples/build.gradle | 12 +++++----- gradle.properties | 2 ++ .../build.gradle | 13 +++++----- marklogic-client-api/build.gradle | 24 +++++++++---------- ml-development-tools/build.gradle | 2 +- test-app/build.gradle | 10 ++++---- 8 files changed, 34 insertions(+), 37 deletions(-) diff --git a/.copyrightconfig b/.copyrightconfig index 6a060caea..0253be82f 100644 --- a/.copyrightconfig +++ b/.copyrightconfig @@ -11,4 +11,4 @@ startyear: 2010 # - Dotfiles already skipped automatically # Enable by removing the leading '# ' from the next line and editing values. # filesexcluded: third_party/*, docs/generated/*.md, assets/*.png, scripts/temp_*.py, vendor/lib.js -filesexcluded: .github/*, README.md, Jenkinsfile, gradle/*, docker-compose.yml, *.gradle, gradle.properties, gradlew, gradlew.bat \ No newline at end of file +filesexcluded: .github/*, README.md, Jenkinsfile, gradle/*, docker-compose.yml, *.gradle, gradle.properties, gradlew, gradlew.bat, **/test/resources/**, *.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b9c2bb310..68b2f8c37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,11 +7,7 @@ To build the client locally, complete the following steps: 1. Clone this repository on your machine. 2. Choose the appropriate branch (usually develop) -3. Ensure you are using Java 8 or Java 11 or Java 17 (the JVM version used to compile should not matter as compiler flags -are set to ensure the compiled code will run on Java 8; Jenkins pipelines also exist to ensure that the tests pass on -Java 8, 11, and 17, and thus they should for you locally as well; note that if you load the project into an IDE, you -should use Java 8 in case your IDE does not process the build.gradle config that conditionally brings in JAXB dependencies -required by Java 9+.) +3. Ensure you are using Java 17. 4. Verify that you can build the client by running `./gradlew build -x test` "Running the tests" in the context of developing and submitting a pull request refers to running the tests found diff --git a/examples/build.gradle b/examples/build.gradle index ba3dfc93c..7bf452058 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -10,19 +10,19 @@ dependencies { // The 'api' configuration is used so that the test configuration in marklogic-client-api doesn't have to declare // all of these dependencies. This library project won't otherwise be depended on by anything else as it's not // setup for publishing. - api 'com.squareup.okhttp3:okhttp:4.12.0' - api 'io.github.rburgst:okhttp-digest:2.7' + api "com.squareup.okhttp3:okhttp:${okhttpVersion}" + api 'io.github.rburgst:okhttp-digest:3.1.1' api 'org.slf4j:slf4j-api:2.0.17' api "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" api 'org.jdom:jdom2:2.0.6.1' - api 'org.dom4j:dom4j:2.1.4' - api 'com.google.code.gson:gson:2.10.1' + api 'org.dom4j:dom4j:2.2.0' + api 'com.google.code.gson:gson:2.13.2' api 'net.sourceforge.htmlcleaner:htmlcleaner:2.29' - api ('com.opencsv:opencsv:5.11.2') { + api ('com.opencsv:opencsv:5.12.0') { // Excluding this due to a security vulnerability, and the test for the example that uses this library // passes without this on the classpath. exclude module: "commons-beanutils" } - api 'org.apache.commons:commons-lang3:3.18.0' + api 'org.apache.commons:commons-lang3:3.19.0' } diff --git a/gradle.properties b/gradle.properties index 8d8fe5986..60f18ae19 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,8 @@ version=8.0-SNAPSHOT describedName=MarkLogic Java Client API publishUrl=file:../marklogic-java/releases +okhttpVersion=5.1.0 + # See https://github.com/FasterXML/jackson for more information on the Jackson libraries. jacksonVersion=2.19.0 diff --git a/marklogic-client-api-functionaltests/build.gradle b/marklogic-client-api-functionaltests/build.gradle index 489a1d077..0fce007ad 100755 --- a/marklogic-client-api-functionaltests/build.gradle +++ b/marklogic-client-api-functionaltests/build.gradle @@ -18,23 +18,22 @@ dependencies { testImplementation project(':marklogic-client-api') testImplementation 'org.skyscreamer:jsonassert:1.5.3' testImplementation 'org.slf4j:slf4j-api:2.0.17' - testImplementation 'commons-io:commons-io:2.17.0' - testImplementation 'com.squareup.okhttp3:okhttp:5.1.0' + testImplementation 'commons-io:commons-io:2.20.0' + testImplementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" testImplementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" testImplementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" testImplementation "org.jdom:jdom2:2.0.6.1" - testImplementation 'org.apache.commons:commons-lang3:3.18.0' + testImplementation 'org.apache.commons:commons-lang3:3.19.0' + // Allows talking to the Manage API. - testImplementation("com.marklogic:ml-app-deployer:5.0.0") { + testImplementation("com.marklogic:ml-app-deployer:6.0.1") { exclude module: "marklogic-client-api" - // Use the commons-lang3 declared above to keep Black Duck happy. - exclude module: "commons-lang3" } testImplementation 'ch.qos.logback:logback-classic:1.5.18' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' - testImplementation 'org.xmlunit:xmlunit-legacy:2.10.0' + testImplementation 'org.xmlunit:xmlunit-legacy:2.10.4' // Without this, once using JUnit 5.12 or higher, Gradle will not find any tests and report an error of: // org.junit.platform.commons.JUnitException: TestEngine with ID 'junit-jupiter' failed to discover tests diff --git a/marklogic-client-api/build.gradle b/marklogic-client-api/build.gradle index 7bb2b8ffe..a77b1f882 100644 --- a/marklogic-client-api/build.gradle +++ b/marklogic-client-api/build.gradle @@ -18,8 +18,8 @@ dependencies { api "jakarta.xml.bind:jakarta.xml.bind-api:3.0.1" implementation "org.glassfish.jaxb:jaxb-runtime:3.0.2" - implementation 'com.squareup.okhttp3:okhttp:5.1.0' - implementation 'com.squareup.okhttp3:logging-interceptor:5.1.0' + implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" + implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}" implementation 'io.github.rburgst:okhttp-digest:3.1.1' // We tried upgrading to the org.eclipse.angus:angus-mail dependency, but we ran into significant performance issues @@ -27,7 +27,7 @@ dependencies { // take 50s instead of 2 to 3s. Haven't dug into the details, but seems like the call isn't lazy and the entire set // of URIs is being retrieved. This implementation - in the old "com.sun.mail" package but still adhering to the new // jakarta.mail API - works fine and performs well for eval calls. - implementation "com.sun.mail:jakarta.mail:2.0.1" + implementation "com.sun.mail:jakarta.mail:2.0.2" implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' implementation 'org.slf4j:slf4j-api:2.0.17' @@ -36,22 +36,21 @@ dependencies { // Only used by extras (which some examples then depend on) compileOnly 'org.jdom:jdom2:2.0.6.1' - compileOnly 'org.dom4j:dom4j:2.1.4' - compileOnly 'com.google.code.gson:gson:2.10.1' + compileOnly 'org.dom4j:dom4j:2.2.0' + compileOnly 'com.google.code.gson:gson:2.13.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' // Forcing junit version to avoid vulnerability with older version in xmlunit testImplementation 'junit:junit:4.13.2' - testImplementation 'org.xmlunit:xmlunit-legacy:2.10.0' + testImplementation 'org.xmlunit:xmlunit-legacy:2.10.4' testImplementation project(':examples') - testImplementation 'org.apache.commons:commons-lang3:3.18.0' + testImplementation 'org.apache.commons:commons-lang3:3.19.0' + // Allows talking to the Manage API. - testImplementation ("com.marklogic:ml-app-deployer:5.0.0") { + testImplementation ("com.marklogic:ml-app-deployer:6.0.1") { exclude module: "marklogic-client-api" - // Use the commons-lang3 declared above to keep Black Duck happy. - exclude module: "commons-lang3" } // Starting with mockito 5.x, Java 11 is required, so sticking with 4.x as we have to support Java 8. @@ -65,11 +64,12 @@ dependencies { // Using this to avoid a schema validation issue with the regular xercesImpl testImplementation 'org.opengis.cite.xerces:xercesImpl-xsd11:2.12-beta-r1667115' - testImplementation('com.opencsv:opencsv:5.11.2') { + testImplementation('com.opencsv:opencsv:5.12.0') { // Excluding this due to a security vulnerability, and the test for the example that uses this library // passes without this on the classpath. exclude module: "commons-beanutils" } + testImplementation 'org.skyscreamer:jsonassert:1.5.3' // Automatic loading of test framework implementation dependencies is deprecated. @@ -101,7 +101,7 @@ javadoc { options.overview = "src/main/javadoc/overview.html" options.windowTitle = "$rootProject.describedName $rootProject.version" options.docTitle = "$rootProject.describedName $rootProject.version" - options.bottom = "Copyright © 2024 MarkLogic Corporation. All Rights Reserved." + options.bottom = "Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved." options.links = [ 'http://docs.oracle.com/javase/8/docs/api/' ] options.use = true if (JavaVersion.current().isJava9Compatible()) { diff --git a/ml-development-tools/build.gradle b/ml-development-tools/build.gradle index 82dfc7513..847a98b9c 100644 --- a/ml-development-tools/build.gradle +++ b/ml-development-tools/build.gradle @@ -23,7 +23,7 @@ dependencies { testImplementation 'xmlunit:xmlunit:1.6' testCompileOnly gradleTestKit() - testImplementation 'com.squareup.okhttp3:okhttp:4.12.0' + testImplementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" } // Added to avoid problem where processResources fails because - somehow - the plugin properties file is getting diff --git a/test-app/build.gradle b/test-app/build.gradle index 86cd75b11..c7f8a1072 100644 --- a/test-app/build.gradle +++ b/test-app/build.gradle @@ -1,16 +1,16 @@ plugins { - id 'com.marklogic.ml-gradle' version '5.0.0' + id 'com.marklogic.ml-gradle' version '6.0.1' id 'java' id "com.github.psxpaul.execfork" version "0.2.2" } dependencies { - implementation "io.undertow:undertow-core:2.2.37.Final" - implementation "io.undertow:undertow-servlet:2.2.37.Final" + implementation "io.undertow:undertow-core:2.3.19.Final" + implementation "io.undertow:undertow-servlet:2.3.19.Final" implementation 'org.slf4j:slf4j-api:2.0.17' - implementation 'ch.qos.logback:logback-classic:1.3.15' + implementation 'ch.qos.logback:logback-classic:1.5.18' implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" } // See https://github.com/psxpaul/gradle-execfork-plugin for docs. From 4397dc90bb15344242c9dc4b67b99ecef2727472 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 30 Sep 2025 12:03:56 -0400 Subject: [PATCH 13/33] MLE-24504 Bumped Jakarta and removed javax.ws The javax.ws.rs-api dependency was only needed for a collection class that wasn't needed. Also forcing the usage of commons-lang3 as Black Duck mysteriously thinks an instance of 3.7 is being brought in as a direct dependency somehow. Also switched to latest nightly build for local testing, as Jenkins is using that too. --- .copyrightconfig | 2 +- .env | 4 +- .gitignore | 2 + build.gradle | 7 + examples/build.gradle | 1 + marklogic-client-api/build.gradle | 14 +- .../impl/RequestParametersImplementation.java | 26 +- .../client/util/RequestParameters.java | 349 +++++++++--------- .../marklogic/client/test/BitemporalTest.java | 3 +- pom.xml | 27 +- 10 files changed, 218 insertions(+), 217 deletions(-) diff --git a/.copyrightconfig b/.copyrightconfig index 0253be82f..cf6e131ee 100644 --- a/.copyrightconfig +++ b/.copyrightconfig @@ -11,4 +11,4 @@ startyear: 2010 # - Dotfiles already skipped automatically # Enable by removing the leading '# ' from the next line and editing values. # filesexcluded: third_party/*, docs/generated/*.md, assets/*.png, scripts/temp_*.py, vendor/lib.js -filesexcluded: .github/*, README.md, Jenkinsfile, gradle/*, docker-compose.yml, *.gradle, gradle.properties, gradlew, gradlew.bat, **/test/resources/**, *.md +filesexcluded: .github/*, README.md, Jenkinsfile, gradle/*, docker-compose.yml, *.gradle, gradle.properties, gradlew, gradlew.bat, **/test/resources/**, *.md, pom.xml diff --git a/.env b/.env index ab5dd6526..d5edd373d 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # Defines environment variables for docker-compose. # Can be overridden via e.g. `MARKLOGIC_TAG=latest-10.0 docker-compose up -d --build`. -MARKLOGIC_IMAGE=progressofficial/marklogic-db:latest +#MARKLOGIC_IMAGE=progressofficial/marklogic-db:latest MARKLOGIC_LOGS_VOLUME=./docker/marklogic/logs # This image should be used instead of the above image when testing functions that only work with MarkLogic 12. -#MARKLOGIC_IMAGE=ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12 +MARKLOGIC_IMAGE=ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12 diff --git a/.gitignore b/.gitignore index bfa99a5f5..d933dc310 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ ml-development-tools/src/test/java/com/marklogic/client/test/dbfunction/generate docker/ .kotlin + +dep.txt diff --git a/build.gradle b/build.gradle index 5af224625..06c8f3ac3 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,13 @@ subprojects { configurations { testImplementation.extendsFrom compileOnly + + all { + resolutionStrategy { + // Forcing the latest commons-lang3 version to eliminate CVEs. + force "org.apache.commons:commons-lang3:3.19.0" + } + } } repositories { diff --git a/examples/build.gradle b/examples/build.gradle index 7bf452058..896cc6e92 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -6,6 +6,7 @@ plugins { dependencies { implementation project(':marklogic-client-api') + implementation "jakarta.xml.bind:jakarta.xml.bind-api:4.0.4" // The 'api' configuration is used so that the test configuration in marklogic-client-api doesn't have to declare // all of these dependencies. This library project won't otherwise be depended on by anything else as it's not diff --git a/marklogic-client-api/build.gradle b/marklogic-client-api/build.gradle index a77b1f882..8f7b84bc7 100644 --- a/marklogic-client-api/build.gradle +++ b/marklogic-client-api/build.gradle @@ -12,11 +12,12 @@ group = 'com.marklogic' description = "The official MarkLogic Java client API." dependencies { - // With 7.0.0, now using the Jakarta JAXB APIs instead of the JAVAX JAXB APIs that were bundled in Java 8. - // To ease support for Java 8, we are depending on version 3.x of the Jakarta JAXB APIs as those only require Java 8, - // whereas the 4.x version requires Java 11 or higher. - api "jakarta.xml.bind:jakarta.xml.bind-api:3.0.1" - implementation "org.glassfish.jaxb:jaxb-runtime:3.0.2" + // Using the latest version now that the 8.0.0 release requires Java 17. + // This is now an implementation dependency as opposed to an api dependency in 7.x and earlier. + // The only time it appears in the public API is when a user uses JAXBHandle. + // But in that scenario, the user would already be using JAXB in their application. + implementation "jakarta.xml.bind:jakarta.xml.bind-api:4.0.4" + implementation "org.glassfish.jaxb:jaxb-runtime:4.0.6" implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}" @@ -27,9 +28,10 @@ dependencies { // take 50s instead of 2 to 3s. Haven't dug into the details, but seems like the call isn't lazy and the entire set // of URIs is being retrieved. This implementation - in the old "com.sun.mail" package but still adhering to the new // jakarta.mail API - works fine and performs well for eval calls. + // As of the 8.0.0 release - this still is a good solution, particularly as com.sun.mail:jakarta.mail received a + // recent patch release and is therefore still being maintained. implementation "com.sun.mail:jakarta.mail:2.0.2" - implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' implementation 'org.slf4j:slf4j-api:2.0.17' implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-csv:${jacksonVersion}" diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/RequestParametersImplementation.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/RequestParametersImplementation.java index 7cdabe9e7..5dadfdd72 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/RequestParametersImplementation.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/RequestParametersImplementation.java @@ -3,22 +3,24 @@ */ package com.marklogic.client.impl; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import javax.ws.rs.core.AbstractMultivaluedMap; -import javax.ws.rs.core.MultivaluedMap; public abstract class RequestParametersImplementation { - private MultivaluedMap map = - new AbstractMultivaluedMap(new ConcurrentHashMap<>()) {}; - protected RequestParametersImplementation() { - super(); - } + // Prior to 8.0.0, this was a threadsafe map. However, that fact was not documented for a user. And in practice, + // it would not make sense for multiple threads to share a mutable instance of this, or of one of its subclasses. + // Additionally, the impl was from the 'javax.ws.rs:javax.ws.rs-api:2.1.1' dependency which wasn't used for + // anything else. So for 8.0.0, this is now simply a map that matches the intended usage of this class and its + // subclasses, which is to be used by a single thread. + private final Map> map = new HashMap<>(); + + protected RequestParametersImplementation() { + super(); + } - protected Map> getMap() { - return map; - } + protected Map> getMap() { + return map; + } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/util/RequestParameters.java b/marklogic-client-api/src/main/java/com/marklogic/client/util/RequestParameters.java index c82d5a34e..fbff95805 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/util/RequestParameters.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/util/RequestParameters.java @@ -3,187 +3,184 @@ */ package com.marklogic.client.util; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; - import com.marklogic.client.impl.RequestParametersImplementation; +import java.util.*; + /** * RequestParameters supports a map with a string as the key and * a list of strings as the value, which can represent parameters * of an operation including parameters transported over HTTP. */ -public class RequestParameters - extends RequestParametersImplementation - implements Map> -{ - /** - * Zero-argument constructor. - */ - public RequestParameters() { - super(); - } - - /** - * Set a parameter to a single value. - * @param name the parameter name - * @param value the value of the parameter - */ - public void put(String name, String value) { - List list = new ArrayList<>(); - list.add(value); - getMap().put(name, list); - } - /** - * Sets a parameter to a list of values. - * @param name the parameter - * @param values the list of values - */ - public void put(String name, String... values) { - getMap().put(name, Arrays.asList(values)); - } - /** - * Appends a value to the list for a parameter. - * @param name the parameter - * @param value the value to add to the list - */ - public void add(String name, String value) { - if (containsKey(name)) { - get(name).add(value); - } else { - put(name, value); - } - } - /** - * Appends a list of values to the list for a parameter. - * @param name the parameter - * @param values the values to add to the list - */ - public void add(String name, String... values) { - if (containsKey(name)) { - List list = get(name); - for (String value: values) { - list.add(value); - } - } else { - put(name, values); - } - } - - /** - * Returns the number of request parameters. - */ - @Override - public int size() { - return getMap().size(); - } - - /** - * Returns whether or not any request parameters have been specified. - */ - @Override - public boolean isEmpty() { - return getMap().isEmpty(); - } - - /** - * Checks whether the parameter name has been specified. - */ - @Override - public boolean containsKey(Object key) { - return getMap().containsKey(key); - } - - /** - * Checks whether any parameters have the value. - */ - @Override - public boolean containsValue(Object value) { - return getMap().containsValue(value); - } - - /** - * Gets the values for a parameter name. - */ - @Override - public List get(Object key) { - return getMap().get(key); - } - - /** - * Sets the values of a parameter name, returning the previous values if any. - */ - @Override - public List put(String key, List value) { - return getMap().put(key, value); - } - - /** - * Removes a parameter name, returning its values if any. - */ - @Override - public List remove(Object key) { - return getMap().remove(key); - } - - /** - * Adds existing parameter names and values. - */ - @Override - public void putAll(Map> m) { - getMap().putAll(m); - } - - /** - * Removes all parameters. - */ - @Override - public void clear() { - getMap().clear(); - } - - /** - * Returns the set of specified parameter names. - */ - @Override - public Set keySet() { - return getMap().keySet(); - } - - /** - * Returns a list of value lists. - */ - @Override - public Collection> values() { - return getMap().values(); - } - - /** - * Returns a set of parameter-list entries. - */ - @Override - public Set>> entrySet() { - return getMap().entrySet(); - } - - /** - * Creates a copy of the parameters, prepending a namespace prefix - * to each parameter name. - * @param prefix the prefix to prepend - * @return the copy of the parameters - */ - public RequestParameters copy(String prefix) { - String keyPrefix = prefix+":"; - - RequestParameters copy = new RequestParameters(); - for (Map.Entry> entry: entrySet()) { - copy.put(keyPrefix+entry.getKey(), entry.getValue()); - } - - return copy; - } +public class RequestParameters extends RequestParametersImplementation implements Map> { + + public RequestParameters() { + } + + /** + * Set a parameter to a single value. + * + * @param name the parameter name + * @param value the value of the parameter + */ + public void put(String name, String value) { + List list = new ArrayList<>(); + list.add(value); + getMap().put(name, list); + } + + /** + * Sets a parameter to a list of values. + * + * @param name the parameter + * @param values the list of values + */ + public void put(String name, String... values) { + getMap().put(name, Arrays.asList(values)); + } + + /** + * Appends a value to the list for a parameter. + * + * @param name the parameter + * @param value the value to add to the list + */ + public void add(String name, String value) { + if (containsKey(name)) { + get(name).add(value); + } else { + put(name, value); + } + } + + /** + * Appends a list of values to the list for a parameter. + * + * @param name the parameter + * @param values the values to add to the list + */ + public void add(String name, String... values) { + if (containsKey(name)) { + List list = get(name); + for (String value : values) { + list.add(value); + } + } else { + put(name, values); + } + } + + /** + * Returns the number of request parameters. + */ + @Override + public int size() { + return getMap().size(); + } + + /** + * Returns whether any request parameters have been specified. + */ + @Override + public boolean isEmpty() { + return getMap().isEmpty(); + } + + /** + * Checks whether the parameter name has been specified. + */ + @Override + public boolean containsKey(Object key) { + return getMap().containsKey(key); + } + + /** + * Checks whether any parameters have the value. + */ + @Override + public boolean containsValue(Object value) { + return getMap().containsValue(value); + } + + /** + * Gets the values for a parameter name. + */ + @Override + public List get(Object key) { + return getMap().get(key); + } + + /** + * Sets the values of a parameter name, returning the previous values if any. + */ + @Override + public List put(String key, List value) { + return getMap().put(key, value); + } + + /** + * Removes a parameter name, returning its values if any. + */ + @Override + public List remove(Object key) { + return getMap().remove(key); + } + + /** + * Adds existing parameter names and values. + */ + @Override + public void putAll(Map> m) { + getMap().putAll(m); + } + + /** + * Removes all parameters. + */ + @Override + public void clear() { + getMap().clear(); + } + + /** + * Returns the set of specified parameter names. + */ + @Override + public Set keySet() { + return getMap().keySet(); + } + + /** + * Returns a list of value lists. + */ + @Override + public Collection> values() { + return getMap().values(); + } + + /** + * Returns a set of parameter-list entries. + */ + @Override + public Set>> entrySet() { + return getMap().entrySet(); + } + + /** + * Creates a copy of the parameters, prepending a namespace prefix + * to each parameter name. + * + * @param prefix the prefix to prepend + * @return the copy of the parameters + */ + public RequestParameters copy(String prefix) { + String keyPrefix = prefix + ":"; + + RequestParameters copy = new RequestParameters(); + for (Map.Entry> entry : entrySet()) { + copy.put(keyPrefix + entry.getKey(), entry.getValue()); + } + + return copy; + } } diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java index 98e43c4d8..553d4f801 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java @@ -136,7 +136,8 @@ public void b_testBulk() throws Exception { } @Test - public void c_testOther() throws Exception { + @Disabled("Needs updating based on recent 12 nightly server changes") + public void c_testOther() { String version1 = "" + uniqueTerm + " version1" + diff --git a/pom.xml b/pom.xml index 210ca83e1..37374b60f 100644 --- a/pom.xml +++ b/pom.xml @@ -11,53 +11,42 @@ It is not intended to be used to build this project. 4.0.0 com.marklogic marklogic-client-api - 7.2.0 + 8.0.0 jakarta.xml.bind jaxb-api - 3.0.1 - - - org.glassfish.jaxb - jaxb-runtime - 3.0.2 + 4.0.4 runtime org.glassfish.jaxb - jaxb-core - 3.0.2 + jaxb-runtime + 4.0.6 runtime com.squareup.okhttp3 okhttp - 4.12.0 + 5.1.0 runtime com.squareup.okhttp3 logging-interceptor - 4.12.0 + 5.1.0 runtime io.github.rburgst okhttp-digest - 2.7 + 3.1.1 runtime com.sun.mail jakarta.mail - 2.0.1 - runtime - - - javax.ws.rs - javax.ws.rs-api - 2.1.1 + 2.0.2 runtime From 9173770dfe8a491814bf502fb9416ba89644f6cc Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 30 Sep 2025 15:56:08 -0400 Subject: [PATCH 14/33] MLE-24504 Fixing functional tests Copilot missed this one --- marklogic-client-api-functionaltests/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/marklogic-client-api-functionaltests/build.gradle b/marklogic-client-api-functionaltests/build.gradle index 0fce007ad..39d866ac0 100755 --- a/marklogic-client-api-functionaltests/build.gradle +++ b/marklogic-client-api-functionaltests/build.gradle @@ -16,6 +16,7 @@ test { dependencies { testImplementation project(':marklogic-client-api') + testImplementation "jakarta.xml.bind:jakarta.xml.bind-api:4.0.4" testImplementation 'org.skyscreamer:jsonassert:1.5.3' testImplementation 'org.slf4j:slf4j-api:2.0.17' testImplementation 'commons-io:commons-io:2.20.0' From 8ba60dbdbb42ad5b888f6094d6275238e606d6cf Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 30 Sep 2025 15:46:01 -0400 Subject: [PATCH 15/33] MLE-24505 Modernizing all Gradle config I think there's more to be done here - haven't chatted with Copilot yet - but this at least gets things looking very similar to our other repos that use maven-publish. --- build.gradle | 26 +-- examples/build.gradle | 8 +- .../client/example/cookbook/README.md | 53 ----- .../build.gradle | 44 ++-- marklogic-client-api/build.gradle | 214 +++++++----------- marklogic-client-api/gradle.properties | 8 + ...gressDataCloudAuthenticationDebugger.java} | 2 +- test-app/build.gradle | 12 +- 8 files changed, 133 insertions(+), 234 deletions(-) delete mode 100644 examples/src/main/java/com/marklogic/client/example/cookbook/README.md create mode 100644 marklogic-client-api/gradle.properties rename marklogic-client-api/src/test/java/com/marklogic/client/test/{MarkLogicCloudAuthenticationDebugger.java => ProgressDataCloudAuthenticationDebugger.java} (97%) diff --git a/build.gradle b/build.gradle index 06c8f3ac3..89d038dbd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,22 +1,9 @@ -// Copyright © 2024 MarkLogic Corporation. All Rights Reserved. - -// We need the properties plugin to work on both marklogic-client-api and test-app. The 'plugins' Gradle syntax can't be -// used for that. So we have to add the properties plugin to the buildscript classpath and then apply the properties -// plugin via subprojects below. -buildscript { - repositories { - maven { - url = "https://plugins.gradle.org/m2/" - } - } - dependencies { - classpath "net.saliman:gradle-properties-plugin:1.5.2" - } -} +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ subprojects { - apply plugin: "net.saliman.properties" - apply plugin: 'java' + apply plugin: 'java-library' tasks.withType(JavaCompile) { options.encoding = 'UTF-8' @@ -46,6 +33,11 @@ subprojects { } test { + useJUnitPlatform() + testLogging { + events = ['started', 'passed', 'skipped', 'failed'] + exceptionFormat = 'full' + } systemProperty "file.encoding", "UTF-8" systemProperty "javax.xml.stream.XMLOutputFactory", "com.sun.xml.internal.stream.XMLOutputFactoryImpl" } diff --git a/examples/build.gradle b/examples/build.gradle index 896cc6e92..3e123799c 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,8 +1,6 @@ -// Copyright © 2025 MarkLogic Corporation. All Rights Reserved. - -plugins { - id "java-library" -} +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ dependencies { implementation project(':marklogic-client-api') diff --git a/examples/src/main/java/com/marklogic/client/example/cookbook/README.md b/examples/src/main/java/com/marklogic/client/example/cookbook/README.md deleted file mode 100644 index f47296fd9..000000000 --- a/examples/src/main/java/com/marklogic/client/example/cookbook/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Using Cookbook Examples - -The most important use of cookbook examples is reading the source code. You -can do this on [github](https://github.com/marklogic/java-client-api) or on -your machine once you've cloned the code from github. - -To run the examples, first edit the -[Example.properties](../../../../../../resources/Example.properties) file in the -distribution to specify the connection parameters for your server. Most -Cookbook examples have a main method, so they can be run from the command-line -like so: - - java -cp $CLASSPATH com.marklogic.client.example.cookbook.DocumentWrite - -This, of course, requires that you have all necessary dependencies in the env -variable $CLASSPATH. You can get the classpath for your machine by executing the the following gradle task - - ./gradlew printClasspath - -# Testing Cookbook Examples - -Most cookbook examples pass their unit test if they run without error. First -edit the [Example.properties](../../../../../../resources/Example.properties) file -in the distribution to specify the connection parameters for your server. Then -run `./gradlew test` while specifying the unit test you want to run, for example: - - ./gradlew java-client-api:test -Dtest.single=DocumentWriteTest - -The above command runs the DocumentWriteTest unit test in java-client-api sub project. - -# Creating a Cookbook Example - -We encourage community-contributed cookbook examples! Make sure you follow -the guidelines in [CONTRIBUTING.md](../../../../../../../../CONTRIBUTING.md) -when you submit a pull request. Each cookbook example should be runnable from -the command-line, so it should have a static `main` method. The approach in -the code should come as close as possible to production code (code one would -reasonably expect to use in a production application), while remaining as -simple as possible to facilitate grokking for newbies to the Java Client API. -It should have helpful comments throughout, including javadocs since it will -show up in the published javadocs. It should be added to -[AllCookbookExamples.java](https://github.com/marklogic/java-client-api/blob/master/marklogic-client-api/src/main/java/com/marklogic/client/example/cookbook/AllCookbookExamples.java) -in order of recommended examples for developers to review. - -It should have a unit test added to -[this package](https://github.com/marklogic/java-client-api/tree/master/marklogic-client-api/src/test/java/com/marklogic/client/test/example/cookbook). -The unit test can test whatever is needed, however most cookbook unit tests -just run the class and consider it success if no errors are thrown. Some -cookbook examples, such as SSLClientCreator and KerberosClientCreator cannot be -included in unit tests because the unit tests require a server configured with -digest authentication and those tests require a different authentication -scheme. Any cookbook examples not included in unit tests run the risk of -breaking without anyone noticing--hence we have unit tests whenever possible. diff --git a/marklogic-client-api-functionaltests/build.gradle b/marklogic-client-api-functionaltests/build.gradle index 39d866ac0..cc78be9d8 100755 --- a/marklogic-client-api-functionaltests/build.gradle +++ b/marklogic-client-api-functionaltests/build.gradle @@ -3,13 +3,9 @@ */ test { - useJUnitPlatform() - testLogging{ - events 'started','passed', 'skipped' - } - // For use in testing TestDatabaseClientKerberosFromFile - systemProperty "keytabFile", System.getProperty("keytabFile") - systemProperty "principal", System.getProperty("principal") + // For use in testing TestDatabaseClientKerberosFromFile + systemProperty "keytabFile", System.getProperty("keytabFile") + systemProperty "principal", System.getProperty("principal") systemProperty "TEST_USE_REVERSE_PROXY_SERVER", testUseReverseProxyServer } @@ -43,34 +39,34 @@ dependencies { tasks.register("runFragileTests", Test) { useJUnitPlatform() - description = "These are called 'fragile' because they'll pass when run by themselves, but when run as part of the " + - "full suite, there seem to be one or more other fast functional tests that run before them and cause some of " + - "their test methods to break. The Jenkinsfile thus calls these first before running the other functional " + - "tests." - include "com/marklogic/client/fastfunctest/TestQueryOptionBuilder.class" - include "com/marklogic/client/fastfunctest/TestRawCombinedQuery.class" - include "com/marklogic/client/fastfunctest/TestRawStructuredQuery.class" + description = "These are called 'fragile' because they'll pass when run by themselves, but when run as part of the " + + "full suite, there seem to be one or more other fast functional tests that run before them and cause some of " + + "their test methods to break. The Jenkinsfile thus calls these first before running the other functional " + + "tests." + include "com/marklogic/client/fastfunctest/TestQueryOptionBuilder.class" + include "com/marklogic/client/fastfunctest/TestRawCombinedQuery.class" + include "com/marklogic/client/fastfunctest/TestRawStructuredQuery.class" } tasks.register("runFastFunctionalTests", Test) { useJUnitPlatform() - description = "Run all fast functional tests that don't setup/teardown custom app servers / databases" - include "com/marklogic/client/fastfunctest/**" - // Exclude the "fragile" ones - exclude "com/marklogic/client/fastfunctest/TestQueryOptionBuilder.class" - exclude "com/marklogic/client/fastfunctest/TestRawCombinedQuery.class" - exclude "com/marklogic/client/fastfunctest/TestRawStructuredQuery.class" + description = "Run all fast functional tests that don't setup/teardown custom app servers / databases" + include "com/marklogic/client/fastfunctest/**" + // Exclude the "fragile" ones + exclude "com/marklogic/client/fastfunctest/TestQueryOptionBuilder.class" + exclude "com/marklogic/client/fastfunctest/TestRawCombinedQuery.class" + exclude "com/marklogic/client/fastfunctest/TestRawStructuredQuery.class" } tasks.register("runSlowFunctionalTests", Test) { useJUnitPlatform() - description = "Run slow functional tests; i.e. those that setup/teardown custom app servers / databases" - include "com/marklogic/client/datamovement/functionaltests/**" - include "com/marklogic/client/functionaltest/**" + description = "Run slow functional tests; i.e. those that setup/teardown custom app servers / databases" + include "com/marklogic/client/datamovement/functionaltests/**" + include "com/marklogic/client/functionaltest/**" } tasks.register("runFunctionalTests") { - dependsOn(runFragileTests, runFastFunctionalTests, runSlowFunctionalTests) + dependsOn(runFragileTests, runFastFunctionalTests, runSlowFunctionalTests) } runFastFunctionalTests.mustRunAfter runFragileTests runSlowFunctionalTests.mustRunAfter runFastFunctionalTests diff --git a/marklogic-client-api/build.gradle b/marklogic-client-api/build.gradle index 8f7b84bc7..40a2a0dc4 100644 --- a/marklogic-client-api/build.gradle +++ b/marklogic-client-api/build.gradle @@ -3,14 +3,9 @@ */ plugins { - id 'java-library' id 'maven-publish' } -group = 'com.marklogic' - -description = "The official MarkLogic Java client API." - dependencies { // Using the latest version now that the 8.0.0 release requires Java 17. // This is now an implementation dependency as opposed to an api dependency in 7.x and earlier. @@ -51,7 +46,7 @@ dependencies { testImplementation 'org.apache.commons:commons-lang3:3.19.0' // Allows talking to the Manage API. - testImplementation ("com.marklogic:ml-app-deployer:6.0.1") { + testImplementation("com.marklogic:ml-app-deployer:6.0.1") { exclude module: "marklogic-client-api" } @@ -92,143 +87,102 @@ test { systemProperty "TEST_USE_REVERSE_PROXY_SERVER", testUseReverseProxyServer } -task sourcesJar(type: Jar) { - archiveClassifier = 'sources' - exclude ('property', '*.xsd', '*.xjb') - from sourceSets.main.allSource -} - -javadoc { - maxMemory="6000m" - options.overview = "src/main/javadoc/overview.html" - options.windowTitle = "$rootProject.describedName $rootProject.version" - options.docTitle = "$rootProject.describedName $rootProject.version" - options.bottom = "Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved." - options.links = [ 'http://docs.oracle.com/javase/8/docs/api/' ] - options.use = true - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption('html4', true) - } - exclude([ - '**/impl/**', '**/jaxb/**', '**/test/**' - ]) -// workaround for bug in options.docFilesSubDirs = true - doLast{ - copy{ - from "${projectDir}/src/main/javadoc/doc-files" - into "${buildDir}/docs/javadoc/doc-files" - } - } -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - archiveClassifier = 'javadoc' - from javadoc.destinationDir -} - -Node pomCustomizations = new NodeBuilder(). project { - name "$rootProject.describedName" - packaging 'jar' - textdescription "$project.description" - url 'https://github.com/marklogic/java-client-api' - - scm { - url 'git@github.com:marklogic/java-client-api.git' - connection 'scm:git:git@github.com:marklogic/java-client-api.git' - developerConnection 'scm:git:git@github.com:marklogic/java-client-api.git' - } - - licenses { - license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - - developers { - developer { - name 'MarkLogic' - email 'java-sig@marklogic.com' - organization 'MarkLogic' - organizationUrl 'https://www.marklogic.com' - } - developer { - name 'MarkLogic Github Contributors' - email 'general@developer.marklogic.com' - organization 'Github Contributors' - organizationUrl 'https://github.com/marklogic/java-client-api/graphs/contributors' - } - } -} - -publishing { - publications { - mainJava(MavenPublication) { - from components.java - - pom.withXml { - asNode().append(pomCustomizations.packaging) - asNode().append(pomCustomizations.name) - asNode().appendNode("description", pomCustomizations.textdescription.text()) - asNode().append(pomCustomizations.url) - asNode().append(pomCustomizations.licenses) - asNode().append(pomCustomizations.developers) - asNode().append(pomCustomizations.scm) - } - artifact sourcesJar - artifact javadocJar - } - } - repositories { - maven { - if(project.hasProperty("mavenUser")) { - credentials { - username mavenUser - password mavenPassword - } - } - url = publishUrl - } - } -} - -task printClassPath() { - doLast { - println sourceSets.main.runtimeClasspath.asPath+':'+sourceSets.test.runtimeClasspath.asPath - } -} - -task generatePomForDependencyGraph(dependsOn: "generatePomFileForMainJavaPublication") { - description = "Prepare for a release by making a copy of the generated pom file in the root directory so that it " + - "can enable Github's Dependency Graph feature, which does not yet support Gradle" - doLast { - def preamble = '' - def comment = "" - def fileText = file("build/publications/mainJava/pom-default.xml").getText() - file("../pom.xml").setText(fileText.replace(preamble, preamble + comment)) - } -} - -task testRows(type: Test) { +tasks.register("testRows", Test) { useJUnitPlatform() description = "Run all 'rows' tests; i.e. those exercising Optic and Optic Update functionality" include "com/marklogic/client/test/rows/**" } -task debugCloudAuth(type: JavaExec) { +tasks.register("debugCloudAuth", JavaExec) { description = "Test program for manual testing of cloud-based authentication against a Progress Data Cloud instance" - mainClass = 'com.marklogic.client.test.MarkLogicCloudAuthenticationDebugger' + mainClass = 'com.marklogic.client.test.ProgressDataCloudAuthenticationDebugger' classpath = sourceSets.test.runtimeClasspath args = [cloudHost, cloudKey, cloudBasePath] } -task runXmlSmokeTests(type: Test) { +tasks.register("runXmlSmokeTests", Test) { + useJUnitPlatform() description = "Run a bunch of XML-related tests for smoke-testing on a particular JVM" include "com/marklogic/client/test/BufferableHandleTest.class" include "com/marklogic/client/test/EvalTest.class" include "com/marklogic/client/test/HandleAsTest.class" include "com/marklogic/client/test/JAXBHandleTest.class" } + +// Publishing setup - see https://docs.gradle.org/current/userguide/publishing_setup.html . +java { + withJavadocJar() + withSourcesJar() +} + +javadoc { + maxMemory = "6000m" + options.overview = "src/main/javadoc/overview.html" + options.windowTitle = "MarkLogic Java Client API $rootProject.version" + options.docTitle = "MarkLogic Java Client API $rootProject.version" + options.bottom = "Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved." + options.use = true + exclude([ + '**/impl/**', '**/jaxb/**', '**/test/**' + ]) +// workaround for bug in options.docFilesSubDirs = true + doLast { + copy { + from "${projectDir}/src/main/javadoc/doc-files" + into "${layout.buildDirectory.get()}/docs/javadoc/doc-files" + } + } +} + +publishing { + publications { + mainJava(MavenPublication) { + from components.java + pom { + name = "${group}:${project.name}" + description = "The MarkLogic Java Client API" + packaging = "jar" + url = "https://github.com/marklogic/java-client-api" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "marklogic" + name = "MarkLogic Github Contributors" + email = "general@developer.marklogic.com" + organization = "MarkLogic" + organizationUrl = "https://www.marklogic.com" + } + } + scm { + url = "git@github.com:marklogic/java-client-api.git" + connection = "scm:git:git@github.com:marklogic/java-client-api.git" + developerConnection = "scm:git:git@github.com:marklogic/java-client-api.git" + } + } + } + } + repositories { + maven { + if (project.hasProperty("mavenUser")) { + credentials { + username = mavenUser + password = mavenPassword + } + url publishUrl + allowInsecureProtocol = true + } else { + name = "central" + url = mavenCentralUrl + credentials { + username = mavenCentralUsername + password = mavenCentralPassword + } + } + } + } +} diff --git a/marklogic-client-api/gradle.properties b/marklogic-client-api/gradle.properties new file mode 100644 index 000000000..4bee29e3b --- /dev/null +++ b/marklogic-client-api/gradle.properties @@ -0,0 +1,8 @@ +# Define these on the command line to publish to OSSRH +# See https://central.sonatype.org/publish/publish-gradle/#credentials for more information +mavenCentralUsername= +mavenCentralPassword= +mavenCentralUrl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ +#signing.keyId=YourKeyId +#signing.password=YourPublicKeyPassword +#signing.secretKeyRingFile=PathToYourKeyRingFile diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/MarkLogicCloudAuthenticationDebugger.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/ProgressDataCloudAuthenticationDebugger.java similarity index 97% rename from marklogic-client-api/src/test/java/com/marklogic/client/test/MarkLogicCloudAuthenticationDebugger.java rename to marklogic-client-api/src/test/java/com/marklogic/client/test/ProgressDataCloudAuthenticationDebugger.java index b4d3c7611..e542317b6 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/MarkLogicCloudAuthenticationDebugger.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/ProgressDataCloudAuthenticationDebugger.java @@ -16,7 +16,7 @@ * "localhost" as the cloud host, "username:password" (often "admin:the admin password") as the apiKey, and * "local/manage" as the basePath. */ -public class MarkLogicCloudAuthenticationDebugger { +public class ProgressDataCloudAuthenticationDebugger { public static void main(String[] args) throws Exception { String cloudHost = args[0]; diff --git a/test-app/build.gradle b/test-app/build.gradle index c7f8a1072..a06a500b5 100644 --- a/test-app/build.gradle +++ b/test-app/build.gradle @@ -1,6 +1,10 @@ +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ + plugins { + id "net.saliman.properties" version "1.5.2" id 'com.marklogic.ml-gradle' version '6.0.1' - id 'java' id "com.github.psxpaul.execfork" version "0.2.2" } @@ -22,9 +26,9 @@ tasks.register("runReverseProxyServer", com.github.psxpaul.task.JavaExecFork) { "directly to MarkLogic" classpath = sourceSets.main.runtimeClasspath main = "com.marklogic.client.test.ReverseProxyServer" - workingDir = "$buildDir" - standardOutput = file("$buildDir/reverse-proxy.log") - errorOutput = file("$buildDir/reverse-proxy-error.log") + workingDir = "${layout.buildDirectory.get()}" + standardOutput = file("${layout.buildDirectory.get()}/reverse-proxy.log") + errorOutput = file("${layout.buildDirectory.get()}/reverse-proxy-error.log") } tasks.register("runBlockingReverseProxyServer", JavaExec) { From f1542bdd26b0aa1425e1d4f2bf2619430f5e6020 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Wed, 1 Oct 2025 10:34:30 -0400 Subject: [PATCH 16/33] MLE-24505 Configuring custom Test tasks correctly This avoids Gradle warnings and also gets the reverse proxy server tests working properly again. Also disabling the reverse proxy tests, as those apparently were not hitting the reverse proxy before due to a Gradle misconfiguration that this PR fixes. Opened MLE-24523 to fix those later. --- Jenkinsfile | 40 ++++++++++--------- build.gradle | 14 ++++--- .../build.gradle | 25 +++++++----- marklogic-client-api/build.gradle | 9 +++-- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index aca93c979..73ddfdf1b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -173,8 +173,9 @@ pipeline{ export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api ./gradlew cleanTest marklogic-client-api:test - ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFastFunctionalTests || true ''' + // Omitting this until MLE-24523 can be addressed + // ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFastFunctionalTests || true junit '**/build/**/TEST*.xml' } post { @@ -222,24 +223,25 @@ pipeline{ } } - stage('regressions-11-reverseProxy') { - when { - allOf { - branch 'develop' - expression {return params.regressions} - } - } - steps { - runTestsWithReverseProxy("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-11") - junit '**/build/**/TEST*.xml' - } - post { - always { - updateWorkspacePermissions() - tearDownDocker() - } - } - } + // Omitting this until MLE-24523 can be addressed +// stage('regressions-11-reverseProxy') { +// when { +// allOf { +// branch 'develop' +// expression {return params.regressions} +// } +// } +// steps { +// runTestsWithReverseProxy("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-11") +// junit '**/build/**/TEST*.xml' +// } +// post { +// always { +// updateWorkspacePermissions() +// tearDownDocker() +// } +// } +// } stage('regressions-12') { when { diff --git a/build.gradle b/build.gradle index 89d038dbd..a87d5146e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,11 +5,6 @@ subprojects { apply plugin: 'java-library' - tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' - options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] - } - java { toolchain { languageVersion = JavaLanguageVersion.of(17) @@ -32,12 +27,19 @@ subprojects { mavenCentral() } - test { + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] + } + + tasks.withType(Test).configureEach { useJUnitPlatform() testLogging { events = ['started', 'passed', 'skipped', 'failed'] exceptionFormat = 'full' } + + // Will remove this in a future PR to determine if they're needed or not. systemProperty "file.encoding", "UTF-8" systemProperty "javax.xml.stream.XMLOutputFactory", "com.sun.xml.internal.stream.XMLOutputFactoryImpl" } diff --git a/marklogic-client-api-functionaltests/build.gradle b/marklogic-client-api-functionaltests/build.gradle index cc78be9d8..a40f43f59 100755 --- a/marklogic-client-api-functionaltests/build.gradle +++ b/marklogic-client-api-functionaltests/build.gradle @@ -2,14 +2,6 @@ * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ -test { - // For use in testing TestDatabaseClientKerberosFromFile - systemProperty "keytabFile", System.getProperty("keytabFile") - systemProperty "principal", System.getProperty("principal") - - systemProperty "TEST_USE_REVERSE_PROXY_SERVER", testUseReverseProxyServer -} - dependencies { testImplementation project(':marklogic-client-api') testImplementation "jakarta.xml.bind:jakarta.xml.bind-api:4.0.4" @@ -37,20 +29,30 @@ dependencies { testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.13.4" } +tasks.withType(Test).configureEach { + // For use in testing TestDatabaseClientKerberosFromFile + systemProperty "keytabFile", System.getProperty("keytabFile") + systemProperty "principal", System.getProperty("principal") + + systemProperty "TEST_USE_REVERSE_PROXY_SERVER", testUseReverseProxyServer +} + tasks.register("runFragileTests", Test) { - useJUnitPlatform() description = "These are called 'fragile' because they'll pass when run by themselves, but when run as part of the " + "full suite, there seem to be one or more other fast functional tests that run before them and cause some of " + "their test methods to break. The Jenkinsfile thus calls these first before running the other functional " + "tests." + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath include "com/marklogic/client/fastfunctest/TestQueryOptionBuilder.class" include "com/marklogic/client/fastfunctest/TestRawCombinedQuery.class" include "com/marklogic/client/fastfunctest/TestRawStructuredQuery.class" } tasks.register("runFastFunctionalTests", Test) { - useJUnitPlatform() description = "Run all fast functional tests that don't setup/teardown custom app servers / databases" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath include "com/marklogic/client/fastfunctest/**" // Exclude the "fragile" ones exclude "com/marklogic/client/fastfunctest/TestQueryOptionBuilder.class" @@ -59,8 +61,9 @@ tasks.register("runFastFunctionalTests", Test) { } tasks.register("runSlowFunctionalTests", Test) { - useJUnitPlatform() description = "Run slow functional tests; i.e. those that setup/teardown custom app servers / databases" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath include "com/marklogic/client/datamovement/functionaltests/**" include "com/marklogic/client/functionaltest/**" } diff --git a/marklogic-client-api/build.gradle b/marklogic-client-api/build.gradle index 40a2a0dc4..47372a6aa 100644 --- a/marklogic-client-api/build.gradle +++ b/marklogic-client-api/build.gradle @@ -77,8 +77,7 @@ dependencies { } // Ensure that mlHost and mlPassword can override the defaults of localhost/admin if they've been modified -test { - useJUnitPlatform() +tasks.withType(Test).configureEach { systemProperty "TEST_HOST", mlHost systemProperty "TEST_ADMIN_PASSWORD", mlPassword // Needed by the tests for the example programs @@ -88,8 +87,9 @@ test { } tasks.register("testRows", Test) { - useJUnitPlatform() description = "Run all 'rows' tests; i.e. those exercising Optic and Optic Update functionality" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath include "com/marklogic/client/test/rows/**" } @@ -101,8 +101,9 @@ tasks.register("debugCloudAuth", JavaExec) { } tasks.register("runXmlSmokeTests", Test) { - useJUnitPlatform() description = "Run a bunch of XML-related tests for smoke-testing on a particular JVM" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath include "com/marklogic/client/test/BufferableHandleTest.class" include "com/marklogic/client/test/EvalTest.class" include "com/marklogic/client/test/HandleAsTest.class" From 30cf9acfea70b37290c5733ddbfc597e143a8ffe Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Wed, 1 Oct 2025 11:28:59 -0400 Subject: [PATCH 17/33] MLE-24505 More Gradle tweaks Eliminating more warnings, and forcing an error on a compiler warning. Got rid of two systemProperty values that I don't think are needed, but we'll see what happens in the full test run. --- build.gradle | 26 +++++++++++++------------- gradle.properties | 1 - marklogic-client-api/build.gradle | 12 ++++++------ ml-development-tools/build.gradle | 21 +++++++++++++-------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/build.gradle b/build.gradle index a87d5146e..887eb4302 100644 --- a/build.gradle +++ b/build.gradle @@ -27,9 +27,11 @@ subprojects { mavenCentral() } - tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' - options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] + // Allows for identifying compiler warnings and treating them as errors. + tasks.withType(JavaCompile).configureEach { + options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation", "-Werror"] + options.deprecation = true + options.warnings = true } tasks.withType(Test).configureEach { @@ -38,17 +40,15 @@ subprojects { events = ['started', 'passed', 'skipped', 'failed'] exceptionFormat = 'full' } - - // Will remove this in a future PR to determine if they're needed or not. - systemProperty "file.encoding", "UTF-8" - systemProperty "javax.xml.stream.XMLOutputFactory", "com.sun.xml.internal.stream.XMLOutputFactoryImpl" } - // Until we do a cleanup of javadoc errors, the build (and specifically the javadoc task) fails on Java 11 - // and higher. Preventing that until the cleanup can occur. - javadoc.failOnError = false + tasks.withType(Javadoc).configureEach { + // Until we do a cleanup of javadoc errors, the build (and specifically the javadoc task) fails on Java 11 + // and higher. Preventing that until the cleanup can occur. + failOnError = false - // Ignores warnings on param tags with no descriptions. Will remove this once javadoc errors are addressed. - // Until then, it's just a lot of noise. - javadoc.options.addStringOption('Xdoclint:none', '-quiet') + // Ignores warnings on param tags with no descriptions. Will remove this once javadoc errors are addressed. + // Until then, it's just a lot of noise. + options.addStringOption('Xdoclint:none', '-quiet') + } } diff --git a/gradle.properties b/gradle.properties index 60f18ae19..62b1ed3dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,5 @@ group=com.marklogic version=8.0-SNAPSHOT -describedName=MarkLogic Java Client API publishUrl=file:../marklogic-java/releases okhttpVersion=5.1.0 diff --git a/marklogic-client-api/build.gradle b/marklogic-client-api/build.gradle index 47372a6aa..b100e5a91 100644 --- a/marklogic-client-api/build.gradle +++ b/marklogic-client-api/build.gradle @@ -140,14 +140,14 @@ publishing { mainJava(MavenPublication) { from components.java pom { - name = "${group}:${project.name}" + name = "${project.group}:${project.name}" description = "The MarkLogic Java Client API" packaging = "jar" url = "https://github.com/marklogic/java-client-api" licenses { license { name = "The Apache License, Version 2.0" - url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" } } developers { @@ -160,9 +160,9 @@ publishing { } } scm { - url = "git@github.com:marklogic/java-client-api.git" - connection = "scm:git:git@github.com:marklogic/java-client-api.git" - developerConnection = "scm:git:git@github.com:marklogic/java-client-api.git" + url = "https://github.com/marklogic/java-client-api" + connection = "https://github.com/marklogic/java-client-api" + developerConnection = "https://github.com/marklogic/java-client-api" } } } @@ -174,7 +174,7 @@ publishing { username = mavenUser password = mavenPassword } - url publishUrl + url = publishUrl allowInsecureProtocol = true } else { name = "central" diff --git a/ml-development-tools/build.gradle b/ml-development-tools/build.gradle index 847a98b9c..5eeead94a 100644 --- a/ml-development-tools/build.gradle +++ b/ml-development-tools/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /* * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ @@ -12,6 +14,10 @@ plugins { dependencies { compileOnly gradleApi() + + // This is a runtime dependency of marklogic-client-api but is needed for compiling. + compileOnly "jakarta.xml.bind:jakarta.xml.bind-api:4.0.4" + implementation project(':marklogic-client-api') implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.1.0' implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}" @@ -29,7 +35,7 @@ dependencies { // Added to avoid problem where processResources fails because - somehow - the plugin properties file is getting // copied twice. This started occurring with the upgrade of Gradle from 6.x to 7.x. tasks.processResources { - duplicatesStrategy = "exclude" + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } tasks.register("mlDevelopmentToolsJar", Jar) { @@ -45,7 +51,7 @@ gradlePlugin { id = 'com.marklogic.ml-development-tools' displayName = 'ml-development-tools MarkLogic Data Service Tools' description = 'ml-development-tools plugin for developing data services on MarkLogic' - tags.set(['marklogic', 'progress']) + tags = ['marklogic', 'progress'] implementationClass = 'com.marklogic.client.tools.gradle.ToolsPlugin' } } @@ -53,7 +59,7 @@ gradlePlugin { publishing { publications { - main(MavenPublication) { + mainJava(MavenPublication) { from components.java } } @@ -70,11 +76,10 @@ publishing { } } -compileKotlin { - kotlinOptions.jvmTarget = '17' -} -compileTestKotlin { - kotlinOptions.jvmTarget = '17' +tasks.withType(KotlinCompile).configureEach { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } } tasks.register("generateTests", JavaExec) { From d8f15199c009ef9ee95d37e7ef5056a5bbf910d1 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Wed, 1 Oct 2025 13:32:52 -0400 Subject: [PATCH 18/33] MLE-24505 Giving Gradle 9.1 a spin No warnings or errors locally. Also shifting to the 11.3.2 build temporarily to try things out. And fixing a problem with the ml-development-tools tests. --- Jenkinsfile | 4 ++-- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 +---- gradlew.bat | 3 +-- .../build.gradle | 2 ++ marklogic-client-api/build.gradle | 2 ++ 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 73ddfdf1b..d1f98da47 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -166,7 +166,7 @@ pipeline{ } } steps { - setupDockerMarkLogic("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:12.0.0-ubi-rootless-2.2.0") + setupDockerMarkLogic("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:11.3.2-ubi-rootless-2.2.2") sh label:'run marklogic-client-api tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR @@ -212,7 +212,7 @@ pipeline{ } } steps { - runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-11") + runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:11.3.2-ubi-rootless-2.2.2") junit '**/build/**/TEST*.xml' } post { diff --git a/build.gradle b/build.gradle index 887eb4302..5b4a9c41e 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ subprojects { } tasks.withType(Test).configureEach { - useJUnitPlatform() + // Can't use useJUnitPlatform here as it breaks ml-development-tools testLogging { events = ['started', 'passed', 'skipped', 'failed'] exceptionFormat = 'full' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55baabb587c669f562ae36f953de2481846..8bdaf60c75ab801e22807dde59e12a8735a34077 100644 GIT binary patch delta 37256 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 delta 35551 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da47..2e1113280 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 23d15a936..adff685a0 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index 5eed7ee84..e509b2dd8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/marklogic-client-api-functionaltests/build.gradle b/marklogic-client-api-functionaltests/build.gradle index a40f43f59..d9cebfa63 100755 --- a/marklogic-client-api-functionaltests/build.gradle +++ b/marklogic-client-api-functionaltests/build.gradle @@ -30,6 +30,8 @@ dependencies { } tasks.withType(Test).configureEach { + useJUnitPlatform() + // For use in testing TestDatabaseClientKerberosFromFile systemProperty "keytabFile", System.getProperty("keytabFile") systemProperty "principal", System.getProperty("principal") diff --git a/marklogic-client-api/build.gradle b/marklogic-client-api/build.gradle index b100e5a91..a8c48a096 100644 --- a/marklogic-client-api/build.gradle +++ b/marklogic-client-api/build.gradle @@ -78,6 +78,8 @@ dependencies { // Ensure that mlHost and mlPassword can override the defaults of localhost/admin if they've been modified tasks.withType(Test).configureEach { + useJUnitPlatform() + systemProperty "TEST_HOST", mlHost systemProperty "TEST_ADMIN_PASSWORD", mlPassword // Needed by the tests for the example programs From 12fb4c1b7f1e45288c31c8cd57cc9f743f3d5fc1 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 2 Oct 2025 09:58:44 -0400 Subject: [PATCH 19/33] MLE-24511 Updating a few tests The Bitemp test is failing due to an ml-gradle bug, but I took the opportunity to clean it up so that each test can run separately without depending on the other. --- .env | 6 +- .../marklogic/client/test/BitemporalTest.java | 592 +++++++++--------- .../com/marklogic/client/test/Common.java | 15 +- .../test/rows/FromSearchWithOptionsTest.java | 16 +- .../client/test/ssl/OneWaySSLTest.java | 6 - 5 files changed, 322 insertions(+), 313 deletions(-) diff --git a/.env b/.env index d5edd373d..c67e5ae05 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # Defines environment variables for docker-compose. # Can be overridden via e.g. `MARKLOGIC_TAG=latest-10.0 docker-compose up -d --build`. -#MARKLOGIC_IMAGE=progressofficial/marklogic-db:latest MARKLOGIC_LOGS_VOLUME=./docker/marklogic/logs - -# This image should be used instead of the above image when testing functions that only work with MarkLogic 12. MARKLOGIC_IMAGE=ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12 + +# Latest public release +#MARKLOGIC_IMAGE=progressofficial/marklogic-db:latest diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java index 553d4f801..9d447d098 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/BitemporalTest.java @@ -15,297 +15,319 @@ import com.marklogic.client.io.StringHandle; import com.marklogic.client.query.*; import com.marklogic.client.query.StructuredQueryBuilder.TemporalOperator; -import org.junit.jupiter.api.*; +import com.marklogic.mgmt.ManageClient; +import com.marklogic.mgmt.resource.temporal.TemporalCollectionLSQTManager; +import jakarta.xml.bind.DatatypeConverter; +import org.custommonkey.xmlunit.exceptions.XpathException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.w3c.dom.Document; -import jakarta.xml.bind.DatatypeConverter; import java.util.Calendar; import java.util.Random; import static org.custommonkey.xmlunit.XMLAssert.assertXpathEvaluatesTo; import static org.junit.jupiter.api.Assertions.*; -@TestMethodOrder(MethodOrderer.MethodName.class) -public class BitemporalTest { - // src/test/resources/bootstrap.xqy is run by com.marklogic.client.test.util.TestServerBootstrapper - // and sets up the "temporal-collection" and required underlying axes - // system-axis and valid-axis which have required underlying range indexes - // system-start, system-end, valid-start, and valid-end - static String temporalCollection = "temporal-collection"; - static XMLDocumentManager docMgr; - static QueryManager queryMgr; - static String uniqueBulkTerm = "temporalBulkDoc" + new Random().nextInt(10000); - static String uniqueTerm = "temporalDoc" + new Random().nextInt(10000); - static String docId = "test-" + uniqueTerm + ".xml"; - - @BeforeAll - public static void beforeClass() { - Common.connect(); - docMgr = Common.client.newXMLDocumentManager(); - queryMgr = Common.client.newQueryManager(); - } - @AfterAll - public static void afterClass() { - cleanUp(); - } - - @Test - public void a_testCreate() throws Exception { - String contents = "" + - "" + - "" + - "2014-08-19T00:00:00Z" + - "2014-08-19T00:00:01Z" + - ""; - TemporalDescriptor desc = docMgr.create(docMgr.newDocumentUriTemplate("xml"), - null, new StringHandle(contents), null, null, temporalCollection); - assertNotNull(desc); - assertNotNull(desc.getUri()); - assertTrue(desc.getUri().endsWith(".xml")); - String lastWriteTimestamp = desc.getTemporalSystemTime(); - Calendar lastWriteTime = DatatypeConverter.parseDateTime(lastWriteTimestamp); - assertNotNull(lastWriteTime); - } - - @Test - public void b_testBulk() throws Exception { - String prefix = "test_" + uniqueBulkTerm; - String doc1 = "" + - uniqueBulkTerm + " doc1" + - "" + - "" + - "2014-08-19T00:00:00Z" + - "2014-08-19T00:00:01Z" + - ""; - String doc2 = "" + - uniqueBulkTerm + " doc2" + - "" + - "" + - "2014-08-19T00:00:02Z" + - "2014-08-19T00:00:03Z" + - ""; - String doc3 = "" + - uniqueBulkTerm + " doc3" + - "" + - "" + - "2014-08-19T00:00:03Z" + - "2014-08-19T00:00:04Z" + - ""; - String doc4 = "" + - uniqueBulkTerm + " doc4" + - "" + - "" + - "2014-08-19T00:00:05Z" + - "2014-08-19T00:00:06Z" + - ""; - DocumentWriteSet writeSet = docMgr.newWriteSet(); - writeSet.add(prefix + "_1.xml", new StringHandle(doc1).withFormat(Format.XML)); - writeSet.add(prefix + "_2.xml", new StringHandle(doc2).withFormat(Format.XML)); - writeSet.add(prefix + "_3.xml", new StringHandle(doc3).withFormat(Format.XML)); - writeSet.add(prefix + "_4.xml", new StringHandle(doc4).withFormat(Format.XML)); - docMgr.write(writeSet, null, null, temporalCollection); - // do it one more time so we have two versions of each - writeSet = docMgr.newWriteSet(); - writeSet.add(prefix + "_1.xml", new StringHandle(doc1).withFormat(Format.XML)); - writeSet.add(prefix + "_2.xml", new StringHandle(doc2).withFormat(Format.XML)); - writeSet.add(prefix + "_3.xml", new StringHandle(doc3).withFormat(Format.XML)); - writeSet.add(prefix + "_4.xml", new StringHandle(doc4).withFormat(Format.XML)); - docMgr.write(writeSet, null, null, temporalCollection); - - StringQueryDefinition query = queryMgr.newStringDefinition().withCriteria(uniqueBulkTerm); - try ( DocumentPage page = docMgr.search(query, 0) ) { - assertEquals(8, page.size()); - for ( DocumentRecord record : page ) { - Document doc = record.getContentAs(Document.class); - if ( record.getUri().startsWith(prefix + "_1") ) { - assertXpathEvaluatesTo("2014-08-19T00:00:00Z", "//valid-start", doc); - continue; - } else if ( record.getUri().startsWith(prefix + "_2") ) { - assertXpathEvaluatesTo("2014-08-19T00:00:02Z", "//valid-start", doc); - continue; - } else if ( record.getUri().startsWith(prefix + "_3") ) { - assertXpathEvaluatesTo("2014-08-19T00:00:03Z", "//valid-start", doc); - continue; - } else if ( record.getUri().startsWith(prefix + "_4") ) { - assertXpathEvaluatesTo("2014-08-19T00:00:05Z", "//valid-start", doc); - continue; - } - throw new IllegalStateException("Unexpected doc:[" + record.getUri() + "]"); - } - } - } - - @Test - @Disabled("Needs updating based on recent 12 nightly server changes") - public void c_testOther() { - - String version1 = "" + - uniqueTerm + " version1" + - "" + - "" + - "2014-08-19T00:00:00Z" + - "2014-08-19T00:00:01Z" + - ""; - String version2 = "" + - uniqueTerm + " version2" + - "" + - "" + - "2014-08-19T00:00:02Z" + - "2014-08-19T00:00:03Z" + - ""; - String version3 = "" + - uniqueTerm + " version3" + - "" + - "" + - "2014-08-19T00:00:03Z" + - "2014-08-19T00:00:04Z" + - ""; - String version4 = "" + - uniqueTerm + " version4" + - "" + - "" + - "2014-08-19T00:00:05Z" + - "2014-08-19T00:00:06Z" + - ""; - - // write four versions of the same document - StringHandle handle1 = new StringHandle(version1).withFormat(Format.XML); - docMgr.write(docId, null, handle1, null, null, temporalCollection); - StringHandle handle2 = new StringHandle(version2).withFormat(Format.XML); - docMgr.write(docId, null, handle2, null, null, temporalCollection); - StringHandle handle3 = new StringHandle(version3).withFormat(Format.XML); - TemporalDescriptor desc = docMgr.write(docId, null, handle3, null, null, temporalCollection); - assertNotNull(desc); - assertEquals(docId, desc.getUri()); - String thirdWriteTimestamp = desc.getTemporalSystemTime(); - assertNotNull(thirdWriteTimestamp); - - StringHandle handle4 = new StringHandle(version4).withFormat(Format.XML); - docMgr.write(docId, null, handle4, null, null, temporalCollection); - - // make sure non-temporal document read only returns the latest version - try ( DocumentPage readResults = docMgr.read(docId) ) { - assertEquals(1, readResults.size()); - DocumentRecord latestDoc = readResults.next(); - assertEquals(docId, latestDoc.getUri()); - } - - // make sure a simple term query returns all versions of bulk and other docs - StructuredQueryBuilder sqb = queryMgr.newStructuredQueryBuilder(); - StructuredQueryDefinition termsQuery = - sqb.or( sqb.term(uniqueTerm), sqb.term(uniqueBulkTerm) ); - long start = 1; - try ( DocumentPage termQueryResults = docMgr.search(termsQuery, start) ) { - assertEquals(12, termQueryResults.size()); - } - - StructuredQueryDefinition currentQuery = sqb.temporalLsqtQuery(temporalCollection, thirdWriteTimestamp, 1); - StructuredQueryDefinition currentDocQuery = sqb.and(termsQuery, currentQuery); - try { - // query with lsqt of last inserted document - // will throw an error because lsqt has not yet advanced - try ( DocumentPage results = docMgr.search(currentDocQuery, start) ) { - fail("Negative test should have generated a FailedRequestException of type TEMPORAL-GTLSQT"); - } - } catch (FailedRequestException e) { - assertTrue(e.getMessage().contains("TEMPORAL-GTLSQT")); - } - - // now update lsqt - Common.connectServerAdmin().newXMLDocumentManager().advanceLsqt(temporalCollection); - - // query again with lsqt of last inserted document - // will match the first three versions -- not the last because it's equal to - // not greater than the timestamp of this lsqt query - try ( DocumentPage currentDocQueryResults = docMgr.search(currentDocQuery, start) ) { - assertEquals(11, currentDocQueryResults.size()); - } - - // query with blank lsqt indicating current time - // will match all four versions - currentQuery = sqb.temporalLsqtQuery(temporalCollection, "", 1); - currentDocQuery = sqb.and(termsQuery, currentQuery); - try ( DocumentPage currentDocQueryResults = docMgr.search(currentDocQuery, start) ) { - assertEquals(12, currentDocQueryResults.size()); - } - - StructuredQueryBuilder.Axis validAxis = sqb.axis("valid-axis"); - - // create a time axis to query the versions against - Calendar start1 = DatatypeConverter.parseDateTime("2014-08-19T00:00:00Z"); - Calendar end1 = DatatypeConverter.parseDateTime("2014-08-19T00:00:04Z"); - StructuredQueryBuilder.Period period1 = sqb.period(start1, end1); - - // find all documents contained in the time range of our query axis - StructuredQueryDefinition periodQuery1 = sqb.and(termsQuery, - sqb.temporalPeriodRange(validAxis, TemporalOperator.ALN_CONTAINED_BY, period1)); - try ( DocumentPage periodQuery1Results = docMgr.search(periodQuery1, start) ) { - assertEquals(3, periodQuery1Results.size()); - } - - // create a second time axis to query the versions against - Calendar start2 = DatatypeConverter.parseDateTime("2014-08-19T00:00:04Z"); - Calendar end2 = DatatypeConverter.parseDateTime("2014-08-19T00:00:07Z"); - StructuredQueryBuilder.Period period2 = sqb.period(start2, end2); - - // find all documents contained in the time range of our second query axis - StructuredQueryDefinition periodQuery2 = sqb.and(termsQuery, - sqb.temporalPeriodRange(validAxis, TemporalOperator.ALN_CONTAINED_BY, period2)); - try ( DocumentPage periodQuery2Results = docMgr.search(periodQuery2, start) ) { - assertEquals(3, periodQuery2Results.size()); - for ( DocumentRecord result : periodQuery2Results ) { - if ( docId.equals(result.getUri()) ) { - continue; - } else if ( result.getUri().startsWith("test_" + uniqueBulkTerm + "_4") ) { - continue; - } - fail("Unexpected uri for ALN_CONTAINED_BY test:" + result.getUri()); - } - } - - // find all documents where valid time is after system time in the document - StructuredQueryBuilder.Axis systemAxis = sqb.axis("system-axis"); - StructuredQueryDefinition periodCompareQuery1 = sqb.and(termsQuery, - sqb.temporalPeriodCompare(systemAxis, TemporalOperator.ALN_AFTER, validAxis)); - try ( DocumentPage periodCompareQuery1Results = docMgr.search(periodCompareQuery1, start) ) { - assertEquals(12, periodCompareQuery1Results.size()); - } - - // find all documents where valid time is before system time in the document - StructuredQueryDefinition periodCompareQuery2 = sqb.and(termsQuery, - sqb.temporalPeriodCompare(systemAxis, TemporalOperator.ALN_BEFORE, validAxis)); - try ( DocumentPage periodCompareQuery2Results = docMgr.search(periodCompareQuery2, start) ) { - assertEquals(0, periodCompareQuery2Results.size()); - } - - // check that we get a system time when we delete - desc = docMgr.delete(docId, null, temporalCollection); - assertNotNull(desc); - assertEquals(docId, desc.getUri()); - assertNotNull(desc.getTemporalSystemTime()); - - } - - - static public void cleanUp() { - DatabaseClient client = Common.newServerAdminClient(); - try { - QueryManager queryMgr = client.newQueryManager(); - queryMgr.setPageLength(1000); - QueryDefinition query = queryMgr.newStringDefinition(); - query.setCollections(temporalCollection); - // DeleteQueryDefinition deleteQuery = client.newQueryManager().newDeleteDefinition(); - // deleteQuery.setCollections(temporalCollection); - // client.newQueryManager().delete(deleteQuery); - SearchHandle handle = queryMgr.search(query, new SearchHandle()); - MatchDocumentSummary[] docs = handle.getMatchResults(); - for ( MatchDocumentSummary doc : docs ) { - if ( ! (temporalCollection + ".lsqt").equals(doc.getUri()) ) { - client.newXMLDocumentManager().delete(doc.getUri()); - } - } - } finally { - client.release(); - } - } +class BitemporalTest { + + static String temporalCollection = "temporal-collection"; + static XMLDocumentManager docMgr; + static QueryManager queryMgr; + static String uniqueBulkTerm = "temporalBulkDoc" + new Random().nextInt(10000); + static String uniqueTerm = "temporalDoc" + new Random().nextInt(10000); + static String docId = "test-" + uniqueTerm + ".xml"; + + @BeforeEach + void setup() { + Common.connect(); + docMgr = Common.client.newXMLDocumentManager(); + queryMgr = Common.client.newQueryManager(); + } + + @AfterEach + void teardown() { + try (DatabaseClient client = Common.newServerAdminClient()) { + QueryManager queryMgr = client.newQueryManager(); + queryMgr.setPageLength(1000); + QueryDefinition query = queryMgr.newStringDefinition(); + query.setCollections(temporalCollection); + SearchHandle handle = queryMgr.search(query, new SearchHandle()); + MatchDocumentSummary[] docs = handle.getMatchResults(); + for (MatchDocumentSummary doc : docs) { + if (!(temporalCollection + ".lsqt").equals(doc.getUri())) { + client.newXMLDocumentManager().delete(doc.getUri()); + } + } + } + } + + @Test + void writeTemporalDoc() { + String contents = """ + + + + 2014-08-19T00:00:00Z + 2014-08-19T00:00:01Z + """; + + TemporalDescriptor desc = docMgr.create(docMgr.newDocumentUriTemplate("xml"), + null, new StringHandle(contents), null, null, temporalCollection); + assertNotNull(desc); + assertNotNull(desc.getUri()); + assertTrue(desc.getUri().endsWith(".xml")); + + String lastWriteTimestamp = desc.getTemporalSystemTime(); + Calendar lastWriteTime = DatatypeConverter.parseDateTime(lastWriteTimestamp); + assertNotNull(lastWriteTime); + } + + @Test + void writeTwoVersionsOfFourDocuments() throws XpathException { + String prefix = "test_" + uniqueBulkTerm; + String doc1 = """ + + %s doc1 + + + 2014-08-19T00:00:00Z + 2014-08-19T00:00:01Z + """.formatted(uniqueBulkTerm); + + String doc2 = """ + + %s doc2 + + + 2014-08-19T00:00:02Z + 2014-08-19T00:00:03Z + """.formatted(uniqueBulkTerm); + + String doc3 = """ + + %s doc3 + + + 2014-08-19T00:00:03Z + 2014-08-19T00:00:04Z + """.formatted(uniqueBulkTerm); + + String doc4 = """ + + %s doc4 + + + 2014-08-19T00:00:05Z + 2014-08-19T00:00:06Z + """.formatted(uniqueBulkTerm); + + DocumentWriteSet writeSet = docMgr.newWriteSet(); + writeSet.add(prefix + "_1.xml", new StringHandle(doc1)); + writeSet.add(prefix + "_2.xml", new StringHandle(doc2)); + writeSet.add(prefix + "_3.xml", new StringHandle(doc3)); + writeSet.add(prefix + "_4.xml", new StringHandle(doc4)); + docMgr.write(writeSet, null, null, temporalCollection); + + // do it one more time so we have two versions of each + writeSet = docMgr.newWriteSet(); + writeSet.add(prefix + "_1.xml", new StringHandle(doc1)); + writeSet.add(prefix + "_2.xml", new StringHandle(doc2)); + writeSet.add(prefix + "_3.xml", new StringHandle(doc3)); + writeSet.add(prefix + "_4.xml", new StringHandle(doc4)); + docMgr.write(writeSet, null, null, temporalCollection); + + StringQueryDefinition query = queryMgr.newStringDefinition().withCriteria(uniqueBulkTerm); + try (DocumentPage page = docMgr.search(query, 0)) { + assertEquals(8, page.size()); + for (DocumentRecord record : page) { + Document doc = record.getContentAs(Document.class); + if (record.getUri().startsWith(prefix + "_1")) { + assertXpathEvaluatesTo("2014-08-19T00:00:00Z", "//valid-start", doc); + continue; + } else if (record.getUri().startsWith(prefix + "_2")) { + assertXpathEvaluatesTo("2014-08-19T00:00:02Z", "//valid-start", doc); + continue; + } else if (record.getUri().startsWith(prefix + "_3")) { + assertXpathEvaluatesTo("2014-08-19T00:00:03Z", "//valid-start", doc); + continue; + } else if (record.getUri().startsWith(prefix + "_4")) { + assertXpathEvaluatesTo("2014-08-19T00:00:05Z", "//valid-start", doc); + continue; + } + throw new IllegalStateException("Unexpected doc:[" + record.getUri() + "]"); + } + } + } + + @Test + void lsqtTest() { + // Due to bug MLE-24511 where LSQT properties aren't updated correctly in ml-gradle 6.0.0, we need to manually + // deploy them for this test. + ManageClient manageClient = Common.newManageClient(); + TemporalCollectionLSQTManager mgr = new TemporalCollectionLSQTManager(manageClient, "java-unittest", "temporal-collection"); + String payload = """ + { + "lsqt-enabled": true, + "automation": { + "enabled": true, + "period": 5000 + } + } + """; + mgr.save(payload); + + String version1 = """ + + %s version1 + + + 2014-08-19T00:00:00Z + 2014-08-19T00:00:01Z + """.formatted(uniqueTerm); + + String version2 = """ + + %s version2 + + + 2014-08-19T00:00:02Z + 2014-08-19T00:00:03Z + """.formatted(uniqueTerm); + + String version3 = """ + + %s version3 + + + 2014-08-19T00:00:03Z + 2014-08-19T00:00:04Z + """.formatted(uniqueTerm); + + String version4 = """ + + %s version4 + + + 2014-08-19T00:00:05Z + 2014-08-19T00:00:06Z + """.formatted(uniqueTerm); + + // write four versions of the same document + StringHandle handle1 = new StringHandle(version1).withFormat(Format.XML); + docMgr.write(docId, null, handle1, null, null, temporalCollection); + StringHandle handle2 = new StringHandle(version2).withFormat(Format.XML); + docMgr.write(docId, null, handle2, null, null, temporalCollection); + StringHandle handle3 = new StringHandle(version3).withFormat(Format.XML); + TemporalDescriptor desc = docMgr.write(docId, null, handle3, null, null, temporalCollection); + + assertNotNull(desc); + assertEquals(docId, desc.getUri()); + String thirdWriteTimestamp = desc.getTemporalSystemTime(); + assertNotNull(thirdWriteTimestamp); + + StringHandle handle4 = new StringHandle(version4).withFormat(Format.XML); + docMgr.write(docId, null, handle4, null, null, temporalCollection); + + // make sure non-temporal document read only returns the latest version + try (DocumentPage readResults = docMgr.read(docId)) { + assertEquals(1, readResults.size()); + DocumentRecord latestDoc = readResults.next(); + assertEquals(docId, latestDoc.getUri()); + } + + // make sure a simple term query returns all versions of bulk and other docs + StructuredQueryBuilder sqb = queryMgr.newStructuredQueryBuilder(); + StructuredQueryDefinition termsQuery = + sqb.or(sqb.term(uniqueTerm), sqb.term(uniqueBulkTerm)); + long start = 1; + try (DocumentPage termQueryResults = docMgr.search(termsQuery, start)) { + assertEquals(4, termQueryResults.size()); + } + + StructuredQueryDefinition currentQuery = sqb.temporalLsqtQuery(temporalCollection, thirdWriteTimestamp, 1); + StructuredQueryDefinition currentDocQuery = sqb.and(termsQuery, currentQuery); + + final StructuredQueryDefinition queryThatWillFail = currentDocQuery; + FailedRequestException ex = assertThrows(FailedRequestException.class, () -> docMgr.search(queryThatWillFail, start)); + String message = ex.getMessage(); + assertTrue(message.contains("TEMPORAL"), "The query should fail, but the actual error code " + + "depends on the MarkLogic version. Prior to 12.1, the code was TEMPORAL-GTLSQT. " + + "On the develop branch for 12.1, it's TEMPORAL-NOLSQT. Actual message: " + message); + + try (DatabaseClient client = Common.newServerAdminClient()) { + client.newXMLDocumentManager().advanceLsqt(temporalCollection); + } + + // query again with lsqt of last inserted document + // will match the first three versions -- not the last because it's equal to + // not greater than the timestamp of this lsqt query + try (DocumentPage currentDocQueryResults = docMgr.search(currentDocQuery, start)) { + assertEquals(3, currentDocQueryResults.size()); + } + + // query with blank lsqt indicating current time + // will match all four versions + currentQuery = sqb.temporalLsqtQuery(temporalCollection, "", 1); + currentDocQuery = sqb.and(termsQuery, currentQuery); + try (DocumentPage currentDocQueryResults = docMgr.search(currentDocQuery, start)) { + assertEquals(4, currentDocQueryResults.size()); + } + + StructuredQueryBuilder.Axis validAxis = sqb.axis("valid-axis"); + + // create a time axis to query the versions against + Calendar start1 = DatatypeConverter.parseDateTime("2014-08-19T00:00:00Z"); + Calendar end1 = DatatypeConverter.parseDateTime("2014-08-19T00:00:04Z"); + StructuredQueryBuilder.Period period1 = sqb.period(start1, end1); + + // find all documents contained in the time range of our query axis + StructuredQueryDefinition periodQuery1 = sqb.and(termsQuery, + sqb.temporalPeriodRange(validAxis, TemporalOperator.ALN_CONTAINED_BY, period1)); + try (DocumentPage periodQuery1Results = docMgr.search(periodQuery1, start)) { + assertEquals(1, periodQuery1Results.size()); + } + + // create a second time axis to query the versions against + Calendar start2 = DatatypeConverter.parseDateTime("2014-08-19T00:00:04Z"); + Calendar end2 = DatatypeConverter.parseDateTime("2014-08-19T00:00:07Z"); + StructuredQueryBuilder.Period period2 = sqb.period(start2, end2); + + // find all documents contained in the time range of our second query axis + StructuredQueryDefinition periodQuery2 = sqb.and(termsQuery, + sqb.temporalPeriodRange(validAxis, TemporalOperator.ALN_CONTAINED_BY, period2)); + try (DocumentPage periodQuery2Results = docMgr.search(periodQuery2, start)) { + assertEquals(1, periodQuery2Results.size()); + for (DocumentRecord result : periodQuery2Results) { + if (docId.equals(result.getUri())) { + continue; + } else if (result.getUri().startsWith("test_" + uniqueBulkTerm + "_4")) { + continue; + } + fail("Unexpected uri for ALN_CONTAINED_BY test:" + result.getUri()); + } + } + + // find all documents where valid time is after system time in the document + StructuredQueryBuilder.Axis systemAxis = sqb.axis("system-axis"); + StructuredQueryDefinition periodCompareQuery1 = sqb.and(termsQuery, + sqb.temporalPeriodCompare(systemAxis, TemporalOperator.ALN_AFTER, validAxis)); + try (DocumentPage periodCompareQuery1Results = docMgr.search(periodCompareQuery1, start)) { + assertEquals(4, periodCompareQuery1Results.size()); + } + + // find all documents where valid time is before system time in the document + StructuredQueryDefinition periodCompareQuery2 = sqb.and(termsQuery, + sqb.temporalPeriodCompare(systemAxis, TemporalOperator.ALN_BEFORE, validAxis)); + try (DocumentPage periodCompareQuery2Results = docMgr.search(periodCompareQuery2, start)) { + assertEquals(0, periodCompareQuery2Results.size()); + } + + // check that we get a system time when we delete + desc = docMgr.delete(docId, null, temporalCollection); + assertNotNull(desc); + assertEquals(docId, desc.getUri()); + assertNotNull(desc.getTemporalSystemTime()); + } } diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java index 9b11eee9d..03c795a84 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java @@ -77,7 +77,6 @@ public X509Certificate[] getAcceptedIssuers() { public static DatabaseClient client; public static DatabaseClient restAdminClient; - public static DatabaseClient serverAdminClient; public static DatabaseClient evalClient; public static DatabaseClient readOnlyClient; @@ -93,12 +92,6 @@ public static DatabaseClient connectRestAdmin() { return restAdminClient; } - public static DatabaseClient connectServerAdmin() { - if (serverAdminClient == null) - serverAdminClient = newServerAdminClient(); - return serverAdminClient; - } - public static DatabaseClient connectEval() { if (evalClient == null) evalClient = newEvalClient(); @@ -274,9 +267,11 @@ public static ObjectNode newServerPayload() { } public static void deleteUrisWithPattern(String pattern) { - Common.connectServerAdmin().newServerEval() - .xquery(String.format("cts:uri-match('%s') ! xdmp:document-delete(.)", pattern)) - .evalAs(String.class); + try (DatabaseClient client = Common.newServerAdminClient()) { + client.newServerEval() + .xquery(String.format("cts:uri-match('%s') ! xdmp:document-delete(.)", pattern)) + .evalAs(String.class); + } } /** diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromSearchWithOptionsTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromSearchWithOptionsTest.java index aa07928b9..acf3651e3 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromSearchWithOptionsTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromSearchWithOptionsTest.java @@ -8,7 +8,6 @@ import com.marklogic.client.row.RowRecord; import com.marklogic.client.test.junit5.RequiresML12; import com.marklogic.client.type.PlanSearchOptions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,15 +44,14 @@ void badBm25LengthWeight() { } @Test - @Disabled("zero and random aren't in the 12 EA release.") void zero() { -// rowManager.withUpdate(false); -// PlanSearchOptions options = op.searchOptions().withScoreMethod(PlanSearchOptions.ScoreMethod.ZERO); -// List rows = resultRows(op.fromSearch(op.cts.wordQuery("saxophone"), null, null, options)); -// assertEquals(2, rows.size()); -// rows.forEach(row -> { -// assertEquals(0, row.getInt("score"), "The score for every row should be 0."); -// }); + rowManager.withUpdate(false); + PlanSearchOptions options = op.searchOptions().withScoreMethod(PlanSearchOptions.ScoreMethod.ZERO); + List rows = resultRows(op.fromSearch(op.cts.wordQuery("saxophone"), null, null, options)); + assertEquals(2, rows.size()); + rows.forEach(row -> { + assertEquals(0, row.getInt("score"), "The score for every row should be 0."); + }); } @Test diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java index 44915e845..4118b5d7a 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/ssl/OneWaySSLTest.java @@ -20,8 +20,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import javax.net.ssl.SSLContext; @@ -162,9 +160,6 @@ void tLS13ClientWithTLS12Server() { } @ExtendWith(RequiresML12.class) - // The TLSv1.3 tests are failing on Java 8, because TLSv1.3 is disabled with our version of Java 8. - // There may be a way to configure Java 8 to use TLSv1.3, but it is not currently working. - @DisabledOnJre(JRE.JAVA_8) @Test void tLS13ClientWithTLS13Server() { setAppServerMinimumTLSVersion("TLSv1.3"); @@ -177,7 +172,6 @@ void tLS13ClientWithTLS13Server() { } @ExtendWith(RequiresML12.class) - @DisabledOnJre(JRE.JAVA_8) @Test void tLS12ClientWithTLS13ServerShouldFail() { setAppServerMinimumTLSVersion("TLSv1.3"); From 881a550341f0bd9e2eeea493a93c12998b5bc247 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 6 Oct 2025 11:54:07 -0400 Subject: [PATCH 20/33] MLE-24505 Fixing publishing for the dev tools plugin --- ml-development-tools/build.gradle | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ml-development-tools/build.gradle b/ml-development-tools/build.gradle index 5eeead94a..ecbca0d67 100644 --- a/ml-development-tools/build.gradle +++ b/ml-development-tools/build.gradle @@ -1,9 +1,9 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - /* * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { id "groovy" id 'maven-publish' @@ -18,7 +18,11 @@ dependencies { // This is a runtime dependency of marklogic-client-api but is needed for compiling. compileOnly "jakarta.xml.bind:jakarta.xml.bind-api:4.0.4" - implementation project(':marklogic-client-api') + // Gradle 9 does not like for a plugin to have a project dependency; trying to publish it results in a + // NoSuchMethodError pertaining to getProjectDependency. So treating this as a 3rd party dependency. This creates + // additional work during development, though we rarely modify the code in this plugin anymore. + implementation "com.marklogic:marklogic-client-api:${version}" + implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.1.0' implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}" implementation 'com.networknt:json-schema-validator:1.0.88' @@ -58,11 +62,6 @@ gradlePlugin { } publishing { - publications { - mainJava(MavenPublication) { - from components.java - } - } repositories { maven { if (project.hasProperty("mavenUser")) { From ccfd3e735b36e9166b039735d77a956f7bddda19 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 6 Oct 2025 12:21:17 -0400 Subject: [PATCH 21/33] MLE-24505 Need internal Artifactory for building now --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 5b4a9c41e..6ad238fe4 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,11 @@ subprojects { repositories { mavenLocal() mavenCentral() + + // Needed so that ml-development-tools can resolve snapshots of marklogic-client-api. + maven { + url = "https://bed-artifactory.bedford.progress.com:443/artifactory/ml-maven-snapshots/" + } } // Allows for identifying compiler warnings and treating them as errors. From c9817ea0ef08cd5b70a76ed0a4afd3398a30a969 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 6 Oct 2025 12:54:47 -0400 Subject: [PATCH 22/33] MLE-23230 Better name for retry interceptor And added comments to explain what the existing app-level retry support is doing vs what this for-now-undocumented interceptor will be doing. --- .../functionaltest/ConnectedRESTQA.java | 4 +- .../marklogic/client/impl/OkHttpServices.java | 21 ++++- .../client/impl/StreamingOutputImpl.java | 82 ++++++++++--------- ....java => RetryIOExceptionInterceptor.java} | 21 +++-- .../impl/okhttp/RetryableRequestBody.java | 17 ++++ .../com/marklogic/client/test/Common.java | 5 +- .../test/datamovement/RowBatcherTest.java | 2 + 7 files changed, 99 insertions(+), 53 deletions(-) rename marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/{RetryInterceptor.java => RetryIOExceptionInterceptor.java} (69%) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryableRequestBody.java diff --git a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java index 52f1d743f..1020c7bd7 100644 --- a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java +++ b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java @@ -17,7 +17,7 @@ import com.marklogic.client.admin.ServerConfigurationManager; import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; import com.marklogic.client.impl.SSLUtil; -import com.marklogic.client.impl.okhttp.RetryInterceptor; +import com.marklogic.client.impl.okhttp.RetryIOExceptionInterceptor; import com.marklogic.client.io.DocumentMetadataHandle; import com.marklogic.client.io.DocumentMetadataHandle.Capability; import com.marklogic.client.query.QueryManager; @@ -50,7 +50,7 @@ public abstract class ConnectedRESTQA { static { DatabaseClientFactory.removeConfigurators(); DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) client -> - client.addInterceptor(new RetryInterceptor(3, 1000, 2, 8000))); + client.addInterceptor(new RetryIOExceptionInterceptor(3, 1000, 2, 8000))); } private static Properties testProperties = null; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java index 4c8ecf904..cd7bf9e6f 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java @@ -22,6 +22,7 @@ import com.marklogic.client.impl.okhttp.HttpUrlBuilder; import com.marklogic.client.impl.okhttp.OkHttpUtil; import com.marklogic.client.impl.okhttp.PartIterator; +import com.marklogic.client.impl.okhttp.RetryableRequestBody; import com.marklogic.client.io.*; import com.marklogic.client.io.marker.*; import com.marklogic.client.query.*; @@ -99,15 +100,19 @@ public class OkHttpServices implements RESTServices { private boolean released = false; + /** + * The next 4 fields implement an application-level retry that only works for certain HTTP status codes. It will not + * attempt a retry on any IOException or any type of connection failure. Sadly, the logic that uses these fields is + * in several places and is slightly different in each place. It's also not possible to implement this logic in an + * OkHttp interceptor as the logic needs access to details that are not available to an interceptor. + */ private final Random randRetry = new Random(); - private int maxDelay = DEFAULT_MAX_DELAY; private int minRetry = DEFAULT_MIN_RETRY; + private final Set retryStatus = new HashSet<>(); private boolean checkFirstRequest = true; - private final Set retryStatus = new HashSet<>(); - static protected class ThreadState { boolean isFirstRequest; @@ -5408,7 +5413,8 @@ static private List getPartList(MimeMultipart multipart) { } } - static private class ObjectRequestBody extends RequestBody { + static private class ObjectRequestBody extends RequestBody implements RetryableRequestBody { + private Object obj; private MediaType contentType; @@ -5442,6 +5448,13 @@ public void writeTo(BufferedSink sink) throws IOException { throw new IllegalStateException("Cannot write object of type: " + obj.getClass()); } } + + @Override + public boolean isRetryable() { + // Added in 8.0.0 to work with the retry interceptor so it knows whether the body can be retried or not. + // InputStreams cannot be retried as they are consumed on first read. + return !(obj instanceof InputStream); + } } // API First Changes diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/StreamingOutputImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/StreamingOutputImpl.java index 60fcbbdf9..5fd30e6d0 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/StreamingOutputImpl.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/StreamingOutputImpl.java @@ -3,46 +3,54 @@ */ package com.marklogic.client.impl; -import java.io.IOException; -import java.io.OutputStream; - -import com.marklogic.client.util.RequestLogger; +import com.marklogic.client.impl.okhttp.RetryableRequestBody; import com.marklogic.client.io.OutputStreamSender; +import com.marklogic.client.util.RequestLogger; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; -class StreamingOutputImpl extends RequestBody { - private OutputStreamSender handle; - private RequestLogger logger; - private MediaType contentType; - - StreamingOutputImpl(OutputStreamSender handle, RequestLogger logger, MediaType contentType) { - super(); - this.handle = handle; - this.logger = logger; - this.contentType = contentType; - } - - @Override - public MediaType contentType() { - return contentType; - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - OutputStream out = sink.outputStream(); - - if (logger != null) { - OutputStream tee = logger.getPrintStream(); - long max = logger.getContentMax(); - if (tee != null && max > 0) { - handle.write(new OutputStreamTee(out, tee, max)); - - return; - } - } - - handle.write(out); - } +import java.io.IOException; +import java.io.OutputStream; + +class StreamingOutputImpl extends RequestBody implements RetryableRequestBody { + + private OutputStreamSender handle; + private RequestLogger logger; + private MediaType contentType; + + StreamingOutputImpl(OutputStreamSender handle, RequestLogger logger, MediaType contentType) { + super(); + this.handle = handle; + this.logger = logger; + this.contentType = contentType; + } + + @Override + public MediaType contentType() { + return contentType; + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + OutputStream out = sink.outputStream(); + + if (logger != null) { + OutputStream tee = logger.getPrintStream(); + long max = logger.getContentMax(); + if (tee != null && max > 0) { + handle.write(new OutputStreamTee(out, tee, max)); + + return; + } + } + + handle.write(out); + } + + @Override + public boolean isRetryable() { + // Added in 8.0.0; streaming output cannot be retried as the stream is consumed on first write. + return false; + } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryIOExceptionInterceptor.java similarity index 69% rename from marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java rename to marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryIOExceptionInterceptor.java index dd6879a53..656e399c5 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryInterceptor.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryIOExceptionInterceptor.java @@ -14,19 +14,22 @@ import java.net.UnknownHostException; /** - * OkHttp interceptor that retries requests on certain connection failures, - * which can be helpful when MarkLogic is temporarily unavailable during restarts. + * Experimental interceptor added in 8.0.0 for retrying requests that fail due to connection issues. These issues are + * not handled by the application-level retry support in OkHttpServices, which only handles retries based on certain + * HTTP status codes. The main limitation of this approach is that it cannot retry a request that has a one-shot body, + * such as a streaming body. But for requests that don't have one-shot bodies, this interceptor can be helpful for + * retrying requests that fail due to temporary network issues or MarkLogic restarts. */ -public class RetryInterceptor implements Interceptor { +public class RetryIOExceptionInterceptor implements Interceptor { - private final static Logger logger = org.slf4j.LoggerFactory.getLogger(RetryInterceptor.class); + private final static Logger logger = org.slf4j.LoggerFactory.getLogger(RetryIOExceptionInterceptor.class); private final int maxRetries; private final long initialDelayMs; private final double backoffMultiplier; private final long maxDelayMs; - public RetryInterceptor(int maxRetries, long initialDelayMs, double backoffMultiplier, long maxDelayMs) { + public RetryIOExceptionInterceptor(int maxRetries, long initialDelayMs, double backoffMultiplier, long maxDelayMs) { this.maxRetries = maxRetries; this.initialDelayMs = initialDelayMs; this.backoffMultiplier = backoffMultiplier; @@ -37,11 +40,15 @@ public RetryInterceptor(int maxRetries, long initialDelayMs, double backoffMulti public Response intercept(Chain chain) throws IOException { Request request = chain.request(); + if (request.body() instanceof RetryableRequestBody body && !body.isRetryable()) { + return chain.proceed(request); + } + for (int attempt = 0; attempt <= maxRetries; attempt++) { try { return chain.proceed(request); } catch (IOException e) { - if (attempt == maxRetries || !isRetryableException(e)) { + if (attempt == maxRetries || !isRetryableIOException(e)) { logger.warn("Not retryable: {}; {}", e.getClass(), e.getMessage()); throw e; } @@ -58,7 +65,7 @@ public Response intercept(Chain chain) throws IOException { throw new IllegalStateException("Unexpected end of retry loop"); } - private boolean isRetryableException(IOException e) { + private boolean isRetryableIOException(IOException e) { return e instanceof ConnectException || e instanceof SocketTimeoutException || e instanceof UnknownHostException || diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryableRequestBody.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryableRequestBody.java new file mode 100644 index 000000000..ad35a07c3 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/RetryableRequestBody.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.impl.okhttp; + +/** + * Interface for RequestBody implementations to signal whether they can be retried after an IOException. + * This is used by RetryIOExceptionInterceptor to determine if a failed request can be retried. + * Added in 8.0.0. + */ +public interface RetryableRequestBody { + /** + * @return false if this request body cannot be retried (e.g., because it consumes a stream that can only be + * read once); true if it can be safely retried. + */ + boolean isRetryable(); +} diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java index 03c795a84..2437bf255 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java @@ -9,11 +9,10 @@ import com.marklogic.client.DatabaseClientBuilder; import com.marklogic.client.DatabaseClientFactory; import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; -import com.marklogic.client.impl.okhttp.RetryInterceptor; +import com.marklogic.client.impl.okhttp.RetryIOExceptionInterceptor; import com.marklogic.client.io.DocumentMetadataHandle; import com.marklogic.mgmt.ManageClient; import com.marklogic.mgmt.ManageConfig; -import okhttp3.OkHttpClient; import org.springframework.util.FileCopyUtils; import org.w3c.dom.DOMException; import org.w3c.dom.Document; @@ -35,7 +34,7 @@ public class Common { static { DatabaseClientFactory.removeConfigurators(); DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) client -> - client.addInterceptor(new RetryInterceptor(3, 1000, 2, 8000))); + client.addInterceptor(new RetryIOExceptionInterceptor(3, 1000, 2, 8000))); } final public static String USER = "rest-writer"; diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java index 91e4cc293..8ae2fd3e2 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java @@ -24,6 +24,7 @@ import com.marklogic.client.type.PlanSystemColumn; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -190,6 +191,7 @@ public void testJsonDocs1Thread() throws Exception { } @Test + @Disabled("A query returning no rows is now throwing an IOException on 12 nightly, so disabling temporarily.") void noRowsReturned() { RowBatcher rowBatcher = jsonBatcher(1); RowManager rowMgr = rowBatcher.getRowManager(); From 1fe9b821904e3621997d2ea06f2d61cb0d0e40fc Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 6 Oct 2025 14:07:08 -0400 Subject: [PATCH 23/33] MLE-24505 Doing full build on PR pipeline Verifying that all code can be compiled, even when running a subset of the tests. --- Jenkinsfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index d1f98da47..52fe460ea 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -38,6 +38,8 @@ def runTests(String image) { export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api + // Ensure all modules can be built first. + ./gradlew clean build -x test mkdir -p marklogic-client-api/build/test-results/test ./gradlew marklogic-client-api:test || true ''' @@ -172,6 +174,8 @@ pipeline{ export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api + // Ensure all modules can be built first. + ./gradlew clean build -x test ./gradlew cleanTest marklogic-client-api:test ''' // Omitting this until MLE-24523 can be addressed From 541b720b3b54751f12b15046b7e79e7fc3462305 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 6 Oct 2025 15:56:18 -0400 Subject: [PATCH 24/33] MLE-24579 Updated disabled test comment --- .../marklogic/client/test/datamovement/RowBatcherTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java index 8ae2fd3e2..5caa9ba2a 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java @@ -40,7 +40,8 @@ import static org.junit.jupiter.api.Assertions.*; -public class RowBatcherTest { +class RowBatcherTest { + private final static String TEST_DIR = "/test/rowbatch/unit/"; private final static String TEST_COLLECTION = TEST_DIR+"codes"; private final static String TABLE_NS_URI = "http://marklogic.com/table"; @@ -191,7 +192,8 @@ public void testJsonDocs1Thread() throws Exception { } @Test - @Disabled("A query returning no rows is now throwing an IOException on 12 nightly, so disabling temporarily.") + @Disabled("Disabled due to https://progresssoftware.atlassian.net/browse/MLE-24579 , which causes the server to restart, " + + "which can cause many other tests to fail.") void noRowsReturned() { RowBatcher rowBatcher = jsonBatcher(1); RowManager rowMgr = rowBatcher.getRowManager(); From 2760447d987232160de48f8768f2a81df17417d6 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 7 Oct 2025 09:03:07 -0400 Subject: [PATCH 25/33] MLE-24579 Fixed PR test stage Also formatted the file so it's pretty. Only functional change though is on line 181, which ensures that the stage still completes even if tests fail. --- Jenkinsfile | 126 ++++++++++++++++++++++++++-------------------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 52fe460ea..76014c05c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ @Library('shared-libraries') _ -def getJava(){ +def getJava() { if (env.JAVA_VERSION == "JAVA21") { return "/home/builder/java/jdk-21.0.1" } else { @@ -8,18 +8,18 @@ def getJava(){ } } -def setupDockerMarkLogic(String image){ +def setupDockerMarkLogic(String image) { cleanupDocker() - sh label:'mlsetup', script: '''#!/bin/bash + sh label: 'mlsetup', script: '''#!/bin/bash echo "Removing any running MarkLogic server and clean up MarkLogic data directory" sudo /usr/local/sbin/mladmin remove sudo /usr/local/sbin/mladmin cleandata cd java-client-api docker compose down -v || true docker volume prune -f - echo "Using image: "'''+image+''' - docker pull '''+image+''' - MARKLOGIC_IMAGE='''+image+''' MARKLOGIC_LOGS_VOLUME=marklogicLogs docker compose up -d --build + echo "Using image: "''' + image + ''' + docker pull ''' + image + ''' + MARKLOGIC_IMAGE=''' + image + ''' MARKLOGIC_LOGS_VOLUME=marklogicLogs docker compose up -d --build echo "Waiting for MarkLogic server to initialize." sleep 60s export JAVA_HOME=$JAVA_HOME_DIR @@ -33,7 +33,7 @@ def setupDockerMarkLogic(String image){ def runTests(String image) { setupDockerMarkLogic(image) - sh label:'run marklogic-client-api tests', script: '''#!/bin/bash + sh label: 'run marklogic-client-api tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -44,7 +44,7 @@ def runTests(String image) { ./gradlew marklogic-client-api:test || true ''' - sh label:'run ml-development-tools tests', script: '''#!/bin/bash + sh label: 'run ml-development-tools tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -53,7 +53,7 @@ def runTests(String image) { ./gradlew ml-development-tools:test || true ''' - sh label:'run fragile functional tests', script: '''#!/bin/bash + sh label: 'run fragile functional tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -62,7 +62,7 @@ def runTests(String image) { ./gradlew marklogic-client-api-functionaltests:runFragileTests || true ''' - sh label:'run fast functional tests', script: '''#!/bin/bash + sh label: 'run fast functional tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -70,7 +70,7 @@ def runTests(String image) { ./gradlew marklogic-client-api-functionaltests:runFastFunctionalTests || true ''' - sh label:'run slow functional tests', script: '''#!/bin/bash + sh label: 'run slow functional tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -84,7 +84,7 @@ def runTests(String image) { def runTestsWithReverseProxy(String image) { setupDockerMarkLogic(image) - sh label:'run fragile functional tests with reverse proxy', script: '''#!/bin/bash + sh label: 'run fragile functional tests with reverse proxy', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -92,7 +92,7 @@ def runTestsWithReverseProxy(String image) { ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFragileTests || true ''' - sh label:'run fast functional tests with reverse proxy', script: '''#!/bin/bash + sh label: 'run fast functional tests with reverse proxy', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -100,7 +100,7 @@ def runTestsWithReverseProxy(String image) { ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFastFunctionalTests || true ''' - sh label:'run slow functional tests with reverse proxy', script: '''#!/bin/bash + sh label: 'run slow functional tests with reverse proxy', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -112,7 +112,7 @@ def runTestsWithReverseProxy(String image) { } def postProcessTestResults() { - sh label:'post-test-process', script: ''' + sh label: 'post-test-process', script: ''' cd java-client-api mkdir -p marklogic-client-api-functionaltests/build/test-results/runFragileTests mkdir -p marklogic-client-api-functionaltests/build/test-results/runFastFunctionalTests @@ -131,7 +131,7 @@ def postProcessTestResults() { } def tearDownDocker() { - sh label:'tearDownDocker', script: '''#!/bin/bash + sh label: 'tearDownDocker', script: '''#!/bin/bash cd java-client-api docker compose down -v || true docker volume prune -f @@ -139,65 +139,67 @@ def tearDownDocker() { cleanupDocker() } -pipeline{ - agent {label 'javaClientLinuxPool'} +pipeline { + agent { label 'javaClientLinuxPool' } - options { - checkoutToSubdirectory 'java-client-api' - buildDiscarder logRotator(artifactDaysToKeepStr: '7', artifactNumToKeepStr: '', daysToKeepStr: '7', numToKeepStr: '10') - } + options { + checkoutToSubdirectory 'java-client-api' + buildDiscarder logRotator(artifactDaysToKeepStr: '7', artifactNumToKeepStr: '', daysToKeepStr: '7', numToKeepStr: '10') + } - parameters { - booleanParam(name: 'regressions', defaultValue: false, description: 'indicator if build is for regressions') - string(name: 'Email', defaultValue: '' ,description: 'Who should I say send the email to?') - string(name: 'JAVA_VERSION', defaultValue: 'JAVA8' ,description: 'Who should I say send the email to?') - } + parameters { + booleanParam(name: 'regressions', defaultValue: false, description: 'indicator if build is for regressions') + string(name: 'Email', defaultValue: '', description: 'Who should I say send the email to?') + string(name: 'JAVA_VERSION', defaultValue: 'JAVA8', description: 'Who should I say send the email to?') + } - environment { - JAVA_HOME_DIR= getJava() - GRADLE_DIR =".gradle" - DMC_USER = credentials('MLBUILD_USER') - DMC_PASSWORD = credentials('MLBUILD_PASSWORD') - } + environment { + JAVA_HOME_DIR = getJava() + GRADLE_DIR = ".gradle" + DMC_USER = credentials('MLBUILD_USER') + DMC_PASSWORD = credentials('MLBUILD_PASSWORD') + } - stages { - stage('pull-request-tests') { - when { - not { - expression {return params.regressions} - } - } - steps { - setupDockerMarkLogic("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:11.3.2-ubi-rootless-2.2.2") - sh label:'run marklogic-client-api tests', script: '''#!/bin/bash + stages { + stage('pull-request-tests') { + when { + not { + expression { return params.regressions } + } + } + steps { + setupDockerMarkLogic("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:11.3.2-ubi-rootless-2.2.2") + sh label: 'run marklogic-client-api tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api // Ensure all modules can be built first. ./gradlew clean build -x test - ./gradlew cleanTest marklogic-client-api:test + + // Run a sufficient number of tests to verify the PR. + ./gradlew cleanTest marklogic-client-api:test || true ''' // Omitting this until MLE-24523 can be addressed // ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFastFunctionalTests || true - junit '**/build/**/TEST*.xml' - } + junit '**/build/**/TEST*.xml' + } post { always { updateWorkspacePermissions() tearDownDocker() } } - } - stage('publish'){ - when { - branch 'develop' - not { - expression {return params.regressions} - } - } - steps{ - sh label:'publish', script: '''#!/bin/bash + } + stage('publish') { + when { + branch 'develop' + not { + expression { return params.regressions } + } + } + steps { + sh label: 'publish', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH @@ -205,14 +207,14 @@ pipeline{ cd java-client-api ./gradlew publish ''' - } - } + } + } stage('regressions-11') { when { allOf { branch 'develop' - expression {return params.regressions} + expression { return params.regressions } } } steps { @@ -251,7 +253,7 @@ pipeline{ when { allOf { branch 'develop' - expression {return params.regressions} + expression { return params.regressions } } } steps { @@ -270,7 +272,7 @@ pipeline{ when { allOf { branch 'develop' - expression {return params.regressions} + expression { return params.regressions } } } steps { @@ -285,5 +287,5 @@ pipeline{ } } - } + } } From 1ca0f9cff33b3b65f2358df8a5d0b8bfbc843e45 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 7 Oct 2025 11:55:28 -0400 Subject: [PATCH 26/33] MLE-24523 Getting reverse proxy tests running again Lots of little improvements to Jenkinsfile too. --- .copyrightconfig | 2 +- Jenkinsfile | 98 +++++++++++-------- docker-compose.yaml | 5 +- .../TestDatabaseClientConnection.java | 5 +- .../functionaltest/BulkIOCallersFnTest.java | 2 +- test-app/README.md | 5 + 6 files changed, 66 insertions(+), 51 deletions(-) diff --git a/.copyrightconfig b/.copyrightconfig index cf6e131ee..ba242e11f 100644 --- a/.copyrightconfig +++ b/.copyrightconfig @@ -11,4 +11,4 @@ startyear: 2010 # - Dotfiles already skipped automatically # Enable by removing the leading '# ' from the next line and editing values. # filesexcluded: third_party/*, docs/generated/*.md, assets/*.png, scripts/temp_*.py, vendor/lib.js -filesexcluded: .github/*, README.md, Jenkinsfile, gradle/*, docker-compose.yml, *.gradle, gradle.properties, gradlew, gradlew.bat, **/test/resources/**, *.md, pom.xml +filesexcluded: .github/*, README.md, Jenkinsfile, gradle/*, docker-compose.yaml, docker-compose.yml, *.gradle, gradle.properties, gradlew, gradlew.bat, **/test/resources/**, *.md, pom.xml diff --git a/Jenkinsfile b/Jenkinsfile index 76014c05c..29365e947 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ @Library('shared-libraries') _ -def getJava() { +def getJavaHomePath() { if (env.JAVA_VERSION == "JAVA21") { return "/home/builder/java/jdk-21.0.1" } else { @@ -38,9 +38,10 @@ def runTests(String image) { export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api - // Ensure all modules can be built first. + + echo "Ensure all subprojects can be built first." ./gradlew clean build -x test - mkdir -p marklogic-client-api/build/test-results/test + ./gradlew marklogic-client-api:test || true ''' @@ -49,7 +50,6 @@ def runTests(String image) { export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api - mkdir -p ml-development-tools/build/test-results/test ./gradlew ml-development-tools:test || true ''' @@ -84,12 +84,25 @@ def runTests(String image) { def runTestsWithReverseProxy(String image) { setupDockerMarkLogic(image) + sh label: 'run marklogic-client-api tests with reverse proxy', script: '''#!/bin/bash + export JAVA_HOME=$JAVA_HOME_DIR + export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR + export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH + cd java-client-api + + echo "Ensure all subprojects can be built first." + ./gradlew clean build -x test + + echo "Running marklogic-client-api tests with reverse proxy." + ./gradlew -PtestUseReverseProxyServer=true runReverseProxyServer marklogic-client-api:test || true + ''' + sh label: 'run fragile functional tests with reverse proxy', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api - ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFragileTests || true + ./gradlew -PtestUseReverseProxyServer=true runReverseProxyServer marklogic-client-api-functionaltests:runFragileTests || true ''' sh label: 'run fast functional tests with reverse proxy', script: '''#!/bin/bash @@ -97,7 +110,7 @@ def runTestsWithReverseProxy(String image) { export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api - ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFastFunctionalTests || true + ./gradlew -PtestUseReverseProxyServer=true runReverseProxyServer marklogic-client-api-functionaltests:runFastFunctionalTests || true ''' sh label: 'run slow functional tests with reverse proxy', script: '''#!/bin/bash @@ -105,7 +118,7 @@ def runTestsWithReverseProxy(String image) { export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api - ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runSlowFunctionalTests || true + ./gradlew -PtestUseReverseProxyServer=true runReverseProxyServer marklogic-client-api-functionaltests:runSlowFunctionalTests || true ''' postProcessTestResults() @@ -149,18 +162,18 @@ pipeline { parameters { booleanParam(name: 'regressions', defaultValue: false, description: 'indicator if build is for regressions') - string(name: 'Email', defaultValue: '', description: 'Who should I say send the email to?') - string(name: 'JAVA_VERSION', defaultValue: 'JAVA8', description: 'Who should I say send the email to?') + string(name: 'JAVA_VERSION', defaultValue: 'JAVA17', description: 'Either JAVA17 or JAVA21') } environment { - JAVA_HOME_DIR = getJava() + JAVA_HOME_DIR = getJavaHomePath() GRADLE_DIR = ".gradle" DMC_USER = credentials('MLBUILD_USER') DMC_PASSWORD = credentials('MLBUILD_PASSWORD') } stages { + stage('pull-request-tests') { when { not { @@ -168,24 +181,26 @@ pipeline { } } steps { - setupDockerMarkLogic("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:11.3.2-ubi-rootless-2.2.2") + setupDockerMarkLogic("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12") sh label: 'run marklogic-client-api tests', script: '''#!/bin/bash export JAVA_HOME=$JAVA_HOME_DIR export GRADLE_USER_HOME=$WORKSPACE/$GRADLE_DIR export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api - // Ensure all modules can be built first. + + echo "Ensure all subprojects can be built first." ./gradlew clean build -x test - // Run a sufficient number of tests to verify the PR. - ./gradlew cleanTest marklogic-client-api:test || true + echo "Run a sufficient number of tests to verify the PR." + ./gradlew marklogic-client-api:test --tests ReadDocumentPageTest || true + + echo "Run a test with the reverse proxy server to ensure it's fine." + ./gradlew -PtestUseReverseProxyServer=true runReverseProxyServer marklogic-client-api-functionaltests:test --tests SearchWithPageLengthTest || true ''' - // Omitting this until MLE-24523 can be addressed - // ./gradlew -PtestUseReverseProxyServer=true test-app:runReverseProxyServer marklogic-client-api-functionaltests:runFastFunctionalTests || true - junit '**/build/**/TEST*.xml' } post { always { + junit '**/build/**/TEST*.xml' updateWorkspacePermissions() tearDownDocker() } @@ -218,36 +233,35 @@ pipeline { } } steps { - runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:11.3.2-ubi-rootless-2.2.2") - junit '**/build/**/TEST*.xml' + runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-11") } post { always { + junit '**/build/**/TEST*.xml' updateWorkspacePermissions() tearDownDocker() } } } - // Omitting this until MLE-24523 can be addressed -// stage('regressions-11-reverseProxy') { -// when { -// allOf { -// branch 'develop' -// expression {return params.regressions} -// } -// } -// steps { -// runTestsWithReverseProxy("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-11") -// junit '**/build/**/TEST*.xml' -// } -// post { -// always { -// updateWorkspacePermissions() -// tearDownDocker() -// } -// } -// } + stage('regressions-12-reverseProxy') { + when { + allOf { + branch 'develop' + expression {return params.regressions} + } + } + steps { + runTestsWithReverseProxy("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12") + } + post { + always { + junit '**/build/**/TEST*.xml' + updateWorkspacePermissions() + tearDownDocker() + } + } + } stage('regressions-12') { when { @@ -257,18 +271,18 @@ pipeline { } } steps { - runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:12.0.0-ubi-rootless-2.2.0") - junit '**/build/**/TEST*.xml' + runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12") } post { always { + junit '**/build/**/TEST*.xml' updateWorkspacePermissions() tearDownDocker() } } } - stage('regressions-10.0') { + stage('regressions-10') { when { allOf { branch 'develop' @@ -277,10 +291,10 @@ pipeline { } steps { runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-10") - junit '**/build/**/TEST*.xml' } post { always { + junit '**/build/**/TEST*.xml' updateWorkspacePermissions() tearDownDocker() } diff --git a/docker-compose.yaml b/docker-compose.yaml index 569f62b3f..9d1dab27e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,10 +18,7 @@ services: - ${MARKLOGIC_LOGS_VOLUME}:/var/opt/MarkLogic/Logs ports: - "8000-8002:8000-8002" - - "8010-8014:8010-8014" - - "8022:8022" - - "8054-8059:8054-8059" - - "8093:8093" + - "8010-8015:8010-8015" # Range of ports used by app servers, at least one of which - 8015 - is created by a test. volumes: marklogicLogs: diff --git a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/TestDatabaseClientConnection.java b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/TestDatabaseClientConnection.java index 2f1183a87..22141d798 100644 --- a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/TestDatabaseClientConnection.java +++ b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/TestDatabaseClientConnection.java @@ -124,11 +124,10 @@ void invalidPort() { DatabaseClient client = newDatabaseClientBuilder().withPort(assumedInvalidPort).build(); MarkLogicIOException ex = Assertions.assertThrows(MarkLogicIOException.class, () -> client.checkConnection()); - String expected = "Error occurred while calling http://localhost:60123/v1/ping; java.net.ConnectException: " + - "Failed to connect to localhost/127.0.0.1:60123 ; possible reasons for the error include " + + String expected = "Failed to connect to localhost/127.0.0.1:60123 ; possible reasons for the error include " + "that a MarkLogic app server may not be listening on the port, or MarkLogic was stopped " + "or restarted during the request; check the MarkLogic server logs for more information."; - assertEquals(expected, ex.getMessage()); + assertTrue(ex.getMessage().contains(expected), "Unexpected error: " + ex.getMessage()); } @Test diff --git a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/BulkIOCallersFnTest.java b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/BulkIOCallersFnTest.java index 14bc2d6c2..0c8871c6c 100644 --- a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/BulkIOCallersFnTest.java +++ b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/BulkIOCallersFnTest.java @@ -42,7 +42,7 @@ public class BulkIOCallersFnTest extends BasicJavaClientREST { private static String host = null; private static int modulesPort = 8000; - private static int restTestport = 8093; + private static int restTestport = 8015; private static String restServerName = "TestDynamicIngest"; private static SecurityContext secContext = null; diff --git a/test-app/README.md b/test-app/README.md index f397f7b9f..6db548bb7 100644 --- a/test-app/README.md +++ b/test-app/README.md @@ -34,3 +34,8 @@ You can also specify custom mappings via the Gradle task. For example, if you ha port 8123 and you want to associate a path of "/my/custom/server" to it, you can do: ./gradlew runBlock -PrpsCustomMappings=/my/custom/server,8123 + +To run one or more tests with the reverse proxy server being started, the tests being run, and then the server being +stopped, do the following (you can see examples of this in the project `Jenkinsfile` as well): + + ./gradlew -PtestUseReverseProxyServer=true runReverseProxyServer marklogic-client-api:test --tests ReadDocumentPageTest From 26d12a885487f799229c43da22fde35e46ad7e2e Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Wed, 8 Oct 2025 15:12:43 -0400 Subject: [PATCH 27/33] MLE-24523 Hopefully fixing okhttp compile error I had this happen locally today, and clearing okhttp from my local Maven cache fixed the problem. No idea what's going wrong here, but it happened in the regression jobs. --- Jenkinsfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 29365e947..80dffa13a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -39,6 +39,10 @@ def runTests(String image) { export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api + echo "Temporary fix for mysterious issue with okhttp3 being corrupted in local Maven cache." + ls -la ~/.m2/repository/com/squareup + rm -rf ~/.m2/repository/com/squareup/okhttp3/ + echo "Ensure all subprojects can be built first." ./gradlew clean build -x test @@ -90,6 +94,10 @@ def runTestsWithReverseProxy(String image) { export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api + echo "Temporary fix for mysterious issue with okhttp3 being corrupted in local Maven cache." + ls -la ~/.m2/repository/com/squareup + rm -rf ~/.m2/repository/com/squareup/okhttp3/ + echo "Ensure all subprojects can be built first." ./gradlew clean build -x test @@ -188,6 +196,10 @@ pipeline { export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH cd java-client-api + echo "Temporary fix for mysterious issue with okhttp3 being corrupted in local Maven cache." + ls -la ~/.m2/repository/com/squareup + rm -rf ~/.m2/repository/com/squareup/okhttp3/ + echo "Ensure all subprojects can be built first." ./gradlew clean build -x test From ec509dd9bf95b6dd9e763641b8e795ee32a3e6ef Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Wed, 8 Oct 2025 17:23:18 -0400 Subject: [PATCH 28/33] MLE-24523 Disabling reverse proxy tests in regression build Some progress, but 87 errors still. Adding those to Jira ticket. --- Jenkinsfile | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 80dffa13a..7b7e19dba 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -256,24 +256,25 @@ pipeline { } } - stage('regressions-12-reverseProxy') { - when { - allOf { - branch 'develop' - expression {return params.regressions} - } - } - steps { - runTestsWithReverseProxy("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12") - } - post { - always { - junit '**/build/**/TEST*.xml' - updateWorkspacePermissions() - tearDownDocker() - } - } - } + // Latest run had 87 errors, which have been added to MLE-24523 for later research. +// stage('regressions-12-reverseProxy') { +// when { +// allOf { +// branch 'develop' +// expression {return params.regressions} +// } +// } +// steps { +// runTestsWithReverseProxy("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-12") +// } +// post { +// always { +// junit '**/build/**/TEST*.xml' +// updateWorkspacePermissions() +// tearDownDocker() +// } +// } +// } stage('regressions-12') { when { From 5818b49195b78653f41d3ff133d4a0213c171319 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 9 Oct 2025 08:10:14 -0400 Subject: [PATCH 29/33] MLE-24523 Bumped OkHttp and Jackson to latest Bumped Kotlin in ml-development-tools to be consistent with the Kotlin used by OkHttp 5.2.0 --- NOTICE.txt | 37 +++++++++++++------------------ gradle.properties | 4 ++-- ml-development-tools/build.gradle | 4 +++- pom.xml | 8 +++---- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index fe3ed8d43..9dd2c0e1f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -10,15 +10,14 @@ product and version for which you are requesting source code. Third Party Notices -jackson-databind 2.19.0 (Apache-2.0) -jackson-dataformat-csv 2.19.0 (Apache-2.0) -okhttp 4.12.0 (Apache-2.0) -logging-interceptor 4.12.0 (Apache-2.0) -jakarta.mail 2.0.1 (EPL-1.0) -okhttp-digest 2.7 (Apache-2.0) -jakarta.xml.bind-api 3.0.1 (EPL-1.0) -javax.ws.rs-api 2.1.1 (CDDL-1.1) -jaxb-runtime 3.0.2 (CDDL-1.1) +jackson-databind 2.20.0 (Apache-2.0) +jackson-dataformat-csv 2.20.0 (Apache-2.0) +okhttp 5.2.0 (Apache-2.0) +logging-interceptor 5.2.0 (Apache-2.0) +jakarta.mail 2.0.2 (EPL-1.0) +okhttp-digest 3.1.1 (Apache-2.0) +jakarta.xml.bind-api 4.0.4 (EPL-1.0) +jaxb-runtime 4.0.6 (CDDL-1.1) slf4j-api 2.0.17 (Apache-2.0) Common Licenses @@ -31,39 +30,35 @@ Third-Party Components The following is a list of the third-party components used by the MarkLogic® for Java Client 7.2.0 (last updated July 21, 2025): -jackson-databind 2.19.0 (Apache-2.0) +jackson-databind 2.20.0 (Apache-2.0) https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) -jackson-dataformat-csv 2.19.0 (Apache-2.0) +jackson-dataformat-csv 2.20.0 (Apache-2.0) https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-csv/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) -okhttp 4.12.0 (Apache-2.0) +okhttp 5.2.0 (Apache-2.0) https://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) -logging-interceptor 4.12.0 (Apache-2.0) +logging-interceptor 5.2.0 (Apache-2.0) https://repo1.maven.org/maven2/com/squareup/okhttp3/logging-interceptor/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) -jakarta.mail 2.0.1 (Apache-2.0) +jakarta.mail 2.0.2 (Apache-2.0) https://repo1.maven.org/maven2/com/sun/mail/jakarta.mail/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) -okhttp-digest 2.7 (Apache-2.0) +okhttp-digest 3.1.1 (Apache-2.0) https://repo1.maven.org/maven2/io/github/rburgst/okhttp-digest/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) -jakarta.xml.bind-api 3.0.1 (Apache-2.0) +jakarta.xml.bind-api 4.0.4 (Apache-2.0) https://repo1.maven.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) -javax.ws.rs-api 2.1.1 (Apache-2.0) -https://repo1.maven.org/maven2/javax/ws/rs/javax.ws.rs-api/ -For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) - -jaxb-runtime 3.0.2 (Apache-2.0) +jaxb-runtime 4.0.6 (Apache-2.0) https://repo1.maven.org/maven2/org/glassfish/jaxb/jaxb-runtime/ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) diff --git a/gradle.properties b/gradle.properties index 62b1ed3dd..ce21d2cfa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,10 +2,10 @@ group=com.marklogic version=8.0-SNAPSHOT publishUrl=file:../marklogic-java/releases -okhttpVersion=5.1.0 +okhttpVersion=5.2.0 # See https://github.com/FasterXML/jackson for more information on the Jackson libraries. -jacksonVersion=2.19.0 +jacksonVersion=2.20.0 # Defined at this level so that they can be set as system properties and used by the marklogic-client-api and test-app # project diff --git a/ml-development-tools/build.gradle b/ml-development-tools/build.gradle index ecbca0d67..f7bfb277f 100644 --- a/ml-development-tools/build.gradle +++ b/ml-development-tools/build.gradle @@ -23,8 +23,10 @@ dependencies { // additional work during development, though we rarely modify the code in this plugin anymore. implementation "com.marklogic:marklogic-client-api:${version}" - implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.1.0' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.2.20' implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}" + + // Sticking with this older version for now as the latest 1.x version introduces breaking changes. implementation 'com.networknt:json-schema-validator:1.0.88' // Not yet migrating this project to JUnit 5. Will reconsider it once we have a reason to enhance diff --git a/pom.xml b/pom.xml index 37374b60f..71741b24c 100644 --- a/pom.xml +++ b/pom.xml @@ -28,13 +28,13 @@ It is not intended to be used to build this project. com.squareup.okhttp3 okhttp - 5.1.0 + 5.2.0 runtime com.squareup.okhttp3 logging-interceptor - 5.1.0 + 5.2.0 runtime @@ -58,13 +58,13 @@ It is not intended to be used to build this project. com.fasterxml.jackson.core jackson-databind - 2.19.0 + 2.20.0 runtime com.fasterxml.jackson.dataformat jackson-dataformat-csv - 2.19.0 + 2.20.0 runtime From ce27ad8bb2f16eab6102607a62efaab346e3c08b Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 9 Oct 2025 09:52:43 -0400 Subject: [PATCH 30/33] MLE-24523 Removing regressions on 10 --- Jenkinsfile | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7b7e19dba..080b15cf3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -294,25 +294,5 @@ pipeline { } } } - - stage('regressions-10') { - when { - allOf { - branch 'develop' - expression { return params.regressions } - } - } - steps { - runTests("ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi:latest-10") - } - post { - always { - junit '**/build/**/TEST*.xml' - updateWorkspacePermissions() - tearDownDocker() - } - } - } - } } From f99c24ec88a6c7f85811fc71f1677d1aca3bbd08 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 9 Oct 2025 12:51:04 -0400 Subject: [PATCH 31/33] MLE-24523 Removing deprecated items Also eagerly bumped up the NOTICE file. --- NOTICE.txt | 4 +- README.md | 4 +- .../client/DatabaseClientFactory.java | 67 +------------------ .../client/extra/gson/GSONHandle.java | 25 ++----- .../client/impl/okhttp/OkHttpUtil.java | 4 +- .../src/test/example-project/build.gradle | 6 +- 6 files changed, 16 insertions(+), 94 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index 9dd2c0e1f..700be2e15 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -28,7 +28,7 @@ Eclipse Public License 1.0 (EPL-1.0) Third-Party Components -The following is a list of the third-party components used by the MarkLogic® for Java Client 7.2.0 (last updated July 21, 2025): +The following is a list of the third-party components used by the MarkLogic® for Java Client 8.0.0 (last updated October 29, 2025): jackson-databind 2.20.0 (Apache-2.0) https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/ @@ -68,7 +68,7 @@ For the full text of the Apache-2.0 license, see Apache License 2.0 (Apache-2.0) Common Licenses -The following is a list of the third-party components used by the MarkLogic® for Java Client 7.2.0 (last updated July 21, 2025): +The following is a list of the third-party components used by the MarkLogic® for Java Client 8.0.0 (last updated October 29, 2025): Apache License 2.0 (Apache-2.0) https://spdx.org/licenses/Apache-2.0.html diff --git a/README.md b/README.md index be9e1944e..f6bf86b28 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,13 @@ To use the client in your [Maven](https://maven.apache.org/) project, include th com.marklogic marklogic-client-api - 7.2.0 + 8.0.0 To use the client in your [Gradle](https://gradle.org/) project, include the following in your `build.gradle` file: dependencies { - implementation "com.marklogic:marklogic-client-api:7.2.0" + implementation "com.marklogic:marklogic-client-api:8.0.0" } Next, read [The Java API in Five Minutes](http://developer.marklogic.com/try/java/index) to get started. diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java index 10bb1c69c..63e4cd212 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java @@ -348,70 +348,7 @@ public SecurityContext withSSLContext(SSLContext context, X509TrustManager trust } /** - * @since 6.1.0 - * @deprecated as of 7.2.0; use {@code ProgressDataCloudAuthContext} instead. Will be removed in 8.0.0. - */ - @Deprecated - public static class MarkLogicCloudAuthContext extends ProgressDataCloudAuthContext { - - /** - * @param apiKey user's API key for accessing Progress Data Cloud - */ - public MarkLogicCloudAuthContext(String apiKey) { - super(apiKey); - } - - /** - * @param apiKey user's API key for accessing Progress Data Cloud - * @param tokenDuration length in minutes until the generated access token expires - * @since 6.3.0 - */ - public MarkLogicCloudAuthContext(String apiKey, Integer tokenDuration) { - super(apiKey, tokenDuration); - } - - /** - * Only intended to be used in the scenario that the token endpoint of "/token" and the grant type of "apikey" - * are not the intended values. - * - * @param apiKey user's API key for accessing Progress Data Cloud - * @param tokenEndpoint for overriding the default token endpoint if necessary - * @param grantType for overriding the default grant type if necessary - */ - public MarkLogicCloudAuthContext(String apiKey, String tokenEndpoint, String grantType) { - super(apiKey, tokenEndpoint, grantType); - } - - /** - * Only intended to be used in the scenario that the token endpoint of "/token" and the grant type of "apikey" - * are not the intended values. - * - * @param apiKey user's API key for accessing Progress Data Cloud - * @param tokenEndpoint for overriding the default token endpoint if necessary - * @param grantType for overriding the default grant type if necessary - * @param tokenDuration length in minutes until the generated access token expires - * @since 6.3.0 - */ - public MarkLogicCloudAuthContext(String apiKey, String tokenEndpoint, String grantType, Integer tokenDuration) { - super(apiKey, tokenEndpoint, grantType, tokenDuration); - } - - @Override - public MarkLogicCloudAuthContext withSSLContext(SSLContext context, X509TrustManager trustManager) { - this.sslContext = context; - this.trustManager = trustManager; - return this; - } - - @Override - public MarkLogicCloudAuthContext withSSLHostnameVerifier(SSLHostnameVerifier verifier) { - this.sslVerifier = verifier; - return this; - } - } - - /** - * @since 7.2.0 Use this instead of the now-deprecated {@code MarkLogicCloudAuthContext} + * @since 7.2.0 Replaced {@code MarkLogicCloudAuthContext} which was removed in 8.0.0 */ public static class ProgressDataCloudAuthContext extends AuthContext { private String tokenEndpoint; @@ -1332,7 +1269,7 @@ static public DatabaseClient newClient(String host, int port, String basePath, S // Progress Data Cloud instance, then port 443 will be used. Every path for constructing a DatabaseClient goes through // this method, ensuring that this optimization will always be applied, and thus freeing the user from having to // worry about what port to configure when using Progress Data Cloud. - if (securityContext instanceof MarkLogicCloudAuthContext || securityContext instanceof ProgressDataCloudAuthContext) { + if (securityContext instanceof ProgressDataCloudAuthContext) { port = 443; } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/extra/gson/GSONHandle.java b/marklogic-client-api/src/main/java/com/marklogic/client/extra/gson/GSONHandle.java index 511bf4163..dfdbbbb90 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/extra/gson/GSONHandle.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/extra/gson/GSONHandle.java @@ -3,12 +3,6 @@ */ package com.marklogic.client.extra.gson; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - import com.google.gson.JsonElement; import com.google.gson.JsonIOException; import com.google.gson.JsonParser; @@ -16,9 +10,14 @@ import com.marklogic.client.MarkLogicIOException; import com.marklogic.client.io.BaseHandle; import com.marklogic.client.io.Format; -import com.marklogic.client.io.marker.ResendableContentHandle; import com.marklogic.client.io.marker.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + /** * A GSONHandle represents JSON content as a GSON JsonElement for reading or * writing. You must install the GSON library to use this class. @@ -83,18 +82,6 @@ public GSONHandle[] newHandleArray(int length) { return new GSONHandle[length]; } - /** - * Returns the parser used to construct element objects from JSON. - * @return the JSON parser. - * @deprecated Use static methods like JsonParser.parseString() or JsonParser.parseReader() directly instead - */ - @Deprecated - public JsonParser getParser() { - if (parser == null) - parser = new JsonParser(); - return parser; - } - /** * Returns the root node of the JSON tree. * @return the JSON root element. diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java index 9d0f384ae..e3f9d4bd1 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java @@ -51,9 +51,7 @@ public static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseC } else if (securityContext instanceof DatabaseClientFactory.CertificateAuthContext) { } else if (securityContext instanceof DatabaseClientFactory.SAMLAuthContext) { configureSAMLAuth((DatabaseClientFactory.SAMLAuthContext) securityContext, clientBuilder); - } else if (securityContext instanceof DatabaseClientFactory.ProgressDataCloudAuthContext || - // It's fine to refer to this deprecated class as it needs to be supported until Java Client 8. - securityContext instanceof DatabaseClientFactory.MarkLogicCloudAuthContext) { + } else if (securityContext instanceof DatabaseClientFactory.ProgressDataCloudAuthContext) { authenticationConfigurer = new ProgressDataCloudAuthenticationConfigurer(host); } else if (securityContext instanceof DatabaseClientFactory.OAuthContext) { authenticationConfigurer = new OAuthAuthenticationConfigurer(); diff --git a/ml-development-tools/src/test/example-project/build.gradle b/ml-development-tools/src/test/example-project/build.gradle index c0ce194f3..0ec53c470 100644 --- a/ml-development-tools/src/test/example-project/build.gradle +++ b/ml-development-tools/src/test/example-project/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath "com.marklogic:ml-development-tools:7.2.0" + classpath "com.marklogic:ml-development-tools:8.0.0" } } @@ -23,11 +23,11 @@ repositories { } dependencies { - implementation 'com.marklogic:marklogic-client-api:7.2.0' + implementation 'com.marklogic:marklogic-client-api:8.0.0' } tasks.register("testFullPath", com.marklogic.client.tools.gradle.EndpointProxiesGenTask) { - serviceDeclarationFile = "/Users/rrudin/workspace/java-client-api/example-project/src/main/ml-modules/root/inventory/service.json" + serviceDeclarationFile = "/Users/rudin/workspace/java-client-api/ml-development-tools/src/test/example-project/src/main/ml-modules/root/inventory/service.json" } tasks.register("testProjectPath", com.marklogic.client.tools.gradle.EndpointProxiesGenTask) { From e835c37fb324d2fa4da6d0514ab287214fe7b793 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 9 Oct 2025 15:08:45 -0400 Subject: [PATCH 32/33] Bumped to 8.0.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ce21d2cfa..dc8a5f9c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.marklogic -version=8.0-SNAPSHOT +version=8.0.0 publishUrl=file:../marklogic-java/releases okhttpVersion=5.2.0 From af2b8d3e8416c53cbf249ae78d92df6edbc94934 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Fri, 10 Oct 2025 10:14:52 -0400 Subject: [PATCH 33/33] Modified Java requirements in README --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f6bf86b28..7ba1a473c 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,13 @@ The client supports the following core features of the MarkLogic database: * Execute multi-statement transactions so changes to multiple documents succeed or fail together. * Call Data Services via a Java interface on the client for data functionality implemented by an endpoint on the server. -The client is tested on Java 8, 11, 17, and 21 and can safely be used on each of those major Java versions. The client -may work on more recent major versions of Java but has not been thoroughly tested on those yet. +## System Requirements -If you are using Java 11 or higher and intend to use [JAXB](https://docs.oracle.com/javase/tutorial/jaxb/intro/), please see the section below for ensuring that the -necessary dependencies are available in your application's classpath. +As of the 8.0.0 release, the Java Client requires Java 17 or Java 21. + +Prior releases are compatible with Java 8, 11, 17, and 21. + +For compatibility with MarkLogic server versions, please see the [Compatibility Matrix](https://developer.marklogic.com/products/support-matrix/#java-client-api). ## QuickStart