From cc94b53da2f63ebd605619500f8366513f6a7d7d Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 22 Sep 2025 12:43:10 -0400 Subject: [PATCH 1/3] 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 6ce1d86984159b2a22afc52848a2ca80024fce6c Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Mon, 22 Sep 2025 14:12:16 -0400 Subject: [PATCH 2/3] JDK HTTP client prototype --- .../client/impl/JdkHttpServices.java | 734 ++++++++++++++++++ 1 file changed, 734 insertions(+) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java new file mode 100644 index 000000000..c3779ba25 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java @@ -0,0 +1,734 @@ +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.impl; + +import com.marklogic.client.*; +import com.marklogic.client.DatabaseClient.ConnectionResult; +import com.marklogic.client.DatabaseClientFactory.SecurityContext; +import com.marklogic.client.bitemporal.TemporalDescriptor; +import com.marklogic.client.bitemporal.TemporalDocumentManager.ProtectionLevel; +import com.marklogic.client.document.*; +import com.marklogic.client.document.DocumentManager.Metadata; +import com.marklogic.client.eval.EvalResultIterator; +import com.marklogic.client.io.Format; +import com.marklogic.client.io.marker.*; +import com.marklogic.client.query.*; +import com.marklogic.client.query.QueryManager.QueryView; +import com.marklogic.client.semantics.GraphPermissions; +import com.marklogic.client.semantics.SPARQLQueryDefinition; +import com.marklogic.client.util.EditableNamespaceContext; +import com.marklogic.client.util.RequestLogger; +import com.marklogic.client.util.RequestParameters; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; + +/** + * Implementation of RESTServices using the JDK HttpClient instead of OkHttp. + * This is a prototype implementation for a future major release. + */ +@SuppressWarnings("unused") // Fields will be used in complete implementation +public class JdkHttpServices implements RESTServices { + + private final String database; + private final URI baseUri; + private final HttpClient httpClient; + private boolean released = false; + + public JdkHttpServices(String host, int port, String basePath, String database, SecurityContext securityContext) { + if (host == null) { + throw new IllegalArgumentException("No host provided"); + } + if (securityContext == null) { + throw new IllegalArgumentException("No security context provided"); + } + + this.database = database; + this.baseUri = buildBaseUri(host, port, basePath, securityContext); + + HttpClient.Builder clientBuilder = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL); + + // Configure SSL if present + SSLContext sslContext = securityContext.getSSLContext(); + if (sslContext != null) { + clientBuilder.sslContext(sslContext); + + // Configure SSL parameters if we have a hostname verifier + if (securityContext.getSSLHostnameVerifier() != null) { + SSLParameters sslParams = new SSLParameters(); + // Note: JDK HttpClient doesn't have direct hostname verifier support like OkHttp + // In a full implementation, we would need to handle this differently + clientBuilder.sslParameters(sslParams); + } + } + + this.httpClient = clientBuilder.build(); + } + + private URI buildBaseUri(String host, int port, String basePath, SecurityContext securityContext) { + StringBuilder uriBuilder = new StringBuilder(); + + // Determine scheme based on SSL context + String scheme = (securityContext.getSSLContext() != null) ? "https" : "http"; + uriBuilder.append(scheme).append("://").append(host).append(":").append(port); + + // Add base path if provided + if (basePath != null && !basePath.trim().isEmpty()) { + if (!basePath.startsWith("/")) { + uriBuilder.append("/"); + } + uriBuilder.append(basePath.trim()); + if (!basePath.endsWith("/")) { + uriBuilder.append("/"); + } + } else { + uriBuilder.append("/"); + } + + // Add the v1 API path + uriBuilder.append("v1/"); + + return URI.create(uriBuilder.toString()); + } + + @Override + public void release() { + if (!released) { + this.released = true; + } + } + + @Override + public Object getClientImplementation() { + return httpClient; + } + + // Stub implementations for all other RESTServices methods + // These would need to be properly implemented in a complete migration + + @Override + public TemporalDescriptor deleteDocument(RequestLogger logger, DocumentDescriptor desc, Transaction transaction, + Set categories, RequestParameters extraParams) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("deleteDocument not yet implemented"); + } + + @Override + @SuppressWarnings("rawtypes") + public boolean getDocument(RequestLogger logger, DocumentDescriptor desc, Transaction transaction, + Set categories, RequestParameters extraParams, + DocumentMetadataReadHandle metadataHandle, AbstractReadHandle contentHandle) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + + String uri = desc.getUri(); + if (uri == null) { + throw new IllegalArgumentException("Document read for document identifier without uri"); + } + + try { + // Build the request URL + URI requestUri = buildDocumentRequestUri(uri, categories, transaction, extraParams); + + // Create HTTP request + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(requestUri) + .timeout(Duration.ofSeconds(30)) + .GET(); + + // Add headers for metadata and content handles + HandleImplementation metadataBase = HandleAccessor.checkHandle(metadataHandle, "metadata"); + HandleImplementation contentBase = HandleAccessor.checkHandle(contentHandle, "content"); + + // Set Accept header based on handle types + String acceptHeader = buildAcceptHeader(metadataBase, contentBase); + if (acceptHeader != null) { + requestBuilder.header(HEADER_ACCEPT, acceptHeader); + } + + // Add transaction ID if present + if (transaction != null) { + requestBuilder.header("ML-Transaction-ID", transaction.getTransactionId()); + } + + // Execute the request + HttpRequest request = requestBuilder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + + // Handle response status codes + int statusCode = response.statusCode(); + + if (statusCode == STATUS_NOT_FOUND) { + throw new ResourceNotFoundException("Could not read non-existent document"); + } + if (statusCode == STATUS_FORBIDDEN) { + throw new ForbiddenUserException("User is not allowed to read documents"); + } + if (statusCode == STATUS_NOT_MODIFIED) { + return false; + } + if (statusCode != STATUS_OK && statusCode != STATUS_PARTIAL_CONTENT) { + throw new FailedRequestException("read failed: HTTP " + statusCode); + } + + // Process the response body into the handle + try (InputStream responseBody = response.body()) { + if (contentBase != null) { + HandleAccessor.receiveContent(contentHandle, responseBody); + } + + // For now, we'll skip metadata handling in this simplified implementation + // A complete implementation would parse multipart responses for both content and metadata + + return true; + } + + } catch (IOException | InterruptedException e) { + throw new FailedRequestException("Failed to send HTTP request: " + e.getMessage(), e); + } + } + + /** + * Builds the request URI for document operations + */ + private URI buildDocumentRequestUri(String docUri, Set categories, Transaction transaction, RequestParameters extraParams) { + StringBuilder uriBuilder = new StringBuilder(baseUri.toString()); + uriBuilder.append("documents"); + + // Add query parameters + List params = new ArrayList<>(); + + // Document URI + params.add("uri=" + URLEncoder.encode(docUri, StandardCharsets.UTF_8)); + + // Database parameter + if (database != null) { + params.add("database=" + URLEncoder.encode(database, StandardCharsets.UTF_8)); + } + + // Categories + if (categories == null || categories.isEmpty()) { + params.add("category=content"); + } else { + for (Metadata category : categories) { + params.add("category=" + category.name().toLowerCase()); + } + } + + // Transaction + if (transaction != null) { + params.add("txid=" + transaction.getTransactionId()); + } + + // Extra parameters + if (extraParams != null) { + for (Map.Entry> entry : extraParams.entrySet()) { + String key = entry.getKey(); + for (String value : entry.getValue()) { + params.add(URLEncoder.encode(key, StandardCharsets.UTF_8) + "=" + + URLEncoder.encode(value, StandardCharsets.UTF_8)); + } + } + } + + // Append query string + if (!params.isEmpty()) { + uriBuilder.append("?").append(String.join("&", params)); + } + + return URI.create(uriBuilder.toString()); + } + + /** + * Builds the Accept header based on the handles provided + */ + @SuppressWarnings("rawtypes") + private String buildAcceptHeader(HandleImplementation metadataBase, HandleImplementation contentBase) { + List acceptTypes = new ArrayList<>(); + + if (metadataBase != null) { + String mimetype = metadataBase.getMimetype(); + if (mimetype != null) { + acceptTypes.add(mimetype); + } + } + + if (contentBase != null) { + String mimetype = contentBase.getMimetype(); + if (mimetype != null) { + acceptTypes.add(mimetype); + } + } + + return acceptTypes.isEmpty() ? null : String.join(", ", acceptTypes); + } + + @Override + public DocumentDescriptor head(RequestLogger logger, String uri, Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("head not yet implemented"); + } + + @Override + public DocumentPage getBulkDocuments(RequestLogger logger, long serverTimestamp, Transaction transaction, + Set categories, Format format, RequestParameters extraParams, + boolean withContent, String... uris) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getBulkDocuments not yet implemented"); + } + + @Override + public DocumentPage getBulkDocuments(RequestLogger logger, long serverTimestamp, SearchQueryDefinition querydef, + long start, long pageLength, Transaction transaction, SearchReadHandle searchHandle, + QueryView view, Set categories, Format format, ServerTransform responseTransform, + RequestParameters extraParams, String forestName) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getBulkDocuments with search not yet implemented"); + } + + @Override + public void postBulkDocuments(RequestLogger logger, DocumentWriteSet writeSet, + ServerTransform transform, Transaction transaction, Format defaultFormat, T output, + String temporalCollection, String extraContentDispositionParams) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("postBulkDocuments not yet implemented"); + } + + @Override + public TemporalDescriptor putDocument(RequestLogger logger, DocumentDescriptor desc, Transaction transaction, + Set categories, RequestParameters extraParams, + DocumentMetadataWriteHandle metadataHandle, AbstractWriteHandle contentHandle) + throws ResourceNotFoundException, ResourceNotResendableException, + ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("putDocument not yet implemented"); + } + + @Override + public DocumentDescriptorImpl postDocument(RequestLogger logger, DocumentUriTemplate template, + Transaction transaction, Set categories, RequestParameters extraParams, + DocumentMetadataWriteHandle metadataHandle, AbstractWriteHandle contentHandle) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("postDocument not yet implemented"); + } + + @Override + public void patchDocument(RequestLogger logger, DocumentDescriptor desc, Transaction transaction, + Set categories, boolean isOnContent, DocumentPatchHandle patchHandle) + throws ResourceNotFoundException, ResourceNotResendableException, + ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("patchDocument not yet implemented"); + } + + @Override + public T search(RequestLogger logger, T searchHandle, SearchQueryDefinition queryDef, + long start, long len, QueryView view, Transaction transaction, String forestName) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("search not yet implemented"); + } + + @Override + public void deleteSearch(RequestLogger logger, DeleteQueryDefinition queryDef, Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("deleteSearch not yet implemented"); + } + + @Override + public void delete(RequestLogger logger, Transaction transaction, Set categories, String... uris) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("delete not yet implemented"); + } + + @Override + public Transaction openTransaction(String name, int timeLimit) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("openTransaction not yet implemented"); + } + + @Override + public void commitTransaction(Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("commitTransaction not yet implemented"); + } + + @Override + public void rollbackTransaction(Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("rollbackTransaction not yet implemented"); + } + + @Override + public T values(Class as, ValuesDefinition valdef, String mimetype, long start, long pageLength, Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("values not yet implemented"); + } + + @Override + public T valuesList(Class as, ValuesListDefinition valdef, String mimetype, Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("valuesList not yet implemented"); + } + + @Override + public T optionsList(Class as, String mimetype, Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("optionsList not yet implemented"); + } + + @Override + public T getValue(RequestLogger logger, String type, String key, + boolean isNullable, String mimetype, Class as) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getValue not yet implemented"); + } + + @Override + public T getValue(RequestLogger logger, String type, String key, Transaction transaction, + boolean isNullable, String mimetype, Class as) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getValue with transaction not yet implemented"); + } + + @Override + public T getValues(RequestLogger logger, String type, String mimetype, Class as) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getValues not yet implemented"); + } + + @Override + public T getValues(RequestLogger reqlog, String type, RequestParameters extraParams, + String mimetype, Class as) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getValues with params not yet implemented"); + } + + @Override + public void putValue(RequestLogger logger, String type, String key, + String mimetype, Object value) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("putValue not yet implemented"); + } + + @Override + public void putValue(RequestLogger logger, String type, String key, RequestParameters extraParams, + String mimetype, Object value) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("putValue with params not yet implemented"); + } + + @Override + public void deleteValue(RequestLogger logger, String type, String key) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("deleteValue not yet implemented"); + } + + @Override + public R uris(RequestLogger reqlog, String method, SearchQueryDefinition qdef, + Boolean filtered, long start, String afterUri, long pageLength, String forestName, R output) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("uris not yet implemented"); + } + + @Override + public R forestInfo(RequestLogger reqlog, + String method, RequestParameters params, SearchQueryDefinition qdef, R output) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("forestInfo not yet implemented"); + } + + @Override + public R getResource(RequestLogger reqlog, String path, + Transaction transaction, RequestParameters params, R output) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getResource not yet implemented"); + } + + @Override + public RESTServiceResultIterator getIteratedResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getIteratedResource not yet implemented"); + } + + @Override + public R putResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + AbstractWriteHandle input, R output) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("putResource not yet implemented"); + } + + @Override + public R putResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + W[] input, R output) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("putResource with array not yet implemented"); + } + + @Override + public R postResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + AbstractWriteHandle input, R output) + throws ResourceNotFoundException, ResourceNotResendableException, + ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("postResource not yet implemented"); + } + + @Override + public R postResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + W[] input, R output) + throws ResourceNotFoundException, ResourceNotResendableException, + ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("postResource with array not yet implemented"); + } + + @Override + public R postResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + W[] input, Map>[] headers, R output) + throws ResourceNotFoundException, ResourceNotResendableException, + ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("postResource with headers not yet implemented"); + } + + @Override + public RESTServiceResultIterator postIteratedResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + AbstractWriteHandle input) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("postIteratedResource not yet implemented"); + } + + @Override + public RESTServiceResultIterator postMultipartForm( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, List contentParams) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("postMultipartForm not yet implemented"); + } + + @Override + public RESTServiceResultIterator postIteratedResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + W[] input) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("postIteratedResource with array not yet implemented"); + } + + @Override + public EvalResultIterator postEvalInvoke(RequestLogger reqlog, String code, String modulePath, + ServerEvaluationCallImpl.Context evalContext, Map variables, + EditableNamespaceContext namespaces, Transaction transaction) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("postEvalInvoke not yet implemented"); + } + + @Override + public R deleteResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, R output) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("deleteResource not yet implemented"); + } + + @Override + public ConnectionResult checkConnection() { + throw new UnsupportedOperationException("checkConnection not yet implemented"); + } + + @Override + public T suggest(Class as, SuggestDefinition suggestionDef) { + throw new UnsupportedOperationException("suggest not yet implemented"); + } + + @Override + public InputStream match(StructureWriteHandle document, String[] candidateRules, String mimeType, ServerTransform transform) { + throw new UnsupportedOperationException("match not yet implemented"); + } + + @Override + public InputStream match(String[] docIds, String[] candidateRules, ServerTransform transform) { + throw new UnsupportedOperationException("match with docIds not yet implemented"); + } + + @Override + public InputStream match(QueryDefinition queryDef, long start, long pageLength, String[] candidateRules, ServerTransform transform) { + throw new UnsupportedOperationException("match with query not yet implemented"); + } + + @Override + public R getGraphUris(RequestLogger reqlog, R output) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getGraphUris not yet implemented"); + } + + @Override + public void readGraph(RequestLogger reqlog, String uri, R output, + Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("readGraph not yet implemented"); + } + + @Override + public void writeGraph(RequestLogger reqlog, String uri, + AbstractWriteHandle input, GraphPermissions permissions, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("writeGraph not yet implemented"); + } + + @Override + public void writeGraphs(RequestLogger reqlog, AbstractWriteHandle input, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("writeGraphs not yet implemented"); + } + + @Override + public void deleteGraph(RequestLogger requestLogger, String uri, + Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("deleteGraph not yet implemented"); + } + + @Override + public void deleteGraphs(RequestLogger requestLogger, Transaction transaction) + throws ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("deleteGraphs not yet implemented"); + } + + @Override + public R executeSparql(RequestLogger reqlog, + SPARQLQueryDefinition qdef, R output, long start, long pageLength, + Transaction transaction, boolean isUpdate) { + throw new UnsupportedOperationException("executeSparql not yet implemented"); + } + + @Override + public boolean exists(String uri) { + throw new UnsupportedOperationException("exists not yet implemented"); + } + + @Override + public void mergeGraph(RequestLogger reqlog, String uri, AbstractWriteHandle input, + GraphPermissions permissions, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("mergeGraph not yet implemented"); + } + + @Override + public void mergeGraphs(RequestLogger reqlog, AbstractWriteHandle input, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("mergeGraphs not yet implemented"); + } + + @Override + public R getPermissions(RequestLogger reqlog, String uri, + R output, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getPermissions not yet implemented"); + } + + @Override + public void deletePermissions(RequestLogger reqlog, String uri, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("deletePermissions not yet implemented"); + } + + @Override + public void writePermissions(RequestLogger reqlog, String uri, + AbstractWriteHandle permissions, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("writePermissions not yet implemented"); + } + + @Override + public void mergePermissions(RequestLogger reqlog, String uri, + AbstractWriteHandle permissions, Transaction transaction) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("mergePermissions not yet implemented"); + } + + @Override + public R getThings(RequestLogger reqlog, String[] iris, R output) + throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("getThings not yet implemented"); + } + + @Override + public String advanceLsqt(RequestLogger reqlog, String temporalCollection, long lag) { + throw new UnsupportedOperationException("advanceLsqt not yet implemented"); + } + + @Override + public void wipeDocument(RequestLogger requestLogger, String temporalDocumentURI, Transaction transaction, + RequestParameters extraParams) { + throw new UnsupportedOperationException("wipeDocument not yet implemented"); + } + + @Override + public void protectDocument(RequestLogger requestLogger, String temporalDocumentURI, Transaction transaction, + RequestParameters extraParams, ProtectionLevel level, String duration, Calendar expiryTime, String archivePath) { + throw new UnsupportedOperationException("protectDocument not yet implemented"); + } + + @Override + public R postResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + AbstractWriteHandle input, R output, String operation) + throws ResourceNotFoundException, ResourceNotResendableException, + ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("postResource with operation not yet implemented"); + } + + @Override + public R postResource( + RequestLogger reqlog, String path, Transaction transaction, RequestParameters params, + AbstractWriteHandle input, R output, String operation, Map> responseHeaders) + throws ResourceNotFoundException, ResourceNotResendableException, + ForbiddenUserException, FailedRequestException { + throw new UnsupportedOperationException("postResource with operation and headers not yet implemented"); + } + + @Override + public void patchDocument(RequestLogger reqlog, DocumentDescriptor desc, Transaction transaction, Set categories, boolean isOnContent, + RequestParameters extraParams, String sourceDocumentURI, DocumentPatchHandle patchHandle) + throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, + FailedRequestException { + throw new UnsupportedOperationException("patchDocument with extra params not yet implemented"); + } + + // API First Additions + @Override + public CallRequest makeEmptyRequest(String endpoint, HttpMethod method, SessionState session) { + throw new UnsupportedOperationException("makeEmptyRequest not yet implemented"); + } + + @Override + public CallRequest makeAtomicBodyRequest(String endpoint, HttpMethod method, SessionState session, CallField... params) { + throw new UnsupportedOperationException("makeAtomicBodyRequest not yet implemented"); + } + + @Override + public CallRequest makeNodeBodyRequest(String endpoint, HttpMethod method, SessionState session, CallField... params) { + throw new UnsupportedOperationException("makeNodeBodyRequest not yet implemented"); + } +} From 181ae795216c3e1ca3215424c079efe1ace6d8e1 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Tue, 30 Sep 2025 08:50:52 -0400 Subject: [PATCH 3/3] More hacking --- .../client/DatabaseClientFactory.java | 3 +- .../client/impl/JdkHttpServices.java | 257 +++++++++++++++--- .../com/marklogic/client/test/Common.java | 2 +- .../client/test/document/GetDocumentTest.java | 20 ++ 4 files changed, 235 insertions(+), 47 deletions(-) create mode 100644 marklogic-client-api/src/test/java/com/marklogic/client/test/document/GetDocumentTest.java 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..d7c16a20a 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 @@ -1338,7 +1338,8 @@ static public DatabaseClient newClient(String host, int port, String basePath, S 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); + RESTServices jdkServices = new JdkHttpServices(host, port, basePath, database, securityContext); + DatabaseClientImpl client = new DatabaseClientImpl(jdkServices, host, port, basePath, database, securityContext, connectionType); client.setHandleRegistry(getHandleRegistry().copy()); return client; } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java index c3779ba25..30f97f176 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java @@ -5,6 +5,7 @@ import com.marklogic.client.*; import com.marklogic.client.DatabaseClient.ConnectionResult; +import com.marklogic.client.DatabaseClientFactory.BasicAuthContext; import com.marklogic.client.DatabaseClientFactory.SecurityContext; import com.marklogic.client.bitemporal.TemporalDescriptor; import com.marklogic.client.bitemporal.TemporalDocumentManager.ProtectionLevel; @@ -25,6 +26,8 @@ import javax.net.ssl.SSLParameters; import java.io.IOException; import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; @@ -33,6 +36,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; +import java.util.stream.Collectors; /** * Implementation of RESTServices using the JDK HttpClient instead of OkHttp. @@ -61,6 +65,17 @@ public JdkHttpServices(String host, int port, String basePath, String database, .connectTimeout(Duration.ofSeconds(30)) .followRedirects(HttpClient.Redirect.NORMAL); + // Configure Basic Authentication if present + if (securityContext instanceof BasicAuthContext) { + BasicAuthContext basicAuth = (BasicAuthContext) securityContext; + clientBuilder.authenticator(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(basicAuth.getUser(), basicAuth.getPassword().toCharArray()); + } + }); + } + // Configure SSL if present SSLContext sslContext = securityContext.getSSLContext(); if (sslContext != null) { @@ -133,6 +148,10 @@ public boolean getDocument(RequestLogger logger, DocumentDescriptor desc, Transa DocumentMetadataReadHandle metadataHandle, AbstractReadHandle contentHandle) throws ResourceNotFoundException, ForbiddenUserException, FailedRequestException { + if (released) { + throw new IllegalStateException("Client has been released"); + } + String uri = desc.getUri(); if (uri == null) { throw new IllegalArgumentException("Document read for document identifier without uri"); @@ -148,9 +167,16 @@ public boolean getDocument(RequestLogger logger, DocumentDescriptor desc, Transa .timeout(Duration.ofSeconds(30)) .GET(); - // Add headers for metadata and content handles - HandleImplementation metadataBase = HandleAccessor.checkHandle(metadataHandle, "metadata"); - HandleImplementation contentBase = HandleAccessor.checkHandle(contentHandle, "content"); + // Check handles and determine what we're requesting + HandleImplementation metadataBase = null; + HandleImplementation contentBase = null; + + if (metadataHandle != null) { + metadataBase = HandleAccessor.checkHandle(metadataHandle, "metadata"); + } + if (contentHandle != null) { + contentBase = HandleAccessor.checkHandle(contentHandle, "content"); + } // Set Accept header based on handle types String acceptHeader = buildAcceptHeader(metadataBase, contentBase); @@ -158,9 +184,9 @@ public boolean getDocument(RequestLogger logger, DocumentDescriptor desc, Transa requestBuilder.header(HEADER_ACCEPT, acceptHeader); } - // Add transaction ID if present - if (transaction != null) { - requestBuilder.header("ML-Transaction-ID", transaction.getTransactionId()); + // Add version header for conditional requests + if (desc.getVersion() != DocumentDescriptor.UNKNOWN_VERSION) { + requestBuilder.header("If-None-Match", "\"" + desc.getVersion() + "\""); } // Execute the request @@ -171,27 +197,46 @@ public boolean getDocument(RequestLogger logger, DocumentDescriptor desc, Transa int statusCode = response.statusCode(); if (statusCode == STATUS_NOT_FOUND) { + response.body().close(); throw new ResourceNotFoundException("Could not read non-existent document"); } if (statusCode == STATUS_FORBIDDEN) { + response.body().close(); throw new ForbiddenUserException("User is not allowed to read documents"); } if (statusCode == STATUS_NOT_MODIFIED) { + response.body().close(); return false; } if (statusCode != STATUS_OK && statusCode != STATUS_PARTIAL_CONTENT) { + response.body().close(); throw new FailedRequestException("read failed: HTTP " + statusCode); } - // Process the response body into the handle + // Process the response try (InputStream responseBody = response.body()) { - if (contentBase != null) { - HandleAccessor.receiveContent(contentHandle, responseBody); + String contentType = response.headers().firstValue("Content-Type").orElse(""); + + // Update descriptor with response headers if needed + updateDescriptorFromHeaders(desc, response); + + if (contentType.startsWith("multipart/mixed") && metadataBase != null && contentBase != null) { + // Handle multipart response (both metadata and content) + // For simplicity, we'll implement this later - for now handle as single part + throw new UnsupportedOperationException("Multipart responses not yet implemented in JDK HTTP client"); + } else { + // Handle single part response + if (contentBase != null) { + // Read content into the content handle + receiveContent(contentBase, responseBody, logger); + } + + if (metadataBase != null && contentBase == null) { + // Only metadata requested + receiveContent(metadataBase, responseBody, logger); + } } - // For now, we'll skip metadata handling in this simplified implementation - // A complete implementation would parse multipart responses for both content and metadata - return true; } @@ -201,54 +246,91 @@ public boolean getDocument(RequestLogger logger, DocumentDescriptor desc, Transa } /** - * Builds the request URI for document operations + * Builds the request URI for document operations using proper URI construction */ private URI buildDocumentRequestUri(String docUri, Set categories, Transaction transaction, RequestParameters extraParams) { - StringBuilder uriBuilder = new StringBuilder(baseUri.toString()); - uriBuilder.append("documents"); + try { + // Build the base path + String path = baseUri.getPath(); + if (!path.endsWith("/")) { + path += "/"; + } + path += "documents"; - // Add query parameters - List params = new ArrayList<>(); + // Build query parameters using proper encoding + Map> queryParams = new LinkedHashMap<>(); - // Document URI - params.add("uri=" + URLEncoder.encode(docUri, StandardCharsets.UTF_8)); + // Document URI - required parameter + addQueryParam(queryParams, "uri", docUri); - // Database parameter - if (database != null) { - params.add("database=" + URLEncoder.encode(database, StandardCharsets.UTF_8)); - } + // Database parameter + if (database != null) { + addQueryParam(queryParams, "database", database); + } - // Categories - if (categories == null || categories.isEmpty()) { - params.add("category=content"); - } else { - for (Metadata category : categories) { - params.add("category=" + category.name().toLowerCase()); + // Categories + if (categories == null || categories.isEmpty()) { + addQueryParam(queryParams, "category", "content"); + } else { + for (Metadata category : categories) { + addQueryParam(queryParams, "category", category.name().toLowerCase()); + } } - } - // Transaction - if (transaction != null) { - params.add("txid=" + transaction.getTransactionId()); - } + // Transaction + if (transaction != null) { + addQueryParam(queryParams, "txid", transaction.getTransactionId()); + } - // Extra parameters - if (extraParams != null) { - for (Map.Entry> entry : extraParams.entrySet()) { - String key = entry.getKey(); - for (String value : entry.getValue()) { - params.add(URLEncoder.encode(key, StandardCharsets.UTF_8) + "=" + - URLEncoder.encode(value, StandardCharsets.UTF_8)); + // Extra parameters + if (extraParams != null) { + for (Map.Entry> entry : extraParams.entrySet()) { + String key = entry.getKey(); + for (String value : entry.getValue()) { + addQueryParam(queryParams, key, value); + } } } + + // Build the query string + String query = buildQueryString(queryParams); + + // Construct the final URI + return new URI( + baseUri.getScheme(), + baseUri.getUserInfo(), + baseUri.getHost(), + baseUri.getPort(), + path, + query.isEmpty() ? null : query, + null // fragment + ); + + } catch (Exception e) { + throw new IllegalArgumentException("Failed to build document request URI: " + e.getMessage(), e); } + } + + /** + * Helper method to add a query parameter to the map + */ + private void addQueryParam(Map> queryParams, String key, String value) { + queryParams.computeIfAbsent(key, k -> new ArrayList<>()).add(value); + } - // Append query string - if (!params.isEmpty()) { - uriBuilder.append("?").append(String.join("&", params)); + /** + * Builds a properly encoded query string from parameters + */ + private String buildQueryString(Map> queryParams) { + if (queryParams.isEmpty()) { + return ""; } - return URI.create(uriBuilder.toString()); + return queryParams.entrySet().stream() + .flatMap(entry -> entry.getValue().stream() + .map(value -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" + + URLEncoder.encode(value, StandardCharsets.UTF_8))) + .collect(Collectors.joining("&")); } /** @@ -731,4 +813,89 @@ public CallRequest makeAtomicBodyRequest(String endpoint, HttpMethod method, Ses public CallRequest makeNodeBodyRequest(String endpoint, HttpMethod method, SessionState session, CallField... params) { throw new UnsupportedOperationException("makeNodeBodyRequest not yet implemented"); } + + // Helper methods for getDocument implementation + + /** + * Updates document descriptor with information from response headers + */ + private void updateDescriptorFromHeaders(DocumentDescriptor desc, HttpResponse response) { + // Update version if ETag header is present + response.headers().firstValue("ETag").ifPresent(etag -> { + if (etag.startsWith("\"") && etag.endsWith("\"")) { + String versionStr = etag.substring(1, etag.length() - 1); + try { + long version = Long.parseLong(versionStr); + if (desc instanceof DocumentDescriptorImpl) { + ((DocumentDescriptorImpl) desc).setVersion(version); + } + } catch (NumberFormatException e) { + // Ignore invalid version + } + } + }); + + // Update content length if present + response.headers().firstValue("Content-Length").ifPresent(lengthStr -> { + try { + long length = Long.parseLong(lengthStr); + if (desc instanceof ContentDescriptor) { + ((ContentDescriptor) desc).setByteLength(length); + } + } catch (NumberFormatException e) { + // Ignore invalid length + } + }); + + // Update format from content type + response.headers().firstValue("Content-Type").ifPresent(contentType -> { + if (desc instanceof ContentDescriptor) { + ((ContentDescriptor) desc).setMimetype(contentType); + Format format = getFormatFromMimetype(contentType); + if (format != null) { + ((ContentDescriptor) desc).setFormat(format); + } + } + }); + } + + /** + * Receives content into a handle, handling different types appropriately + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private void receiveContent(HandleImplementation handleBase, InputStream responseBody, RequestLogger logger) + throws IOException { + Class as = handleBase.receiveAs(); + Object content; + + if (InputStream.class.isAssignableFrom(as)) { + content = responseBody; + } else if (String.class.isAssignableFrom(as)) { + content = new String(responseBody.readAllBytes(), StandardCharsets.UTF_8); + } else { + // For other types, read as bytes and let the handle convert + content = responseBody.readAllBytes(); + } + + Object finalContent = (logger != null) ? logger.copyContent(content) : content; + handleBase.receiveContent(finalContent); + } + + /** + * Gets Format enum from MIME type string + */ + private Format getFormatFromMimetype(String mimetype) { + if (mimetype == null) return null; + + String lowerMimetype = mimetype.toLowerCase(); + if (lowerMimetype.contains("xml")) { + return Format.XML; + } else if (lowerMimetype.contains("json")) { + return Format.JSON; + } else if (lowerMimetype.contains("text")) { + return Format.TEXT; + } else { + return Format.BINARY; + } + } } 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..664911eb0 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 @@ -130,7 +130,7 @@ public static DatabaseClientBuilder newClientBuilder() { .withBasePath(BASE_PATH) .withUsername(USER) .withPassword(PASS) // Most of the test users all have the same password, so we can use a default one here - .withAuthType(AUTH_TYPE) + .withAuthType("basic") .withConnectionType(CONNECTION_TYPE); } diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/document/GetDocumentTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/document/GetDocumentTest.java new file mode 100644 index 000000000..045f5e000 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/document/GetDocumentTest.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.test.document; + +import com.marklogic.client.DatabaseClient; +import com.marklogic.client.io.StringHandle; +import com.marklogic.client.test.Common; +import org.junit.jupiter.api.Test; + +class GetDocumentTest { + + @Test + void jsonDocument() { + try (DatabaseClient client = Common.newClient()) { + var doc = client.newJSONDocumentManager().read("/optic/test/musician1.json", new StringHandle()).get(); + System.out.println(doc); + } + } +}