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 — 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 — 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}