From 715e2db36e88d3901d1825276d35959ed5c32bf4 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Wed, 12 Oct 2022 11:18:16 +0200 Subject: [PATCH 01/10] vertx webclient --- ChangeLog.md | 1 + pom.xml | 60 ++-- .../arangodb/internal/http/CURLLogger.java | 8 +- .../internal/http/HttpConnection.java | 335 ++++++++++-------- .../internal/http/HttpConnectionFactory.java | 2 +- .../internal/http/HttpDeleteWithBody.java | 86 ++--- .../com/arangodb/internal/util/IOUtils.java | 76 ---- 7 files changed, 254 insertions(+), 314 deletions(-) delete mode 100644 src/main/java/com/arangodb/internal/util/IOUtils.java diff --git a/ChangeLog.md b/ChangeLog.md index e45f5540a..0b27cc0df 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - `BaseDocument` and `BaseEdgeDocument` are now `final` - `BaseDocument#getProperties()` and `BaseEdgeDocument#getProperties()` return now an unmodifiable map - changed API method signatures removing throw declarations like: `throws ArangoDBException` (unchecked exception) +- removed passwords from debug level requests logs ### Removed diff --git a/pom.xml b/pom.xml index db102d8d0..1243994ae 100644 --- a/pom.xml +++ b/pom.xml @@ -307,13 +307,23 @@ + - org.slf4j - slf4j-api + org.apache.httpcomponents + httpclient + 4.5.13 + org.apache.httpcomponents - httpclient + httpcore + 4.4.15 + + + + org.slf4j + slf4j-api + 1.7.36 com.fasterxml.jackson.core @@ -327,9 +337,14 @@ com.fasterxml.jackson.core jackson-annotations + + io.vertx + vertx-web-client + com.arangodb jackson-dataformat-velocypack + 3.0.1 true @@ -346,6 +361,7 @@ ch.qos.logback logback-classic + 1.2.11 test @@ -399,39 +415,11 @@ pom - com.arangodb - jackson-dataformat-velocypack - 3.0.1 - - - org.apache.httpcomponents - httpclient - 4.5.13 - - - org.apache.httpcomponents - httpcore - 4.4.15 - - - commons-codec - commons-codec - 1.15 - - - commons-logging - commons-logging - 1.2 - - - org.slf4j - slf4j-api - 1.7.36 - - - ch.qos.logback - logback-classic - 1.2.11 + io.vertx + vertx-stack-depchain + 4.3.4 + pom + import org.junit diff --git a/src/main/java/com/arangodb/internal/http/CURLLogger.java b/src/main/java/com/arangodb/internal/http/CURLLogger.java index 31e4a29a9..edc9a9e5f 100644 --- a/src/main/java/com/arangodb/internal/http/CURLLogger.java +++ b/src/main/java/com/arangodb/internal/http/CURLLogger.java @@ -23,7 +23,6 @@ import com.arangodb.internal.serde.InternalSerde; import com.arangodb.velocystream.Request; import com.arangodb.velocystream.RequestType; -import org.apache.http.auth.Credentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +41,7 @@ private CURLLogger() { public static void log( final String url, final Request request, - final Credentials credentials, + final String user, final String jwt, final InternalSerde util) { final RequestType requestType = request.getRequestType(); @@ -60,9 +59,8 @@ public static void log( buffer.append(" -H '").append(header.getKey()).append(":").append(header.getValue()).append("'"); } } - if (credentials != null) { - buffer.append(" -u ").append(credentials.getUserPrincipal().getName()).append(":") - .append(credentials.getPassword()); + if (user != null) { + buffer.append(" -u ").append(user).append(":"); } if (jwt != null) { buffer.append(" -H ").append("'Authorization: Bearer ").append(jwt).append("'"); diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index 69dbb4dd4..da24a98d5 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -20,40 +20,30 @@ package com.arangodb.internal.http; -import com.arangodb.ArangoDBException; import com.arangodb.DbName; import com.arangodb.Protocol; import com.arangodb.internal.net.Connection; import com.arangodb.internal.net.HostDescription; import com.arangodb.internal.serde.InternalSerde; -import com.arangodb.internal.util.IOUtils; import com.arangodb.internal.util.ResponseUtils; import com.arangodb.velocystream.Request; +import com.arangodb.velocystream.RequestType; import com.arangodb.velocystream.Response; -import org.apache.http.*; -import org.apache.http.auth.AuthenticationException; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.HttpRequestRetryHandler; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.*; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpVersion; +import io.vertx.ext.auth.authentication.UsernamePasswordCredentials; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.ConnectionKeepAliveStrategy; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; -import org.apache.http.impl.auth.BasicScheme; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.message.BasicNameValuePair; -import org.apache.http.protocol.HTTP; -import org.apache.http.ssl.SSLContexts; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +54,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.ExecutionException; import static org.apache.http.HttpHeaders.AUTHORIZATION; @@ -74,70 +64,80 @@ public class HttpConnection implements Connection { private static final Logger LOGGER = LoggerFactory.getLogger(HttpCommunication.class); - private static final ContentType CONTENT_TYPE_APPLICATION_JSON_UTF8 = ContentType.create("application/json", - "utf-8"); + private static final ContentType CONTENT_TYPE_APPLICATION_JSON_UTF8 = ContentType.create("application/json", "utf-8"); private static final ContentType CONTENT_TYPE_VPACK = ContentType.create("application/x-velocypack"); - private final PoolingHttpClientConnectionManager cm; - private final CloseableHttpClient client; private final String user; private final String password; private final InternalSerde util; - private final Boolean useSsl; private final Protocol contentType; - private final HostDescription host; private volatile String jwt = null; + private final WebClient client; private HttpConnection(final HostDescription host, final Integer timeout, final String user, final String password, final Boolean useSsl, final SSLContext sslContext, final HostnameVerifier hostnameVerifier , final InternalSerde util, final Protocol contentType, - final Long ttl, final String httpCookieSpec, - final HttpRequestRetryHandler httpRequestRetryHandler) { + final Long ttl, final String httpCookieSpec) { super(); - this.host = host; this.user = user; this.password = password; - this.useSsl = useSsl; this.util = util; this.contentType = contentType; - final RegistryBuilder registryBuilder = RegistryBuilder - .create(); - if (Boolean.TRUE.equals(useSsl)) { - registryBuilder.register("https", new SSLConnectionSocketFactory( - sslContext != null ? sslContext : SSLContexts.createSystemDefault(), - hostnameVerifier != null ? hostnameVerifier : - SSLConnectionSocketFactory.getDefaultHostnameVerifier() - )); - } else { - registryBuilder.register("http", new PlainConnectionSocketFactory()); - } - cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); - cm.setDefaultMaxPerRoute(1); - cm.setMaxTotal(1); - final RequestConfig.Builder requestConfig = RequestConfig.custom(); - if (timeout != null && timeout >= 0) { - requestConfig.setConnectTimeout(timeout); - requestConfig.setConnectionRequestTimeout(timeout); - requestConfig.setSocketTimeout(timeout); - } - - if (httpCookieSpec != null && httpCookieSpec.length() > 1) { - requestConfig.setCookieSpec(httpCookieSpec); - } - - final ConnectionKeepAliveStrategy keepAliveStrategy = - (response, context) -> HttpConnection.this.getKeepAliveDuration(response); - final HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig.build()) - .setConnectionManager(cm).setKeepAliveStrategy(keepAliveStrategy) - .setRetryHandler(httpRequestRetryHandler != null ? httpRequestRetryHandler : - new DefaultHttpRequestRetryHandler()); - if (ttl != null) { - builder.setConnectionTimeToLive(ttl, TimeUnit.MILLISECONDS); - } - client = builder.build(); + Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(1)); + // TODO: name threads + client = WebClient.create(vertx, new WebClientOptions() + .setUserAgentEnabled(false) + .setFollowRedirects(false) + .setLogActivity(true) + .setKeepAlive(true) + .setTcpKeepAlive(true) + .setPipelining(true) + .setReuseAddress(true) + .setReusePort(true) + .setHttp2ClearTextUpgrade(false) + //TODO: allow configuring HTTP_2 or HTTP_1_1 + .setProtocolVersion(HttpVersion.HTTP_2) + .setDefaultHost(host.getHost()) + .setDefaultPort(host.getPort())); + +// final RegistryBuilder registryBuilder = RegistryBuilder +// .create(); +// if (Boolean.TRUE.equals(useSsl)) { +// registryBuilder.register("https", new SSLConnectionSocketFactory( +// sslContext != null ? sslContext : SSLContexts.createSystemDefault(), +// hostnameVerifier != null ? hostnameVerifier : +// SSLConnectionSocketFactory.getDefaultHostnameVerifier() +// )); +// } else { +// registryBuilder.register("http", new PlainConnectionSocketFactory()); +// } +// cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); +// cm.setDefaultMaxPerRoute(1); +// cm.setMaxTotal(1); +// final RequestConfig.Builder requestConfig = RequestConfig.custom(); +// if (timeout != null && timeout >= 0) { +// requestConfig.setConnectTimeout(timeout); +// requestConfig.setConnectionRequestTimeout(timeout); +// requestConfig.setSocketTimeout(timeout); +// } +// +// if (httpCookieSpec != null && httpCookieSpec.length() > 1) { +// requestConfig.setCookieSpec(httpCookieSpec); +// } +// +// final ConnectionKeepAliveStrategy keepAliveStrategy = +// (response, context) -> HttpConnection.this.getKeepAliveDuration(response); +// final HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig.build()) +// .setConnectionManager(cm).setKeepAliveStrategy(keepAliveStrategy) +// .setRetryHandler(httpRequestRetryHandler != null ? httpRequestRetryHandler : +// new DefaultHttpRequestRetryHandler()); +// if (ttl != null) { +// builder.setConnectionTimeToLive(ttl, TimeUnit.MILLISECONDS); +// } +// client = builder.build(); } - private static String buildUrl(final String baseUrl, final Request request) { - StringBuilder sb = new StringBuilder().append(baseUrl); + private static String buildUrl(final Request request) { + StringBuilder sb = new StringBuilder(); DbName dbName = request.getDbName(); if (dbName != null && !dbName.get().isEmpty()) { sb.append("/_db/").append(dbName.getEncoded()); @@ -165,119 +165,148 @@ private static List toList(final Map parameters) return paramList; } - private static void addHeader(final Request request, final HttpRequestBase httpRequest) { + private static void addHeader(final Request request, final HttpRequest httpRequest) { for (final Entry header : request.getHeaderParam().entrySet()) { - httpRequest.addHeader(header.getKey(), header.getValue()); + httpRequest.putHeader(header.getKey(), header.getValue()); } } - private long getKeepAliveDuration(final HttpResponse response) { - final HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); - while (it.hasNext()) { - final HeaderElement he = it.nextElement(); - final String param = he.getName(); - final String value = he.getValue(); - if (value != null && "timeout".equalsIgnoreCase(param)) { - try { - return Long.parseLong(value) * 1000L; - } catch (final NumberFormatException ignore) { - } - } - } - return 30L * 1000L; - } +// private long getKeepAliveDuration(final HttpResponse response) { +// final HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); +// while (it.hasNext()) { +// final HeaderElement he = it.nextElement(); +// final String param = he.getName(); +// final String value = he.getValue(); +// if (value != null && "timeout".equalsIgnoreCase(param)) { +// try { +// return Long.parseLong(value) * 1000L; +// } catch (final NumberFormatException ignore) { +// } +// } +// } +// return 30L * 1000L; +// } @Override public void close() throws IOException { - cm.shutdown(); - client.close(); +// cm.shutdown(); +// client.close(); } - private HttpRequestBase buildHttpRequestBase(final Request request, final String url) { - final HttpRequestBase httpRequest; - switch (request.getRequestType()) { + private HttpMethod requestTypeToHttpMethod(RequestType requestType) { + switch (requestType) { case POST: - httpRequest = requestWithBody(new HttpPost(url), request); - break; + return HttpMethod.POST; case PUT: - httpRequest = requestWithBody(new HttpPut(url), request); - break; + return HttpMethod.PUT; case PATCH: - httpRequest = requestWithBody(new HttpPatch(url), request); - break; + return HttpMethod.PATCH; case DELETE: - httpRequest = requestWithBody(new HttpDeleteWithBody(url), request); - break; + return HttpMethod.DELETE; case HEAD: - httpRequest = new HttpHead(url); - break; + return HttpMethod.HEAD; case GET: default: - httpRequest = new HttpGet(url); - break; - } - return httpRequest; - } - - private HttpRequestBase requestWithBody(final HttpEntityEnclosingRequestBase httpRequest, final Request request) { - final byte[] body = request.getBody(); - if (body != null) { - if (contentType == Protocol.HTTP_VPACK) { - httpRequest.setEntity(new ByteArrayEntity(body, CONTENT_TYPE_VPACK)); - } else { - httpRequest.setEntity(new ByteArrayEntity(body, CONTENT_TYPE_APPLICATION_JSON_UTF8)); - } + return HttpMethod.GET; } - return httpRequest; } - private String buildBaseUrl(final HostDescription host) { - return (Boolean.TRUE.equals(useSsl) ? "https://" : "http://") + host.getHost() + ":" + host.getPort(); - } +// private HttpRequestBase buildHttpRequestBase(final Request request, final String url) { +// final HttpRequestBase httpRequest; +// switch (request.getRequestType()) { +// case POST: +// httpRequest = requestWithBody(new HttpPost(url), request); +// break; +// case PUT: +// httpRequest = requestWithBody(new HttpPut(url), request); +// break; +// case PATCH: +// httpRequest = requestWithBody(new HttpPatch(url), request); +// break; +// case DELETE: +// httpRequest = requestWithBody(new HttpDeleteWithBody(url), request); +// break; +// case HEAD: +// httpRequest = new HttpHead(url); +// break; +// case GET: +// default: +// httpRequest = new HttpGet(url); +// break; +// } +// return httpRequest; +// } + +// private HttpRequestBase requestWithBody(final HttpEntityEnclosingRequestBase httpRequest, final Request request) { +// final byte[] body = request.getBody(); +// if (body != null) { +// if (contentType == Protocol.HTTP_VPACK) { +// httpRequest.setEntity(new ByteArrayEntity(body, CONTENT_TYPE_VPACK)); +// } else { +// httpRequest.setEntity(new ByteArrayEntity(body, CONTENT_TYPE_APPLICATION_JSON_UTF8)); +// } +// } +// return httpRequest; +// } public Response execute(final Request request) throws IOException { - final String url = buildUrl(buildBaseUrl(host), request); - final HttpRequestBase httpRequest = buildHttpRequestBase(request, url); - httpRequest.setHeader("User-Agent", "Mozilla/5.0 (compatible; ArangoDB-JavaDriver/1.1; +http://mt.orz.at/)"); + String url = buildUrl(request); + HttpRequest httpRequest = client.request(requestTypeToHttpMethod(request.getRequestType()), url); if (contentType == Protocol.HTTP_VPACK) { - httpRequest.setHeader("Accept", "application/x-velocypack"); + httpRequest.putHeader("Accept", "application/x-velocypack"); } addHeader(request, httpRequest); - Credentials credentials = null; if (jwt != null) { - httpRequest.addHeader(AUTHORIZATION, "Bearer " + jwt); + httpRequest.putHeader(AUTHORIZATION, "Bearer " + jwt); + if (LOGGER.isDebugEnabled()) { + CURLLogger.log(url, request, null, jwt, util); + } } else if (user != null) { - credentials = new UsernamePasswordCredentials(user, password != null ? password : ""); - try { - httpRequest.addHeader(new BasicScheme().authenticate(credentials, httpRequest, null)); - } catch (final AuthenticationException e) { - throw new ArangoDBException(e); + // FIXME: credentials as class field + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(user, password != null ? password : ""); + httpRequest.authentication(credentials); + if (LOGGER.isDebugEnabled()) { + CURLLogger.log(url, request, user, jwt, util); + } + } + + byte[] reqBody = request.getBody(); + Buffer buffer; + if (reqBody != null) { + buffer = Buffer.buffer(reqBody); + if (contentType == Protocol.HTTP_VPACK) { + httpRequest.putHeader(HttpHeaders.CONTENT_TYPE.toString(), CONTENT_TYPE_VPACK.toString()); + } else { + httpRequest.putHeader(HttpHeaders.CONTENT_TYPE.toString(), CONTENT_TYPE_APPLICATION_JSON_UTF8.toString()); } + } else { + buffer = Buffer.buffer(); } - if (LOGGER.isDebugEnabled()) { - CURLLogger.log(url, request, credentials, jwt, util); + HttpResponse bufferResponse; + try { + // FIXME: make async API + bufferResponse = httpRequest.sendBuffer(buffer).toCompletionStage().toCompletableFuture().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); } - Response response; - response = buildResponse(client.execute(httpRequest)); + Response response = buildResponse(bufferResponse); checkError(response); return response; } - public Response buildResponse(final CloseableHttpResponse httpResponse) - throws UnsupportedOperationException, IOException { + public Response buildResponse(final HttpResponse httpResponse) throws UnsupportedOperationException { final Response response = new Response(); - response.setResponseCode(httpResponse.getStatusLine().getStatusCode()); - final HttpEntity entity = httpResponse.getEntity(); - if (entity != null && entity.getContent() != null) { - final byte[] content = IOUtils.toByteArray(entity.getContent()); - if (content.length > 0) { - response.setBody(content); + response.setResponseCode(httpResponse.statusCode()); + Buffer body = httpResponse.body(); + if (body != null) { + byte[] bytes = body.getBytes(); + if (bytes.length > 0) { + response.setBody(bytes); } } - final Header[] headers = httpResponse.getAllHeaders(); final Map meta = response.getMeta(); - for (final Header header : headers) { - meta.put(header.getName(), header.getValue()); + for (Entry header : httpResponse.headers()) { + meta.put(header.getKey(), header.getValue()); } return response; } @@ -303,7 +332,7 @@ public static class Builder { private SSLContext sslContext; private HostnameVerifier hostnameVerifier; private Integer timeout; - private HttpRequestRetryHandler httpRequestRetryHandler; +// private HttpRequestRetryHandler httpRequestRetryHandler; public Builder user(final String user) { this.user = user; @@ -360,14 +389,14 @@ public Builder timeout(final Integer timeout) { return this; } - public Builder httpRequestRetryHandler(final HttpRequestRetryHandler httpRequestRetryHandler) { - this.httpRequestRetryHandler = httpRequestRetryHandler; - return this; - } +// public Builder httpRequestRetryHandler(final HttpRequestRetryHandler httpRequestRetryHandler) { +// this.httpRequestRetryHandler = httpRequestRetryHandler; +// return this; +// } public HttpConnection build() { return new HttpConnection(host, timeout, user, password, useSsl, sslContext, hostnameVerifier, util, - contentType, ttl, httpCookieSpec, httpRequestRetryHandler); + contentType, ttl, httpCookieSpec); } } diff --git a/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java b/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java index d39d5375a..ec40e54a7 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java @@ -44,7 +44,7 @@ public HttpConnectionFactory(final Integer timeout, final String user, final Str super(); builder = new HttpConnection.Builder().timeout(timeout).user(user).password(password).useSsl(useSsl) .sslContext(sslContext).hostnameVerifier(hostnameVerifier).serializationUtil(util).contentType(protocol) - .ttl(connectionTtl).httpCookieSpec(httpCookieSpec).httpRequestRetryHandler(httpRequestRetryHandler); + .ttl(connectionTtl).httpCookieSpec(httpCookieSpec); //.httpRequestRetryHandler(httpRequestRetryHandler); } diff --git a/src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java b/src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java index 080e01974..25082672c 100644 --- a/src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java +++ b/src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java @@ -1,43 +1,43 @@ -/* - * DISCLAIMER - * - * Copyright 2016 ArangoDB GmbH, Cologne, Germany - * - * Licensed 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. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.internal.http; - -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; - -import java.net.URI; - -/** - * @author Mark Vollmary - */ -public class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase { - public final static String METHOD_NAME = "DELETE"; - - public HttpDeleteWithBody(final String uri) { - super(); - setURI(URI.create(uri)); - } - - @Override - public String getMethod() { - return METHOD_NAME; - } - -} +/* + * DISCLAIMER + * + * Copyright 2016 ArangoDB GmbH, Cologne, Germany + * + * Licensed 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.internal.http; + +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; + +import java.net.URI; + +/** + * @author Mark Vollmary + */ +public class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase { + public final static String METHOD_NAME = "DELETE"; + + public HttpDeleteWithBody(final String uri) { + super(); + setURI(URI.create(uri)); + } + + @Override + public String getMethod() { + return METHOD_NAME; + } + +} diff --git a/src/main/java/com/arangodb/internal/util/IOUtils.java b/src/main/java/com/arangodb/internal/util/IOUtils.java deleted file mode 100644 index c2985c4f6..000000000 --- a/src/main/java/com/arangodb/internal/util/IOUtils.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2016 ArangoDB GmbH, Cologne, Germany - * - * Licensed 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. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.internal.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; - -/** - * @author Mark Vollmary - */ -public final class IOUtils { - - private static final Logger LOGGER = LoggerFactory.getLogger(IOUtils.class); - - private IOUtils() { - } - - public static String toString(final InputStream input) throws IOException { - return toString(input, "utf-8"); - } - - public static String toString(final InputStream input, final String encode) throws IOException { - try { - final StringBuilder buffer = new StringBuilder(8012); - final InputStreamReader in = new InputStreamReader(new BufferedInputStream(input), encode); - final char[] cbuf = new char[8012]; - int len; - while ((len = in.read(cbuf)) != -1) { - buffer.append(cbuf, 0, len); - } - return buffer.toString(); - } catch (final UnsupportedEncodingException e) { - throw new RuntimeException(e); - } finally { - if (input != null) { - try { - input.close(); - } catch (final IOException e) { - // TODO - } - } - } - } - - public static byte[] toByteArray(final InputStream input) throws IOException { - final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - final byte[] data = new byte[8012]; - while ((nRead = input.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, nRead); - } - buffer.flush(); - return buffer.toByteArray(); - } - -} From 215f3d0e0c3088e63126e554b1c37a03d129945e Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Wed, 12 Oct 2022 12:05:17 +0200 Subject: [PATCH 02/10] fixed CURLLogger --- .../com/arangodb/internal/http/CURLLogger.java | 5 +++-- .../com/arangodb/internal/http/HttpConnection.java | 14 ++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/arangodb/internal/http/CURLLogger.java b/src/main/java/com/arangodb/internal/http/CURLLogger.java index edc9a9e5f..2b2adfdce 100644 --- a/src/main/java/com/arangodb/internal/http/CURLLogger.java +++ b/src/main/java/com/arangodb/internal/http/CURLLogger.java @@ -39,7 +39,8 @@ private CURLLogger() { } public static void log( - final String url, + final String baseUrl, + final String path, final Request request, final String user, final String jwt, @@ -68,7 +69,7 @@ public static void log( if (includeBody) { buffer.append(" -d @-"); } - buffer.append(" '").append(url).append("'"); + buffer.append(" '").append(baseUrl).append(path).append("'"); if (includeBody) { buffer.append("\n"); buffer.append(util.toJsonString(request.getBody())); diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index da24a98d5..4563ff26b 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -69,6 +69,7 @@ public class HttpConnection implements Connection { private final String user; private final String password; private final InternalSerde util; + private final String baseUrl; private final Protocol contentType; private volatile String jwt = null; private final WebClient client; @@ -82,6 +83,7 @@ private HttpConnection(final HostDescription host, final Integer timeout, final this.password = password; this.util = util; this.contentType = contentType; + baseUrl = buildBaseUrl(host, useSsl); Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(1)); // TODO: name threads client = WebClient.create(vertx, new WebClientOptions() @@ -249,9 +251,13 @@ private HttpMethod requestTypeToHttpMethod(RequestType requestType) { // return httpRequest; // } + private String buildBaseUrl(HostDescription host, boolean useSsl) { + return (Boolean.TRUE.equals(useSsl) ? "https://" : "http://") + host.getHost() + ":" + host.getPort(); + } + public Response execute(final Request request) throws IOException { - String url = buildUrl(request); - HttpRequest httpRequest = client.request(requestTypeToHttpMethod(request.getRequestType()), url); + String path = buildUrl(request); + HttpRequest httpRequest = client.request(requestTypeToHttpMethod(request.getRequestType()), path); if (contentType == Protocol.HTTP_VPACK) { httpRequest.putHeader("Accept", "application/x-velocypack"); } @@ -259,14 +265,14 @@ public Response execute(final Request request) throws IOException { if (jwt != null) { httpRequest.putHeader(AUTHORIZATION, "Bearer " + jwt); if (LOGGER.isDebugEnabled()) { - CURLLogger.log(url, request, null, jwt, util); + CURLLogger.log(baseUrl, path, request, null, jwt, util); } } else if (user != null) { // FIXME: credentials as class field UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(user, password != null ? password : ""); httpRequest.authentication(credentials); if (LOGGER.isDebugEnabled()) { - CURLLogger.log(url, request, user, jwt, util); + CURLLogger.log(baseUrl, path, request, user, jwt, util); } } From 3c2b6564cdb0f6123b36412f62009f32e27aebb2 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Wed, 12 Oct 2022 13:25:21 +0200 Subject: [PATCH 03/10] ttl and timeout --- .../internal/http/HttpConnection.java | 106 +++--------------- 1 file changed, 17 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index 4563ff26b..7e2334dbb 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -55,6 +55,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import static org.apache.http.HttpHeaders.AUTHORIZATION; @@ -73,20 +74,29 @@ public class HttpConnection implements Connection { private final Protocol contentType; private volatile String jwt = null; private final WebClient client; + private final Integer timeout; private HttpConnection(final HostDescription host, final Integer timeout, final String user, final String password, - final Boolean useSsl, final SSLContext sslContext, final HostnameVerifier hostnameVerifier - , final InternalSerde util, final Protocol contentType, - final Long ttl, final String httpCookieSpec) { + final Boolean useSsl, final SSLContext sslContext, final HostnameVerifier hostnameVerifier, + final InternalSerde util, final Protocol contentType, final Long ttl, + final String httpCookieSpec) { super(); this.user = user; this.password = password; this.util = util; this.contentType = contentType; + this.timeout = timeout; baseUrl = buildBaseUrl(host, useSsl); Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(1)); // TODO: name threads + + int _ttl = ttl == null ? 0 : Math.toIntExact(ttl / 1000); client = WebClient.create(vertx, new WebClientOptions() + .setConnectTimeout(timeout) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) + .setIdleTimeout(timeout) + .setKeepAliveTimeout(_ttl) + .setHttp2KeepAliveTimeout(_ttl) .setUserAgentEnabled(false) .setFollowRedirects(false) .setLogActivity(true) @@ -101,41 +111,16 @@ private HttpConnection(final HostDescription host, final Integer timeout, final .setDefaultHost(host.getHost()) .setDefaultPort(host.getPort())); -// final RegistryBuilder registryBuilder = RegistryBuilder -// .create(); // if (Boolean.TRUE.equals(useSsl)) { // registryBuilder.register("https", new SSLConnectionSocketFactory( // sslContext != null ? sslContext : SSLContexts.createSystemDefault(), // hostnameVerifier != null ? hostnameVerifier : // SSLConnectionSocketFactory.getDefaultHostnameVerifier() // )); -// } else { -// registryBuilder.register("http", new PlainConnectionSocketFactory()); -// } -// cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); -// cm.setDefaultMaxPerRoute(1); -// cm.setMaxTotal(1); -// final RequestConfig.Builder requestConfig = RequestConfig.custom(); -// if (timeout != null && timeout >= 0) { -// requestConfig.setConnectTimeout(timeout); -// requestConfig.setConnectionRequestTimeout(timeout); -// requestConfig.setSocketTimeout(timeout); // } -// // if (httpCookieSpec != null && httpCookieSpec.length() > 1) { // requestConfig.setCookieSpec(httpCookieSpec); // } -// -// final ConnectionKeepAliveStrategy keepAliveStrategy = -// (response, context) -> HttpConnection.this.getKeepAliveDuration(response); -// final HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig.build()) -// .setConnectionManager(cm).setKeepAliveStrategy(keepAliveStrategy) -// .setRetryHandler(httpRequestRetryHandler != null ? httpRequestRetryHandler : -// new DefaultHttpRequestRetryHandler()); -// if (ttl != null) { -// builder.setConnectionTimeToLive(ttl, TimeUnit.MILLISECONDS); -// } -// client = builder.build(); } private static String buildUrl(final Request request) { @@ -173,24 +158,9 @@ private static void addHeader(final Request request, final HttpRequest httpRe } } -// private long getKeepAliveDuration(final HttpResponse response) { -// final HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); -// while (it.hasNext()) { -// final HeaderElement he = it.nextElement(); -// final String param = he.getName(); -// final String value = he.getValue(); -// if (value != null && "timeout".equalsIgnoreCase(param)) { -// try { -// return Long.parseLong(value) * 1000L; -// } catch (final NumberFormatException ignore) { -// } -// } -// } -// return 30L * 1000L; -// } - @Override public void close() throws IOException { + // TODO // cm.shutdown(); // client.close(); } @@ -213,51 +183,15 @@ private HttpMethod requestTypeToHttpMethod(RequestType requestType) { } } -// private HttpRequestBase buildHttpRequestBase(final Request request, final String url) { -// final HttpRequestBase httpRequest; -// switch (request.getRequestType()) { -// case POST: -// httpRequest = requestWithBody(new HttpPost(url), request); -// break; -// case PUT: -// httpRequest = requestWithBody(new HttpPut(url), request); -// break; -// case PATCH: -// httpRequest = requestWithBody(new HttpPatch(url), request); -// break; -// case DELETE: -// httpRequest = requestWithBody(new HttpDeleteWithBody(url), request); -// break; -// case HEAD: -// httpRequest = new HttpHead(url); -// break; -// case GET: -// default: -// httpRequest = new HttpGet(url); -// break; -// } -// return httpRequest; -// } - -// private HttpRequestBase requestWithBody(final HttpEntityEnclosingRequestBase httpRequest, final Request request) { -// final byte[] body = request.getBody(); -// if (body != null) { -// if (contentType == Protocol.HTTP_VPACK) { -// httpRequest.setEntity(new ByteArrayEntity(body, CONTENT_TYPE_VPACK)); -// } else { -// httpRequest.setEntity(new ByteArrayEntity(body, CONTENT_TYPE_APPLICATION_JSON_UTF8)); -// } -// } -// return httpRequest; -// } - private String buildBaseUrl(HostDescription host, boolean useSsl) { return (Boolean.TRUE.equals(useSsl) ? "https://" : "http://") + host.getHost() + ":" + host.getPort(); } public Response execute(final Request request) throws IOException { String path = buildUrl(request); - HttpRequest httpRequest = client.request(requestTypeToHttpMethod(request.getRequestType()), path); + HttpRequest httpRequest = client + .request(requestTypeToHttpMethod(request.getRequestType()), path) + .timeout(timeout); if (contentType == Protocol.HTTP_VPACK) { httpRequest.putHeader("Accept", "application/x-velocypack"); } @@ -338,7 +272,6 @@ public static class Builder { private SSLContext sslContext; private HostnameVerifier hostnameVerifier; private Integer timeout; -// private HttpRequestRetryHandler httpRequestRetryHandler; public Builder user(final String user) { this.user = user; @@ -395,11 +328,6 @@ public Builder timeout(final Integer timeout) { return this; } -// public Builder httpRequestRetryHandler(final HttpRequestRetryHandler httpRequestRetryHandler) { -// this.httpRequestRetryHandler = httpRequestRetryHandler; -// return this; -// } - public HttpConnection build() { return new HttpConnection(host, timeout, user, password, useSsl, sslContext, hostnameVerifier, util, contentType, ttl, httpCookieSpec); From d2587b47bb5c2d445277da115c771b0a25f71354 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Wed, 12 Oct 2022 13:55:08 +0200 Subject: [PATCH 04/10] credentials refactoring --- .../arangodb/internal/http/CURLLogger.java | 8 ---- .../internal/http/HttpConnection.java | 41 ++++++++----------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/arangodb/internal/http/CURLLogger.java b/src/main/java/com/arangodb/internal/http/CURLLogger.java index 2b2adfdce..9bfda6641 100644 --- a/src/main/java/com/arangodb/internal/http/CURLLogger.java +++ b/src/main/java/com/arangodb/internal/http/CURLLogger.java @@ -42,8 +42,6 @@ public static void log( final String baseUrl, final String path, final Request request, - final String user, - final String jwt, final InternalSerde util) { final RequestType requestType = request.getRequestType(); final boolean includeBody = (requestType == RequestType.POST || requestType == RequestType.PUT @@ -60,12 +58,6 @@ public static void log( buffer.append(" -H '").append(header.getKey()).append(":").append(header.getValue()).append("'"); } } - if (user != null) { - buffer.append(" -u ").append(user).append(":"); - } - if (jwt != null) { - buffer.append(" -H ").append("'Authorization: Bearer ").append(jwt).append("'"); - } if (includeBody) { buffer.append(" -d @-"); } diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index 7e2334dbb..ed4b330ea 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -35,6 +35,7 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpVersion; +import io.vertx.ext.auth.authentication.TokenCredentials; import io.vertx.ext.auth.authentication.UsernamePasswordCredentials; import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.HttpResponse; @@ -56,6 +57,7 @@ import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static org.apache.http.HttpHeaders.AUTHORIZATION; @@ -63,32 +65,30 @@ * @author Mark Vollmary */ public class HttpConnection implements Connection { - private static final Logger LOGGER = LoggerFactory.getLogger(HttpCommunication.class); private static final ContentType CONTENT_TYPE_APPLICATION_JSON_UTF8 = ContentType.create("application/json", "utf-8"); private static final ContentType CONTENT_TYPE_VPACK = ContentType.create("application/x-velocypack"); - private final String user; - private final String password; + private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private final InternalSerde util; private final String baseUrl; private final Protocol contentType; - private volatile String jwt = null; + private volatile String auth; private final WebClient client; private final Integer timeout; + private final Vertx vertx; private HttpConnection(final HostDescription host, final Integer timeout, final String user, final String password, final Boolean useSsl, final SSLContext sslContext, final HostnameVerifier hostnameVerifier, final InternalSerde util, final Protocol contentType, final Long ttl, final String httpCookieSpec) { super(); - this.user = user; - this.password = password; this.util = util; this.contentType = contentType; this.timeout = timeout; baseUrl = buildBaseUrl(host, useSsl); - Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(1)); - // TODO: name threads + auth = new UsernamePasswordCredentials(user, password != null ? password : "").toHttpAuthorization(); + vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(1)); + vertx.runOnContext(e -> Thread.currentThread().setName("adb-eventloop-" + THREAD_COUNT.getAndIncrement())); int _ttl = ttl == null ? 0 : Math.toIntExact(ttl / 1000); client = WebClient.create(vertx, new WebClientOptions() @@ -160,9 +160,8 @@ private static void addHeader(final Request request, final HttpRequest httpRe @Override public void close() throws IOException { - // TODO -// cm.shutdown(); -// client.close(); + client.close(); + vertx.close(); } private HttpMethod requestTypeToHttpMethod(RequestType requestType) { @@ -196,18 +195,10 @@ public Response execute(final Request request) throws IOException { httpRequest.putHeader("Accept", "application/x-velocypack"); } addHeader(request, httpRequest); - if (jwt != null) { - httpRequest.putHeader(AUTHORIZATION, "Bearer " + jwt); - if (LOGGER.isDebugEnabled()) { - CURLLogger.log(baseUrl, path, request, null, jwt, util); - } - } else if (user != null) { - // FIXME: credentials as class field - UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(user, password != null ? password : ""); - httpRequest.authentication(credentials); - if (LOGGER.isDebugEnabled()) { - CURLLogger.log(baseUrl, path, request, user, jwt, util); - } + httpRequest.putHeader(AUTHORIZATION, auth); + + if (LOGGER.isDebugEnabled()) { + CURLLogger.log(baseUrl, path, request, util); } byte[] reqBody = request.getBody(); @@ -257,7 +248,9 @@ protected void checkError(final Response response) { @Override public void setJwt(String jwt) { - this.jwt = jwt; + if (jwt != null) { + auth = new TokenCredentials(jwt).toHttpAuthorization(); + } } public static class Builder { From 5c4f5884bd9266dbfa0473a39e56a02b82d4fd77 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Mon, 17 Oct 2022 11:55:07 +0200 Subject: [PATCH 05/10] BenchmarkTest --- .../internal/http/HttpConnection.java | 2 +- src/test/java/perf/Benchmark.java | 123 ++++++++++++++++++ src/test/java/perf/SyncBenchmarkTest.java | 60 +++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/test/java/perf/Benchmark.java create mode 100644 src/test/java/perf/SyncBenchmarkTest.java diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index ed4b330ea..a49fe7f2b 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -87,7 +87,7 @@ private HttpConnection(final HostDescription host, final Integer timeout, final this.timeout = timeout; baseUrl = buildBaseUrl(host, useSsl); auth = new UsernamePasswordCredentials(user, password != null ? password : "").toHttpAuthorization(); - vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(1)); + vertx = Vertx.vertx(new VertxOptions().setPreferNativeTransport(true).setEventLoopPoolSize(1)); vertx.runOnContext(e -> Thread.currentThread().setName("adb-eventloop-" + THREAD_COUNT.getAndIncrement())); int _ttl = ttl == null ? 0 : Math.toIntExact(ttl / 1000); diff --git a/src/test/java/perf/Benchmark.java b/src/test/java/perf/Benchmark.java new file mode 100644 index 000000000..8fade9e17 --- /dev/null +++ b/src/test/java/perf/Benchmark.java @@ -0,0 +1,123 @@ +package perf; + +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class Benchmark { + + private static final int SYNC_THREADS = 128; + private final CountDownLatch completed = new CountDownLatch(1); + private volatile Long startTime = null; + private volatile Long endTime = null; + private volatile int targetCount = Integer.MAX_VALUE; + private final AtomicInteger counter = new AtomicInteger(); + private final ExecutorService es = Executors.newFixedThreadPool(SYNC_THREADS); + private final int warmupDurationSeconds; + private final int numberOfRequests; + + public Benchmark(int warmupDurationSeconds, int numberOfRequests) { + this.warmupDurationSeconds = warmupDurationSeconds; + this.numberOfRequests = numberOfRequests; + } + + public void run() { + // warmup + startBenchmark(); + + // start monitor / warmup + startMonitor(); + + // start benchmark + startMeasuring(); + } + + private void startMonitor() { + for (int i = 0; i < warmupDurationSeconds; i++) { + counter.set(0); + long start = new Date().getTime(); + try { + Thread.sleep(1_000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + long current = new Date().getTime(); + long elapsed = current - start; + double reqsPerSec = 1_000.0 * counter.get() / elapsed; + System.out.println("reqs/s: \t" + reqsPerSec); + } + } + + private void startBenchmark() { + start(); + new Thread(() -> { + try { + completed.await(); + // wait graceful shutdown + Thread.sleep(1_000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // force shutdown + es.shutdown(); + shutdown(); + }).start(); + } + + private void startMeasuring() { + counter.set(0); + targetCount = numberOfRequests; + startTime = System.currentTimeMillis(); + } + + public long waitComplete() { + try { + completed.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return endTime - startTime; + } + + /** + * @return req/s + */ + public long getThroughput() { + return targetCount * 1000L / (endTime - startTime); + } + + /** + * notify the success of #count requests + * + * @return whether more requests should be performed + */ + private boolean success() { + if (endTime != null) return false; + if (counter.addAndGet(1) >= targetCount) { + endTime = System.currentTimeMillis(); + completed.countDown(); + return false; + } + return true; + } + + private void start() { + for (int i = 0; i < SYNC_THREADS; i++) { + es.execute(() -> { + boolean more = true; + while (more) { + sendRequest(); + more = success(); + } + }); + } + } + + protected abstract void sendRequest(); + + protected abstract void shutdown(); + +} diff --git a/src/test/java/perf/SyncBenchmarkTest.java b/src/test/java/perf/SyncBenchmarkTest.java new file mode 100644 index 000000000..fb8fea25f --- /dev/null +++ b/src/test/java/perf/SyncBenchmarkTest.java @@ -0,0 +1,60 @@ +package perf; + +import com.arangodb.ArangoDB; +import com.arangodb.DbName; +import com.arangodb.Protocol; +import com.arangodb.velocystream.Request; +import com.arangodb.velocystream.RequestType; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@Disabled +public class SyncBenchmarkTest { + private final int warmupDurationSeconds = 15; + private final int numberOfRequests = 1_000_000; + + @ParameterizedTest + @EnumSource(Protocol.class) + void getVersion(Protocol protocol) { + ArangoDB adb = new ArangoDB.Builder().useProtocol(protocol).build(); + Benchmark benchmark = new Benchmark(warmupDurationSeconds, numberOfRequests) { + @Override + protected void sendRequest() { + adb.getVersion(); + } + + @Override + protected void shutdown() { + adb.shutdown(); + } + }; + benchmark.run(); + System.out.println("elapsed time [ms]: \t" + benchmark.waitComplete()); + System.out.println("throughput [req/s]: \t" + benchmark.getThroughput()); + } + + @ParameterizedTest + @EnumSource(Protocol.class) + void getVersionWithDetails(Protocol protocol) { + ArangoDB adb = new ArangoDB.Builder().useProtocol(protocol).build(); + Benchmark benchmark = new Benchmark(warmupDurationSeconds, numberOfRequests) { + private final Request request = new Request(DbName.SYSTEM, RequestType.GET, + "/_api/version").putQueryParam("details", true); + + @Override + protected void sendRequest() { + adb.execute(request); + } + + @Override + protected void shutdown() { + adb.shutdown(); + } + }; + benchmark.run(); + System.out.println("elapsed time [ms]: \t" + benchmark.waitComplete()); + System.out.println("throughput [req/s]: \t" + benchmark.getThroughput()); + } + +} From 90a45c6946d73e809d5a56f9039957bb6a8ce229 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Mon, 17 Oct 2022 15:35:30 +0200 Subject: [PATCH 06/10] ssl config --- .../internal/http/HttpConnection.java | 71 ++++++++++++++++--- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index a49fe7f2b..f7eae8dfc 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -20,6 +20,7 @@ package com.arangodb.internal.http; +import com.arangodb.ArangoDBException; import com.arangodb.DbName; import com.arangodb.Protocol; import com.arangodb.internal.net.Connection; @@ -29,12 +30,18 @@ import com.arangodb.velocystream.Request; import com.arangodb.velocystream.RequestType; import com.arangodb.velocystream.Response; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; +import io.netty.handler.ssl.JdkSslContext; import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.JdkSSLEngineOptions; +import io.vertx.core.spi.tls.SslContextFactory; import io.vertx.ext.auth.authentication.TokenCredentials; import io.vertx.ext.auth.authentication.UsernamePasswordCredentials; import io.vertx.ext.web.client.HttpRequest; @@ -43,6 +50,7 @@ import io.vertx.ext.web.client.WebClientOptions; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.entity.ContentType; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; @@ -51,6 +59,7 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -91,7 +100,8 @@ private HttpConnection(final HostDescription host, final Integer timeout, final vertx.runOnContext(e -> Thread.currentThread().setName("adb-eventloop-" + THREAD_COUNT.getAndIncrement())); int _ttl = ttl == null ? 0 : Math.toIntExact(ttl / 1000); - client = WebClient.create(vertx, new WebClientOptions() + + WebClientOptions webClientOptions = new WebClientOptions() .setConnectTimeout(timeout) .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) .setIdleTimeout(timeout) @@ -107,20 +117,54 @@ private HttpConnection(final HostDescription host, final Integer timeout, final .setReusePort(true) .setHttp2ClearTextUpgrade(false) //TODO: allow configuring HTTP_2 or HTTP_1_1 +// .setProtocolVersion(HttpVersion.HTTP_1_1) .setProtocolVersion(HttpVersion.HTTP_2) + .setUseAlpn(true) .setDefaultHost(host.getHost()) - .setDefaultPort(host.getPort())); - -// if (Boolean.TRUE.equals(useSsl)) { -// registryBuilder.register("https", new SSLConnectionSocketFactory( -// sslContext != null ? sslContext : SSLContexts.createSystemDefault(), -// hostnameVerifier != null ? hostnameVerifier : -// SSLConnectionSocketFactory.getDefaultHostnameVerifier() -// )); -// } + .setDefaultPort(host.getPort()); + + // FIXME: replace hostnameVerifier option with verifyHost: bool + webClientOptions.setVerifyHost(hostnameVerifier != null && !(hostnameVerifier instanceof NoopHostnameVerifier)); + + if (Boolean.TRUE.equals(useSsl)) { + SSLContext ctx; + if (sslContext != null) { + ctx = sslContext; + } else { + try { + ctx = SSLContext.getDefault(); + } catch (NoSuchAlgorithmException e) { + throw new ArangoDBException(e); + } + } + + webClientOptions.setSsl(true) + .setJdkSslEngineOptions(new JdkSSLEngineOptions() { + @Override + public JdkSSLEngineOptions copy() { + return this; + } + + @Override + public SslContextFactory sslContextFactory() { + return () -> new JdkSslContext( + ctx, + true, + null, + IdentityCipherSuiteFilter.INSTANCE, + ApplicationProtocolConfig.DISABLED, + ClientAuth.NONE, + null, + false + ); + } + }); + } // if (httpCookieSpec != null && httpCookieSpec.length() > 1) { // requestConfig.setCookieSpec(httpCookieSpec); // } + + client = WebClient.create(vertx, webClientOptions); } private static String buildUrl(final Request request) { @@ -218,7 +262,12 @@ public Response execute(final Request request) throws IOException { // FIXME: make async API bufferResponse = httpRequest.sendBuffer(buffer).toCompletionStage().toCompletableFuture().get(); } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else { + throw new ArangoDBException(e); + } } Response response = buildResponse(bufferResponse); checkError(response); From 8ad2b0ffb14d8212d7e989869f0239c65ffc4955 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Tue, 18 Oct 2022 09:13:50 +0200 Subject: [PATCH 07/10] hostname verifier refactoring --- src/main/java/com/arangodb/ArangoDB.java | 31 ++++--------------- .../internal/InternalArangoDBBuilder.java | 13 ++------ .../internal/http/HttpConnection.java | 27 +++++++--------- .../internal/http/HttpConnectionFactory.java | 10 +++--- .../arangodb/example/ssl/SslExampleTest.java | 2 +- 5 files changed, 26 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/arangodb/ArangoDB.java b/src/main/java/com/arangodb/ArangoDB.java index 3983b7b55..f1cc0d6d5 100644 --- a/src/main/java/com/arangodb/ArangoDB.java +++ b/src/main/java/com/arangodb/ArangoDB.java @@ -40,10 +40,8 @@ import com.arangodb.serde.JacksonSerde; import com.arangodb.velocystream.Request; import com.arangodb.velocystream.Response; -import org.apache.http.client.HttpRequestRetryHandler; import javax.annotation.concurrent.ThreadSafe; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import java.io.InputStream; import java.util.Collection; @@ -450,30 +448,13 @@ public Builder sslContext(final SSLContext sslContext) { } /** - * Sets the {@link javax.net.ssl.HostnameVerifier} to be used when using ssl with http protocol. + * Set whether hostname verification is enabled * - * @param hostnameVerifier HostnameVerifier to be used + * @param verifyHost {@code true} if enabled * @return {@link ArangoDB.Builder} */ - public Builder hostnameVerifier(final HostnameVerifier hostnameVerifier) { - setHostnameVerifier(hostnameVerifier); - return this; - } - - /** - * Sets the {@link HttpRequestRetryHandler} to be used when using http protocol. - * - * @param httpRequestRetryHandler HttpRequestRetryHandler to be used - * @return {@link ArangoDB.Builder} - *

- *

- * NOTE: - * Some ArangoDB HTTP endpoints do not honor RFC-2616 HTTP 1.1 specification in respect to - * 9.1 Safe and Idempotent Methods. - * Please refer to HTTP API Documentation for details. - */ - public Builder httpRequestRetryHandler(final HttpRequestRetryHandler httpRequestRetryHandler) { - setHttpRequestRetryHandler(httpRequestRetryHandler); + public Builder verifyHost(final Boolean verifyHost) { + setVerifyHost(verifyHost); return this; } @@ -621,8 +602,8 @@ public ArangoDB build() { final ConnectionFactory connectionFactory = (protocol == null || Protocol.VST == protocol) ? new VstConnectionFactorySync(host, timeout, connectionTtl, keepAliveInterval, useSsl, sslContext) - : new HttpConnectionFactory(timeout, user, password, useSsl, sslContext, hostnameVerifier, serde, - protocol, connectionTtl, httpCookieSpec, httpRequestRetryHandler); + : new HttpConnectionFactory(timeout, user, password, useSsl, sslContext, verifyHost, serde, + protocol, connectionTtl, httpCookieSpec); final Collection hostList = createHostList(max, connectionFactory); final HostResolver hostResolver = createHostResolver(hostList, max, connectionFactory); diff --git a/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java b/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java index 63ba8e603..98f0d2bcf 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java +++ b/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java @@ -26,11 +26,9 @@ import com.arangodb.internal.net.*; import com.arangodb.internal.util.HostUtils; import com.arangodb.serde.ArangoSerde; -import org.apache.http.client.HttpRequestRetryHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; @@ -71,9 +69,8 @@ public abstract class InternalArangoDBBuilder { protected String jwt; protected Boolean useSsl; protected String httpCookieSpec; - protected HttpRequestRetryHandler httpRequestRetryHandler; protected SSLContext sslContext; - protected HostnameVerifier hostnameVerifier; + protected Boolean verifyHost; protected Integer chunksize; protected Integer maxConnections; protected Long connectionTtl; @@ -284,12 +281,8 @@ protected void setSslContext(final SSLContext sslContext) { this.sslContext = sslContext; } - protected void setHostnameVerifier(final HostnameVerifier hostnameVerifier) { - this.hostnameVerifier = hostnameVerifier; - } - - protected void setHttpRequestRetryHandler(final HttpRequestRetryHandler httpRequestRetryHandler) { - this.httpRequestRetryHandler = httpRequestRetryHandler; + protected void setVerifyHost(final Boolean verifyHost) { + this.verifyHost = verifyHost; } protected void setChunksize(final Integer chunksize) { diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index f7eae8dfc..1af1a014a 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -50,13 +50,10 @@ import io.vertx.ext.web.client.WebClientOptions; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.entity.ContentType; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import java.io.IOException; import java.security.NoSuchAlgorithmException; @@ -75,8 +72,8 @@ */ public class HttpConnection implements Connection { private static final Logger LOGGER = LoggerFactory.getLogger(HttpCommunication.class); - private static final ContentType CONTENT_TYPE_APPLICATION_JSON_UTF8 = ContentType.create("application/json", "utf-8"); - private static final ContentType CONTENT_TYPE_VPACK = ContentType.create("application/x-velocypack"); + private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8"; + private static final String CONTENT_TYPE_VPACK = "application/x-velocypack"; private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private final InternalSerde util; private final String baseUrl; @@ -87,7 +84,7 @@ public class HttpConnection implements Connection { private final Vertx vertx; private HttpConnection(final HostDescription host, final Integer timeout, final String user, final String password, - final Boolean useSsl, final SSLContext sslContext, final HostnameVerifier hostnameVerifier, + final Boolean useSsl, final SSLContext sslContext, final Boolean verifyHost, final InternalSerde util, final Protocol contentType, final Long ttl, final String httpCookieSpec) { super(); @@ -123,8 +120,6 @@ private HttpConnection(final HostDescription host, final Integer timeout, final .setDefaultHost(host.getHost()) .setDefaultPort(host.getPort()); - // FIXME: replace hostnameVerifier option with verifyHost: bool - webClientOptions.setVerifyHost(hostnameVerifier != null && !(hostnameVerifier instanceof NoopHostnameVerifier)); if (Boolean.TRUE.equals(useSsl)) { SSLContext ctx; @@ -138,7 +133,9 @@ private HttpConnection(final HostDescription host, final Integer timeout, final } } - webClientOptions.setSsl(true) + webClientOptions + .setSsl(true) + .setVerifyHost(verifyHost != null ? verifyHost : true) .setJdkSslEngineOptions(new JdkSSLEngineOptions() { @Override public JdkSSLEngineOptions copy() { @@ -250,9 +247,9 @@ public Response execute(final Request request) throws IOException { if (reqBody != null) { buffer = Buffer.buffer(reqBody); if (contentType == Protocol.HTTP_VPACK) { - httpRequest.putHeader(HttpHeaders.CONTENT_TYPE.toString(), CONTENT_TYPE_VPACK.toString()); + httpRequest.putHeader(HttpHeaders.CONTENT_TYPE.toString(), CONTENT_TYPE_VPACK); } else { - httpRequest.putHeader(HttpHeaders.CONTENT_TYPE.toString(), CONTENT_TYPE_APPLICATION_JSON_UTF8.toString()); + httpRequest.putHeader(HttpHeaders.CONTENT_TYPE.toString(), CONTENT_TYPE_APPLICATION_JSON_UTF8); } } else { buffer = Buffer.buffer(); @@ -312,7 +309,7 @@ public static class Builder { private HostDescription host; private Long ttl; private SSLContext sslContext; - private HostnameVerifier hostnameVerifier; + private Boolean verifyHost; private Integer timeout; public Builder user(final String user) { @@ -360,8 +357,8 @@ public Builder sslContext(final SSLContext sslContext) { return this; } - public Builder hostnameVerifier(final HostnameVerifier hostnameVerifier) { - this.hostnameVerifier = hostnameVerifier; + public Builder verifyHost(final Boolean verifyHost) { + this.verifyHost = verifyHost; return this; } @@ -371,7 +368,7 @@ public Builder timeout(final Integer timeout) { } public HttpConnection build() { - return new HttpConnection(host, timeout, user, password, useSsl, sslContext, hostnameVerifier, util, + return new HttpConnection(host, timeout, user, password, useSsl, sslContext, verifyHost, util, contentType, ttl, httpCookieSpec); } } diff --git a/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java b/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java index ec40e54a7..bb5b226eb 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java @@ -25,9 +25,7 @@ import com.arangodb.internal.net.ConnectionFactory; import com.arangodb.internal.net.HostDescription; import com.arangodb.internal.serde.InternalSerde; -import org.apache.http.client.HttpRequestRetryHandler; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; /** @@ -38,13 +36,13 @@ public class HttpConnectionFactory implements ConnectionFactory { private final HttpConnection.Builder builder; public HttpConnectionFactory(final Integer timeout, final String user, final String password, final Boolean useSsl, - final SSLContext sslContext, final HostnameVerifier hostnameVerifier, + final SSLContext sslContext, final Boolean verifyHost, final InternalSerde util, final Protocol protocol, final Long connectionTtl, - final String httpCookieSpec, final HttpRequestRetryHandler httpRequestRetryHandler) { + final String httpCookieSpec) { super(); builder = new HttpConnection.Builder().timeout(timeout).user(user).password(password).useSsl(useSsl) - .sslContext(sslContext).hostnameVerifier(hostnameVerifier).serializationUtil(util).contentType(protocol) - .ttl(connectionTtl).httpCookieSpec(httpCookieSpec); //.httpRequestRetryHandler(httpRequestRetryHandler); + .sslContext(sslContext).verifyHost(verifyHost).serializationUtil(util).contentType(protocol) + .ttl(connectionTtl).httpCookieSpec(httpCookieSpec); } diff --git a/src/test/java/com/arangodb/example/ssl/SslExampleTest.java b/src/test/java/com/arangodb/example/ssl/SslExampleTest.java index 1fd34647d..f6320c309 100644 --- a/src/test/java/com/arangodb/example/ssl/SslExampleTest.java +++ b/src/test/java/com/arangodb/example/ssl/SslExampleTest.java @@ -76,7 +76,7 @@ void noopHostnameVerifier() throws Exception { .password("test") .useSsl(true) .sslContext(createSslContext()) - .hostnameVerifier(NoopHostnameVerifier.INSTANCE) + .verifyHost(false) .useProtocol(Protocol.HTTP_JSON) .build(); final ArangoDBVersion version = arangoDB.getVersion(); From 158bd41e1e70182897770b5b3c3cfadc04d181d8 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Tue, 18 Oct 2022 10:10:51 +0200 Subject: [PATCH 08/10] removed apache httpclient --- pom.xml | 13 ------ .../internal/http/HttpConnection.java | 45 +++++++++---------- .../internal/http/HttpDeleteWithBody.java | 43 ------------------ .../arangodb/example/ssl/SslExampleTest.java | 1 - 4 files changed, 22 insertions(+), 80 deletions(-) delete mode 100644 src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java diff --git a/pom.xml b/pom.xml index 1243994ae..b8d177cb9 100644 --- a/pom.xml +++ b/pom.xml @@ -307,19 +307,6 @@ - - - org.apache.httpcomponents - httpclient - 4.5.13 - - - - org.apache.httpcomponents - httpcore - 4.4.15 - - org.slf4j slf4j-api diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index 1af1a014a..532d09044 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -48,24 +48,22 @@ import io.vertx.ext.web.client.HttpResponse; import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.client.WebClientOptions; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; +import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static org.apache.http.HttpHeaders.AUTHORIZATION; /** * @author Mark Vollmary @@ -172,25 +170,26 @@ private static String buildUrl(final Request request) { } sb.append(request.getRequest()); if (!request.getQueryParam().isEmpty()) { - if (request.getRequest().contains("?")) { - sb.append("&"); - } else { - sb.append("?"); + sb.append("?"); + + try { + for (Iterator> iterator = request.getQueryParam().entrySet().iterator(); iterator.hasNext(); ) { + Entry param = iterator.next(); + if (param.getValue() != null) { + sb.append(URLEncoder.encode(param.getKey(), StandardCharsets.UTF_8.toString())); + sb.append("="); + sb.append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8.toString())); + if (iterator.hasNext()) { + sb.append("&"); + } + } + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); } - final String paramString = URLEncodedUtils.format(toList(request.getQueryParam()), "utf-8"); - sb.append(paramString); - } - return sb.toString(); - } - private static List toList(final Map parameters) { - final ArrayList paramList = new ArrayList<>(parameters.size()); - for (final Entry param : parameters.entrySet()) { - if (param.getValue() != null) { - paramList.add(new BasicNameValuePair(param.getKey(), param.getValue())); - } } - return paramList; + return sb.toString(); } private static void addHeader(final Request request, final HttpRequest httpRequest) { @@ -236,7 +235,7 @@ public Response execute(final Request request) throws IOException { httpRequest.putHeader("Accept", "application/x-velocypack"); } addHeader(request, httpRequest); - httpRequest.putHeader(AUTHORIZATION, auth); + httpRequest.putHeader(HttpHeaders.AUTHORIZATION.toString(), auth); if (LOGGER.isDebugEnabled()) { CURLLogger.log(baseUrl, path, request, util); diff --git a/src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java b/src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java deleted file mode 100644 index 25082672c..000000000 --- a/src/main/java/com/arangodb/internal/http/HttpDeleteWithBody.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2016 ArangoDB GmbH, Cologne, Germany - * - * Licensed 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. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.internal.http; - -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; - -import java.net.URI; - -/** - * @author Mark Vollmary - */ -public class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase { - public final static String METHOD_NAME = "DELETE"; - - public HttpDeleteWithBody(final String uri) { - super(); - setURI(URI.create(uri)); - } - - @Override - public String getMethod() { - return METHOD_NAME; - } - -} diff --git a/src/test/java/com/arangodb/example/ssl/SslExampleTest.java b/src/test/java/com/arangodb/example/ssl/SslExampleTest.java index f6320c309..764f1ae52 100644 --- a/src/test/java/com/arangodb/example/ssl/SslExampleTest.java +++ b/src/test/java/com/arangodb/example/ssl/SslExampleTest.java @@ -23,7 +23,6 @@ import com.arangodb.ArangoDB; import com.arangodb.Protocol; import com.arangodb.entity.ArangoDBVersion; -import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; From 0776a0d57b2f132945a3891be111131ba9d68451 Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Tue, 18 Oct 2022 10:43:09 +0200 Subject: [PATCH 09/10] removed httpCookieSpec --- src/main/java/com/arangodb/ArangoDB.java | 2 +- .../arangodb/internal/InternalArangoDBBuilder.java | 7 ------- .../com/arangodb/internal/http/HttpConnection.java | 14 ++------------ .../internal/http/HttpConnectionFactory.java | 5 ++--- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/arangodb/ArangoDB.java b/src/main/java/com/arangodb/ArangoDB.java index f1cc0d6d5..8c19a6247 100644 --- a/src/main/java/com/arangodb/ArangoDB.java +++ b/src/main/java/com/arangodb/ArangoDB.java @@ -603,7 +603,7 @@ public ArangoDB build() { final ConnectionFactory connectionFactory = (protocol == null || Protocol.VST == protocol) ? new VstConnectionFactorySync(host, timeout, connectionTtl, keepAliveInterval, useSsl, sslContext) : new HttpConnectionFactory(timeout, user, password, useSsl, sslContext, verifyHost, serde, - protocol, connectionTtl, httpCookieSpec); + protocol, connectionTtl); final Collection hostList = createHostList(max, connectionFactory); final HostResolver hostResolver = createHostResolver(hostList, max, connectionFactory); diff --git a/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java b/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java index 98f0d2bcf..2b6ca4120 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java +++ b/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java @@ -50,7 +50,6 @@ public abstract class InternalArangoDBBuilder { private static final String PROPERTY_KEY_PASSWORD = "arangodb.password"; private static final String PROPERTY_KEY_JWT = "arangodb.jwt"; private static final String PROPERTY_KEY_USE_SSL = "arangodb.usessl"; - private static final String PROPERTY_KEY_COOKIE_SPEC = "arangodb.httpCookieSpec"; private static final String PROPERTY_KEY_V_STREAM_CHUNK_CONTENT_SIZE = "arangodb.chunksize"; private static final String PROPERTY_KEY_MAX_CONNECTIONS = "arangodb.connections.max"; private static final String PROPERTY_KEY_CONNECTION_TTL = "arangodb.connections.ttl"; @@ -68,7 +67,6 @@ public abstract class InternalArangoDBBuilder { protected String password; protected String jwt; protected Boolean useSsl; - protected String httpCookieSpec; protected SSLContext sslContext; protected Boolean verifyHost; protected Integer chunksize; @@ -148,10 +146,6 @@ private static Boolean loadUseSsl(final Properties properties, final Boolean cur getProperty(properties, PROPERTY_KEY_USE_SSL, currentValue, ArangoDefaults.DEFAULT_USE_SSL)); } - private static String loadhttpCookieSpec(final Properties properties, final String currentValue) { - return getProperty(properties, PROPERTY_KEY_COOKIE_SPEC, currentValue, ""); - } - private static Integer loadChunkSize(final Properties properties, final Integer currentValue) { return Integer.parseInt(getProperty(properties, PROPERTY_KEY_V_STREAM_CHUNK_CONTENT_SIZE, currentValue, ArangoDefaults.CHUNK_DEFAULT_CONTENT_SIZE)); @@ -242,7 +236,6 @@ protected void loadProperties(final Properties properties) { password = loadPassword(properties, password); jwt = loadJwt(properties, jwt); useSsl = loadUseSsl(properties, useSsl); - httpCookieSpec = loadhttpCookieSpec(properties, httpCookieSpec); chunksize = loadChunkSize(properties, chunksize); maxConnections = loadMaxConnections(properties, maxConnections); connectionTtl = loadConnectionTtl(properties, connectionTtl); diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index 532d09044..f35040a33 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -83,8 +83,7 @@ public class HttpConnection implements Connection { private HttpConnection(final HostDescription host, final Integer timeout, final String user, final String password, final Boolean useSsl, final SSLContext sslContext, final Boolean verifyHost, - final InternalSerde util, final Protocol contentType, final Long ttl, - final String httpCookieSpec) { + final InternalSerde util, final Protocol contentType, final Long ttl) { super(); this.util = util; this.contentType = contentType; @@ -155,9 +154,6 @@ public SslContextFactory sslContextFactory() { } }); } -// if (httpCookieSpec != null && httpCookieSpec.length() > 1) { -// requestConfig.setCookieSpec(httpCookieSpec); -// } client = WebClient.create(vertx, webClientOptions); } @@ -303,7 +299,6 @@ public static class Builder { private String password; private InternalSerde util; private Boolean useSsl; - private String httpCookieSpec; private Protocol contentType; private HostDescription host; private Long ttl; @@ -331,11 +326,6 @@ public Builder useSsl(final Boolean useSsl) { return this; } - public Builder httpCookieSpec(String httpCookieSpec) { - this.httpCookieSpec = httpCookieSpec; - return this; - } - public Builder contentType(final Protocol contentType) { this.contentType = contentType; return this; @@ -368,7 +358,7 @@ public Builder timeout(final Integer timeout) { public HttpConnection build() { return new HttpConnection(host, timeout, user, password, useSsl, sslContext, verifyHost, util, - contentType, ttl, httpCookieSpec); + contentType, ttl); } } diff --git a/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java b/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java index bb5b226eb..5e0abc74b 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnectionFactory.java @@ -37,12 +37,11 @@ public class HttpConnectionFactory implements ConnectionFactory { public HttpConnectionFactory(final Integer timeout, final String user, final String password, final Boolean useSsl, final SSLContext sslContext, final Boolean verifyHost, - final InternalSerde util, final Protocol protocol, final Long connectionTtl, - final String httpCookieSpec) { + final InternalSerde util, final Protocol protocol, final Long connectionTtl) { super(); builder = new HttpConnection.Builder().timeout(timeout).user(user).password(password).useSsl(useSsl) .sslContext(sslContext).verifyHost(verifyHost).serializationUtil(util).contentType(protocol) - .ttl(connectionTtl).httpCookieSpec(httpCookieSpec); + .ttl(connectionTtl); } From dcab131b3d82c77e6aa7737a2dcf20665fcca31f Mon Sep 17 00:00:00 2001 From: Michele Rastelli Date: Tue, 18 Oct 2022 14:10:14 +0200 Subject: [PATCH 10/10] case-insensitive headers --- src/main/java/com/arangodb/ArangoDB.java | 2 +- .../com/arangodb/internal/ArangoExecutor.java | 4 ++-- .../arangodb/internal/ArangoRequestParam.java | 4 ++-- .../internal/InternalArangoCollection.java | 2 +- .../internal/InternalArangoDBBuilder.java | 2 +- .../internal/InternalArangoDatabase.java | 10 ++------ .../internal/cursor/ArangoCursorImpl.java | 2 +- .../cursor/entity/InternalCursorEntity.java | 15 ++++++++---- .../arangodb/internal/http/CURLLogger.java | 1 + .../internal/http/HttpConnection.java | 4 +--- .../internal/serde/InternalDeserializers.java | 2 +- .../arangodb/internal/util/RequestUtils.java | 4 ++-- .../arangodb/internal/util/ResponseUtils.java | 6 ++--- .../com/arangodb/velocystream/Request.java | 23 ++++++++++++++++-- .../com/arangodb/velocystream/Response.java | 24 +++++++++++++++---- 15 files changed, 69 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/arangodb/ArangoDB.java b/src/main/java/com/arangodb/ArangoDB.java index 8c19a6247..fc9eb6d3f 100644 --- a/src/main/java/com/arangodb/ArangoDB.java +++ b/src/main/java/com/arangodb/ArangoDB.java @@ -349,7 +349,7 @@ private static Protocol loadProtocol(final Properties properties, final Protocol return Protocol.valueOf( getProperty(properties, PROPERTY_KEY_PROTOCOL, currentValue, ArangoDefaults.DEFAULT_NETWORK_PROTOCOL) - .toUpperCase(Locale.ENGLISH)); + .toUpperCase(Locale.ROOT)); } @Override diff --git a/src/main/java/com/arangodb/internal/ArangoExecutor.java b/src/main/java/com/arangodb/internal/ArangoExecutor.java index 711308012..37917dad8 100644 --- a/src/main/java/com/arangodb/internal/ArangoExecutor.java +++ b/src/main/java/com/arangodb/internal/ArangoExecutor.java @@ -48,14 +48,14 @@ protected T createResult(final Type type, final Response response) { } protected final void interceptResponse(Response response) { - String queueTime = response.getMeta().get("X-Arango-Queue-Time-Seconds"); + String queueTime = response.getMeta("X-Arango-Queue-Time-Seconds"); if (queueTime != null) { qtMetrics.add(Double.parseDouble(queueTime)); } } protected final Request interceptRequest(Request request) { - request.putHeaderParam("X-Arango-Queue-Time-Seconds", timeoutS); + request.putHeaderParam("x-arango-queue-time-seconds", timeoutS); return request; } diff --git a/src/main/java/com/arangodb/internal/ArangoRequestParam.java b/src/main/java/com/arangodb/internal/ArangoRequestParam.java index 7db9e599b..16c497994 100644 --- a/src/main/java/com/arangodb/internal/ArangoRequestParam.java +++ b/src/main/java/com/arangodb/internal/ArangoRequestParam.java @@ -28,8 +28,8 @@ public final class ArangoRequestParam { public static final String SYSTEM = "_system"; public static final String DATABASE = "database"; public static final String WAIT_FOR_SYNC = "waitForSync"; - public static final String IF_NONE_MATCH = "If-None-Match"; - public static final String IF_MATCH = "If-Match"; + public static final String IF_NONE_MATCH = "if-none-match"; + public static final String IF_MATCH = "if-match"; public static final String KEEP_NULL = "keepNull"; private ArangoRequestParam() { diff --git a/src/main/java/com/arangodb/internal/InternalArangoCollection.java b/src/main/java/com/arangodb/internal/InternalArangoCollection.java index ea5d283ce..fdc1f24c1 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoCollection.java +++ b/src/main/java/com/arangodb/internal/InternalArangoCollection.java @@ -196,7 +196,7 @@ protected ResponseDeserializer> getDocumentsResponseD final Class type, final DocumentReadOptions options) { return response -> { final MultiDocumentEntity multiDocument = new MultiDocumentEntity<>(); - boolean potentialDirtyRead = Boolean.parseBoolean(response.getMeta().get("X-Arango-Potential-Dirty-Read")); + boolean potentialDirtyRead = Boolean.parseBoolean(response.getMeta("X-Arango-Potential-Dirty-Read")); multiDocument.setPotentialDirtyRead(potentialDirtyRead); final Collection docs = new ArrayList<>(); final Collection errors = new ArrayList<>(); diff --git a/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java b/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java index 2b6ca4120..38e6b6441 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java +++ b/src/main/java/com/arangodb/internal/InternalArangoDBBuilder.java @@ -187,7 +187,7 @@ private static LoadBalancingStrategy loadLoadBalancingStrategy( final Properties properties, final LoadBalancingStrategy currentValue) { return LoadBalancingStrategy.valueOf(getProperty(properties, PROPERTY_KEY_LOAD_BALANCING_STRATEGY, currentValue, - ArangoDefaults.DEFAULT_LOAD_BALANCING_STRATEGY).toUpperCase(Locale.ENGLISH)); + ArangoDefaults.DEFAULT_LOAD_BALANCING_STRATEGY).toUpperCase(Locale.ROOT)); } protected static String getProperty( diff --git a/src/main/java/com/arangodb/internal/InternalArangoDatabase.java b/src/main/java/com/arangodb/internal/InternalArangoDatabase.java index c1da4db4d..edfe70b70 100644 --- a/src/main/java/com/arangodb/internal/InternalArangoDatabase.java +++ b/src/main/java/com/arangodb/internal/InternalArangoDatabase.java @@ -162,10 +162,7 @@ protected Request queryRequest(final String query, final Map bin protected Request queryNextRequest(final String id, final AqlQueryOptions options, Map meta) { final Request request = request(dbName, RequestType.POST, PATH_API_CURSOR, id); - - if (meta != null) { - request.getHeaderParam().putAll(meta); - } + request.putHeaderParams(meta); final AqlQueryOptions opt = options != null ? options : new AqlQueryOptions(); @@ -179,10 +176,7 @@ protected Request queryNextRequest(final String id, final AqlQueryOptions option protected Request queryCloseRequest(final String id, final AqlQueryOptions options, Map meta) { final Request request = request(dbName, RequestType.DELETE, PATH_API_CURSOR, id); - - if (meta != null) { - request.getHeaderParam().putAll(meta); - } + request.putHeaderParams(meta); final AqlQueryOptions opt = options != null ? options : new AqlQueryOptions(); diff --git a/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java b/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java index 8f3ac6f0a..cff3dd4b5 100644 --- a/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java +++ b/src/main/java/com/arangodb/internal/cursor/ArangoCursorImpl.java @@ -51,7 +51,7 @@ public ArangoCursorImpl(final InternalArangoDatabase db, final ArangoCurso this.type = type; iterator = createIterator(this, db, execute, result); id = result.getId(); - this.isPontentialDirtyRead = Boolean.parseBoolean(result.getMeta().get("X-Arango-Potential-Dirty-Read")); + this.isPontentialDirtyRead = Boolean.parseBoolean(result.getMeta().get("x-arango-potential-dirty-read")); } protected ArangoCursorIterator createIterator( diff --git a/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java b/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java index 1ab557b49..abaa5ad41 100644 --- a/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java +++ b/src/main/java/com/arangodb/internal/cursor/entity/InternalCursorEntity.java @@ -26,7 +26,10 @@ import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @author Mark Vollmary @@ -89,22 +92,24 @@ public JsonNode getResult() { return result; } + @Override public Map getMeta() { if (meta == null) return Collections.emptyMap(); - return meta; + return Collections.unmodifiableMap(meta); } + @Override public void setMeta(Map meta) { - this.meta = cleanupMeta(meta); + this.meta = cleanupMeta(new HashMap<>(meta)); } /** * @return remove not allowed (valid storable) meta information */ public Map cleanupMeta(Map meta) { - meta.remove("Content-Length"); - meta.remove("Transfer-Encoding"); - meta.remove("X-Arango-Queue-Time-Seconds"); + meta.remove("content-length"); + meta.remove("transfer-encoding"); + meta.remove("x-arango-queue-time-seconds"); return meta; } diff --git a/src/main/java/com/arangodb/internal/http/CURLLogger.java b/src/main/java/com/arangodb/internal/http/CURLLogger.java index 9bfda6641..b2e72418b 100644 --- a/src/main/java/com/arangodb/internal/http/CURLLogger.java +++ b/src/main/java/com/arangodb/internal/http/CURLLogger.java @@ -52,6 +52,7 @@ public static void log( buffer.append("cat <<-___EOB___ | "); } buffer.append("curl -X ").append(requestType); + // FIXME: add --http2 in case protocol is HTTP/2 buffer.append(" --dump -"); if (request.getHeaderParam().size() > 0) { for (final Entry header : request.getHeaderParam().entrySet()) { diff --git a/src/main/java/com/arangodb/internal/http/HttpConnection.java b/src/main/java/com/arangodb/internal/http/HttpConnection.java index f35040a33..0303fc740 100644 --- a/src/main/java/com/arangodb/internal/http/HttpConnection.java +++ b/src/main/java/com/arangodb/internal/http/HttpConnection.java @@ -58,7 +58,6 @@ import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.Iterator; -import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -276,9 +275,8 @@ public Response buildResponse(final HttpResponse httpResponse) throws Un response.setBody(bytes); } } - final Map meta = response.getMeta(); for (Entry header : httpResponse.headers()) { - meta.put(header.getKey(), header.getValue()); + response.putMeta(header.getKey(), header.getValue()); } return response; } diff --git a/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java b/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java index 6abb792ce..d99449a64 100644 --- a/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java +++ b/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java @@ -82,7 +82,7 @@ public Response deserialize(final JsonParser p, final DeserializationContext ctx response.setType(it.next().intValue()); response.setResponseCode(it.next().intValue()); if (it.hasNext()) { - response.setMeta(readTreeAsValue(p, ctxt, it.next(), Map.class)); + response.putMetas(readTreeAsValue(p, ctxt, it.next(), Map.class)); } return response; } diff --git a/src/main/java/com/arangodb/internal/util/RequestUtils.java b/src/main/java/com/arangodb/internal/util/RequestUtils.java index 2b5f56b47..1539b8a76 100644 --- a/src/main/java/com/arangodb/internal/util/RequestUtils.java +++ b/src/main/java/com/arangodb/internal/util/RequestUtils.java @@ -29,7 +29,7 @@ */ public final class RequestUtils { - public static final String HEADER_ALLOW_DIRTY_READ = "X-Arango-Allow-Dirty-Read"; + public static final String HEADER_ALLOW_DIRTY_READ = "x-arango-allow-dirty-read"; private RequestUtils() { super(); @@ -40,7 +40,7 @@ public static Request allowDirtyRead(final Request request) { } public static AccessType determineAccessType(final Request request) { - if (request.getHeaderParam().containsKey(HEADER_ALLOW_DIRTY_READ)) { + if (request.containsHeaderParam(HEADER_ALLOW_DIRTY_READ)) { return AccessType.DIRTY_READ; } if (request.getRequestType() == RequestType.GET) { diff --git a/src/main/java/com/arangodb/internal/util/ResponseUtils.java b/src/main/java/com/arangodb/internal/util/ResponseUtils.java index d09cd181b..fcab67e88 100644 --- a/src/main/java/com/arangodb/internal/util/ResponseUtils.java +++ b/src/main/java/com/arangodb/internal/util/ResponseUtils.java @@ -36,7 +36,7 @@ public final class ResponseUtils { private static final int ERROR_STATUS = 300; private static final int ERROR_INTERNAL = 503; - private static final String HEADER_ENDPOINT = "X-Arango-Endpoint"; + private static final String HEADER_ENDPOINT = "x-arango-endpoint"; private ResponseUtils() { super(); @@ -45,9 +45,9 @@ private ResponseUtils() { public static void checkError(final InternalSerde util, final Response response) { final int responseCode = response.getResponseCode(); if (responseCode >= ERROR_STATUS) { - if (responseCode == ERROR_INTERNAL && response.getMeta().containsKey(HEADER_ENDPOINT)) { + if (responseCode == ERROR_INTERNAL && response.containsMeta(HEADER_ENDPOINT)) { throw new ArangoDBRedirectException(String.format("Response Code: %s", responseCode), - response.getMeta().get(HEADER_ENDPOINT)); + response.getMeta(HEADER_ENDPOINT)); } else if (response.getBody() != null) { final ErrorEntity errorEntity = util.deserialize(response.getBody(), ErrorEntity.class); ArangoDBException e = new ArangoDBException(errorEntity); diff --git a/src/main/java/com/arangodb/velocystream/Request.java b/src/main/java/com/arangodb/velocystream/Request.java index 666b00e21..4802c6c00 100644 --- a/src/main/java/com/arangodb/velocystream/Request.java +++ b/src/main/java/com/arangodb/velocystream/Request.java @@ -22,7 +22,9 @@ import com.arangodb.DbName; +import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -91,12 +93,29 @@ public Request putQueryParam(final String key, final Object value) { } public Map getHeaderParam() { - return headerParam; + return Collections.unmodifiableMap(headerParam); + } + + public String getHeaderParam(final String key) { + return headerParam.get(key.toLowerCase(Locale.ROOT)); + } + + public boolean containsHeaderParam(final String key) { + return headerParam.containsKey(key.toLowerCase(Locale.ROOT)); } public Request putHeaderParam(final String key, final String value) { if (value != null) { - headerParam.put(key, value); + headerParam.put(key.toLowerCase(Locale.ROOT), value); + } + return this; + } + + public Request putHeaderParams(final Map params) { + if (params != null) { + for (Map.Entry it : params.entrySet()) { + putHeaderParam(it.getKey(), it.getValue()); + } } return this; } diff --git a/src/main/java/com/arangodb/velocystream/Response.java b/src/main/java/com/arangodb/velocystream/Response.java index 1f7b164bd..9b5cf6fd8 100644 --- a/src/main/java/com/arangodb/velocystream/Response.java +++ b/src/main/java/com/arangodb/velocystream/Response.java @@ -20,7 +20,9 @@ package com.arangodb.velocystream; +import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -31,7 +33,7 @@ public class Response { private int version = 1; private int type = 2; private int responseCode; - private Map meta; + private final Map meta; private byte[] body = null; public Response() { @@ -64,11 +66,25 @@ public void setResponseCode(final int responseCode) { } public Map getMeta() { - return meta; + return Collections.unmodifiableMap(meta); } - public void setMeta(final Map meta) { - this.meta = meta; + public String getMeta(final String key) { + return meta.get(key.toLowerCase(Locale.ROOT)); + } + + public boolean containsMeta(final String key) { + return meta.containsKey(key.toLowerCase(Locale.ROOT)); + } + + public void putMeta(final String key, final String value) { + this.meta.put(key.toLowerCase(Locale.ROOT), value); + } + + public void putMetas(final Map meta) { + for (Map.Entry it : meta.entrySet()) { + putMeta(it.getKey(), it.getValue()); + } } public byte[] getBody() {