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}