001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.http.protocol; 018 019import static org.forgerock.http.util.Json.readJson; 020import static org.forgerock.http.util.Json.writeJson; 021import static java.nio.charset.StandardCharsets.ISO_8859_1; 022import static java.nio.charset.StandardCharsets.UTF_8; 023import static org.forgerock.util.Utils.closeSilently; 024 025import java.io.BufferedReader; 026import java.io.ByteArrayOutputStream; 027import java.io.Closeable; 028import java.io.IOException; 029import java.io.InputStream; 030import java.io.InputStreamReader; 031import java.io.OutputStream; 032import java.io.StringWriter; 033import java.io.UnsupportedEncodingException; 034import java.io.Writer; 035import java.nio.charset.Charset; 036 037import org.forgerock.http.header.ContentEncodingHeader; 038import org.forgerock.http.header.ContentLengthHeader; 039import org.forgerock.http.header.ContentTypeHeader; 040import org.forgerock.http.io.BranchingInputStream; 041import org.forgerock.http.io.IO; 042 043/** 044 * Message content. An entity wraps a BranchingInputStream and provides various 045 * convenience methods for accessing the content, transparently handling content 046 * encoding. The underlying input stream can be branched in order to perform 047 * repeated reads of the data. This is achieved by either calling 048 * {@link #push()}, {@link #newDecodedContentReader(Charset)}, or 049 * {@link #newDecodedContentInputStream()}. The branch can then be closed by 050 * calling {@link #pop} on the entity, or {@code close()} on the returned 051 * {@link #newDecodedContentReader(Charset) BufferedReader} or 052 * {@link #newDecodedContentInputStream() InputStream}. Calling {@link #close} 053 * on the entity fully closes the input stream invaliding any branches in the 054 * process. 055 * <p> 056 * Several convenience methods are provided for accessing the entity as either 057 * {@link #getBytes() byte}, {@link #getString() string}, or {@link #getJson() 058 * JSON} content. 059 */ 060public final class Entity implements Closeable { 061 062 /* 063 * Implementation note: this class lazily creates the alternative string and 064 * json representations. Updates to the json content, string content, bytes, 065 * or input stream invalidates the other representations accordingly. The 066 * setters cascade updates in the following order: setJson() -> setString() 067 * -> setBytes() -> setRawInputStream(). 068 */ 069 070 /** The Content-Type used when setting the entity to JSON. */ 071 static final String APPLICATION_JSON_CHARSET_UTF_8 = "application/json; charset=UTF-8"; 072 073 /** Default content stream. */ 074 private static final BranchingInputStream EMPTY_STREAM = IO 075 .newBranchingInputStream(new byte[0]); 076 077 /** The encapsulating Message which may have content encoding headers. */ 078 private final Message message; 079 080 /** The input stream from which all branches are created. */ 081 private BranchingInputStream trunk; 082 083 /** The most recently created branch. */ 084 private BranchingInputStream head; 085 086 /** Cached and lazily created JSON representation of the entity. */ 087 private Object json; 088 089 /** Cached and lazily created String representation of the entity. */ 090 private String string; 091 092 Entity(final Message message) { 093 this.message = message; 094 setEmpty(); 095 } 096 097 /** 098 * Defensive copy constructor. 099 */ 100 Entity(final Message message, final Entity entity) throws IOException { 101 this.message = message; 102 setRawContentInputStream(entity.trunk.copy()); 103 } 104 105 /** 106 * Returns {@code true} if this entity's raw content is empty. 107 * 108 * @return {@code true} if this entity's raw content is empty. 109 */ 110 public boolean isRawContentEmpty() { 111 return isDecodedContentEmpty(); 112 } 113 114 /** 115 * Returns {@code true} if this entity's decoded content is empty. 116 * 117 * @return {@code true} if this entity's decoded content is empty. 118 */ 119 public boolean isDecodedContentEmpty() { 120 try { 121 push(); 122 try { 123 return head.read() == -1; 124 } finally { 125 pop(); 126 } 127 } catch (IOException e) { 128 return true; 129 } 130 } 131 132 /** 133 * Mark this entity as being empty. 134 */ 135 public void setEmpty() { 136 setRawContentInputStream(EMPTY_STREAM); 137 } 138 139 /** 140 * Closes all resources associated with this entity. Any open streams will 141 * be closed, and the underlying content reset back to a zero length. 142 */ 143 @Override 144 public void close() { 145 setEmpty(); 146 } 147 148 /** 149 * Copies the decoded content of this entity to the provided writer. After 150 * the method returns it will no longer be possible to read data from this 151 * entity. This method does not push or pop branches. It does, however, 152 * decode the content according to the {@code Content-Encoding} header if it 153 * is present in the message. 154 * 155 * @param out 156 * The destination writer. 157 * @throws IOException 158 * If an IO error occurred while copying the decoded content. 159 */ 160 public void copyDecodedContentTo(final OutputStream out) throws IOException { 161 IO.stream(getDecodedInputStream(head), out); 162 out.flush(); 163 } 164 165 /** 166 * Copies the decoded content of this entity to the provided writer. After 167 * the method returns it will no longer be possible to read data from this 168 * entity. This method does not push or pop branches. It does, however, 169 * decode the content according to the {@code Content-Encoding} and 170 * {@code Content-Type} headers if they are present in the message. 171 * 172 * @param out 173 * The destination writer. 174 * @throws IOException 175 * If an IO error occurred while copying the decoded content. 176 */ 177 public void copyDecodedContentTo(final Writer out) throws IOException { 178 IO.stream(getBufferedReader(head, null), out); 179 out.flush(); 180 } 181 182 /** 183 * Copies the raw content of this entity to the provided output stream. 184 * After the method returns it will no longer be possible to read data from 185 * this entity. This method does not push or pop branches nor does it 186 * perform any decoding of the raw data. 187 * 188 * @param out 189 * The destination output stream. 190 * @throws IOException 191 * If an IO error occurred while copying the raw content. 192 */ 193 public void copyRawContentTo(final OutputStream out) throws IOException { 194 IO.stream(head, out); 195 out.flush(); 196 } 197 198 /** 199 * Returns a byte array containing a copy of the decoded content of this 200 * entity. Calling this method does not change the state of the underlying 201 * input stream. Subsequent changes to the content of this entity will not 202 * be reflected in the returned byte array, nor will changes in the returned 203 * byte array be reflected in the content. 204 * 205 * @return A byte array containing a copy of the decoded content of this 206 * entity (never {@code null}). 207 * @throws IOException 208 * If an IO error occurred while reading the content. 209 */ 210 public byte[] getBytes() throws IOException { 211 push(); 212 try { 213 final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 214 copyDecodedContentTo(bytes); 215 return bytes.toByteArray(); 216 } finally { 217 pop(); 218 } 219 } 220 221 /** 222 * Returns the content of this entity decoded as a JSON object. Calling this 223 * method does not change the state of the underlying input stream. 224 * Subsequent changes to the content of this entity will not be reflected in 225 * the returned JSON object, nor will changes in the returned JSON object be 226 * reflected in the content. 227 * 228 * @return The content of this entity decoded as a JSON object, which will 229 * be {@code null} only if the content represents the JSON 230 * {@code null} value. 231 * @throws IOException 232 * If an IO error occurred while reading the content or if the 233 * JSON is malformed. 234 */ 235 public Object getJson() throws IOException { 236 if (json == null) { 237 try (BufferedReader reader = newDecodedContentReader(UTF_8) /* RFC 7159 */) { 238 json = readJson(reader); 239 } 240 } 241 return json; 242 } 243 244 /** 245 * Returns an input stream representing the raw content of this entity. 246 * Reading from the input stream will update the state of this entity. 247 * 248 * @return An input stream representing the raw content of this entity. 249 */ 250 public InputStream getRawContentInputStream() { 251 return head; 252 } 253 254 /** 255 * Returns the content of this entity decoded as a string. Calling this 256 * method does not change the state of the underlying input stream. 257 * Subsequent changes to the content of this entity will not be reflected in 258 * the returned string, nor will changes in the returned string be reflected 259 * in the content. 260 * 261 * @return The content of this entity decoded as a string (never 262 * {@code null}). 263 * @throws IOException 264 * If an IO error occurred while reading the content. 265 */ 266 public String getString() throws IOException { 267 if (string == null) { 268 push(); 269 try { 270 final StringWriter writer = new StringWriter(); 271 copyDecodedContentTo(writer); 272 string = writer.toString(); 273 } finally { 274 pop(); 275 } 276 } 277 return string; 278 } 279 280 /** 281 * Returns a branched input stream representing the decoded content of this 282 * entity. Reading from the returned input stream will NOT update the state 283 * of this entity. 284 * <p> 285 * The entity will be decompressed based on any codings that are specified 286 * in the {@code Content-Encoding} header. 287 * <p> 288 * <b>Note:</b> The caller is responsible for calling the input stream's 289 * {@code close} method when it is finished reading the entity. 290 * 291 * @return A buffered input stream for reading the decoded entity. 292 * @throws UnsupportedEncodingException 293 * If content encoding are not supported. 294 * @throws IOException 295 * If an IO error occurred while reading the content. 296 */ 297 public InputStream newDecodedContentInputStream() throws IOException { 298 final BranchingInputStream headBranch = head.branch(); 299 try { 300 return getDecodedInputStream(headBranch); 301 } catch (final IOException e) { 302 closeSilently(headBranch); 303 throw e; 304 } 305 } 306 307 /** 308 * Returns a branched reader representing the decoded content of this 309 * entity. Reading from the returned reader will NOT update the state of 310 * this entity. 311 * <p> 312 * The entity will be decoded and/or decompressed based on any codings that 313 * are specified in the {@code Content-Encoding} header. 314 * <p> 315 * If {@code charset} is not {@code null} then it will be used to decode the 316 * entity, otherwise the character set specified in the message's 317 * {@code Content-Type} header (if present) will be used, otherwise the 318 * default {@code ISO-8859-1} character set. 319 * <p> 320 * <b>Note:</b> The caller is responsible for calling the reader's 321 * {@code close} method when it is finished reading the entity. 322 * 323 * @param charset 324 * The character set to decode with, or message-specified or 325 * default if {@code null}. 326 * @return A buffered reader for reading the decoded entity. 327 * @throws UnsupportedEncodingException 328 * If content encoding or charset are not supported. 329 * @throws IOException 330 * If an IO error occurred while reading the content. 331 */ 332 public BufferedReader newDecodedContentReader(final Charset charset) 333 throws IOException { 334 final BranchingInputStream headBranch = head.branch(); 335 try { 336 return getBufferedReader(headBranch, charset); 337 } catch (final IOException e) { 338 closeSilently(headBranch); 339 throw e; 340 } 341 } 342 343 /** 344 * Restores the underlying input stream to the state it had immediately 345 * before the last call to {@link #push}. 346 */ 347 public void pop() { 348 closeSilently(head); 349 head = head.parent(); 350 } 351 352 /** 353 * Saves the current position of the underlying input stream and creates a 354 * new buffered input stream. Subsequent attempts to read from this entity, 355 * e.g. using {@link #copyRawContentTo(OutputStream) copyRawContentTo} or 356 * {@code #getRawInputStream()}, will not impact the saved state. 357 * <p> 358 * Use the {@link #pop} method to restore the entity to the previous state 359 * it had before this method was called. 360 * 361 * @throws IOException 362 * If this entity has been closed. 363 */ 364 public void push() throws IOException { 365 head = head.branch(); 366 } 367 368 /** 369 * Sets the content of this entity to the raw data contained in the provided 370 * byte array. Calling this method will close any existing streams 371 * associated with the entity. Also sets the {@code Content-Length} header, 372 * overwriting any existing header. 373 * <p> 374 * Note: This method does not attempt to encode the entity based-on any 375 * codings specified in the {@code Content-Encoding} header. 376 * 377 * @param value 378 * A byte array containing the raw data. 379 */ 380 public void setBytes(final byte[] value) { 381 if (value == null || value.length == 0) { 382 message.getHeaders().put(ContentLengthHeader.NAME, 0); 383 setEmpty(); 384 } else { 385 message.getHeaders().put(ContentLengthHeader.NAME, value.length); 386 setRawContentInputStream(IO.newBranchingInputStream(value)); 387 } 388 } 389 390 /** 391 * Sets the content of this entity to the JSON representation of the 392 * provided object. Calling this method will close any existing streams 393 * associated with the entity. Also sets the {@code Content-Type} and 394 * {@code Content-Length} headers, overwriting any existing header. 395 * <p> 396 * Note: This method does not attempt to encode the entity based-on any 397 * codings specified in the {@code Content-Encoding} header. 398 * 399 * @param value 400 * The object whose JSON representation is to be store in this 401 * entity. 402 */ 403 public void setJson(final Object value) { 404 message.getHeaders().put(ContentTypeHeader.NAME, APPLICATION_JSON_CHARSET_UTF_8); 405 try { 406 setBytes(writeJson(value)); 407 } catch (IOException e) { 408 // TODO do something better than a runtime exception :) 409 throw new RuntimeException("Cannot produce JSON from " + value, e); 410 } 411 json = value; 412 } 413 414 /** 415 * Sets the content of this entity to the provided input stream. Calling 416 * this method will close any existing streams associated with the entity. 417 * No headers will be set. 418 * 419 * @param is 420 * The input stream. 421 */ 422 public void setRawContentInputStream(final BranchingInputStream is) { 423 closeSilently(trunk); // Closes all sub-branches 424 trunk = is != null ? is : EMPTY_STREAM; 425 head = trunk; 426 string = null; 427 json = null; 428 } 429 430 /** 431 * Sets the content of this entity to the provided string. Calling this 432 * method will close any existing streams associated with the entity. Also 433 * sets the {@code Content-Length} header, overwriting any existing header. 434 * <p> 435 * The character set specified in the message's {@code Content-Type} header 436 * (if present) will be used, otherwise the default {@code ISO-8859-1} 437 * character set. 438 * <p> 439 * Note: This method does not attempt to encode the entity based-on any 440 * codings specified in the {@code Content-Encoding} header. 441 * 442 * @param value 443 * The string whose value is to be store in this entity. 444 */ 445 public void setString(final String value) { 446 setBytes(value != null ? value.getBytes(cs(null)) : null); 447 string = value; 448 } 449 450 /** 451 * Returns the content of this entity decoded as a string. Calling this 452 * method does not change the state of the underlying input stream. 453 * 454 * @return The content of this entity decoded as a string (never 455 * {@code null}). 456 */ 457 @Override 458 public String toString() { 459 try { 460 return getString(); 461 } catch (final IOException e) { 462 return e.getMessage(); 463 } 464 } 465 466 private Charset cs(final Charset charset) { 467 if (charset != null) { 468 return charset; 469 } 470 // use Content-Type charset if not explicitly specified 471 final Charset contentType = ContentTypeHeader.valueOf(message).getCharset(); 472 if (contentType != null) { 473 return contentType; 474 } 475 // use default per RFC 2616 if not resolved 476 return ISO_8859_1; 477 } 478 479 private BufferedReader getBufferedReader(final InputStream is, final Charset charset) 480 throws IOException { 481 return new BufferedReader(new InputStreamReader(getDecodedInputStream(is), cs(charset))); 482 } 483 484 private InputStream getDecodedInputStream(final InputStream is) 485 throws IOException { 486 return ContentEncodingHeader.valueOf(message).decode(is); 487 } 488}