Coverage Report - com.jcabi.http.request.BaseRequest
 
Classes in this File Line Coverage Branch Coverage Complexity
BaseRequest
72%
45/62
50%
24/48
1.529
BaseRequest$BaseBody
62%
15/24
0%
0/8
1.529
BaseRequest$BaseURI
90%
20/22
14%
2/14
1.529
 
 1  
 /**
 2  
  * Copyright (c) 2011-2015, jcabi.com
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions
 7  
  * are met: 1) Redistributions of source code must retain the above
 8  
  * copyright notice, this list of conditions and the following
 9  
  * disclaimer. 2) Redistributions in binary form must reproduce the above
 10  
  * copyright notice, this list of conditions and the following
 11  
  * disclaimer in the documentation and/or other materials provided
 12  
  * with the distribution. 3) Neither the name of the jcabi.com nor
 13  
  * the names of its contributors may be used to endorse or promote
 14  
  * products derived from this software without specific prior written
 15  
  * permission.
 16  
  *
 17  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 18  
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 19  
  * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 20  
  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 21  
  * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 22  
  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23  
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 24  
  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 25  
  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 26  
  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 27  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 28  
  * OF THE POSSIBILITY OF SUCH DAMAGE.
 29  
  */
 30  
 package com.jcabi.http.request;
 31  
 
 32  
 import com.jcabi.aspects.Immutable;
 33  
 import com.jcabi.aspects.Loggable;
 34  
 import com.jcabi.http.ImmutableHeader;
 35  
 import com.jcabi.http.Request;
 36  
 import com.jcabi.http.RequestBody;
 37  
 import com.jcabi.http.RequestURI;
 38  
 import com.jcabi.http.Response;
 39  
 import com.jcabi.http.Wire;
 40  
 import com.jcabi.immutable.Array;
 41  
 import com.jcabi.log.Logger;
 42  
 import java.io.ByteArrayInputStream;
 43  
 import java.io.IOException;
 44  
 import java.io.InputStream;
 45  
 import java.io.StringWriter;
 46  
 import java.io.UnsupportedEncodingException;
 47  
 import java.lang.reflect.Constructor;
 48  
 import java.lang.reflect.InvocationTargetException;
 49  
 import java.net.URI;
 50  
 import java.net.URLEncoder;
 51  
 import java.nio.charset.Charset;
 52  
 import java.util.Collection;
 53  
 import java.util.LinkedList;
 54  
 import java.util.Map;
 55  
 import javax.json.Json;
 56  
 import javax.json.JsonStructure;
 57  
 import javax.ws.rs.core.UriBuilder;
 58  
 import lombok.EqualsAndHashCode;
 59  
 
 60  
 /**
 61  
  * Base implementation of {@link Request}.
 62  
  *
 63  
  * @author Yegor Bugayenko (yegor@tpc2.com)
 64  
  * @version $Id: d3741aae9922a9939b313cbce12cb97d35c9f7cf $
 65  
  * @since 0.8
 66  
  * @checkstyle ClassDataAbstractionCoupling (500 lines)
 67  
  * @see Request
 68  
  * @see Response
 69  
  */
 70  
 @Immutable
 71  66
 @EqualsAndHashCode(of = { "home", "mtd", "hdrs", "content" })
 72  
 @Loggable(Loggable.DEBUG)
 73  
 @SuppressWarnings("PMD.TooManyMethods")
 74  426
 final class BaseRequest implements Request {
 75  
 
 76  
     /**
 77  
      * The encoding to use.
 78  
      */
 79  
     private static final String ENCODING = "UTF-8";
 80  
 
 81  
     /**
 82  
      * The Charset to use.
 83  
      * @checkstyle ConstantUsageCheck (3 lines)
 84  
      */
 85  1
     private static final Charset CHARSET =
 86  
         Charset.forName(BaseRequest.ENCODING);
 87  
 
 88  
     /**
 89  
      * An empty immutable {@code byte} array.
 90  
      */
 91  1
     private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
 92  
 
 93  
     /**
 94  
      * Wire to use.
 95  
      */
 96  
     private final transient Wire wire;
 97  
 
 98  
     /**
 99  
      * Request URI.
 100  
      */
 101  
     private final transient String home;
 102  
 
 103  
     /**
 104  
      * Method to use.
 105  
      */
 106  
     private final transient String mtd;
 107  
 
 108  
     /**
 109  
      * Socket timeout to use.
 110  
      */
 111  
     private final transient int connect;
 112  
 
 113  
     /**
 114  
      * Read timeout to use.
 115  
      */
 116  
     private final transient int read;
 117  
 
 118  
     /**
 119  
      * Headers.
 120  
      */
 121  
     private final transient Array<Map.Entry<String, String>> hdrs;
 122  
 
 123  
     /**
 124  
      * Body to use.
 125  
      */
 126  
     @Immutable.Array
 127  
     private final transient byte[] content;
 128  
 
 129  
     /**
 130  
      * Public ctor.
 131  
      * @param wre Wire
 132  
      * @param uri The resource to work with
 133  
      */
 134  
     BaseRequest(final Wire wre, final String uri) {
 135  129
         this(
 136  
             wre, uri,
 137  
             new Array<Map.Entry<String, String>>(),
 138  
             Request.GET, BaseRequest.EMPTY_BYTE_ARRAY
 139  
         );
 140  129
     }
 141  
 
 142  
     /**
 143  
      * Public ctor.
 144  
      * @param wre Wire
 145  
      * @param uri The resource to work with
 146  
      * @param headers Headers
 147  
      * @param method HTTP method
 148  
      * @param body HTTP request body
 149  
      * @checkstyle ParameterNumber (5 lines)
 150  
      */
 151  
     BaseRequest(final Wire wre, final String uri,
 152  
         final Iterable<Map.Entry<String, String>> headers,
 153  
         final String method, final byte[] body) {
 154  249
         this(wre, uri, headers, method, body, 0, 0);
 155  249
     }
 156  
 
 157  
     /**
 158  
      * Public ctor.
 159  
      * @param wre Wire
 160  
      * @param uri The resource to work with
 161  
      * @param headers Headers
 162  
      * @param method HTTP method
 163  
      * @param body HTTP request body
 164  
      * @param cnct Connect timeout for http connection
 165  
      * @param rdd Read timeout for http connection
 166  
      * @checkstyle ParameterNumber (5 lines)
 167  
      */
 168  
     BaseRequest(final Wire wre, final String uri,
 169  
         final Iterable<Map.Entry<String, String>> headers,
 170  
         final String method, final byte[] body,
 171  285
         final int cnct, final int rdd) {
 172  285
         this.wire = wre;
 173  285
         URI addr = URI.create(uri);
 174  285
         if (addr.getPath().isEmpty()) {
 175  9
             addr = UriBuilder.fromUri(addr).path("/").build();
 176  
         }
 177  285
         this.home = addr.toString();
 178  285
         this.hdrs = new Array<Map.Entry<String, String>>(headers);
 179  285
         this.mtd = method;
 180  285
         this.content = body.clone();
 181  285
         this.connect = cnct;
 182  285
         this.read = rdd;
 183  285
     }
 184  
 
 185  
     @Override
 186  
     public RequestURI uri() {
 187  259
         return new BaseRequest.BaseURI(this, this.home);
 188  
     }
 189  
 
 190  
     @Override
 191  
     public Request header(final String name, final Object value) {
 192  30
         return new BaseRequest(
 193  
             this.wire,
 194  
             this.home,
 195  
             this.hdrs.with(new ImmutableHeader(name, value.toString())),
 196  
             this.mtd,
 197  
             this.content
 198  
         );
 199  
     }
 200  
 
 201  
     @Override
 202  
     public Request reset(final String name) {
 203  0
         final Collection<Map.Entry<String, String>> headers =
 204  
             new LinkedList<>();
 205  0
         final String key = ImmutableHeader.normalize(name);
 206  0
         for (final Map.Entry<String, String> header : this.hdrs) {
 207  0
             if (!header.getKey().equals(key)) {
 208  0
                 headers.add(header);
 209  
             }
 210  0
         }
 211  0
         return new BaseRequest(
 212  
             this.wire,
 213  
             this.home,
 214  
             headers,
 215  
             this.mtd,
 216  
             this.content
 217  
         );
 218  
     }
 219  
 
 220  
     @Override
 221  
     public RequestBody body() {
 222  15
         return new BaseRequest.BaseBody(this, this.content);
 223  
     }
 224  
 
 225  
     @Override
 226  
     public Request method(final String method) {
 227  47
         return new BaseRequest(
 228  
             this.wire,
 229  
             this.home,
 230  
             this.hdrs,
 231  
             method,
 232  
             this.content
 233  
         );
 234  
     }
 235  
 
 236  
     @Override
 237  
     public Request timeout(final int cnct, final int rdd) {
 238  0
         return new BaseRequest(
 239  
             this.wire,
 240  
             this.home,
 241  
             this.hdrs,
 242  
             this.mtd,
 243  
             this.content,
 244  
             cnct,
 245  
             rdd
 246  
         );
 247  
     }
 248  
 
 249  
     @Override
 250  
     public Response fetch() throws IOException {
 251  135
         return this.fetchResponse(new ByteArrayInputStream(this.content));
 252  
     }
 253  
 
 254  
     @Override
 255  
     public Response fetch(final InputStream stream) throws IOException {
 256  4
         if (this.content.length > 0) {
 257  2
             throw new IllegalStateException(
 258  
                 "Request Body is not empty, use fetch() instead"
 259  
             );
 260  
         }
 261  2
         return this.fetchResponse(stream);
 262  
     }
 263  
 
 264  
     @Override
 265  
     public <T extends Wire> Request through(final Class<T> type,
 266  
         final Object... args) {
 267  36
         Constructor<?> ctor = null;
 268  46
         for (final Constructor<?> opt : type.getDeclaredConstructors()) {
 269  46
             if (opt.getParameterTypes().length == args.length + 1) {
 270  36
                 ctor = opt;
 271  36
                 break;
 272  
             }
 273  
         }
 274  36
         if (ctor == null) {
 275  0
             throw new IllegalArgumentException(
 276  
                 String.format(
 277  
                     "class %s doesn't have a ctor with %d argument(s)",
 278  
                     type.getName(), args.length
 279  
                 )
 280  
             );
 281  
         }
 282  36
         final Object[] params = new Object[args.length + 1];
 283  36
         params[0] = this.wire;
 284  36
         System.arraycopy(args, 0, params, 1, args.length);
 285  
         final Wire decorated;
 286  
         try {
 287  36
             decorated = Wire.class.cast(ctor.newInstance(params));
 288  0
         } catch (final InstantiationException
 289  
             | IllegalAccessException | InvocationTargetException ex) {
 290  0
             throw new IllegalStateException(ex);
 291  36
         }
 292  36
         return new BaseRequest(
 293  
             decorated,
 294  
             this.home,
 295  
             this.hdrs,
 296  
             this.mtd,
 297  
             this.content,
 298  
             this.connect,
 299  
             this.read
 300  
         );
 301  
     }
 302  
 
 303  
     @Override
 304  
     @SuppressWarnings("PMD.ConsecutiveLiteralAppends")
 305  
     public String toString() {
 306  0
         final URI uri = URI.create(this.home);
 307  0
         final StringBuilder text = new StringBuilder("HTTP/1.1 ")
 308  
             .append(this.mtd).append(' ')
 309  
             .append(uri.getPath())
 310  
             .append(" (")
 311  
             .append(uri.getHost())
 312  
             .append(")\n");
 313  0
         for (final Map.Entry<String, String> header : this.hdrs) {
 314  0
             text.append(
 315  
                 Logger.format(
 316  
                     "%s: %s\n",
 317  
                     header.getKey(),
 318  
                     header.getValue()
 319  
                 )
 320  
             );
 321  0
         }
 322  0
         return text.append('\n')
 323  
             .append(new RequestBody.Printable(this.content).toString())
 324  
             .toString();
 325  
     }
 326  
 
 327  
     /**
 328  
      * Fetch response from server.
 329  
      * @param stream The content to send.
 330  
      * @return The obtained response
 331  
      * @throws IOException If an IO exception occurs.
 332  
      */
 333  
     private Response fetchResponse(final InputStream stream)
 334  
         throws IOException {
 335  137
         final long start = System.currentTimeMillis();
 336  137
         final Response response = this.wire.send(
 337  
             this, this.home, this.mtd,
 338  
             this.hdrs, stream, this.connect,
 339  
             this.read
 340  
         );
 341  137
         final URI uri = URI.create(this.home);
 342  137
         Logger.info(
 343  
             this,
 344  
             "#fetch(%s %s%s %s): [%d %s] in %[ms]s",
 345  
             this.mtd,
 346  
             uri.getHost(),
 347  
             // @checkstyle AvoidInlineConditionalsCheck (1 line)
 348  
             uri.getPort() > 0 ? String.format(":%d", uri.getPort()) : "",
 349  
             uri.getPath(),
 350  
             response.status(),
 351  
             response.reason(),
 352  
             System.currentTimeMillis() - start
 353  
         );
 354  137
         return response;
 355  
     }
 356  
 
 357  
     /**
 358  
      * Base URI.
 359  
      */
 360  
     @Immutable
 361  0
     @EqualsAndHashCode(of = "address")
 362  
     @Loggable(Loggable.DEBUG)
 363  
     private static final class BaseURI implements RequestURI {
 364  
         /**
 365  
          * URI encapsulated.
 366  
          */
 367  
         private final transient String address;
 368  
         /**
 369  
          * Base request encapsulated.
 370  
          */
 371  
         private final transient BaseRequest owner;
 372  
         /**
 373  
          * Public ctor.
 374  
          * @param req Request
 375  
          * @param uri The URI to start with
 376  
          */
 377  293
         BaseURI(final BaseRequest req, final String uri) {
 378  293
             this.owner = req;
 379  293
             this.address = uri;
 380  293
         }
 381  
         @Override
 382  
         public String toString() {
 383  0
             return this.address;
 384  
         }
 385  
         @Override
 386  
         public Request back() {
 387  29
             return new BaseRequest(
 388  
                 this.owner.wire,
 389  
                 this.address,
 390  
                 this.owner.hdrs,
 391  
                 this.owner.mtd,
 392  
                 this.owner.content
 393  
             );
 394  
         }
 395  
         @Override
 396  
         public URI get() {
 397  230
             return URI.create(this.owner.home);
 398  
         }
 399  
         @Override
 400  
         public RequestURI set(final URI uri) {
 401  9
             return new BaseRequest.BaseURI(this.owner, uri.toString());
 402  
         }
 403  
         @Override
 404  
         public RequestURI queryParam(final String name, final Object value) {
 405  5
             return new BaseRequest.BaseURI(
 406  
                 this.owner,
 407  
                 UriBuilder.fromUri(this.address)
 408  
                     .queryParam(name, "{value}")
 409  
                     .build(value).toString()
 410  
             );
 411  
         }
 412  
         @Override
 413  
         public RequestURI queryParams(final Map<String, String> map) {
 414  1
             final UriBuilder uri = UriBuilder.fromUri(this.address);
 415  1
             final Object[] values = new Object[map.size()];
 416  1
             int idx = 0;
 417  1
             for (final Map.Entry<String, String> pair : map.entrySet()) {
 418  1
                 uri.queryParam(pair.getKey(), String.format("{x%d}", idx));
 419  1
                 values[idx] = pair.getValue();
 420  1
                 ++idx;
 421  1
             }
 422  1
             return new BaseRequest.BaseURI(
 423  
                 this.owner,
 424  
                 uri.build(values).toString()
 425  
             );
 426  
         }
 427  
         @Override
 428  
         public RequestURI path(final String segment) {
 429  17
             return new BaseRequest.BaseURI(
 430  
                 this.owner,
 431  
                 UriBuilder.fromUri(this.address)
 432  
                     .path(segment)
 433  
                     .build().toString()
 434  
             );
 435  
         }
 436  
         @Override
 437  
         public RequestURI userInfo(final String info) {
 438  1
             return new BaseRequest.BaseURI(
 439  
                 this.owner,
 440  
                 UriBuilder.fromUri(this.address)
 441  
                     .userInfo(info)
 442  
                     .build().toString()
 443  
             );
 444  
         }
 445  
         @Override
 446  
         public RequestURI port(final int num) {
 447  1
             return new BaseRequest.BaseURI(
 448  
                 this.owner,
 449  
                 UriBuilder.fromUri(this.address)
 450  
                     .port(num).build().toString()
 451  
             );
 452  
         }
 453  
     }
 454  
 
 455  
     /**
 456  
      * Base URI.
 457  
      */
 458  
     @Immutable
 459  0
     @EqualsAndHashCode(of = "text")
 460  
     @Loggable(Loggable.DEBUG)
 461  
     private static final class BaseBody implements RequestBody {
 462  
         /**
 463  
          * Content encapsulated.
 464  
          */
 465  
         @Immutable.Array
 466  
         private final transient byte[] text;
 467  
         /**
 468  
          * Base request encapsulated.
 469  
          */
 470  
         private final transient BaseRequest owner;
 471  
         /**
 472  
          * URL form character to prepend.
 473  
          */
 474  
         private final transient String prepend;
 475  
         /**
 476  
          * Public ctor.
 477  
          * @param req Request
 478  
          * @param body Text to encapsulate
 479  
          */
 480  
         BaseBody(final BaseRequest req, final byte[] body) {
 481  26
             this(req, body, "");
 482  26
         }
 483  
         /**
 484  
          * Public ctor.
 485  
          * @param req Request
 486  
          * @param body Text to encapsulate
 487  
          * @param pre Character to prepend
 488  
          */
 489  32
         BaseBody(final BaseRequest req, final byte[] body, final String pre) {
 490  32
             this.owner = req;
 491  32
             this.text = body.clone();
 492  32
             this.prepend = pre;
 493  32
         }
 494  
         @Override
 495  
         public String toString() {
 496  0
             return new RequestBody.Printable(this.text).toString();
 497  
         }
 498  
         @Override
 499  
         public Request back() {
 500  14
             return new BaseRequest(
 501  
                 this.owner.wire,
 502  
                 this.owner.home,
 503  
                 this.owner.hdrs,
 504  
                 this.owner.mtd,
 505  
                 this.text
 506  
             );
 507  
         }
 508  
         @Override
 509  
         public String get() {
 510  7
             return new String(this.text, BaseRequest.CHARSET);
 511  
         }
 512  
         @Override
 513  
         public RequestBody set(final String txt) {
 514  11
             return this.set(txt.getBytes(BaseRequest.CHARSET));
 515  
         }
 516  
         @Override
 517  
         public RequestBody set(final JsonStructure json) {
 518  1
             final StringWriter writer = new StringWriter();
 519  1
             Json.createWriter(writer).write(json);
 520  1
             return this.set(writer.toString());
 521  
         }
 522  
         @Override
 523  
         public RequestBody set(final byte[] txt) {
 524  11
             return new BaseRequest.BaseBody(this.owner, txt);
 525  
         }
 526  
         @Override
 527  
         public RequestBody formParam(final String name, final Object value) {
 528  
             try {
 529  6
                 return new BaseRequest.BaseBody(
 530  
                     this.owner,
 531  
                     new StringBuilder(this.get())
 532  
                         .append(this.prepend)
 533  
                         .append(name)
 534  
                         .append('=')
 535  
                         .append(
 536  
                             URLEncoder.encode(
 537  
                                 value.toString(),
 538  
                                 BaseRequest.ENCODING
 539  
                             )
 540  
                         )
 541  
                         .toString()
 542  
                         .getBytes(BaseRequest.CHARSET),
 543  
                     "&"
 544  
                 );
 545  0
             } catch (final UnsupportedEncodingException ex) {
 546  0
                 throw new IllegalStateException(ex);
 547  
             }
 548  
         }
 549  
         @Override
 550  
         public RequestBody formParams(final Map<String, String> params) {
 551  0
             RequestBody body = this;
 552  0
             for (final Map.Entry<String, String> param : params.entrySet()) {
 553  0
                 body = body.formParam(param.getKey(), param.getValue());
 554  0
             }
 555  0
             return body;
 556  
         }
 557  
     }
 558  
 
 559  
 }