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