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.resource;
014
015import static org.apache.juneau.common.internal.StringUtils.*;
016
017import java.io.*;
018import java.util.function.*;
019
020import org.apache.http.*;
021import org.apache.juneau.annotation.*;
022import org.apache.juneau.assertions.*;
023import org.apache.juneau.http.entity.*;
024import org.apache.juneau.http.header.*;
025import org.apache.juneau.internal.*;
026
027/**
028 * A basic {@link org.apache.juneau.http.resource.HttpResource} implementation with additional features.
029 *
030 * Provides the following features:
031 * <ul class='spaced-list'>
032 *    <li>
033 *       Caching.
034 *    <li>
035 *       Fluent setters.
036 *    <li>
037 *       Fluent assertions.
038 *    <li>
039 *       Externally-supplied/dynamic content.
040 * </ul>
041 *
042 * <h5 class='section'>See Also:</h5><ul>
043 *    <li class='link'><a class="doclink" href="../../../../../index.html#juneau-rest-common">juneau-rest-common</a>
044 * </ul>
045 */
046@BeanIgnore  /* Use toString() to serialize */
047@FluentSetters
048public class BasicResource implements HttpResource {
049
050   //-----------------------------------------------------------------------------------------------------------------
051   // Instance
052   //-----------------------------------------------------------------------------------------------------------------
053
054   BasicHttpEntity entity;
055   HeaderList headers = HeaderList.create();
056   boolean unmodifiable;
057
058   /**
059    * Constructor.
060    *
061    * @param entity The entity that makes up this resource content.
062    */
063   public BasicResource(BasicHttpEntity entity) {
064      this.entity = entity;
065   }
066
067   /**
068    * Copy constructor.
069    *
070    * @param copyFrom The bean bean copied.
071    */
072   public BasicResource(BasicResource copyFrom) {
073      this.entity = copyFrom.entity.copy();
074      this.headers = copyFrom.headers.copy();
075   }
076
077
078   /**
079    * Constructor.
080    *
081    * <p>
082    * This is the constructor used when parsing an HTTP response.
083    *
084    * @param response The HTTP response to copy from.  Must not be <jk>null</jk>.
085    * @throws IOException Rethrown from {@link HttpEntity#getContent()}.
086    */
087   public BasicResource(HttpResponse response) throws IOException {
088      this(new StreamEntity());
089      copyFrom(response);
090   }
091
092   /**
093    * Creates a builder for this class initialized with the contents of this bean.
094    *
095    * <p>
096    * Allows you to create a modifiable copy of this bean.
097    *
098    * @return A new builder bean.
099    */
100   public BasicResource copy() {
101      return new BasicResource(this);
102   }
103
104   /**
105    * Copies the contents of the specified HTTP response to this builder.
106    *
107    * @param response The response to copy from.  Must not be null.
108    * @return This object.
109    * @throws IOException If content could not be retrieved.
110    */
111   public BasicResource copyFrom(HttpResponse response) throws IOException {
112      addHeaders(response.getAllHeaders());
113      setContent(response.getEntity().getContent());
114      return this;
115   }
116
117   //-----------------------------------------------------------------------------------------------------------------
118   // Properties
119   //-----------------------------------------------------------------------------------------------------------------
120
121
122   /**
123    * Specifies whether this bean should be unmodifiable.
124    * <p>
125    * When enabled, attempting to set any properties on this bean will cause an {@link UnsupportedOperationException}.
126    *
127    * @return This object.
128    */
129   @FluentSetter
130   public BasicResource setUnmodifiable() {
131      unmodifiable = true;
132      entity.setUnmodifiable();
133      headers.setUnmodifiable();
134      return this;
135   }
136
137   /**
138    * Returns <jk>true</jk> if this bean is unmodifiable.
139    *
140    * @return <jk>true</jk> if this bean is unmodifiable.
141    */
142   public boolean isUnmodifiable() {
143      return unmodifiable;
144   }
145
146   /**
147    * Throws an {@link UnsupportedOperationException} if the unmodifiable flag is set on this bean.
148    */
149   protected final void assertModifiable() {
150      if (unmodifiable)
151         throw new UnsupportedOperationException("Bean is read-only");
152   }
153
154   /**
155    * Returns access to the underlying builder for the HTTP entity.
156    *
157    * @return The underlying builder for the HTTP entity.
158    */
159   public HttpEntity getEntity() {
160      return entity;
161   }
162
163   /**
164    * Sets the content on this entity bean.
165    *
166    * @param value The entity content, can be <jk>null</jk>.
167    * @return This object.
168    */
169   @FluentSetter
170   public BasicResource setContent(Object value) {
171      entity.setContent(value);
172      return this;
173   }
174
175   /**
176    * Sets the content on this entity bean from a supplier.
177    *
178    * <p>
179    * Repeatable entities such as {@link StringEntity} use this to allow the entity content to be resolved at
180    * serialization time.
181    *
182    * @param value The entity content, can be <jk>null</jk>.
183    * @return This object.
184    */
185   @FluentSetter
186   public BasicResource setContent(Supplier<?> value) {
187      entity.setContent(value);
188      return this;
189   }
190
191   /**
192    * Sets the content type on this entity bean.
193    *
194    * @param value The new <c>Content-Type</c> header, or <jk>null</jk> to unset.
195    * @return This object.
196    */
197   @FluentSetter
198   public BasicResource setContentType(String value) {
199      entity.setContentType(value);
200      return this;
201   }
202
203   /**
204    * Sets the content type on this entity bean.
205    *
206    * @param value The new <c>Content-Type</c> header, or <jk>null</jk> to unset.
207    * @return This object.
208    */
209   @FluentSetter
210   public BasicResource setContentType(ContentType value) {
211      entity.setContentType(value);
212      return this;
213   }
214
215   /**
216    * Sets the content length on this entity bean.
217    *
218    * @param value The new <c>Content-Length</c> header value, or <c>-1</c> to unset.
219    * @return This object.
220    */
221   @FluentSetter
222   public BasicResource setContentLength(long value) {
223      entity.setContentLength(value);
224      return this;
225   }
226
227   /**
228    * Sets the content encoding header on this entity bean.
229    *
230    * @param value The new <c>Content-Encoding</c> header, or <jk>null</jk> to unset.
231    * @return This object.
232    */
233   @FluentSetter
234   public BasicResource setContentEncoding(String value) {
235      entity.setContentEncoding(value);
236      return this;
237   }
238
239   /**
240    * Sets the content encoding header on this entity bean.
241    *
242    * @param value The new <c>Content-Encoding</c> header, or <jk>null</jk> to unset.
243    * @return This object.
244    */
245   @FluentSetter
246   public BasicResource setContentEncoding(ContentEncoding value) {
247      entity.setContentEncoding(value);
248      return this;
249   }
250
251   /**
252    * Sets the 'chunked' flag value to <jk>true</jk>.
253    *
254    * <h5 class='section'>Notes:</h5><ul>
255    *    <li>If the {@link HttpEntity#getContentLength()} method returns a negative value, the HttpClient code will always
256    *       use chunked encoding.
257    * </ul>
258    *
259    * @return This object.
260    */
261   @FluentSetter
262   public BasicResource setChunked() {
263      entity.setChunked();
264      return this;
265   }
266
267   /**
268    * Sets the 'chunked' flag value.
269    *
270    * <h5 class='section'>Notes:</h5><ul>
271    *    <li class='note'>If the {@link HttpEntity#getContentLength()} method returns a negative value, the HttpClient code will always
272    *       use chunked encoding.
273    * </ul>
274    *
275    * @param value The new value for this flag.
276    * @return This object.
277    */
278   @FluentSetter
279   public BasicResource setChunked(boolean value) {
280      entity.setChunked(value);
281      return this;
282   }
283
284   /**
285    * Specifies that the contents of this resource should be cached into an internal byte array so that it can
286    * be read multiple times.
287    *
288    * @return This object.
289    * @throws IOException If entity could not be read into memory.
290    */
291   @FluentSetter
292   public BasicResource setCached() throws IOException {
293      entity.setCached();
294      return this;
295   }
296
297   //-----------------------------------------------------------------------------------------------------------------
298   // BasicHeaderGroup setters.
299   //-----------------------------------------------------------------------------------------------------------------
300
301   /**
302    * Sets the specified header in this builder.
303    *
304    * <p>
305    * This is a no-op if either the name or value is <jk>null</jk>.
306    *
307    * @param name The header name.
308    * @param value The header value.
309    * @return This object.
310    */
311   public BasicResource setHeader(String name, String value) {
312      if (name != null && value != null)
313         headers.set(name, value);
314      return this;
315   }
316
317   /**
318    * Appends the specified header to the end of the headers in this builder.
319    *
320    * <p>
321    * This is a no-op if either the name or value is <jk>null</jk>.
322    *
323    * @param name The header name.
324    * @param value The header value.
325    * @return This object.
326    */
327   public BasicResource addHeader(String name, String value) {
328      if (name != null && value != null)
329         headers.append(name, value);
330      return this;
331   }
332
333   /**
334    * Sets the specified headers in this builder.
335    *
336    * @param value The new value.
337    * @return This object.
338    */
339   @FluentSetter
340   public BasicResource setHeaders(HeaderList value) {
341      headers = value.copy();
342      return this;
343   }
344
345   /**
346    * Sets the specified headers in this builder.
347    *
348    * @param values The headers to add.  <jk>null</jk> values are ignored.
349    * @return This object.
350    */
351   public BasicResource setHeaders(Header...values) {
352      for (Header h : values) {
353         if (h != null) {
354            String n = h.getName();
355            String v = h.getValue();
356            if (isNotEmpty(n)) {
357               if (n.equalsIgnoreCase("content-type"))
358                  setContentType(v);
359               else if (n.equalsIgnoreCase("content-encoding"))
360                  setContentEncoding(v);
361               else if (n.equalsIgnoreCase("content-length"))
362                  setContentLength(Long.parseLong(v));
363               else
364                  headers.set(h);
365            }
366         }
367      }
368      return this;
369   }
370
371   /**
372    * Appends the specified headers to the end of the headers in this builder.
373    *
374    * @param values The headers to set.  <jk>null</jk> headers and headers with <jk>null</jk> names or values are ignored.
375    * @return This object.
376    */
377   public BasicResource addHeaders(Header...values) {
378      for (Header h : values) {
379         if (h != null) {
380            String n = h.getName();
381            String v = h.getValue();
382            if (isNotEmpty(n)) {
383               if (n.equalsIgnoreCase("content-type"))
384                  setContentType(v);
385               else if (n.equalsIgnoreCase("content-encoding"))
386                  setContentEncoding(v);
387               else if (n.equalsIgnoreCase("content-length"))
388                  setContentLength(Long.parseLong(v));
389               else
390                  headers.append(h);
391            }
392         }
393      }
394      return this;
395   }
396
397   //-----------------------------------------------------------------------------------------------------------------
398   // Other methods
399   //-----------------------------------------------------------------------------------------------------------------
400
401   /**
402    * Converts the contents of the entity of this resource as a string.
403    *
404    * <p>
405    * Note that this may exhaust the content on non-repeatable, non-cached entities.
406    *
407    * @return The contents of this entity as a string.
408    * @throws IOException If a problem occurred while trying to read the content.
409    */
410   public String asString() throws IOException {
411      return entity.asString();
412   }
413
414   /**
415    * Converts the contents of the entity of this resource as a byte array.
416    *
417    * <p>
418    * Note that this may exhaust the content on non-repeatable, non-cached entities.
419    *
420    * @return The contents of this entity as a byte array.
421    * @throws IOException If a problem occurred while trying to read the content.
422    */
423   public byte[] asBytes() throws IOException {
424      return entity.asBytes();
425   }
426
427   /**
428    * Returns an assertion on the contents of the entity of this resource.
429    *
430    * <p>
431    * Note that this may exhaust the content on non-repeatable, non-cached entities.
432    *
433    * @return A new fluent assertion.
434    * @throws IOException If a problem occurred while trying to read the byte array.
435    */
436   public FluentStringAssertion<BasicResource> assertString() throws IOException {
437      return new FluentStringAssertion<>(asString(), this);
438   }
439
440   /**
441    * Returns an assertion on the contents of the entity of this resource.
442    *
443    * <p>
444    * Note that this may exhaust the content on non-repeatable, non-cached entities.
445    *
446    * @return A new fluent assertion.
447    * @throws IOException If a problem occurred while trying to read the byte array.
448    */
449   public FluentByteArrayAssertion<BasicResource> assertBytes() throws IOException {
450      return new FluentByteArrayAssertion<>(asBytes(), this);
451   }
452
453   //-----------------------------------------------------------------------------------------------------------------
454   // Path-through methods.
455   //-----------------------------------------------------------------------------------------------------------------
456
457   @Override /* HttpEntity */
458   public long getContentLength() {
459      return entity.getContentLength();
460   }
461
462   @Override /* HttpEntity */
463   public boolean isRepeatable() {
464      return entity.isRepeatable();
465   }
466
467   @Override /* HttpEntity */
468   public boolean isChunked() {
469      return entity.isChunked();
470   }
471
472   @Override /* HttpEntity */
473   public Header getContentType() {
474      return entity.getContentType();
475   }
476
477   @Override /* HttpEntity */
478   public Header getContentEncoding() {
479      return entity.getContentEncoding();
480   }
481
482   @Override /* HttpEntity */
483   public boolean isStreaming() {
484      return entity.isStreaming();
485   }
486
487   @Override
488   public void consumeContent() throws IOException {}
489
490   @Override /* HttpEntity */
491   public InputStream getContent() throws IOException, UnsupportedOperationException {
492      return entity.getContent();
493   }
494
495   @Override /* HttpEntity */
496   public void writeTo(OutputStream outStream) throws IOException {
497      entity.writeTo(outStream);
498   }
499
500   @Override /* HttpResource */
501   public HeaderList getHeaders() {
502      return headers;
503   }
504
505   // <FluentSetters>
506
507   // </FluentSetters>
508}