001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client;
018
019import java.io.*;
020import java.nio.charset.*;
021import java.time.*;
022import java.util.*;
023import java.util.logging.*;
024
025import org.apache.juneau.rest.client.assertion.*;
026
027/**
028 * An HTTP response returned by {@link RestRequest#run()}.
029 *
030 * <p>
031 * Wraps the transport-layer {@link TransportResponse} and provides higher-level accessors.
032 * Callers <b>must</b> close this response (it implements {@link Closeable}) to release transport resources.
033 *
034 * @since 9.2.1
035 */
036@SuppressWarnings({
037   "resource" // response is owned and closed via RestResponse.close(); getBodyStream() transfers ownership to caller
038})
039public final class RestResponse implements Closeable {
040
041   private final TransportResponse response;
042   private final RestClient client;
043   private final RestRequest request;
044   private final Level debugLevel;
045   private final InputStream body;
046   private byte[] cachedContent;
047   private long cachedContentLength = -1;
048   private boolean debugEmitted;
049   private Duration execTime;
050   // The X-Request-Id echoed by the server for this exchange, captured at onConnect; null when the server echoed
051   // none (or onConnect never ran on a synthetic error).  Never silently falls back to the sent id.
052   private String requestId;
053
054   RestResponse(TransportResponse response, RestClient client) {
055      this(response, client, null, null, 0);
056   }
057
058   RestResponse(TransportResponse response, RestClient client, RestRequest request, Level debugLevel, int debugBodyCap) {
059      this.response = response;
060      this.client = client;
061      this.request = request;
062      this.debugLevel = debugLevel;
063      var originalBody = response.getBody();
064      if (debugLevel == Level.FINEST && originalBody != null)
065         this.body = new BoundedCaptureInputStream(originalBody, debugBodyCap);
066      else
067         this.body = originalBody;
068      if (originalBody == null) {
069         cachedContentLength = 0;
070      } else {
071         var h = response.getFirstHeader("Content-Length");
072         if (h != null) {
073            try {
074               cachedContentLength = Long.parseLong(h.value());
075            } catch (NumberFormatException e) {
076               cachedContentLength = -1;
077            }
078         }
079      }
080   }
081
082   /**
083    * Returns the client that produced this response.
084    *
085    * @return The client that produced this response. Never <jk>null</jk>.
086    */
087   public RestClient getClient() {
088      return client;
089   }
090
091   /**
092    * Returns the HTTP status code.
093    *
094    * @return The status code (e.g. {@code 200}).
095    */
096   public int getStatusCode() {
097      return response.getStatusCode();
098   }
099
100   /**
101    * Returns the reason phrase (e.g. {@code "OK"}), or {@code null} if absent.
102    *
103    * @return The reason phrase, possibly <jk>null</jk>.
104    */
105   public String getReasonPhrase() {
106      return response.getReasonPhrase();
107   }
108
109   /**
110    * Returns the {@code X-Request-Id} correlation id the server echoed for this exchange.
111    *
112    * <p>
113    * Captured at {@code onConnect} from the response header (which may differ from the sent id if the server
114    * sanitized it).  Returns <jk>null</jk> when the server echoed no id, or on a synthetic no-response error where
115    * {@code onConnect} never ran &mdash; it never silently falls back to the sent id.
116    *
117    * @return The echoed correlation id, or <jk>null</jk> if none was observed.
118    * @since 10.0.0
119    */
120   public String getRequestId() {
121      return requestId;
122   }
123
124   /**
125    * Records the correlation id echoed by the server for this exchange (auto-send interceptor internal use).
126    *
127    * @param value The echoed id.
128    */
129   void setEchoedRequestId(String value) {
130      this.requestId = value;
131   }
132
133   /**
134    * Returns all response headers.
135    *
136    * @return An unmodifiable list. Never <jk>null</jk>.
137    */
138   public List<TransportHeader> getHeaders() {
139      return response.getHeaders();
140   }
141
142   /**
143    * Returns the first response header with the given name (case-insensitive), or {@code null} if absent.
144    *
145    * @param name The header name. Must not be <jk>null</jk>.
146    * @return The first matching header, or <jk>null</jk> if absent.
147    */
148   public TransportHeader getFirstHeader(String name) {
149      return response.getFirstHeader(name);
150   }
151
152   /**
153    * Returns the response body as a UTF-8 string, or {@code null} if there is no body.
154    *
155    * @return The body as a string, possibly <jk>null</jk>.
156    * @throws IOException If an I/O error occurs reading the body.
157    */
158   public String getBodyAsString() throws IOException {
159      var stream = this.body;
160      if (stream == null)
161         return null;
162      return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
163   }
164
165   /**
166    * Returns the raw response body stream, or {@code null} if there is no body.
167    *
168    * <p>
169    * Callers must not close this stream directly — close the {@link RestResponse} instead.
170    *
171    * @return The body stream, possibly <jk>null</jk>.
172    */
173   public InputStream getBodyStream() {
174      return body;
175   }
176
177   /**
178    * Asserts that the status code is in the 2xx range.
179    *
180    * @return This object (for chaining).
181    * @throws RestCallException If the status code is not 2xx.
182    */
183   public RestResponse assertOk() throws RestCallException {
184      var sc = response.getStatusCode();
185      if (sc < 200 || sc > 299)
186         throw new RestCallException(sc, "Expected 2xx status but got " + sc + " " + response.getReasonPhrase());
187      return this;
188   }
189
190   /**
191    * Asserts that the status code equals the expected value.
192    *
193    * @param expected The expected status code.
194    * @return This object (for chaining).
195    * @throws RestCallException If the status code does not match.
196    */
197   public RestResponse assertStatus(int expected) throws RestCallException {
198      var actual = response.getStatusCode();
199      if (actual != expected)
200         throw new RestCallException(actual, "Expected status " + expected + " but got " + actual);
201      return this;
202   }
203
204   /**
205    * Returns a fluent body accessor for this response.
206    *
207    * @return A new body accessor. Never <jk>null</jk>.
208    */
209   public ResponseBody body() {
210      return new ResponseBody(this);
211   }
212
213   /**
214    * Returns a fluent assertion object for this response.
215    *
216    * <p>
217    * Use this for test-style validation of the response in production or test code:
218    * <p class='bjava'>
219    *    <jv>resp</jv>.assertThat()
220    *       .statusCode(200)
221    *       .body().contains(<js>"alice"</js>);
222    * </p>
223    *
224    * @return A new assertion object. Never <jk>null</jk>.
225    */
226   public ResponseAssertion assertThat() {
227      return new ResponseAssertion(this);
228   }
229
230   /**
231    * Returns a fluent header accessor for the named response header.
232    *
233    * @param name The header name (case-insensitive). Must not be <jk>null</jk>.
234    * @return A new header accessor. Never <jk>null</jk>.
235    */
236   public ResponseHeader header(String name) {
237      return new ResponseHeader(name, this);
238   }
239
240   @Override /* Closeable */
241   public void close() throws IOException {
242      if (body instanceof BoundedCaptureInputStream bcis) {
243         bcis.drain();
244         cachedContent = bcis.getCapturedBytes();
245         cachedContentLength = bcis.getTotalBytesRead();
246      }
247      if (!debugEmitted && client != null && debugLevel != null) {
248         var thrown = request != null ? request.getException() : null;
249         RestClientDebugPipeline.emit(client.debugLogger, client.debugFormatter, debugLevel, request, this, thrown);
250         debugEmitted = true;
251      }
252      response.close();
253   }
254
255   void setExecTime(Duration value) {
256      execTime = value;
257   }
258
259   /**
260    * Returns response body bytes captured for debug logging (up to the configured body cap).
261    *
262    * @return Captured response bytes, or <jk>null</jk> if not captured.
263    */
264   public byte[] getCachedContent() {
265      return cachedContent;
266   }
267
268   /**
269    * Returns the total response body length observed while reading/draining, or {@code -1} if unknown.
270    *
271    * @return The total body length.
272    */
273   public long getCachedContentLength() {
274      return cachedContentLength;
275   }
276
277   Duration getExecTime() {
278      return execTime;
279   }
280
281   private static final class BoundedCaptureInputStream extends FilterInputStream {
282      private final ByteArrayOutputStream capture = new ByteArrayOutputStream();
283      private final int cap;
284      private long totalBytesRead;
285
286      BoundedCaptureInputStream(InputStream in, int cap) {
287         super(in);
288         this.cap = cap;
289      }
290
291      byte[] getCapturedBytes() {
292         return capture.toByteArray();
293      }
294
295      long getTotalBytesRead() {
296         return totalBytesRead;
297      }
298
299      void drain() throws IOException {
300         while (read() != -1) {
301            // consume
302         }
303      }
304
305      @Override
306      public int read() throws IOException {
307         var b = super.read();
308         if (b == -1)
309            return -1;
310         totalBytesRead++;
311         if (capture.size() < cap)
312            capture.write(b);
313         return b;
314      }
315
316      @Override
317      public int read(byte[] b, int off, int len) throws IOException {
318         var count = super.read(b, off, len);
319         if (count <= 0)
320            return count;
321         totalBytesRead += count;
322         var remaining = cap - capture.size();
323         if (remaining > 0)
324            capture.write(b, off, Math.min(count, remaining));
325         return count;
326      }
327   }
328}