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