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<StringResource></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}