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.util.*;
020
021import org.apache.juneau.*;
022import org.apache.juneau.http.annotation.*;
023
024/**
025 * Represents the contents of a text file with convenience methods for resolving SVL variables and adding
026 * HTTP response headers.
027 *
028 * <p>
029 * <br>These objects can be returned as responses by REST methods.
030 *
031 * <p>
032 * <l>ReaderResources</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 strings.
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.ReaderResource}
040 * </ul>
041 */
042@Response
043public class ReaderResource implements Writable {
044
045   private final MediaType mediaType;
046   private final Map<String,Object> headers;
047
048   @SuppressWarnings("javadoc")
049   protected final Object[] contents;
050
051   /**
052    * Constructor.
053    *
054    * @param b Builder containing values to initialize this object with.
055    * @throws IOException Thrown by underlying stream.
056    */
057   protected ReaderResource(Builder b) throws IOException {
058      this(b.mediaType, b.headers, b.cached, b.contents.toArray());
059   }
060
061   /**
062    * Constructor.
063    *
064    * @param mediaType The resource media type.
065    * @param headers The HTTP response headers for this streamed resource.
066    * @param cached
067    *    Identifies if this resource is cached in memory.
068    *    <br>If <jk>true</jk>, the contents will be loaded into a String for fast retrieval.
069    * @param contents
070    *    The resource contents.
071    *    <br>If multiple contents are specified, the results will be concatenated.
072    *    <br>Contents can be any of the following:
073    *    <ul>
074    *       <li><c>InputStream</c>
075    *       <li><c>Reader</c> - Converted to UTF-8 bytes.
076    *       <li><c>File</c>
077    *       <li><c>CharSequence</c> - Converted to UTF-8 bytes.
078    *    </ul>
079    * @throws IOException Thrown by underlying stream.
080    */
081   public ReaderResource(MediaType mediaType, Map<String,Object> headers, boolean cached, Object...contents) throws IOException {
082      this.mediaType = mediaType;
083      this.headers = immutableMap(headers);
084      this.contents = cached ? new Object[]{readAll(contents)} : contents;
085   }
086
087   //-----------------------------------------------------------------------------------------------------------------
088   // Builder
089   //-----------------------------------------------------------------------------------------------------------------
090
091   /**
092    * Creates a new instance of a {@link Builder} for this class.
093    *
094    * @return A new instance of a {@link Builder}.
095    */
096   public static Builder create() {
097      return new Builder();
098   }
099
100   /**
101    * Builder class for constructing {@link ReaderResource} objects.
102    *
103    * <ul class='seealso'>
104    *    <li class='link'>{@doc juneau-rest-server.RestMethod.ReaderResource}
105    * </ul>
106    */
107   @SuppressWarnings("javadoc")
108   public static class Builder {
109
110      public ArrayList<Object> contents = new ArrayList<>();
111      public MediaType mediaType;
112      public Map<String,Object> headers = new LinkedHashMap<>();
113      public boolean cached;
114
115      /**
116       * Specifies the resource media type string.
117       *
118       * @param mediaType The resource media type string.
119       * @return This object (for method chaining).
120       */
121      public Builder mediaType(String mediaType) {
122         this.mediaType = MediaType.forString(mediaType);
123         return this;
124      }
125
126      /**
127       * Specifies the resource media type string.
128       *
129       * @param mediaType The resource media type string.
130       * @return This object (for method chaining).
131       */
132      public Builder mediaType(MediaType mediaType) {
133         this.mediaType = mediaType;
134         return this;
135      }
136
137      /**
138       * Specifies the contents for this resource.
139       *
140       * <p>
141       * This method can be called multiple times to add more content.
142       *
143       * @param contents
144       *    The resource contents.
145       *    <br>If multiple contents are specified, the results will be concatenated.
146       *    <br>Contents can be any of the following:
147       *    <ul>
148       *       <li><c>InputStream</c>
149       *       <li><c>Reader</c> - Converted to UTF-8 bytes.
150       *       <li><c>File</c>
151       *       <li><c>CharSequence</c> - Converted to UTF-8 bytes.
152       *    </ul>
153       * @return This object (for method chaining).
154       */
155      public Builder contents(Object...contents) {
156         this.contents.addAll(Arrays.asList(contents));
157         return this;
158      }
159
160      /**
161       * Specifies an HTTP response header value.
162       *
163       * @param name The HTTP header name.
164       * @param value
165       *    The HTTP header value.
166       *    <br>Will be converted to a <c>String</c> using {@link Object#toString()}.
167       * @return This object (for method chaining).
168       */
169      public Builder header(String name, Object value) {
170         this.headers.put(name, value);
171         return this;
172      }
173
174      /**
175       * Specifies HTTP response header values.
176       *
177       * @param headers
178       *    The HTTP headers.
179       *    <br>Values will be converted to <c>Strings</c> using {@link Object#toString()}.
180       * @return This object (for method chaining).
181       */
182      public Builder headers(Map<String,Object> headers) {
183         this.headers.putAll(headers);
184         return this;
185      }
186
187      /**
188       * Specifies that this resource is intended to be cached.
189       *
190       * <p>
191       * This will trigger the contents to be loaded into a String for fast serializing.
192       *
193       * @return This object (for method chaining).
194       */
195      public Builder cached() {
196         this.cached = true;
197         return this;
198      }
199
200      /**
201       * Create a new {@link ReaderResource} using values in this builder.
202       *
203       * @return A new immutable {@link ReaderResource} object.
204       * @throws IOException Thrown by underlying stream.
205       */
206      public ReaderResource build() throws IOException {
207         return new ReaderResource(this);
208      }
209   }
210
211   //-----------------------------------------------------------------------------------------------------------------
212   // Properties
213   //-----------------------------------------------------------------------------------------------------------------
214
215   /**
216    * Get the HTTP response headers.
217    *
218    * @return
219    *    The HTTP response headers.
220    *    <br>An unmodifiable map.
221    *    <br>Never <jk>null</jk>.
222    */
223   @ResponseHeader("*")
224   public Map<String,Object> getHeaders() {
225      return headers;
226   }
227
228   @ResponseBody
229   @Override /* Writeable */
230   public Writer writeTo(Writer w) throws IOException {
231      for (Object o : contents)
232         pipe(o, w);
233      return w;
234   }
235
236   @ResponseHeader("Content-Type")
237   @Override /* Writeable */
238   public MediaType getMediaType() {
239      return mediaType;
240   }
241
242   @Override /* Object */
243   public String toString() {
244      try {
245         if (contents.length == 1)
246            return read(contents[0]);
247         return writeTo(new StringWriter()).toString();
248      } catch (IOException e) {
249         throw new RuntimeException(e);
250      }
251   }
252
253   /**
254    * Same as {@link #toString()} but strips comments from the text before returning it.
255    *
256    * <p>
257    * Supports stripping comments from the following media types: HTML, XHTML, XML, JSON, Javascript, CSS.
258    *
259    * @return The resource contents stripped of any comments.
260    */
261   public String toCommentStrippedString() {
262      String s = toString();
263      String subType = mediaType.getSubType();
264      if ("html".equals(subType) || "xhtml".equals(subType) || "xml".equals(subType))
265         s = s.replaceAll("(?s)<!--(.*?)-->\\s*", "");
266      else if ("json".equals(subType) || "javascript".equals(subType) || "css".equals(subType))
267         s = s.replaceAll("(?s)\\/\\*(.*?)\\*\\/\\s*", "");
268      return s;
269   }
270
271   /**
272    * Returns the contents of this resource.
273    *
274    * @return The contents of this resource.
275    */
276   public Reader getContents() {
277      if (contents.length == 1 && contents[0] instanceof Reader) {
278         return (Reader)contents[0];
279      }
280      return new StringReader(toString());
281   }
282}