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.classic.resource;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.util.function.*;
023
024import org.apache.http.*;
025import org.apache.juneau.commons.bean.*;
026import org.apache.juneau.http.UnmodifiableBean;
027import org.apache.juneau.http.classic.entity.*;
028import org.apache.juneau.http.classic.header.*;
029import org.apache.juneau.bct.assertions.*;
030
031/**
032 * A basic {@link 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 * <p>
047 * This is a self-typed (CRTP) root: a leaf declares <c>class StringResource <jk>extends</jk> BasicResource&lt;StringResource&gt;</c>
048 * and inherits leaf-typed fluent setters with no covariant overrides.
049 *
050 * <h5 class='section'>See Also:</h5><ul>
051 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common</a>
052 * </ul>
053 *
054 * @param <SELF> The self type for fluent setters.
055 */
056@BeanIgnore /* Use toString() to serialize */
057@SuppressWarnings({
058   "java:S119", // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
059   "java:S1206", // equals/hashCode not overridden; value equality not practical for this class
060   "resource" // Depends on entity (streams); value equality not practical
061})
062public abstract class BasicResource<SELF extends BasicResource<SELF>> implements HttpResource {
063   BasicHttpEntity<?> entity;
064   HeaderList headers = HeaderList.create();
065
066   /**
067    * Constructor.
068    *
069    * @param entity The entity that makes up this resource content.  Must not be <jk>null</jk>.
070    */
071   protected BasicResource(BasicHttpEntity<?> entity) {
072      this.entity = entity;
073   }
074
075   /**
076    * Copy constructor.
077    *
078    * @param copyFrom The bean being copied.  Must not be <jk>null</jk>.
079    */
080   protected BasicResource(BasicResource<?> copyFrom) {
081      this.entity = copyFrom.entity.copy();
082      this.headers = copyFrom.headers.copy();
083   }
084
085   /**
086    * Constructor.
087    *
088    * <p>
089    * This is the constructor used when parsing an HTTP response.
090    *
091    * @param response The HTTP response to copy from.  Must not be <jk>null</jk>.
092    * @throws IOException Rethrown from {@link HttpEntity#getContent()}.
093    */
094   protected BasicResource(HttpResponse response) throws IOException {
095      this(new StreamEntity());
096      copyFrom(response);
097   }
098
099   /**
100    * Appends the specified header to the end of the headers in this builder.
101    *
102    * <p>
103    * This is a no-op if either the name or value is <jk>null</jk>.
104    *
105    * @param name The header name.
106    * @param value The header value.
107    * @return This object.
108    */
109   public SELF addHeader(String name, String value) {
110      if (nn(name) && nn(value))
111         headers.append(name, value);
112      return self();
113   }
114
115   /**
116    * Appends the specified headers to the end of the headers in this builder.
117    *
118    * @param values The headers to set.  <jk>null</jk> headers and headers with <jk>null</jk> names or values are ignored.
119    * @return This object.
120    */
121   public SELF addHeaders(Header...values) {
122      for (var h : values) {
123         if (nn(h)) {
124            var n = h.getName();
125            var v = h.getValue();
126            if (ine(n)) {
127               if (eqic(n, "content-type"))
128                  setContentType(v);
129               else if (eqic(n, "content-encoding"))
130                  setContentEncoding(v);
131               else if (eqic(n, "content-length"))
132                  setContentLength(Long.parseLong(v));
133               else
134                  headers.append(h);
135            }
136         }
137      }
138      return self();
139   }
140
141   /**
142    * Converts the contents of the entity of this resource as a byte array.
143    *
144    * <p>
145    * Note that this may exhaust the content on non-repeatable, non-cached entities.
146    *
147    * @return The contents of this entity as a byte array.
148    * @throws IOException If a problem occurred while trying to read the content.
149    */
150   public byte[] asBytes() throws IOException {
151      return entity.asBytes();
152   }
153
154   /**
155    * Returns an assertion on the contents of the entity of this resource.
156    *
157    * <p>
158    * Note that this may exhaust the content on non-repeatable, non-cached entities.
159    *
160    * @return A new fluent assertion.
161    * @throws IOException If a problem occurred while trying to read the byte array.
162    */
163   public FluentByteArrayAssertion<BasicResource<?>> assertBytes() throws IOException {
164      return new FluentByteArrayAssertion<>(asBytes(), this);
165   }
166
167   /**
168    * Returns an assertion on the contents of the entity of this resource.
169    *
170    * <p>
171    * Note that this may exhaust the content on non-repeatable, non-cached entities.
172    *
173    * @return A new fluent assertion.
174    * @throws IOException If a problem occurred while trying to read the byte array.
175    */
176   public FluentStringAssertion<BasicResource<?>> assertString() throws IOException {
177      return new FluentStringAssertion<>(asString(), this);
178   }
179
180   /**
181    * Converts the contents of the entity of this resource as a string.
182    *
183    * <p>
184    * Note that this may exhaust the content on non-repeatable, non-cached entities.
185    *
186    * @return The contents of this entity as a string.
187    * @throws IOException If a problem occurred while trying to read the content.
188    */
189   public String asString() throws IOException {
190      return entity.asString();
191   }
192
193   @Override
194   public void consumeContent() throws IOException {
195      // No-op: Intentional empty implementation for optional interface method
196   }
197
198   /**
199    * Creates a builder for this class initialized with the contents of this bean.
200    *
201    * <p>
202    * Allows you to create a modifiable copy of this bean.
203    *
204    * @return A new builder bean.
205    */
206   public abstract SELF copy();
207
208   /**
209    * Copies the contents of the specified HTTP response to this builder.
210    *
211    * @param response The response to copy from.  Must not be <jk>null</jk>.
212    * @return This object.
213    * @throws IOException If content could not be retrieved.
214    */
215   public SELF copyFrom(HttpResponse response) throws IOException {
216      reqnn("response", response);
217      addHeaders(response.getAllHeaders());
218      setContent(response.getEntity().getContent());
219      return self();
220   }
221
222   @Override /* Overridden from HttpEntity */
223   public InputStream getContent() throws IOException, UnsupportedOperationException { return entity.getContent(); }
224
225   @Override /* Overridden from HttpEntity */
226   public Header getContentEncoding() { return entity.getContentEncoding(); }
227
228   @Override /* Overridden from HttpEntity */
229   public long getContentLength() { return entity.getContentLength(); }
230
231   @Override /* Overridden from HttpEntity */
232   public Header getContentType() { return entity.getContentType(); }
233
234   /**
235    * Returns access to the underlying builder for the HTTP entity.
236    *
237    * @return The underlying builder for the HTTP entity.
238    */
239   public HttpEntity getEntity() { return entity; }
240
241   @Override /* Overridden from HttpResource */
242   public HeaderList getHeaders() { return headers; }
243
244   @Override /* Overridden from HttpEntity */
245   public boolean isChunked() { return entity.isChunked(); }
246
247   @Override /* Overridden from HttpEntity */
248   public boolean isRepeatable() { return entity.isRepeatable(); }
249
250   @Override /* Overridden from HttpEntity */
251   public boolean isStreaming() { return entity.isStreaming(); }
252
253   /**
254    * Returns <jk>true</jk> if this bean is unmodifiable.
255    *
256    * @return <jk>true</jk> if this bean is unmodifiable.
257    */
258   public boolean isUnmodifiable() { return this instanceof UnmodifiableBean; }
259
260   /**
261    * Specifies that the contents of this resource should be cached into an internal byte array so that it can
262    * be read multiple times.
263    *
264    * @return This object.
265    * @throws IOException If entity could not be read into memory.
266    */
267   public SELF setCached() throws IOException {
268      entity.setCached();
269      return self();
270   }
271
272   /**
273    * Sets the 'chunked' flag value to <jk>true</jk>.
274    *
275    * <h5 class='section'>Notes:</h5><ul>
276    *    <li>If the {@link HttpEntity#getContentLength()} method returns a negative value, the HttpClient code will always
277    *       use chunked encoding.
278    * </ul>
279    *
280    * @return This object.
281    */
282   public SELF setChunked() {
283      entity.setChunked();
284      return self();
285   }
286
287   /**
288    * Sets the 'chunked' flag value.
289    *
290    * <h5 class='section'>Notes:</h5><ul>
291    *    <li class='note'>If the {@link HttpEntity#getContentLength()} method returns a negative value, the HttpClient code will always
292    *       use chunked encoding.
293    * </ul>
294    *
295    * @param value The new value for this flag.
296    * @return This object.
297    */
298   public SELF setChunked(boolean value) {
299      entity.setChunked(value);
300      return self();
301   }
302
303   /**
304    * Sets the content on this entity bean.
305    *
306    * @param value The entity content.  Can be <jk>null</jk> (the effect depends on the concrete leaf type: e.g. treated as empty for {@link StringEntity}/{@link ByteArrayEntity}, or a {@link NullPointerException} on read for {@link StreamEntity}/{@link ReaderEntity}/{@link FileEntity}).
307    * @return This object.
308    */
309   public SELF setContent(Object value) {
310      entity.setContent(value);
311      return self();
312   }
313
314   /**
315    * Sets the content on this entity bean from a supplier.
316    *
317    * <p>
318    * Repeatable entities such as {@link StringEntity} use this to allow the entity content to be resolved at
319    * serialization time.
320    *
321    * @param value The entity content, can be <jk>null</jk> (treated as a supplier that always returns <jk>null</jk>).
322    * @return This object.
323    */
324   public SELF setContent(Supplier<?> value) {
325      entity.setContent(value);
326      return self();
327   }
328
329   /**
330    * Sets the content encoding header on this entity bean.
331    *
332    * @param value The new <c>Content-Encoding</c> header, or <jk>null</jk> to unset.
333    * @return This object.
334    */
335   public SELF setContentEncoding(ContentEncoding value) {
336      entity.setContentEncoding(value);
337      return self();
338   }
339
340   /**
341    * Sets the content encoding header on this entity bean.
342    *
343    * @param value The new <c>Content-Encoding</c> header, or <jk>null</jk> to unset.
344    * @return This object.
345    */
346   public SELF setContentEncoding(String value) {
347      entity.setContentEncoding(value);
348      return self();
349   }
350
351   /**
352    * Sets the content length on this entity bean.
353    *
354    * @param value The new <c>Content-Length</c> header value, or <c>-1</c> to unset.
355    * @return This object.
356    */
357   public SELF setContentLength(long value) {
358      entity.setContentLength(value);
359      return self();
360   }
361
362   /**
363    * Sets the content type on this entity bean.
364    *
365    * @param value The new <c>Content-Type</c> header, or <jk>null</jk> to unset.
366    * @return This object.
367    */
368   public SELF setContentType(ContentType value) {
369      entity.setContentType(value);
370      return self();
371   }
372
373   /**
374    * Sets the content type on this entity bean.
375    *
376    * @param value The new <c>Content-Type</c> header, or <jk>null</jk> to unset.
377    * @return This object.
378    */
379   public SELF setContentType(String value) {
380      entity.setContentType(value);
381      return self();
382   }
383
384   /**
385    * Sets the specified header in this builder.
386    *
387    * <p>
388    * This is a no-op if either the name or value is <jk>null</jk>.
389    *
390    * @param name The header name.
391    * @param value The header value.
392    * @return This object.
393    */
394   public SELF setHeader(String name, String value) {
395      if (nn(name) && nn(value))
396         headers.set(name, value);
397      return self();
398   }
399
400   /**
401    * Sets the specified headers in this builder.
402    *
403    * @param values The headers to add.  <jk>null</jk> values are ignored.
404    * @return This object.
405    */
406   public SELF setHeaders(Header...values) {
407      for (var h : values) {
408         if (nn(h)) {
409            var n = h.getName();
410            var v = h.getValue();
411            if (ine(n)) {
412               if (eqic(n, "content-type"))
413                  setContentType(v);
414               else if (eqic(n, "content-encoding"))
415                  setContentEncoding(v);
416               else if (eqic(n, "content-length"))
417                  setContentLength(Long.parseLong(v));
418               else
419                  headers.set(h);
420            }
421         }
422      }
423      return self();
424   }
425
426   /**
427    * Sets the specified headers in this builder.
428    *
429    * @param value The new value.  Must not be <jk>null</jk>.
430    * @return This object.
431    */
432   public SELF setHeaders(HeaderList value) {
433      return modify(() -> headers = value.copy());
434   }
435
436   /**
437    * Returns an unmodifiable snapshot of this bean.
438    *
439    * <p>
440    * Idempotency: if this bean is already an unmodifiable snapshot, it is returned as-is.
441    *
442    * @return An unmodifiable snapshot of this bean, or this bean if it is already unmodifiable.
443    */
444   public abstract SELF unmodifiable();
445
446   @Override /* Overridden from HttpEntity */
447   public void writeTo(OutputStream outStream) throws IOException {
448      entity.writeTo(outStream);
449   }
450
451   /**
452    * Returns this object cast to the self type.
453    *
454    * @return This object.
455    */
456   @SuppressWarnings({
457      "unchecked" // CRTP self-type cast is safe: SELF is bound to the concrete leaf type.
458   })
459   protected final SELF self() { return (SELF) this; }
460
461   /**
462    * Single mutation funnel for direct-field state changes on this bean.
463    *
464    * <p>
465    * Each leaf's nested {@code Unmodifiable} snapshot overrides this method to throw.  Mutators that delegate to the
466    * entity or header sub-beans are frozen by the {@link #freeze()} deep-freeze of those sub-beans; this funnel
467    * additionally covers the direct-field {@link #setHeaders(HeaderList)} reassignment.
468    *
469    * @param mutation The state change to apply.
470    * @return This object.
471    */
472   protected SELF modify(Runnable mutation) {
473      mutation.run();
474      return self();
475   }
476
477   /**
478    * Deep-freeze hook invoked from a leaf's nested {@code Unmodifiable} constructor after the snapshot copy completes.
479    *
480    * <p>
481    * The mutable sub-beans are frozen here by <b>direct field assignment</b> (never via a setter, which would route
482    * through the throwing {@link #modify(Runnable)} / frozen sub-beans and blow up during construction).
483    */
484   protected void freeze() {
485      entity = entity.unmodifiable();   // DIRECT field write — snapshot the copied entity.
486      headers = headers.unmodifiable(); // DIRECT field write — snapshot the copied HeaderList.
487   }
488}