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 static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.nio.charset.*;
023
024import org.apache.juneau.http.response.*;
025import org.apache.juneau.marshall.parser.*;
026import org.apache.juneau.marshall.sse.*;
027import org.apache.juneau.marshall.stream.*;
028
029/**
030 * A fluent accessor for an HTTP response body returned by {@link RestResponse}.
031 *
032 * <p>
033 * Provides convenient methods for reading the body as a string, byte array, or stream.
034 * The body is read lazily; each method reads and returns the body content once.
035 *
036 * <p>
037 * Obtain instances via {@link RestResponse#body()}.
038 *
039 * <h5 class='section'>See Also:</h5><ul>
040 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
041 * </ul>
042 *
043 * @since 9.2.1
044 */
045@SuppressWarnings({
046   "resource" // Eclipse resource analysis: response is borrowed; caller closes it after reading body
047})
048public final class ResponseBody {
049
050   private final RestResponse response;
051
052   ResponseBody(RestResponse response) {
053      this.response = response;
054   }
055
056   /**
057    * Returns the response body as a UTF-8 string.
058    *
059    * @return The body string, or <jk>null</jk> if the response has no body.
060    * @throws IOException If an I/O error occurs reading the body.
061    */
062   public String asString() throws IOException {
063      return response.getBodyAsString();
064   }
065
066   /**
067    * Returns the response body decoded with the given charset.
068    *
069    * @param charset The character set to use for decoding. Must not be <jk>null</jk>.
070    * @return The body string, or <jk>null</jk> if the response has no body.
071    * @throws IOException If an I/O error occurs reading the body.
072    */
073   public String asString(Charset charset) throws IOException {
074      var stream = response.getBodyStream();
075      if (stream == null)
076         return null;
077      return new String(stream.readAllBytes(), charset);
078   }
079
080   /**
081    * Returns the response body as a byte array.
082    *
083    * @return The body bytes, or <jk>null</jk> if the response has no body.
084    * @throws IOException If an I/O error occurs reading the body.
085    */
086   @SuppressWarnings({
087      "java:S1168" // 'null' is a documented sentinel distinguishing 'no response body' (e.g. HTTP 204) from an empty body; callers rely on it (see RestClientFeatures_Test.l04_responseBody_nullBody).
088   })
089   public byte[] asBytes() throws IOException {
090      var stream = response.getBodyStream();
091      if (stream == null)
092         return null;
093      return stream.readAllBytes();
094   }
095
096   /**
097    * Returns the raw response body stream.
098    *
099    * <p>
100    * Callers should not close this stream directly; close the parent {@link RestResponse} instead.
101    *
102    * @return The body stream, or <jk>null</jk> if the response has no body.
103    */
104   public InputStream asStream() {
105      return response.getBodyStream();
106   }
107
108   /**
109    * Reads the response body into a byte array and returns it, or returns {@code null} if there is no body.
110    *
111    * <p>
112    * Equivalent to {@link #asBytes()}.
113    *
114    * @return The body bytes, or <jk>null</jk> if the response has no body.
115    * @throws IOException If an I/O error occurs reading the body.
116    */
117   public byte[] readAllBytes() throws IOException {
118      return asBytes();
119   }
120
121   /**
122    * Opens a token/record-streaming cursor over the response body using the parser negotiated from the response
123    * {@code Content-Type}.
124    *
125    * <p>
126    * Equivalent to {@link #asCursor(Parser, Class) asCursor(negotiatedParser, type)} where the parser is selected
127    * from the response {@code Content-Type} header; when no registered parser matches and no default parser is
128    * configured, a <c>415 Unsupported Media Type</c> is thrown.  Use {@link #asCursor(Parser, Class)} to read with an
129    * explicit parser.
130    *
131    * @param <T> The cursor type ({@link RecordReader}, {@link TokenReader}, or a concrete subtype).
132    * @param type The declared cursor type. Must not be <jk>null</jk>.
133    * @return A cursor over the live response body. Never <jk>null</jk>.
134    * @throws IOException If the body is missing, the parser does not support the requested cursor surface, or the
135    *    produced cursor is not assignable to {@code type}.
136    */
137   public <T> T asCursor(Class<T> type) throws IOException {
138      return asCursor(negotiatedParser(), type);
139   }
140
141   /**
142    * Parses the response body to {@code type} using the parser negotiated from the response {@code Content-Type}.
143    *
144    * <p>
145    * When the header is absent or matches no registered parser and no default parser is configured, a
146    * <c>415 Unsupported Media Type</c> is thrown.  Use {@link #as(Parser, Class)} to force a specific parser,
147    * bypassing content negotiation.
148    *
149    * @param <T> The type to parse to.
150    * @param type The type to parse to. Must not be <jk>null</jk>.
151    * @return The parsed body, or <jk>null</jk> if the response has no body.
152    * @throws IOException If an I/O error occurs reading the body or the body could not be parsed.
153    */
154   public <T> T as(Class<T> type) throws IOException {
155      return as(negotiatedParser(), type);
156   }
157
158   /**
159    * Parses the response body to {@code type} using the given parser.
160    *
161    * <p>
162    * This forces the supplied parser, bypassing the {@code Content-Type} negotiation performed by {@link #as(Class)}.
163    *
164    * <p>
165    * A parse failure is surfaced strictly as an {@link IOException} wrapping the underlying {@link ParseException};
166    * the malformed body is never returned to the caller.
167    *
168    * @param <T> The type to parse to.
169    * @param parser The parser to use. Must not be <jk>null</jk>.
170    * @param type The type to parse to. Must not be <jk>null</jk>.
171    * @return The parsed body, or <jk>null</jk> if the response has no body.
172    * @throws IOException If an I/O error occurs reading the body or the body could not be parsed.
173    */
174   public <T> T as(Parser parser, Class<T> type) throws IOException {
175      reqnn("parser", parser);
176      reqnn("type", type);
177      var body = response.getBodyAsString();
178      if (body == null)
179         return null;
180      try {
181         return parser.read(body, type);
182      } catch (ParseException e) {
183         throw ioex(e);
184      }
185   }
186
187   private Parser negotiatedParser() {
188      var h = response.getFirstHeader("Content-Type");
189      var ct = h == null ? null : h.value();
190      return response.getClient().getMatchingParser(ct).orElseThrow(() -> new UnsupportedMediaType(
191         "No parser matched the response Content-Type '%s' and no default parser is configured on the client.", ct));
192   }
193
194   /**
195    * Opens a token/record-streaming cursor over the response body using the given parser.
196    *
197    * <p>
198    * This forces the supplied parser, bypassing the {@code Content-Type} negotiation performed by
199    * {@link #asCursor(Class)}.
200    *
201    * <p>
202    * The cursor reads directly from the live response stream &mdash; the body is not buffered into memory.  The
203    * caller owns the returned cursor and the parent {@link RestResponse}; close them when done.
204    *
205    * <p>
206    * When {@code type} is (or extends) {@link TokenReader} the parser must implement {@link TokenReadable};
207    * otherwise it must implement {@link RecordReadable}.
208    *
209    * @param <T> The cursor type ({@link RecordReader}, {@link TokenReader}, or a concrete subtype).
210    * @param parser The parser that opens the cursor. Must not be <jk>null</jk>.
211    * @param type The declared cursor type. Must not be <jk>null</jk>.
212    * @return A cursor over the live response body. Never <jk>null</jk>.
213    * @throws IOException If the body is missing, the parser does not support the requested cursor surface, or the
214    *    produced cursor is not assignable to {@code type}.
215    */
216   @SuppressWarnings({
217      "unchecked" // The produced cursor is verified assignable to 'type' before the cast.
218   })
219   public <T> T asCursor(Parser parser, Class<T> type) throws IOException {
220      reqnn("parser", parser);
221      reqnn("type", type);
222
223      var isToken = TokenReader.class.isAssignableFrom(type);
224      var supported = isToken ? parser instanceof TokenReadable : parser instanceof RecordReadable;
225      if (! supported)
226         throw ioex("Parser '%s' does not support the %s surface.", parser.getClass().getName(), isToken ? "token-reader" : "record-reader");
227
228      var stream = response.getBodyStream();
229      if (stream == null)
230         throw new IOException("Response has no body to open a cursor over.");
231
232      Object input = parser.isReaderParser() ? new InputStreamReader(stream, StandardCharsets.UTF_8) : stream;
233      var cursor = isToken
234         ? ((TokenReadable) parser).readTokens(input)
235         : ((RecordReadable) parser).readRecords(input);
236
237      if (! type.isInstance(cursor))
238         throw ioex("Parser '%s' produced cursor type '%s' which is not assignable to the declared type '%s'.", parser.getClass().getName(), cursor == null ? "null" : cursor.getClass().getName(), type.getName());
239
240      return (T) cursor;
241   }
242
243   /**
244    * Opens a closeable, cancellable cursor over a <c>text/event-stream</c> response body.
245    *
246    * <p>
247    * Reads the live response body as UTF-8 and dispatches one {@link SseEvent} at a time via the returned
248    * {@link SseEventReader}. The stream is not buffered into memory &mdash; events are parsed incrementally as bytes
249    * arrive, which is required for a long-lived Streamable-HTTP channel (see {@code juneau-rest-client-mcp}).
250    *
251    * <p>
252    * Closing the returned reader first closes the parent {@link RestResponse} (transport-layer cancel / release),
253    * then closes the reader itself.  This ordering is what makes the cursor <i>cancellable</i>: callers can stop
254    * consuming an open SSE channel at any time by calling {@link SseEventReader#close()}, including from another
255    * thread, without waiting for the server to finish sending events.
256    *
257    * <p>
258    * Transport-specific release behavior varies: this API guarantees that both the response and reader are closed in
259    * the above order, but the exact connection-release mechanism is determined by the active transport.
260    *
261    * <h5 class='section'>Notes:</h5><ul>
262    *    <li>This cursor is not thread-safe, except for using {@link SseEventReader#close()} as a cancellation signal from another thread.
263    *    <li>Mid-stream read failures are surfaced as {@link UncheckedIOException} from {@link SseEventReader#hasNext()} / {@link SseEventReader#next()} (per {@link SseEventReader} contract).
264    *    <li>Iterating after close throws.
265    * </ul>
266    *
267    * @return A cursor over the live response body. Never <jk>null</jk>.
268    * @throws IOException If the response has no body to open a stream over.
269    */
270   public SseEventReader asEventStream() throws IOException {
271      var stream = response.getBodyStream();
272      if (stream == null)
273         throw ioex("Response has no body to open an event stream over.");
274      return new ClosingSseEventReader(new InputStreamReader(stream, StandardCharsets.UTF_8), response);
275   }
276
277   /**
278    * An {@link SseEventReader} whose {@link #close()} also closes the parent {@link RestResponse}, so closing the
279    * cursor releases the underlying transport connection (cancellation) rather than merely stopping local reads.
280    */
281   private static final class ClosingSseEventReader extends SseEventReader {
282
283      private final RestResponse response;
284
285      ClosingSseEventReader(Reader in, RestResponse response) {
286         super(in);
287         this.response = response;
288      }
289
290      @Override /* Overridden from SseEventReader */
291      public void close() throws IOException {
292         try {
293            response.close();
294         } finally {
295            super.close();
296         }
297      }
298   }
299}