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}