All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.gravitino.client.HTTPClient Maven / Gradle / Ivy

Go to download

Gravitino is a high-performance, geo-distributed and federated metadata lake.

There is a newer version: 0.6.1-incubating
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.gravitino.client;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.gravitino.auth.AuthConstants;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.exceptions.RESTException;
import org.apache.gravitino.rest.RESTRequest;
import org.apache.gravitino.rest.RESTResponse;
import org.apache.gravitino.rest.RESTUtils;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.impl.EnglishReasonPhraseCatalog;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.net.URIBuilder;

/**
 * An HttpClient for usage with the REST catalog.
 *
 * 

This class provides functionality for making HTTP requests to a REST API and processing the * corresponding responses. It supports common HTTP methods like GET, POST, PUT, DELETE, and HEAD. * Additionally, it allows handling server error responses using a custom error handler. * *

Referred from core/src/main/java/org/apache/iceberg/rest/HTTPClient.java */ public class HTTPClient implements RESTClient { private static final String VERSION_HEADER = "application/vnd.gravitino.v1+json"; private final String uri; private final CloseableHttpClient httpClient; private final ObjectMapper mapper; private final AuthDataProvider authDataProvider; // Handler to be executed before connecting to the server. private final Runnable beforeConnectHandler; // Handler status enum HandlerStatus { // The handler has not been executed yet. Start, // The handler has been executed successfully. Finished, // The handler is currently running. Running, } // The status of the handler. private volatile HandlerStatus handlerStatus = HandlerStatus.Start; /** * Constructs an instance of HTTPClient with the provided information. * * @param uri The base URI of the REST API. * @param baseHeaders A map of base headers to be included in all HTTP requests. * @param objectMapper The ObjectMapper used for JSON serialization and deserialization. * @param authDataProvider The provider of authentication data. * @param beforeConnectHandler The function to be executed before connecting to the server. */ private HTTPClient( String uri, Map baseHeaders, ObjectMapper objectMapper, AuthDataProvider authDataProvider, Runnable beforeConnectHandler) { this.uri = uri; this.mapper = objectMapper; HttpClientBuilder clientBuilder = HttpClients.custom(); if (baseHeaders != null) { clientBuilder.setDefaultHeaders( baseHeaders.entrySet().stream() .map(e -> new BasicHeader(e.getKey(), e.getValue())) .collect(Collectors.toList())); } this.httpClient = clientBuilder.build(); this.authDataProvider = authDataProvider; if (beforeConnectHandler == null) { handlerStatus = HandlerStatus.Finished; } this.beforeConnectHandler = beforeConnectHandler; } /** * Extracts the response body as a string from the provided HTTP response. * * @param response The HTTP response from which the response body will be extracted. * @return The response body as a string. * @throws RESTException If an error occurs during conversion of the response body to a string. */ private String extractResponseBodyAsString(CloseableHttpResponse response) { try { if (response.getEntity() == null) { return null; } // EntityUtils.toString returns null when HttpEntity.getContent returns null. return EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (IOException | ParseException e) { throw new RESTException(e, "Failed to convert HTTP response body to string"); } } /** * Checks if the response indicates a successful response. * *

According to the spec, the only currently defined/used "success" responses are 200 and 202. * * @param response The response to check for success. * @return True if the response is successful, false otherwise. */ private boolean isSuccessful(CloseableHttpResponse response) { int code = response.getCode(); return code == HttpStatus.SC_OK || code == HttpStatus.SC_ACCEPTED || code == HttpStatus.SC_NO_CONTENT; } /** * Builds an error response based on the provided HTTP response. * *

This method extracts the reason phrase from the response and uses it as the message for the * ErrorResponse. If the reason phrase doesn't exist, it retrieves the standard reason phrase from * the English phrase catalog. * * @param response The response from which the ErrorResponse is built. * @return An ErrorResponse object representing the REST error response. */ private ErrorResponse buildRestErrorResponse(CloseableHttpResponse response) { String responseReason = response.getReasonPhrase(); String message = responseReason != null && !responseReason.isEmpty() ? responseReason : EnglishReasonPhraseCatalog.INSTANCE.getReason(response.getCode(), null /* ignored */); return ErrorResponse.restError(message); } /** * Processes a failed response through the provided error. * *

This method takes a response representing a failed response from an HTTP request. It tries * to parse the response body using the provided parseResponse method. * * @param response The failed response from the HTTP request. * @param responseBody The response body as a string (can be null). * @param errorHandler The error handler (as a Consumer) used to handle the error response. * @throws RESTException If the error handler does not throw an exception or an error occurs * during parsing. */ private void throwFailure( CloseableHttpResponse response, String responseBody, Consumer errorHandler) { ErrorResponse errorResponse = null; if (responseBody != null) { try { if (errorHandler instanceof ErrorHandler) { errorResponse = ((ErrorHandler) errorHandler).parseResponse(response.getCode(), responseBody, mapper); } else { errorResponse = ErrorResponse.unknownError( String.format( "Unknown error handler %s, response body won't be parsed %s", errorHandler.getClass().getName(), responseBody)); } } catch (UncheckedIOException | IllegalArgumentException e) { // It's possible to receive a non-successful response that isn't a properly defined // BaseResponse due to various reasons, such as server misconfiguration or // unanticipated external factors. // In such cases, we handle the situation by building an error response for the user. // Examples of such scenarios include network timeouts or load balancers returning default // 5xx responses. } } if (errorResponse == null) { errorResponse = buildRestErrorResponse(response); } errorHandler.accept(errorResponse); // Throw an exception in case the provided error handler does not throw. throw new RESTException("Unhandled error: %s", errorResponse); } /** * Builds a URI for the HTTP request using the given path and query parameters. * *

This method constructs a URI by combining the base URI (stored in the "uri" field) with the * provided path. If query parameters are provided in the "params" map, they are added to the URI, * ensuring proper encoding of query parameters and that the URI is well-formed. * * @param path The URL path to append to the base URI. * @param params A map of query parameters (key-value pairs) to include in the URI (can be null). * @return The constructed URI for the HTTP request. * @throws RESTException If there is an issue building the URI from the base URI and query * parameters. */ private URI buildUri(String path, Map params) { String baseUri = String.format("%s/%s", uri, path); try { URIBuilder builder = new URIBuilder(baseUri); if (params != null) { params.forEach(builder::addParameter); } return builder.build(); } catch (URISyntaxException e) { throw new RESTException( "Failed to create request URI from base %s, params %s", baseUri, params); } } /** * Executes an HTTP request and processes the corresponding response. * *

This method is a helper function to execute HTTP requests. * * @param method The HTTP method to use (e.g., GET, POST, PUT, DELETE). * @param path The URL path to send the request to. * @param queryParams A map of query parameters (key-value pairs) to include in the request URL * (can be null). * @param requestBody The content to place in the request body (can be null). * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param The class type of the response for deserialization. (Must be registered with the * ObjectMapper). * @return The response entity parsed and converted to its type T. */ private T execute( Method method, String path, Map queryParams, Object requestBody, Class responseType, Map headers, Consumer errorHandler) { return execute( method, path, queryParams, requestBody, responseType, headers, errorHandler, h -> {}); } /** * Executes an HTTP request and processes the corresponding response with support for response * headers. * *

The method constructs the HTTP request using the provided parameters and sends it to the * server. It then processes the server's response, handling successful responses and server error * responses accordingly. * *

Response headers from the server are extracted and passed to the responseHeaders Consumer * for further processing by the caller. * * @param method The HTTP method to use (e.g., GET, POST, PUT, DELETE). * @param path The URL path to send the request to. * @param queryParams A map of query parameters (key-value pairs) to include in the request URL * (can be null). * @param requestBody The content to place in the request body (can be null). * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param responseHeaders The consumer of the response headers for further processing. * @param The class type of the response for deserialization. (Must be registered with the * ObjectMapper). * @return The response entity parsed and converted to its type T. * @throws RESTException If the provided path is malformed, if there is an issue with the HTTP * request or response processing, or if the errorHandler does not throw an exception for * server error responses. */ @SuppressWarnings("deprecation") private T execute( Method method, String path, Map queryParams, Object requestBody, Class responseType, Map headers, Consumer errorHandler, Consumer> responseHeaders) { if (handlerStatus != HandlerStatus.Finished) { performPreConnectHandler(); } if (path.startsWith("/")) { throw new RESTException( "Received a malformed path for a REST request: %s. Paths should not start with /", path); } HttpUriRequestBase request = new HttpUriRequestBase(method.name(), buildUri(path, queryParams)); if (requestBody instanceof Map) { // encode maps as form data, application/x-www-form-urlencoded addRequestHeaders(request, headers, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()); request.setEntity(toFormEncoding((Map) requestBody)); } else if (requestBody != null) { // other request bodies are serialized as JSON, application/json addRequestHeaders(request, headers, ContentType.APPLICATION_JSON.getMimeType()); request.setEntity(toJson(requestBody)); } else { addRequestHeaders(request, headers, ContentType.APPLICATION_JSON.getMimeType()); } if (authDataProvider != null) { request.setHeader( AuthConstants.HTTP_HEADER_AUTHORIZATION, new String(authDataProvider.getTokenData(), StandardCharsets.UTF_8)); } try (CloseableHttpResponse response = httpClient.execute(request)) { Map respHeaders = Maps.newHashMap(); for (Header header : response.getHeaders()) { respHeaders.put(header.getName(), header.getValue()); } responseHeaders.accept(respHeaders); // Skip parsing the response stream for any successful request not expecting a response body if (response.getCode() == HttpStatus.SC_NO_CONTENT || (responseType == null && isSuccessful(response))) { return null; } String responseBody = extractResponseBodyAsString(response); if (!isSuccessful(response)) { // The provided error handler is expected to throw, but a RESTException.java is thrown if // not. throwFailure(response, responseBody, errorHandler); } if (responseBody == null) { throw new RESTException( "Invalid (null) response body for request (expected %s): method=%s, path=%s, status=%d", responseType != null ? responseType.getSimpleName() : "unknown", method.name(), path, response.getCode()); } try { return mapper.readValue(responseBody, responseType); } catch (JsonProcessingException e) { throw new RESTException( e, "Received a success response code of %d, but failed to parse response body into %s", response.getCode(), responseType != null ? responseType.getSimpleName() : "unknown"); } } catch (IOException e) { throw new RESTException(e, "Error occurred while processing %s request", method); } } private synchronized void performPreConnectHandler() { // beforeConnectHandler is a pre-connection handler that needs to be executed before the first // HTTP request. if the handler execute fails, we set the status to Start to retry the handler. if (handlerStatus == HandlerStatus.Start) { handlerStatus = HandlerStatus.Running; try { beforeConnectHandler.run(); handlerStatus = HandlerStatus.Finished; } catch (Exception e) { handlerStatus = HandlerStatus.Start; throw e; } } } /** * Sends an HTTP HEAD request to the specified path and processes the response. * * @param path The URL path to send the HEAD request to. * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. */ @Override public void head(String path, Map headers, Consumer errorHandler) { execute(Method.HEAD, path, null, null, null, headers, errorHandler); } /** * Sends an HTTP GET request to the specified path and processes the response. * * @param path The URL path to send the GET request to. * @param queryParams A map of query parameters (key-value pairs) to include in the request URL * (can be null). * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T get( String path, Map queryParams, Class responseType, Map headers, Consumer errorHandler) { return execute(Method.GET, path, queryParams, null, responseType, headers, errorHandler); } /** * Sends an HTTP POST request to the specified path with the provided request body and processes * the response. * * @param path The URL path to send the POST request to. * @param body The REST body to place in the request body. * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T post( String path, RESTRequest body, Class responseType, Map headers, Consumer errorHandler) { return execute(Method.POST, path, null, body, responseType, headers, errorHandler); } /** * Sends an HTTP POST request to the specified path with the provided request body and processes * the response with support for response headers. * * @param path The URL path to send the POST request to. * @param body The REST request to place in the request body. * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param responseHeaders The consumer of the response headers for further processing. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T post( String path, RESTRequest body, Class responseType, Map headers, Consumer errorHandler, Consumer> responseHeaders) { return execute( Method.POST, path, null, body, responseType, headers, errorHandler, responseHeaders); } /** * Sends an HTTP PUT request to the specified path with the provided request body and processes * the response. * * @param path The URL path to send the PUT request to. * @param body The REST request to place in the request body. * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T put( String path, RESTRequest body, Class responseType, Map headers, Consumer errorHandler) { return execute(Method.PUT, path, null, body, responseType, headers, errorHandler); } /** * Sends an HTTP PUT request to the specified path with the provided request body and processes * the response with support for response headers. * * @param path The URL path to send the PUT request to. * @param body The REST request to place in the request body. * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param responseHeaders The consumer of the response headers for further processing. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T put( String path, RESTRequest body, Class responseType, Map headers, Consumer errorHandler, Consumer> responseHeaders) { return execute( Method.PUT, path, null, body, responseType, headers, errorHandler, responseHeaders); } /** * Sends an HTTP PATCH request to the specified path with the provided request body and processes * the response with support for response headers. * * @param path The URL path to send the PATCH request to. * @param body The REST request to place in the request body. * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @return The response entity parsed and converted to its type T. * @param The class type of the response for deserialization. */ @Override public T patch( String path, RESTRequest body, Class responseType, Map headers, Consumer errorHandler) { return execute(Method.PATCH, path, null, body, responseType, headers, errorHandler); } /** * Sends an HTTP DELETE request to the specified path without query parameters and processes the * response. * * @param path The URL path to send the DELETE request to. * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T delete( String path, Class responseType, Map headers, Consumer errorHandler) { return execute(Method.DELETE, path, null, null, responseType, headers, errorHandler); } /** * Sends an HTTP DELETE request to the specified path with the provided query parameters and * processes the response. * * @param path The URL path to send the DELETE request to. * @param queryParams A map of query parameters (key-value pairs) to include in the request (can * be null). * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T delete( String path, Map queryParams, Class responseType, Map headers, Consumer errorHandler) { return execute(Method.DELETE, path, queryParams, null, responseType, headers, errorHandler); } /** * Sends an HTTP POST request with form data to the specified path and processes the response. * * @param path The URL path to send the POST request to. * @param formData A map of form data (key-value pairs) to include in the request body. * @param responseType The class type of the response for deserialization (Must be registered with * the ObjectMapper). * @param headers A map of request headers (key-value pairs) to include in the request (can be * null). * @param errorHandler The error handler delegated for HTTP responses, which handles server error * responses. * @param The class type of the response for deserialization. * @return The response entity parsed and converted to its type T. */ @Override public T postForm( String path, Map formData, Class responseType, Map headers, Consumer errorHandler) { return execute(Method.POST, path, null, formData, responseType, headers, errorHandler); } /** * Adds the specified request headers to the given HTTP request along with a specified body MIME * type. * * @param request The HTTP request to which headers are to be added. * @param requestHeaders A map of headers to be added to the request. * @param bodyMimeType The MIME type of the request body. */ private void addRequestHeaders( HttpUriRequest request, Map requestHeaders, String bodyMimeType) { // Some systems require the Content-Type header to be set even for empty-bodied requests to // avoid failures. request.setHeader(HttpHeaders.CONTENT_TYPE, bodyMimeType); request.setHeader(HttpHeaders.ACCEPT, VERSION_HEADER); requestHeaders.forEach(request::setHeader); } /** * Closes the underlying HTTP client gracefully. * * @throws IOException If an I/O error occurs while closing the HTTP client. */ @Override public void close() throws IOException { if (authDataProvider != null) { authDataProvider.close(); } httpClient.close(CloseMode.GRACEFUL); } /** * Creates a new instance of the HTTPClient.Builder with the specified properties. * * @param properties A map of properties (key-value pairs) used to configure the HTTP client. * @return A new instance of HTTPClient.Builder with the provided properties. */ public static Builder builder(Map properties) { return new Builder(properties); } /** * Builder class for configuring and creating instances of HTTPClient. * *

This class allows for setting various configuration options for the HTTP client such as base * URI, request headers, and ObjectMapper. */ public static class Builder { @SuppressWarnings("UnusedVariable") private final Map properties; private final Map baseHeaders = Maps.newHashMap(); private String uri; private ObjectMapper mapper = ObjectMapperProvider.objectMapper(); private AuthDataProvider authDataProvider; private Runnable beforeConnectHandler; private Builder(Map properties) { this.properties = properties; } /** * Sets the base URI for the HTTP client. * * @param baseUri The base URI to be used for all HTTP requests. * @return This Builder instance for method chaining. */ public Builder uri(String baseUri) { Preconditions.checkNotNull(baseUri, "Invalid uri for http client: null"); this.uri = RESTUtils.stripTrailingSlash(baseUri); return this; } /** * Adds a single request header to the HTTP client. * * @param key The header name. * @param value The header value. * @return This Builder instance for method chaining. */ public Builder withHeader(String key, String value) { baseHeaders.put(key, value); return this; } /** * Adds multiple request headers to the HTTP client. * * @param headers A map of request headers (key-value pairs) to be included in all HTTP * requests. * @return This Builder instance for method chaining. */ public Builder withHeaders(Map headers) { baseHeaders.putAll(headers); return this; } /** * Sets the custom ObjectMapper for the HTTP client. * * @param objectMapper The custom ObjectMapper to be used for request/response serialization. * @return This Builder instance for method chaining. */ public Builder withObjectMapper(ObjectMapper objectMapper) { this.mapper = objectMapper; return this; } /** * Sets the preConnect handle for the HTTP client. * * @param beforeConnectHandler The handle run before connect to the server . * @return This Builder instance for method chaining. */ public Builder withPreConnectHandler(Runnable beforeConnectHandler) { this.beforeConnectHandler = beforeConnectHandler; return this; } /** * Sets the AuthDataProvider for the HTTP client. * * @param authDataProvider The authDataProvider providing the data used to authenticate. * @return This Builder instance for method chaining. */ public Builder withAuthDataProvider(AuthDataProvider authDataProvider) { this.authDataProvider = authDataProvider; return this; } /** * Builds and returns an instance of the HTTPClient with the configured options. * * @return An instance of HTTPClient with the configured options. */ public HTTPClient build() { return new HTTPClient(uri, baseHeaders, mapper, authDataProvider, beforeConnectHandler); } } private StringEntity toJson(Object requestBody) { try { return new StringEntity(mapper.writeValueAsString(requestBody), StandardCharsets.UTF_8); } catch (JsonProcessingException e) { throw new RESTException(e, "Failed to write request body: %s", requestBody); } } private StringEntity toFormEncoding(Map formData) { return new StringEntity(RESTUtils.encodeFormData(formData)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy