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}