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 * <ul class='seealso'>
039 *    <li class='link'>{@doc juneau-rest-server.RestMethod.StreamResource}
040 * </ul>
041 */
042@Response
043public class StreamResource implements Streamable {
044
045   private final MediaType mediaType;
046   private final Object[] contents;
047   private final Map<String,Object> headers;
048
049   StreamResource(Builder b) throws IOException {
050      this(b.mediaType, b.headers, b.cached, b.contents.toArray());
051   }
052
053   /**
054    * Constructor.
055    *
056    * @param mediaType The resource media type.
057    * @param headers The HTTP response headers for this streamed resource.
058    * @param cached
059    *    Identifies if this stream resource is cached in memory.
060    *    <br>If <jk>true</jk>, the contents will be loaded into a byte array for fast retrieval.
061    * @param contents
062    *    The resource contents.
063    *    <br>If multiple contents are specified, the results will be concatenated.
064    *    <br>Contents can be any of the following:
065    *    <ul>
066    *       <li><code><jk>byte</jk>[]</code>
067    *       <li><c>InputStream</c>
068    *       <li><c>Reader</c> - Converted to UTF-8 bytes.
069    *       <li><c>File</c>
070    *       <li><c>CharSequence</c> - Converted to UTF-8 bytes.
071    *    </ul>
072    * @throws IOException Thrown by underlying stream.
073    */
074   public StreamResource(MediaType mediaType, Map<String,Object> headers, boolean cached, Object...contents) throws IOException {
075      this.mediaType = mediaType;
076      this.headers = immutableMap(headers);
077      this.contents = cached ? new Object[]{readBytes(contents)} : contents;
078   }
079
080   //-----------------------------------------------------------------------------------------------------------------
081   // Builder
082   //-----------------------------------------------------------------------------------------------------------------
083
084   /**
085    * Creates a new instance of a {@link Builder} for this class.
086    *
087    * @return A new instance of a {@link Builder}.
088    */
089   public static Builder create() {
090      return new Builder();
091   }
092
093   /**
094    * Builder class for constructing {@link StreamResource} objects.
095    *
096    * <ul class='seealso'>
097    *    <li class='link'>{@doc juneau-rest-server.RestMethod.StreamResource}
098    * </ul>
099    */
100   public static class Builder {
101      ArrayList<Object> contents = new ArrayList<>();
102      MediaType mediaType;
103      Map<String,Object> headers = new LinkedHashMap<>();
104      boolean cached;
105
106      /**
107       * Specifies the resource media type string.
108       *
109       * @param mediaType The resource media type string.
110       * @return This object (for method chaining).
111       */
112      public Builder mediaType(String mediaType) {
113         this.mediaType = MediaType.forString(mediaType);
114         return this;
115      }
116
117      /**
118       * Specifies the resource media type string.
119       *
120       * @param mediaType The resource media type string.
121       * @return This object (for method chaining).
122       */
123      public Builder mediaType(MediaType mediaType) {
124         this.mediaType = mediaType;
125         return this;
126      }
127
128      /**
129       * Specifies the contents for this resource.
130       *
131       * <p>
132       * This method can be called multiple times to add more content.
133       *
134       * @param contents
135       *    The resource contents.
136       *    <br>If multiple contents are specified, the results will be concatenated.
137       *    <br>Contents can be any of the following:
138       *    <ul>
139       *       <li><code><jk>byte</jk>[]</code>
140       *       <li><c>InputStream</c>
141       *       <li><c>Reader</c> - Converted to UTF-8 bytes.
142       *       <li><c>File</c>
143       *       <li><c>CharSequence</c> - Converted to UTF-8 bytes.
144       *    </ul>
145       * @return This object (for method chaining).
146       */
147      public Builder contents(Object...contents) {
148         this.contents.addAll(Arrays.asList(contents));
149         return this;
150      }
151
152      /**
153       * Specifies an HTTP response header value.
154       *
155       * @param name The HTTP header name.
156       * @param value
157       *    The HTTP header value.
158       *    <br>Will be converted to a <c>String</c> using {@link Object#toString()}.
159       * @return This object (for method chaining).
160       */
161      public Builder header(String name, Object value) {
162         this.headers.put(name, value);
163         return this;
164      }
165
166      /**
167       * Specifies HTTP response header values.
168       *
169       * @param headers
170       *    The HTTP headers.
171       *    <br>Values will be converted to <c>Strings</c> using {@link Object#toString()}.
172       * @return This object (for method chaining).
173       */
174      public Builder headers(Map<String,Object> headers) {
175         this.headers.putAll(headers);
176         return this;
177      }
178
179      /**
180       * Specifies that this resource is intended to be cached.
181       *
182       * <p>
183       * This will trigger the contents to be loaded into a byte array for fast serializing.
184       *
185       * @return This object (for method chaining).
186       */
187      public Builder cached() {
188         this.cached = true;
189         return this;
190      }
191
192      /**
193       * Create a new {@link StreamResource} using values in this builder.
194       *
195       * @return A new immutable {@link StreamResource} object.
196       * @throws IOException Thrown by underlying stream.
197       */
198      public StreamResource build() throws IOException {
199         return new StreamResource(this);
200      }
201   }
202
203   /**
204    * Get the HTTP response headers.
205    *
206    * @return
207    *    The HTTP response headers.
208    *    <br>An unmodifiable map.
209    *    <br>Never <jk>null</jk>.
210    */
211   @ResponseHeader("*")
212   public Map<String,Object> getHeaders() {
213      return headers;
214   }
215
216   @ResponseBody
217   @Override /* Streamable */
218   public void streamTo(OutputStream os) throws IOException {
219      for (Object c : contents)
220         pipe(c, os);
221      os.flush();
222   }
223
224   @ResponseHeader("Content-Type")
225   @Override /* Streamable */
226   public MediaType getMediaType() {
227      return mediaType;
228   }
229
230   /**
231    * Returns the contents of this stream resource.
232    *
233    * @return The contents of this stream resource.
234    * @throws IOException Thrown by underlying stream.
235    */
236   public InputStream getContents() throws IOException {
237      if (contents.length == 1) {
238         Object c = contents[0];
239         if (c != null) {
240            if (c instanceof byte[])
241               return new ByteArrayInputStream((byte[])c);
242            else if (c instanceof InputStream)
243               return (InputStream)c;
244            else if (c instanceof File)
245               return new FileInputStream((File)c);
246            else if (c instanceof CharSequence)
247               return new ByteArrayInputStream((((CharSequence)c).toString().getBytes(UTF8)));
248         }
249      }
250      byte[][] bc = new byte[contents.length][];
251      int c = 0;
252      for (int i = 0; i < contents.length; i++) {
253         Object o = contents[i];
254         if (o == null)
255            bc[i] = new byte[0];
256         else if (o instanceof byte[])
257            bc[i] = (byte[])o;
258         else if (o instanceof InputStream)
259            bc[i] = readBytes((InputStream)o, 1024);
260         else if (o instanceof Reader)
261            bc[i] = read((Reader)o).getBytes(UTF8);
262         else if (o instanceof File)
263            bc[i] = readBytes((File)o);
264         else if (o instanceof CharSequence)
265            bc[i] = ((CharSequence)o).toString().getBytes(UTF8);
266         c += bc[i].length;
267      }
268      ByteBuffer bb = ByteBuffer.allocate(c);
269      for (byte[] b : bc)
270         bb.put(b);
271      return new ByteArrayInputStream(bb.array());
272   }
273}