You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2026/05/27 08:15:54 UTC

(httpcomponents-client) branch master updated: Improved efficincy of Jakarta REST Response implementation

This is an automated email from the ASF dual-hosted git repository.

ok2c pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git


The following commit(s) were added to refs/heads/master by this push:
     new d989173f9 Improved efficincy of Jakarta REST Response implementation
d989173f9 is described below

commit d989173f9e9c67e1710be47ac44a4617507e1c53
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Mon May 25 11:10:22 2026 +0200

    Improved efficincy of Jakarta REST Response implementation
---
 .../hc/client5/http/rest/RestClientResponse.java   | 189 ++++++++-------------
 .../client5/http/rest/RestInvocationHandler.java   |  30 +++-
 .../hc/client5/http/rest/RestResponseConsumer.java | 160 +++++++++++++++++
 .../client5/http/rest/RestClientResponseTest.java  |   7 +-
 4 files changed, 257 insertions(+), 129 deletions(-)

diff --git a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java
index bf939fe36..2a16f56bb 100644
--- a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java
+++ b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java
@@ -36,8 +36,8 @@
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
@@ -54,13 +54,16 @@
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.NewCookie;
 import jakarta.ws.rs.core.Response;
-
 import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.client5.http.validator.ETag;
+import org.apache.hc.client5.http.validator.ValidatorType;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.message.MessageSupport;
 import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.CharArrayBuffer;
 
 /**
  * Minimal {@link Response} implementation backed by a consumed {@link JsonNode}
@@ -83,30 +86,26 @@ final class RestClientResponse extends Response {
 
     private static final byte[] EMPTY = new byte[0];
 
-    private final int status;
-    private final String reasonPhrase;
-    private final JsonNode body;
-    private final MediaType mediaType;
-    private final MultivaluedMap<String, Object> metadata;
-    private final MultivaluedMap<String, String> stringHeaders;
     private final ObjectMapper objectMapper;
+    private final HttpResponse response;
+    private final JsonNode body;
+    private final ContentType contentType;
+    private final long len;
 
     private boolean closed;
     private Object cachedEntity;
 
-    RestClientResponse(final HttpResponse response, final JsonNode body, final ObjectMapper objectMapper) {
-        this.status = response.getCode();
-        this.reasonPhrase = response.getReasonPhrase();
-        this.body = body;
+    RestClientResponse(
+            final ObjectMapper objectMapper,
+            final HttpResponse response,
+            final JsonNode jsonNode,
+            final ContentType contentType,
+            final long len) {
         this.objectMapper = Args.notNull(objectMapper, "Object mapper");
-        this.metadata = new MultivaluedHashMap<>();
-        this.stringHeaders = new MultivaluedHashMap<>();
-        for (final Header h : response.getHeaders()) {
-            this.metadata.add(h.getName(), h.getValue());
-            this.stringHeaders.add(h.getName(), h.getValue());
-        }
-        final Header ct = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);
-        this.mediaType = ct != null ? toMediaType(ContentType.parse(ct.getValue())) : null;
+        this.response = Args.notNull(response, "Response");
+        this.contentType = contentType;
+        this.body = jsonNode;
+        this.len = len;
     }
 
     private static MediaType toMediaType(final ContentType ct) {
@@ -125,20 +124,21 @@ private static MediaType toMediaType(final ContentType ct) {
 
     @Override
     public int getStatus() {
-        return status;
+        return response.getCode();
     }
 
     @Override
     public StatusType getStatusInfo() {
-        final Status standard = Status.fromStatusCode(status);
-        final String reason = reasonPhrase != null ? reasonPhrase
+        final int statusCode = response.getCode();
+        final Status standard = Status.fromStatusCode(statusCode);
+        final String reason = response.getReasonPhrase() != null ? response.getReasonPhrase()
                 : standard != null ? standard.getReasonPhrase() : "";
-        final Status.Family family = Status.Family.familyOf(status);
+        final Status.Family family = Status.Family.familyOf(statusCode);
         return new StatusType() {
 
             @Override
             public int getStatusCode() {
-                return status;
+                return statusCode;
             }
 
             @Override
@@ -162,7 +162,7 @@ public Object getEntity() {
 
     @Override
     public <T> T readEntity(final Class<T> entityType) {
-        return readEntity(entityType, (Annotation[]) null);
+        return readEntity(entityType, null);
     }
 
     @Override
@@ -245,13 +245,7 @@ private byte[] bodyAsBytes() {
     }
 
     private Charset charset() {
-        if (mediaType != null) {
-            final String cs = mediaType.getParameters().get(MediaType.CHARSET_PARAMETER);
-            if (cs != null) {
-                return Charset.forName(cs);
-            }
-        }
-        return StandardCharsets.UTF_8;
+        return ContentType.getCharset(contentType, StandardCharsets.UTF_8);
     }
 
     @Override
@@ -278,43 +272,25 @@ private void ensureOpen() {
 
     @Override
     public MediaType getMediaType() {
-        return mediaType;
+        return toMediaType(contentType);
     }
 
     @Override
     public Locale getLanguage() {
-        final String lang = getHeaderString(HttpHeaders.CONTENT_LANGUAGE);
-        return lang != null ? Locale.forLanguageTag(lang) : null;
+        final Header h = response.getFirstHeader(HttpHeaders.CONTENT_LANGUAGE);
+        return h != null ? Locale.forLanguageTag(h.getValue()) : null;
     }
 
     @Override
     public int getLength() {
-        final String len = getHeaderString(HttpHeaders.CONTENT_LENGTH);
-        if (len != null) {
-            try {
-                return Integer.parseInt(len);
-            } catch (final NumberFormatException ignore) {
-            }
-        }
-        return hasEntity() ? bodyAsBytes().length : -1;
+        return (int) len;
     }
 
     @Override
     public Set<String> getAllowedMethods() {
-        final List<String> values = headerValues(HttpHeaders.ALLOW);
-        if (values == null || values.isEmpty()) {
-            return Collections.emptySet();
-        }
-        final Set<String> result = new LinkedHashSet<>();
-        for (final String v : values) {
-            for (final String m : v.split(",")) {
-                final String trimmed = m.trim();
-                if (!trimmed.isEmpty()) {
-                    result.add(trimmed.toUpperCase(Locale.ROOT));
-                }
-            }
-        }
-        return result;
+        final LinkedHashSet<String> allowedMethods = new LinkedHashSet<>();
+        MessageSupport.parseTokens(response, HttpHeaders.ALLOW, allowedMethods::add);
+        return allowedMethods;
     }
 
     @Override
@@ -324,51 +300,32 @@ public Map<String, NewCookie> getCookies() {
 
     @Override
     public EntityTag getEntityTag() {
-        final String etag = getHeaderString(HttpHeaders.ETAG);
-        if (etag == null) {
-            return null;
-        }
-        String raw = etag.trim();
-        boolean weak = false;
-        if (raw.startsWith("W/")) {
-            weak = true;
-            raw = raw.substring(2).trim();
-        }
-        if (raw.length() >= 2 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') {
-            raw = raw.substring(1, raw.length() - 1);
-        }
-        return new EntityTag(raw, weak);
+        final ETag eTag = ETag.get(response);
+        return eTag != null ? new EntityTag(eTag.getValue(), eTag.getType() == ValidatorType.WEAK) : null;
     }
 
     @Override
     public Date getDate() {
-        return parseHttpDate(getHeaderString(HttpHeaders.DATE));
+        final Instant instant = DateUtils.parseStandardDate(response, HttpHeaders.DATE);
+        return instant != null ? Date.from(instant) : null;
     }
 
     @Override
     public Date getLastModified() {
-        return parseHttpDate(getHeaderString(HttpHeaders.LAST_MODIFIED));
-    }
-
-    private static Date parseHttpDate(final String value) {
-        if (value == null) {
-            return null;
-        }
-        final Instant instant = DateUtils.parseDate(value, DateUtils.STANDARD_PATTERNS);
+        final Instant instant = DateUtils.parseStandardDate(response, HttpHeaders.LAST_MODIFIED);
         return instant != null ? Date.from(instant) : null;
     }
 
     @Override
     public URI getLocation() {
-        final String loc = getHeaderString(HttpHeaders.LOCATION);
-        if (loc == null) {
-            return null;
-        }
-        try {
-            return new URI(loc);
-        } catch (final URISyntaxException ex) {
-            return null;
+        final Header h = response.getFirstHeader(HttpHeaders.LOCATION);
+        if (h != null) {
+            try {
+                return new URI(h.getValue());
+            } catch (final URISyntaxException ignore) {
+            }
         }
+        return null;
     }
 
     @Override
@@ -394,52 +351,46 @@ public Link.Builder getLinkBuilder(final String relation) {
 
     @Override
     public MultivaluedMap<String, Object> getMetadata() {
-        return metadata;
+        final MultivaluedMap<String, Object> multimap = new MultivaluedHashMap<>();
+        for (final Iterator<Header> it = response.headerIterator(); it.hasNext(); ) {
+            final Header h = it.next();
+            multimap.add(h.getName(), h.getValue());
+        }
+        return multimap;
     }
 
     @Override
     public MultivaluedMap<String, String> getStringHeaders() {
-        return stringHeaders;
+        final MultivaluedMap<String, String> multimap = new MultivaluedHashMap<>();
+        for (final Iterator<Header> it = response.headerIterator(); it.hasNext(); ) {
+            final Header h = it.next();
+            multimap.add(h.getName(), h.getValue());
+        }
+        return multimap;
     }
 
     @Override
     public String getHeaderString(final String name) {
-        final List<String> values = headerValues(name);
-        if (values == null || values.isEmpty()) {
+        final Header[] headers = response.getHeaders(name);
+        if (headers.length == 0) {
             return null;
-        }
-        if (values.size() == 1) {
-            return values.get(0);
-        }
-        final StringBuilder sb = new StringBuilder();
-        for (final String v : values) {
-            if (sb.length() > 0) {
-                sb.append(',');
+        } else if (headers.length == 1) {
+            return headers[0].getValue();
+        } else {
+            final CharArrayBuffer buf = new CharArrayBuffer(128);
+            buf.append(headers[0].getValue());
+            for (int i = 1; i < headers.length; i++) {
+                buf.append(", ");
+                buf.append(headers[i].getValue());
             }
-            sb.append(v);
-        }
-        return sb.toString();
-    }
 
-    private List<String> headerValues(final String name) {
-        for (final Map.Entry<String, List<String>> entry : stringHeaders.entrySet()) {
-            if (entry.getKey().equalsIgnoreCase(name)) {
-                return entry.getValue();
-            }
+            return buf.toString();
         }
-        return null;
     }
 
     @Override
     public String toString() {
-        final StringBuilder sb = new StringBuilder("RestClientResponse[status=");
-        sb.append(status);
-        if (mediaType != null) {
-            sb.append(", mediaType=").append(mediaType);
-        }
-        sb.append(", length=").append(getLength());
-        sb.append(']');
-        return sb.toString();
+        return response.toString();
     }
 
 }
\ No newline at end of file
diff --git a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java
index 5858f4a65..aebb088b0 100644
--- a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java
+++ b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java
@@ -43,6 +43,7 @@
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 import java.util.stream.Collectors;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -68,7 +69,6 @@
 import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
 import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
 import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
-import org.apache.hc.core5.jackson2.http.JsonNodeEntityFallbackConsumer;
 import org.apache.hc.core5.jackson2.http.JsonObjectEntityProducer;
 import org.apache.hc.core5.jackson2.http.JsonResponseConsumers;
 import org.apache.hc.core5.net.URIBuilder;
@@ -183,7 +183,7 @@ private Object executeRequest(final ResourceMethod rm,
 
         final boolean isAsync = isAsync(rm.getMethod());
         final Class<?> rawType = resolveResponseType(rm.getMethod(), isAsync);
-        final CompletableFuture<Object> future = dispatchAsync(rawType, requestProducer);
+        final Future<?> future = dispatchAsync(rawType, requestProducer);
         if (isAsync) {
             return future;
         }
@@ -215,7 +215,7 @@ private static Class<?> resolveResponseType(final Method method, final boolean a
         return Object.class;
     }
 
-    private CompletableFuture<Object> dispatchAsync(final Class<?> rawType,
+    private CompletableFuture<?> dispatchAsync(final Class<?> rawType,
                                                     final BasicRequestProducer requestProducer) {
         if (rawType == void.class || rawType == Void.class) {
             return submit(requestProducer, new BasicResponseConsumer<>(new DiscardingEntityConsumer<>()))
@@ -225,8 +225,26 @@ private CompletableFuture<Object> dispatchAsync(final Class<?> rawType,
                     });
         }
         if (rawType == Response.class) {
-            return submit(requestProducer, new BasicResponseConsumer<>(new JsonNodeEntityFallbackConsumer(objectMapper)))
-                    .thenApply(result -> new RestClientResponse(result.getHead(), result.getBody(), objectMapper));
+            final CompletableFuture<Object> cf = new CompletableFuture<>();
+            httpClient.execute(requestProducer, new RestResponseConsumer(objectMapper), null,
+                    new FutureCallback<>() {
+
+                        @Override
+                        public void completed(final Response result) {
+                            cf.complete(result);
+                        }
+
+                        @Override
+                        public void failed(final Exception ex) {
+                            cf.completeExceptionally(ex);
+                        }
+
+                        @Override
+                        public void cancelled() {
+                            cf.cancel(false);
+                        }
+                    });
+            return cf;
         }
         if (rawType == byte[].class) {
             return submit(requestProducer, new BasicResponseConsumer<>(new BasicAsyncEntityConsumer()))
@@ -277,7 +295,7 @@ public void cancelled() {
         return cf;
     }
 
-    private static Object awaitSync(final CompletableFuture<Object> future) {
+    private static Object awaitSync(final Future<?> future) {
         try {
             return future.get();
         } catch (final ExecutionException ex) {
diff --git a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestResponseConsumer.java b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestResponseConsumer.java
new file mode 100644
index 000000000..42ca110c1
--- /dev/null
+++ b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestResponseConsumer.java
@@ -0,0 +1,160 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.rest;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+
+import jakarta.ws.rs.core.Response;
+import org.apache.hc.core5.concurrent.CallbackContribution;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.nio.AsyncEntityConsumer;
+import org.apache.hc.core5.http.nio.AsyncResponseConsumer;
+import org.apache.hc.core5.http.nio.CapacityChannel;
+import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.jackson2.http.JsonNodeEntityConsumer;
+import org.apache.hc.core5.util.Args;
+
+final class RestResponseConsumer implements AsyncResponseConsumer<Response> {
+
+    private final ObjectMapper objectMapper;
+    private final AtomicReference<AsyncEntityConsumer<?>> entityConsumerRef;
+
+    public RestResponseConsumer(final ObjectMapper objectMapper) {
+        this.objectMapper = Args.notNull(objectMapper, "Object mapper");
+        this.entityConsumerRef = new AtomicReference<>();
+    }
+
+    @Override
+    public void informationResponse(final HttpResponse response, final HttpContext context) {
+    }
+
+    @Override
+    public void consumeResponse(final HttpResponse httpResponse,
+                                final EntityDetails entityDetails,
+                                final HttpContext context,
+                                final FutureCallback<Response> resultCallback) throws HttpException, IOException {
+        if (entityDetails == null) {
+            resultCallback.completed(new RestClientResponse(
+                    objectMapper,
+                    httpResponse,
+                    null,
+                    null,
+                    -1));
+            return;
+        }
+        final ContentType contentType = ContentType.parseLenient(entityDetails.getContentType());
+        if (contentType == null || ContentType.APPLICATION_JSON.isSameMimeType(contentType)) {
+            final AsyncEntityConsumer<JsonNode> entityConsumer = new JsonNodeEntityConsumer(objectMapper.getFactory());
+            entityConsumerRef.set(entityConsumer);
+            entityConsumer.streamStart(entityDetails, new CallbackContribution<>(resultCallback) {
+
+                @Override
+                public void completed(final JsonNode result) {
+                    resultCallback.completed(new RestClientResponse(
+                            objectMapper,
+                            httpResponse,
+                            result,
+                            contentType,
+                            entityDetails.getContentLength()));
+                }
+
+            });
+        } else {
+            final AsyncEntityConsumer<String> entityConsumer = new StringAsyncEntityConsumer();
+            entityConsumerRef.set(entityConsumer);
+            entityConsumer.streamStart(entityDetails, new CallbackContribution<>(resultCallback) {
+
+                @Override
+                public void completed(final String result) {
+                    resultCallback.completed(new RestClientResponse(
+                            objectMapper,
+                            httpResponse,
+                            JsonNodeFactory.instance.textNode(result),
+                            contentType,
+                            entityDetails.getContentLength()));
+                }
+
+            });
+        }
+    }
+
+    @Override
+    public void updateCapacity(final CapacityChannel capacityChannel) throws IOException {
+        final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+        if (entityConsumer != null) {
+            entityConsumer.updateCapacity(capacityChannel);
+        } else {
+            capacityChannel.update(Integer.MAX_VALUE);
+        }
+    }
+
+    @Override
+    public void consume(final ByteBuffer data) throws IOException {
+        final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+        if (entityConsumer != null) {
+            entityConsumer.consume(data);
+        }
+    }
+
+    @Override
+    public void streamEnd(final List<? extends Header> trailers) throws HttpException, IOException {
+        final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+        if (entityConsumer != null) {
+            entityConsumer.streamEnd(trailers);
+        }
+    }
+
+    public void failed(final Exception cause) {
+        final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+        if (entityConsumer != null) {
+            entityConsumer.failed(cause);
+        }
+        releaseResources();
+    }
+
+    @Override
+    public void releaseResources() {
+        final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.getAndSet(null);
+        if (entityConsumer != null) {
+            entityConsumer.releaseResources();
+        }
+    }
+
+}
diff --git a/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java b/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java
index 64217853d..f3a735138 100644
--- a/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java
+++ b/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java
@@ -26,6 +26,8 @@
  */
 package org.apache.hc.client5.http.rest;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.InetSocketAddress;
@@ -44,7 +46,6 @@
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
 import jakarta.ws.rs.core.Response;
-
 import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
 import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
 import org.apache.hc.core5.http.ContentType;
@@ -54,8 +55,6 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 class RestClientResponseTest {
 
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -144,7 +143,7 @@ void responseJsonEntityIsBackedByJsonNode() {
 
         final JsonNode node = OBJECT_MAPPER.createObjectNode().put("id", "abc");
 
-        try (Response response = new RestClientResponse(httpResponse, node, OBJECT_MAPPER)) {
+        try (Response response = new RestClientResponse(OBJECT_MAPPER, httpResponse, node, ContentType.APPLICATION_JSON, -1)) {
             assertThat(response.readEntity(JsonNode.class)).isSameAs(node);
             assertThat(response.readEntity(Echo.class).id).isEqualTo("abc");
             assertThat(response.readEntity(String.class)).isEqualTo("{\"id\":\"abc\"}");