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}