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..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 @@ -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,11 @@ 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); + 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; } @@ -1397,13 +1381,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/JdkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java new file mode 100644 index 000000000..30f97f176 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/JdkHttpServices.java @@ -0,0 +1,901 @@ +/* + * 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.BasicAuthContext; +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.Authenticator; +import java.net.PasswordAuthentication; +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.*; +import java.util.stream.Collectors; + +/** + * 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 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) { + 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 { + + 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"); + } + + 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(); + + // 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); + if (acceptHeader != null) { + requestBuilder.header(HEADER_ACCEPT, acceptHeader); + } + + // Add version header for conditional requests + if (desc.getVersion() != DocumentDescriptor.UNKNOWN_VERSION) { + requestBuilder.header("If-None-Match", "\"" + desc.getVersion() + "\""); + } + + // 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) { + 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 + try (InputStream responseBody = response.body()) { + 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); + } + } + + return true; + } + + } catch (IOException | InterruptedException e) { + throw new FailedRequestException("Failed to send HTTP request: " + e.getMessage(), e); + } + } + + /** + * Builds the request URI for document operations using proper URI construction + */ + private URI buildDocumentRequestUri(String docUri, Set categories, Transaction transaction, RequestParameters extraParams) { + try { + // Build the base path + String path = baseUri.getPath(); + if (!path.endsWith("/")) { + path += "/"; + } + path += "documents"; + + // Build query parameters using proper encoding + Map> queryParams = new LinkedHashMap<>(); + + // Document URI - required parameter + addQueryParam(queryParams, "uri", docUri); + + // Database parameter + if (database != null) { + addQueryParam(queryParams, "database", database); + } + + // 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) { + 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()) { + 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); + } + + /** + * Builds a properly encoded query string from parameters + */ + private String buildQueryString(Map> queryParams) { + if (queryParams.isEmpty()) { + return ""; + } + + 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("&")); + } + + /** + * 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"); + } + + // 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/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; } 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); + } + } +}