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