001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.http;
014
015import static org.apache.juneau.internal.CollectionUtils.*;
016import static org.apache.juneau.internal.IOUtils.*;
017
018import java.io.*;
019import java.nio.*;
020import java.util.*;
021
022import org.apache.juneau.*;
023import org.apache.juneau.http.annotation.*;
024
025/**
026 * Represents the contents of a byte stream file with convenience methods for adding HTTP response headers.
027 *
028 * <p>
029 * <br>These objects can to be returned as responses by REST methods.
030 *
031 * <p>
032 * <l>StreamResources</l> are meant to be thread-safe and reusable objects.
033 * <br>The contents of the request passed into the constructor are immediately converted to read-only byte arrays.
034 *
035 * <p>
036 * Instances of this class can be built using {@link Builder}.
037 *
038 * <h5 class='section'>See Also:</h5>
039 * <ul>
040 *    <li class='link'>{@doc juneau-rest-server.RestMethod.StreamResource}
041 * </ul>
042 */
043@Response
044public class StreamResource implements Streamable {
045
046   private final MediaType mediaType;
047   private final Object[] contents;
048   private final Map<String,Object> headers;
049
050   StreamResource(Builder b) throws IOException {
051      this(b.mediaType, b.headers, b.cached, b.contents.toArray());
052   }
053
054   /**
055    * Constructor.
056    *
057    * @param mediaType The resource media type.
058    * @param headers The HTTP response headers for this streamed resource.
059    * @param cached
060    *    Identifies if this stream resource is cached in memory.
061    *    <br>If <jk>true</jk>, the contents will be loaded into a byte array for fast retrieval.
062    * @param contents
063    *    The resource contents.
064    *    <br>If multiple contents are specified, the results will be concatenated.
065    *    <br>Contents can be any of the following:
066    *    <ul>
067    *       <li><code><jk>byte</jk>[]</code>
068    *       <li><code>InputStream</code>
069    *       <li><code>Reader</code> - Converted to UTF-8 bytes.
070    *       <li><code>File</code>
071    *       <li><code>CharSequence</code> - Converted to UTF-8 bytes.
072    *    </ul>
073    * @throws IOException
074    */
075   public StreamResource(MediaType mediaType, Map<String,Object> headers, boolean cached, Object...contents) throws IOException {
076      this.mediaType = mediaType;
077      this.headers = immutableMap(headers);
078      this.contents = cached ? new Object[]{readBytes(contents)} : contents;
079   }
080
081   //-----------------------------------------------------------------------------------------------------------------
082   // Builder
083   //-----------------------------------------------------------------------------------------------------------------
084
085   /**
086    * Creates a new instance of a {@link Builder} for this class.
087    *
088    * @return A new instance of a {@link Builder}.
089    */
090   public static Builder create() {
091      return new Builder();
092   }
093
094   /**
095    * Builder class for constructing {@link StreamResource} objects.
096    *
097    * <h5 class='section'>See Also:</h5>
098    * <ul>
099    *    <li class='link'>{@doc juneau-rest-server.RestMethod.StreamResource}
100    * </ul>
101    */
102   public static class Builder {
103      ArrayList<Object> contents = new ArrayList<>();
104      MediaType mediaType;
105      Map<String,Object> headers = new LinkedHashMap<>();
106      boolean cached;
107
108      /**
109       * Specifies the resource media type string.
110       *
111       * @param mediaType The resource media type string.
112       * @return This object (for method chaining).
113       */
114      public Builder mediaType(String mediaType) {
115         this.mediaType = MediaType.forString(mediaType);
116         return this;
117      }
118
119      /**
120       * Specifies the resource media type string.
121       *
122       * @param mediaType The resource media type string.
123       * @return This object (for method chaining).
124       */
125      public Builder mediaType(MediaType mediaType) {
126         this.mediaType = mediaType;
127         return this;
128      }
129
130      /**
131       * Specifies the contents for this resource.
132       *
133       * <p>
134       * This method can be called multiple times to add more content.
135       *
136       * @param contents
137       *    The resource contents.
138       *    <br>If multiple contents are specified, the results will be concatenated.
139       *    <br>Contents can be any of the following:
140       *    <ul>
141       *       <li><code><jk>byte</jk>[]</code>
142       *       <li><code>InputStream</code>
143       *       <li><code>Reader</code> - Converted to UTF-8 bytes.
144       *       <li><code>File</code>
145       *       <li><code>CharSequence</code> - Converted to UTF-8 bytes.
146       *    </ul>
147       * @return This object (for method chaining).
148       */
149      public Builder contents(Object...contents) {
150         this.contents.addAll(Arrays.asList(contents));
151         return this;
152      }
153
154      /**
155       * Specifies an HTTP response header value.
156       *
157       * @param name The HTTP header name.
158       * @param value
159       *    The HTTP header value.
160       *    <br>Will be converted to a <code>String</code> using {@link Object#toString()}.
161       * @return This object (for method chaining).
162       */
163      public Builder header(String name, Object value) {
164         this.headers.put(name, value);
165         return this;
166      }
167
168      /**
169       * Specifies HTTP response header values.
170       *
171       * @param headers
172       *    The HTTP headers.
173       *    <br>Values will be converted to <code>Strings</code> using {@link Object#toString()}.
174       * @return This object (for method chaining).
175       */
176      public Builder headers(Map<String,Object> headers) {
177         this.headers.putAll(headers);
178         return this;
179      }
180
181      /**
182       * Specifies that this resource is intended to be cached.
183       *
184       * <p>
185       * This will trigger the contents to be loaded into a byte array for fast serializing.
186       *
187       * @return This object (for method chaining).
188       */
189      public Builder cached() {
190         this.cached = true;
191         return this;
192      }
193
194      /**
195       * Create a new {@link StreamResource} using values in this builder.
196       *
197       * @return A new immutable {@link StreamResource} object.
198       * @throws IOException
199       */
200      public StreamResource build() throws IOException {
201         return new StreamResource(this);
202      }
203   }
204
205   /**
206    * Get the HTTP response headers.
207    *
208    * @return
209    *    The HTTP response headers.
210    *    <br>An unmodifiable map.
211    *    <br>Never <jk>null</jk>.
212    */
213   @ResponseHeader("*")
214   public Map<String,Object> getHeaders() {
215      return headers;
216   }
217
218   @ResponseBody
219   @Override /* Streamable */
220   public void streamTo(OutputStream os) throws IOException {
221      for (Object c : contents)
222         pipe(c, os);
223      os.flush();
224   }
225
226   @ResponseHeader("Content-Type")
227   @Override /* Streamable */
228   public MediaType getMediaType() {
229      return mediaType;
230   }
231
232   /**
233    * Returns the contents of this stream resource.
234    *
235    * @return The contents of this stream resource.
236    * @throws IOException
237    */
238   public InputStream getContents() throws IOException {
239      if (contents.length == 1) {
240         Object c = contents[0];
241         if (c != null) {
242            if (c instanceof byte[])
243               return new ByteArrayInputStream((byte[])c);
244            else if (c instanceof InputStream)
245               return (InputStream)c;
246            else if (c instanceof File)
247               return new FileInputStream((File)c);
248            else if (c instanceof CharSequence)
249               return new ByteArrayInputStream((((CharSequence)c).toString().getBytes(UTF8)));
250         }
251      }
252      byte[][] bc = new byte[contents.length][];
253      int c = 0;
254      for (int i = 0; i < contents.length; i++) {
255         Object o = contents[i];
256         if (o == null)
257            bc[i] = new byte[0];
258         else if (o instanceof byte[])
259            bc[i] = (byte[])o;
260         else if (o instanceof InputStream)
261            bc[i] = readBytes((InputStream)o, 1024);
262         else if (o instanceof Reader)
263            bc[i] = read((Reader)o).getBytes(UTF8);
264         else if (o instanceof File)
265            bc[i] = readBytes((File)o);
266         else if (o instanceof CharSequence)
267            bc[i] = ((CharSequence)o).toString().getBytes(UTF8);
268         c += bc[i].length;
269      }
270      ByteBuffer bb = ByteBuffer.allocate(c);
271      for (byte[] b : bc)
272         bb.put(b);
273      return new ByteArrayInputStream(bb.array());
274   }
275}