├── .0pdd.yml ├── .gitattributes ├── .github └── workflows │ ├── actionlint.yml │ ├── codecov.yml │ ├── copyrights.yml │ ├── markdown-lint.yml │ ├── mvn.yml │ ├── owasp.yml │ ├── pdd.yml │ ├── reuse.yml │ ├── typos.yml │ ├── xcop.yml │ └── yamllint.yml ├── .gitignore ├── .pdd ├── .rultor.yml ├── LICENSE.txt ├── LICENSES └── MIT.txt ├── README.md ├── REUSE.toml ├── pom.xml ├── renovate.json └── src ├── main └── java │ └── com │ └── jcabi │ └── http │ ├── ImmutableHeader.java │ ├── Request.java │ ├── RequestBody.java │ ├── RequestURI.java │ ├── Response.java │ ├── Wire.java │ ├── mock │ ├── GrizzlyQuery.java │ ├── MkAnswer.java │ ├── MkAnswerBodyBytesMatcher.java │ ├── MkAnswerBodyMatcher.java │ ├── MkAnswerHeaderMatcher.java │ ├── MkAnswerMatchers.java │ ├── MkContainer.java │ ├── MkGrizzlyAdapter.java │ ├── MkGrizzlyContainer.java │ ├── MkQuery.java │ ├── MkQueryBodyMatcher.java │ ├── MkQueryHeaderMatcher.java │ ├── MkQueryMatchers.java │ ├── MkQueryUriMatcher.java │ └── package-info.java │ ├── package-info.java │ ├── request │ ├── ApacheRequest.java │ ├── BaseRequest.java │ ├── Boundary.java │ ├── DefaultResponse.java │ ├── FakeRequest.java │ ├── JdkRequest.java │ ├── MultipartBodyBuilder.java │ └── package-info.java │ ├── response │ ├── AbstractResponse.java │ ├── JacksonResponse.java │ ├── JsonResponse.java │ ├── JsoupResponse.java │ ├── RestResponse.java │ ├── WebLinkingResponse.java │ ├── XmlResponse.java │ └── package-info.java │ └── wire │ ├── AbstractHeaderBasedCachingWire.java │ ├── AutoRedirectingWire.java │ ├── BasicAuthWire.java │ ├── CachingWire.java │ ├── CookieOptimizingWire.java │ ├── ETagCachingWire.java │ ├── FcCache.java │ ├── FcWire.java │ ├── LastModifiedCachingWire.java │ ├── OneMinuteWire.java │ ├── RetryWire.java │ ├── TrustedWire.java │ ├── UserAgentWire.java │ ├── VerboseWire.java │ └── package-info.java ├── site ├── apt │ ├── example-auto-redirecting.apt.vm │ ├── example-json-response.apt.vm │ ├── example-mock-container.apt.vm │ ├── example-request.apt.vm │ ├── example-retry-request.apt.vm │ ├── example-web-linking-response.apt.vm │ ├── example-xml-response.apt.vm │ ├── index.apt.vm │ ├── optional-dependencies.apt.vm │ └── pkix-validator.apt.vm ├── resources │ └── CNAME └── site.xml └── test ├── java └── com │ └── jcabi │ └── http │ ├── ImmutableHeaderTest.java │ ├── MockWire.java │ ├── RequestITCase.java │ ├── RequestITCaseTemplate.java │ ├── RequestSecondITCase.java │ ├── RequestTest.java │ ├── RequestTestTemplate.java │ ├── RequestTimeoutLossTest.java │ ├── mock │ ├── GrizzlyQueryTest.java │ ├── MkAnswerMatchersTest.java │ ├── MkContainerTest.java │ ├── MkQueryMatchersTest.java │ └── package-info.java │ ├── package-info.java │ ├── request │ ├── BaseRequestTest.java │ ├── BoundaryTest.java │ ├── DefaultResponseTest.java │ ├── FakeRequestTest.java │ ├── JdkRequestITCase.java │ └── package-info.java │ ├── response │ ├── JacksonResponseTest.java │ ├── JsonResponseTest.java │ ├── JsoupResponseTest.java │ ├── RestResponseITCase.java │ ├── RestResponseTest.java │ ├── WebLinkingResponseTest.java │ ├── XmlResponseTest.java │ └── package-info.java │ └── wire │ ├── AutoRedirectingWireTest.java │ ├── BasicAuthWireITCase.java │ ├── BasicAuthWireTest.java │ ├── CachingWireTest.java │ ├── CookieOptimizingWireTest.java │ ├── ETagCachingWireTest.java │ ├── FcWireTest.java │ ├── LastModifiedCachingWireTest.java │ ├── RetryWireTest.java │ ├── TrustedWireITCase.java │ ├── TrustedWireTest.java │ ├── UserAgentWireTest.java │ ├── VerboseWireTest.java │ └── package-info.java └── resources └── log4j.properties /.0pdd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | errors: 5 | - yegor256@gmail.com 6 | # alerts: 7 | # github: 8 | # - yegor256 9 | 10 | tags: 11 | - pdd 12 | - bug 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Check out all text files in UNIX format, with LF as end of line 2 | # Don't change this file. If you have any ideas about it, please 3 | # submit a separate issue about it and we'll discuss. 4 | 5 | * text=auto eol=lf 6 | *.java ident 7 | *.xml ident 8 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: actionlint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | actionlint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Download actionlint 20 | id: get_actionlint 21 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 22 | shell: bash 23 | - name: Check workflow files 24 | run: ${{ steps.get_actionlint.outputs.executable }} -color 25 | shell: bash 26 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: codecov 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | jobs: 11 | codecov: 12 | timeout-minutes: 15 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-java@v4 17 | with: 18 | distribution: 'temurin' 19 | java-version: 11 20 | - uses: actions/cache@v4 21 | with: 22 | path: ~/.m2/repository 23 | key: maven-${{ hashFiles('**/pom.xml') }} 24 | restore-keys: | 25 | maven- 26 | - run: mvn install -Pjacoco 27 | - uses: codecov/codecov-action@v5 28 | with: 29 | files: ./target/site/jacoco/jacoco.xml 30 | fail_ci_if_error: true 31 | -------------------------------------------------------------------------------- /.github/workflows/copyrights.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: copyrights 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | copyrights: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: yegor256/copyrights-action@0.0.8 20 | -------------------------------------------------------------------------------- /.github/workflows/markdown-lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: markdown-lint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | markdown-lint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: DavidAnson/markdownlint-cli2-action@v20.0.0 20 | -------------------------------------------------------------------------------- /.github/workflows/mvn.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: mvn 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | mvn: 15 | timeout-minutes: 15 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-24.04, windows-2022, macos-15] 20 | java: [11, 17, 19, 21] 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-java@v4 24 | with: 25 | distribution: 'temurin' 26 | java-version: ${{ matrix.java }} 27 | - uses: actions/cache@v4 28 | with: 29 | path: ~/.m2/repository 30 | key: ${{ runner.os }}-jdk-${{ matrix.java }}-maven-${{ hashFiles('**/pom.xml') }} 31 | restore-keys: | 32 | ${{ runner.os }}-jdk-${{ matrix.java }}-maven- 33 | - run: java -version 34 | - run: mvn -version 35 | - run: mvn --errors --batch-mode clean install -Pqulice 36 | -------------------------------------------------------------------------------- /.github/workflows/owasp.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: OWASP 6 | 'on': 7 | schedule: 8 | - cron: '0 0 * * *' 9 | pull_request: 10 | branches: [ master ] 11 | 12 | 13 | jobs: 14 | owasp: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: JDK 11 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: 11 23 | distribution: 'temurin' 24 | - name: Maven 25 | run: mvn --errors --batch-mode -Powasp dependency-check:check 26 | -------------------------------------------------------------------------------- /.github/workflows/pdd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: pdd 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | pdd: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: volodya-lombrozo/pdd-action@master 20 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: reuse 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | reuse: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: fsfe/reuse-action@v5 20 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: typos 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | typos: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: crate-ci/typos@v1.32.0 20 | -------------------------------------------------------------------------------- /.github/workflows/xcop.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: xcop 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | xcop: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: g4s8/xcop-action@master 20 | -------------------------------------------------------------------------------- /.github/workflows/yamllint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: yamllint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | yamllint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: ibiqlik/action-yamllint@v3 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .DS_Store 3 | .idea 4 | .idea/ 5 | .project 6 | .settings/ 7 | *.iml 8 | /bin 9 | local-repo/ 10 | node_modules/ 11 | target/ 12 | -------------------------------------------------------------------------------- /.pdd: -------------------------------------------------------------------------------- 1 | --source=. 2 | --verbose 3 | --exclude target/**/* 4 | --exclude src/main/resources/images/**/* 5 | --rule min-words:20 6 | --rule min-estimate:15 7 | --rule max-estimate:90 8 | -------------------------------------------------------------------------------- /.rultor.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | docker: 6 | image: yegor256/java 7 | assets: 8 | settings.xml: yegor256/home#assets/jcabi/settings.xml 9 | secring.gpg: yegor256/home#assets/secring.gpg 10 | pubring.gpg: yegor256/home#assets/pubring.gpg 11 | install: | 12 | pdd --file=/dev/null 13 | merge: 14 | script: | 15 | mvn clean install -Pqulice --errors --batch-mode 16 | release: 17 | pre: false 18 | script: |- 19 | [[ "${tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit -1 20 | mvn versions:set "-DnewVersion=${tag}" --batch-mode 21 | git commit -am "${tag}" 22 | export MAVEN_OPTS="--add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED" 23 | mvn clean deploy -Pqulice -Psonatype -Pjcabi --batch-mode --errors --settings ../settings.xml 24 | mvn clean site-deploy -Psite --errors --settings ../settings.xml --batch-mode 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2025 Yegor Bugayenko 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 1) Redistributions of source code must retain the above 7 | copyright notice, this list of conditions and the following 8 | disclaimer. 2) Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 3) Neither the name of the jcabi.com nor 12 | the names of its contributors may be used to endorse or promote 13 | products derived from this software without specific prior written 14 | permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 18 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 19 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 25 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 27 | OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2025 Yegor Bugayenko 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 1) Redistributions of source code must retain the above 7 | copyright notice, this list of conditions and the following 8 | disclaimer. 2) Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 3) Neither the name of the jcabi.com nor 12 | the names of its contributors may be used to endorse or promote 13 | products derived from this software without specific prior written 14 | permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 18 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 19 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 25 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 27 | OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![EO principles respected here](https://www.elegantobjects.org/badge.svg)](https://www.elegantobjects.org) 4 | [![DevOps By Rultor.com](https://www.rultor.com/b/jcabi/jcabi-http)](https://www.rultor.com/p/jcabi/jcabi-http) 5 | [![We recommend IntelliJ IDEA](https://www.elegantobjects.org/intellij-idea.svg)](https://www.jetbrains.com/idea/) 6 | 7 | [![mvn](https://github.com/jcabi/jcabi-http/actions/workflows/mvn.yml/badge.svg)](https://github.com/jcabi/jcabi-http/actions/workflows/mvn.yml) 8 | [![PDD status](https://www.0pdd.com/svg?name=jcabi/jcabi-http)](https://www.0pdd.com/p?name=jcabi/jcabi-http) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/742bde48ea6fabdba1ce/maintainability)](https://codeclimate.com/github/jcabi/jcabi-http/maintainability) 10 | [![Maven Central](https://img.shields.io/maven-central/v/com.jcabi/jcabi-http.svg)](https://maven-badges.herokuapp.com/maven-central/com.jcabi/jcabi-http) 11 | [![Javadoc](https://www.javadoc.io/badge/com.jcabi/jcabi-http.svg)](https://www.javadoc.io/doc/com.jcabi/jcabi-http) 12 | 13 | [![codecov](https://codecov.io/gh/jcabi/jcabi-http/branch/master/graph/badge.svg)](https://codecov.io/gh/jcabi/jcabi-http) 14 | [![Hits-of-Code](https://hitsofcode.com/github/jcabi/jcabi-http)](https://hitsofcode.com/view/github/jcabi/jcabi-http) 15 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/jcabi/jcabi-http/blob/master/LICENSE.txt) 16 | 17 | More details are here: [http.jcabi.com](https://http.jcabi.com/index.html). 18 | 19 | Also, read this blog post: [Fluent Java HTTP Client](http://www.yegor256.com/2014/04/11/jcabi-http-intro.html). 20 | 21 | ```java 22 | import com.jcabi.http.Request; 23 | import com.jcabi.http.request.JdkRequest; 24 | import com.jcabi.http.response.RestResponse; 25 | public class Main { 26 | public static void main(String[] args) { 27 | String html = new JdkRequest("https://www.google.com/test") 28 | .uri().path("/users").queryParam("id", 333).back() 29 | .method(Request.GET) 30 | .header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML) 31 | .fetch() 32 | .as(RestResponse.class) 33 | .assertStatus(HttpURLConnection.HTTP_OK) 34 | .body(); 35 | } 36 | } 37 | ``` 38 | 39 | ## How to contribute? 40 | 41 | Fork the repository, make changes, submit a pull request. 42 | We promise to review your changes same day and apply to 43 | the `master` branch, if they look correct. 44 | 45 | Please run Maven build before submitting a pull request: 46 | 47 | ``` 48 | $ mvn clean install -Pqulice 49 | ``` 50 | 51 | Make sure you're using Maven 3.6+ and Java 8+. 52 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | version = 1 5 | [[annotations]] 6 | path = [ 7 | ".DS_Store", 8 | ".gitattributes", 9 | ".gitignore", 10 | ".pdd", 11 | "**.json", 12 | "**.md", 13 | "**.txt", 14 | "**.vm", 15 | "**/.DS_Store", 16 | "**/.gitignore", 17 | "**/.pdd", 18 | "**/*.csv", 19 | "**/*.jpg", 20 | "**/*.json", 21 | "**/*.md", 22 | "**/*.pdf", 23 | "**/*.png", 24 | "**/*.svg", 25 | "**/*.txt", 26 | "**/*.vm", 27 | "**/CNAME", 28 | "CNAME", 29 | "README.md", 30 | "renovate.json", 31 | ] 32 | precedence = "override" 33 | SPDX-FileCopyrightText = "Copyright (c) 2025 Yegor Bugayenko" 34 | SPDX-License-Identifier = "MIT" 35 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/ImmutableHeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import java.util.Map; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | /** 13 | * Immutable HTTP header. 14 | * 15 | * @since 0.10 16 | */ 17 | @Immutable 18 | @ToString 19 | @EqualsAndHashCode(of = { "left", "right" }) 20 | public final class ImmutableHeader implements Map.Entry { 21 | 22 | /** 23 | * Key. 24 | */ 25 | private final transient String left; 26 | 27 | /** 28 | * Value. 29 | */ 30 | private final transient String right; 31 | 32 | /** 33 | * Public ctor. 34 | * @param key The name of it 35 | * @param value The value 36 | */ 37 | public ImmutableHeader(final String key, final String value) { 38 | this.left = ImmutableHeader.normalize(key); 39 | this.right = value; 40 | } 41 | 42 | @Override 43 | public String getKey() { 44 | return this.left; 45 | } 46 | 47 | @Override 48 | public String getValue() { 49 | return this.right; 50 | } 51 | 52 | @Override 53 | public String setValue(final String value) { 54 | throw new UnsupportedOperationException("#setValue()"); 55 | } 56 | 57 | /** 58 | * Normalize key. 59 | * @param key The key to normalize 60 | * @return Normalized key 61 | */ 62 | @SuppressWarnings("PMD.ProhibitPublicStaticMethods") 63 | public static String normalize(final String key) { 64 | final char[] chars = key.toCharArray(); 65 | chars[0] = ImmutableHeader.upper(chars[0]); 66 | for (int pos = 1; pos < chars.length; ++pos) { 67 | if (chars[pos - 1] == '-') { 68 | chars[pos] = ImmutableHeader.upper(chars[pos]); 69 | } 70 | } 71 | return new String(chars); 72 | } 73 | 74 | /** 75 | * Convert char to upper case, if required. 76 | * @param chr The char to convert 77 | * @return Upper-case char 78 | */ 79 | private static char upper(final char chr) { 80 | final char upper; 81 | if (chr >= 'a' && chr <= 'z') { 82 | upper = (char) (chr - ('a' - 'A')); 83 | } else { 84 | upper = chr; 85 | } 86 | return upper; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/RequestBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import jakarta.json.JsonStructure; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Map; 11 | 12 | /** 13 | * Request body. 14 | * 15 | *

Instance of this interface is returned by {@link Request#body()}, 16 | * and can be modified using one of the methods below. When modification 17 | * is done, method {@code back()} returns a modified instance of 18 | * {@link Request}, for example: 19 | * 20 | *

 new JdkRequest("http://my.example.com")
 21 |  *   .header("Content-Type", "application/x-www-form-urlencoded")
 22 |  *   .body()
 23 |  *   .formParam("name", "Jeff Lebowski")
 24 |  *   .formParam("age", "37")
 25 |  *   .formParam("employment", "none")
 26 |  *   .back() // returns a modified instance of Request
 27 |  *   .fetch()
28 | * 29 | *

Instances of this interface are immutable and thread-safe. 30 | * 31 | * @since 0.8 32 | */ 33 | @Immutable 34 | public interface RequestBody { 35 | 36 | /** 37 | * Get back to the request it's related to. 38 | * @return The request we're in 39 | */ 40 | Request back(); 41 | 42 | /** 43 | * Get text content. 44 | * @return Content in UTF-8 45 | */ 46 | String get(); 47 | 48 | /** 49 | * Set text content. 50 | * @param body Body content 51 | * @return New alternated body 52 | */ 53 | RequestBody set(String body); 54 | 55 | /** 56 | * Set JSON content. 57 | * @param json JSON object 58 | * @return New alternated body 59 | * @since 0.11 60 | */ 61 | RequestBody set(JsonStructure json); 62 | 63 | /** 64 | * Set byte array content. 65 | * @param body Body content 66 | * @return New alternated body 67 | */ 68 | RequestBody set(byte[] body); 69 | 70 | /** 71 | * Add form param. 72 | * @param name Query param name 73 | * @param value Value of the query param to set 74 | * @return New alternated body 75 | */ 76 | RequestBody formParam(String name, Object value); 77 | 78 | /** 79 | * Add form params. 80 | * @param params Map of params 81 | * @return New alternated body 82 | * @since 0.10 83 | */ 84 | RequestBody formParams(Map params); 85 | 86 | /** 87 | * Printer of byte array. 88 | * 89 | * @since 1.0 90 | */ 91 | @Immutable 92 | final class Printable { 93 | 94 | /** 95 | * Byte array. 96 | */ 97 | @Immutable.Array 98 | private final transient byte[] array; 99 | 100 | /** 101 | * Ctor. 102 | * @param bytes Bytes to encapsulate 103 | */ 104 | public Printable(final byte[] bytes) { 105 | this.array = copyArray(bytes); 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | final StringBuilder text = new StringBuilder(0); 111 | final char[] chrs = new String( 112 | this.array, StandardCharsets.UTF_8 113 | ).toCharArray(); 114 | if (chrs.length > 0) { 115 | for (final char chr : chrs) { 116 | // @checkstyle MagicNumber (1 line) 117 | if (chr < 128) { 118 | text.append(chr); 119 | } else { 120 | text.append("\\u").append( 121 | Integer.toHexString(chr) 122 | ); 123 | } 124 | } 125 | } else { 126 | text.append("<>"); 127 | } 128 | return text.toString(); 129 | } 130 | 131 | private static byte[] copyArray(final byte[] array) { 132 | byte[] res = new byte[0]; 133 | if (array == null) { 134 | res = array.clone(); 135 | } 136 | return res; 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/RequestURI.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import java.net.URI; 9 | import java.util.Map; 10 | 11 | /** 12 | * Request URI. 13 | * 14 | *

Instance of this interface is returned by {@link Request#uri()}, 15 | * and can be modified using one of the methods below. When modification 16 | * is done, method {@code back()} returns a modified instance of 17 | * {@link Request}, for example: 18 | * 19 | *

 new JdkRequest("http://my.example.com")
20 |  *   .header("Accept", "application/json")
21 |  *   .uri()
22 |  *   .path("/users")
23 |  *   .queryParam("name", "Jeff Lebowski")
24 |  *   .back() // returns a modified instance of Request
25 |  *   .fetch()
26 | * 27 | *

Instances of this interface are immutable and thread-safe. 28 | * 29 | * @since 0.8 30 | * @checkstyle AbbreviationAsWordInNameCheck (100 lines) 31 | */ 32 | @Immutable 33 | public interface RequestURI { 34 | 35 | /** 36 | * Get back to the request it's related to. 37 | * @return The request we're in 38 | */ 39 | Request back(); 40 | 41 | /** 42 | * Get URI. 43 | * @return The destination it is currently pointing to 44 | */ 45 | URI get(); 46 | 47 | /** 48 | * Set URI. 49 | * @param uri URI to set 50 | * @return New alternated URI 51 | */ 52 | RequestURI set(URI uri); 53 | 54 | /** 55 | * Add query param. 56 | * @param name Query param name 57 | * @param value Value of the query param to set 58 | * @return New alternated URI 59 | */ 60 | RequestURI queryParam(String name, Object value); 61 | 62 | /** 63 | * Add query params. 64 | * @param map Map of params to add 65 | * @return New alternated URI 66 | */ 67 | RequestURI queryParams(Map map); 68 | 69 | /** 70 | * Add URI path. 71 | * @param segment Path segment to add 72 | * @return New alternated URI 73 | */ 74 | RequestURI path(String segment); 75 | 76 | /** 77 | * Set user info. 78 | * @param info User info part to set 79 | * @return New alternated URI 80 | */ 81 | RequestURI userInfo(String info); 82 | 83 | /** 84 | * Set port number. 85 | * @param num The port number to set 86 | * @return New altered URI 87 | */ 88 | RequestURI port(int num); 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/Response.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * RESTful response returned by {@link Request#fetch()}. 13 | * 14 | *

You can get this response from one of implementations of {@link Request}: 15 | * 16 | *

 Response response = new JdkRequest("https://www.google.com")
17 |  *   .header("Accept", "text/html")
18 |  *   .fetch();
19 | * 20 | *

Instances of this interface are immutable and thread-safe. 21 | * 22 | * @see com.jcabi.http.request.JdkRequest * 23 | * @since 0.8 24 | */ 25 | @Immutable 26 | public interface Response { 27 | 28 | /** 29 | * Get back to the request it's related to. 30 | * @return The request we're in 31 | */ 32 | Request back(); 33 | 34 | /** 35 | * Get status of the response as a positive integer number. 36 | * @return The status code 37 | */ 38 | int status(); 39 | 40 | /** 41 | * Get status line reason phrase. 42 | * @return The status line reason phrase 43 | */ 44 | String reason(); 45 | 46 | /** 47 | * Get a collection of all headers. 48 | * @return The headers 49 | */ 50 | Map> headers(); 51 | 52 | /** 53 | * Get body as a string, assuming it's {@code UTF-8} (if there is something 54 | * else that can't be translated into a UTF-8 string a runtime exception 55 | * will be thrown). 56 | * 57 | *

DISCLAIMER: 58 | * The only encoding supported here is UTF-8. If the body of response 59 | * contains any chars that can't be used and should be replaced with 60 | * a "replacement character", a {@link RuntimeException} will be thrown. If 61 | * you need to use some other encodings, use 62 | * {@link #binary()} instead. 63 | * 64 | * @return The body, as a UTF-8 string 65 | */ 66 | String body(); 67 | 68 | /** 69 | * Raw body as a an array of bytes. 70 | * @return The body, as a UTF-8 string 71 | */ 72 | byte[] binary(); 73 | 74 | /** 75 | * Convert it to another type, by encapsulation. 76 | * @param type Type to use 77 | * @param Type to use 78 | * @return New response 79 | */ 80 | @SuppressWarnings("PMD.ShortMethodName") 81 | //@checkstyle MethodName (1 lines) 82 | T as(Class type); 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/Wire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.Collection; 11 | import java.util.Map; 12 | 13 | /** 14 | * Wire. 15 | * 16 | *

An instance of this interface can be used in 17 | * {@link Request#through(Class,Object...)} to decorate 18 | * an existing {@code wire}, for example: 19 | * 20 | *

 String html = new JdkRequest("http://google.com")
21 |  *   .through(VerboseWire.class)
22 |  *   .through(RetryWire.class)
23 |  *   .header("Accept", "text/html")
24 |  *   .fetch()
25 |  *   .body();
26 | * 27 | *

Every {@code Wire} decorator passed to {@code through()} method 28 | * wraps a previously existing one. 29 | * 30 | * @since 0.9 31 | */ 32 | @Immutable 33 | //@checkstyle ParameterNumber (16 lines) 34 | public interface Wire { 35 | 36 | /** 37 | * Send request and return response. 38 | * @param req Request 39 | * @param home URI to fetch 40 | * @param method HTTP method 41 | * @param headers Headers 42 | * @param content HTTP body 43 | * @param connect The connect timeout 44 | * @param read The read timeout 45 | * @return Response obtained 46 | * @throws IOException if fails 47 | */ 48 | Response send(Request req, String home, String method, 49 | Collection> headers, InputStream content, 50 | int connect, int read) 51 | throws IOException; 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/GrizzlyQuery.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.ImmutableHeader; 9 | import com.jcabi.immutable.ArrayMap; 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.net.URI; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.Arrays; 16 | import java.util.Collections; 17 | import java.util.LinkedList; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.concurrent.ConcurrentMap; 22 | import org.glassfish.grizzly.http.Method; 23 | import org.glassfish.grizzly.http.server.Request; 24 | 25 | /** 26 | * Mock HTTP query/request. 27 | * 28 | * @since 0.10 29 | */ 30 | @Immutable 31 | final class GrizzlyQuery implements MkQuery { 32 | 33 | /** 34 | * HTTP request method. 35 | */ 36 | private final transient Method mtd; 37 | 38 | /** 39 | * HTTP request content. 40 | */ 41 | @Immutable.Array 42 | private final transient byte[] content; 43 | 44 | /** 45 | * HTTP request URI. 46 | */ 47 | private final transient String home; 48 | 49 | /** 50 | * HTTP request headers. 51 | */ 52 | private final transient ArrayMap> hdrs; 53 | 54 | /** 55 | * Ctor. 56 | * @param request Grizzly request 57 | * @throws IOException If fails 58 | */ 59 | @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors") 60 | GrizzlyQuery(final Request request) throws IOException { 61 | request.setCharacterEncoding(StandardCharsets.UTF_8.toString()); 62 | this.home = GrizzlyQuery.uri(request); 63 | this.mtd = request.getMethod(); 64 | this.hdrs = GrizzlyQuery.headers(request); 65 | this.content = GrizzlyQuery.input(request); 66 | } 67 | 68 | @Override 69 | public URI uri() { 70 | return URI.create(this.home); 71 | } 72 | 73 | @Override 74 | public String method() { 75 | return this.mtd.getMethodString(); 76 | } 77 | 78 | @Override 79 | public Map> headers() { 80 | return Collections.unmodifiableMap(this.hdrs); 81 | } 82 | 83 | @Override 84 | public String body() { 85 | return new String(this.content, StandardCharsets.UTF_8); 86 | } 87 | 88 | @Override 89 | public byte[] binary() { 90 | return Arrays.copyOf(this.content, this.content.length); 91 | } 92 | 93 | /** 94 | * Fetch URI from the request. 95 | * @param request Request 96 | * @return URI 97 | */ 98 | private static String uri(final Request request) { 99 | final StringBuilder uri = new StringBuilder(request.getRequestURI()); 100 | final String query = request.getQueryString(); 101 | if (query != null && !query.isEmpty()) { 102 | uri.append('?').append(query); 103 | } 104 | return uri.toString(); 105 | } 106 | 107 | /** 108 | * Fetch headers from the request. 109 | * @param request Request 110 | * @return Headers 111 | */ 112 | private static ArrayMap> headers( 113 | final Request request 114 | ) { 115 | final ConcurrentMap> headers = 116 | new ConcurrentHashMap<>(0); 117 | final Iterable names = request.getHeaderNames(); 118 | for (final String name : names) { 119 | headers.put( 120 | ImmutableHeader.normalize(name), 121 | GrizzlyQuery.headers(request, name) 122 | ); 123 | } 124 | return new ArrayMap<>(headers); 125 | } 126 | 127 | /** 128 | * Get headers by name. 129 | * @param request Grizzly request 130 | * @param name Name of header 131 | * @return List of values 132 | */ 133 | private static List headers( 134 | final Request request, final String name 135 | ) { 136 | final List list = new LinkedList<>(); 137 | final Iterable values = request.getHeaders(name); 138 | for (final Object value : values) { 139 | list.add(value.toString()); 140 | } 141 | return list; 142 | } 143 | 144 | /** 145 | * Read req. 146 | * @param req Grizzly req 147 | * @return Bytes of input 148 | * @throws IOException If fails 149 | */ 150 | private static byte[] input(final Request req) throws IOException { 151 | // @checkstyle MagicNumber (1 line) 152 | final byte[] buffer = new byte[8192]; 153 | final ByteArrayOutputStream output; 154 | try (InputStream input = req.getInputStream()) { 155 | output = new ByteArrayOutputStream(); 156 | while (true) { 157 | final int bytes = input.read(buffer); 158 | if (bytes == -1) { 159 | break; 160 | } 161 | output.write(buffer, 0, bytes); 162 | } 163 | } 164 | return output.toByteArray(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkAnswerBodyBytesMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import lombok.EqualsAndHashCode; 8 | import lombok.ToString; 9 | import org.hamcrest.Description; 10 | import org.hamcrest.Matcher; 11 | import org.hamcrest.TypeSafeMatcher; 12 | 13 | /** 14 | * Matcher for checking {@link MkAnswer#bodyBytes()} result. 15 | * @since 0.17 16 | */ 17 | @ToString 18 | @EqualsAndHashCode(callSuper = false, of = "matcher") 19 | final class MkAnswerBodyBytesMatcher extends TypeSafeMatcher { 20 | /** 21 | * The Matcher to use against the body. 22 | */ 23 | private final transient Matcher matcher; 24 | 25 | /** 26 | * Ctor. 27 | * @param match The matcher to use for the body 28 | */ 29 | MkAnswerBodyBytesMatcher(final Matcher match) { 30 | super(); 31 | this.matcher = match; 32 | } 33 | 34 | @Override 35 | public void describeTo(final Description description) { 36 | this.matcher.describeTo( 37 | description.appendText("MkAnswer body bytes matching: ") 38 | ); 39 | } 40 | 41 | @Override 42 | public boolean matchesSafely(final MkAnswer item) { 43 | return this.matcher.matches(item.bodyBytes()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkAnswerBodyMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import lombok.EqualsAndHashCode; 8 | import lombok.ToString; 9 | import org.hamcrest.Description; 10 | import org.hamcrest.Matcher; 11 | import org.hamcrest.TypeSafeMatcher; 12 | 13 | /** 14 | * Matcher for checking {@link MkAnswer#body()} result. 15 | * @since 1.5 16 | */ 17 | @ToString 18 | @EqualsAndHashCode(callSuper = false, of = "matcher") 19 | final class MkAnswerBodyMatcher extends TypeSafeMatcher { 20 | /** 21 | * The Matcher to use against the body. 22 | */ 23 | private final transient Matcher matcher; 24 | 25 | /** 26 | * Ctor. 27 | * @param match The matcher to use for the body 28 | */ 29 | MkAnswerBodyMatcher(final Matcher match) { 30 | super(); 31 | this.matcher = match; 32 | } 33 | 34 | @Override 35 | public void describeTo(final Description description) { 36 | this.matcher.describeTo( 37 | description.appendText("MkAnswer body matching: ") 38 | ); 39 | } 40 | 41 | @Override 42 | public boolean matchesSafely(final MkAnswer item) { 43 | return this.matcher.matches(item.body()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkAnswerHeaderMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import lombok.EqualsAndHashCode; 8 | import lombok.ToString; 9 | import org.hamcrest.Description; 10 | import org.hamcrest.Matcher; 11 | import org.hamcrest.TypeSafeMatcher; 12 | 13 | /** 14 | * Matcher for checking {@link MkAnswer#headers()} contents. 15 | * @since 1.5 16 | */ 17 | @ToString 18 | @EqualsAndHashCode(callSuper = false, of = {"header", "matcher"}) 19 | final class MkAnswerHeaderMatcher extends TypeSafeMatcher { 20 | /** 21 | * The header to match. 22 | */ 23 | private final transient String header; 24 | 25 | /** 26 | * The Matcher to use against the header. 27 | */ 28 | private final transient Matcher> matcher; 29 | 30 | /** 31 | * Ctor. 32 | * @param hdr The header to match 33 | * @param match The matcher to use for the header 34 | */ 35 | MkAnswerHeaderMatcher(final String hdr, 36 | final Matcher> match) { 37 | super(); 38 | this.header = hdr; 39 | this.matcher = match; 40 | } 41 | 42 | @Override 43 | public void describeTo(final Description description) { 44 | this.matcher.describeTo( 45 | description.appendText("MkAnswer containing header ") 46 | .appendText(this.header) 47 | .appendText(" with matching value(s) of: ") 48 | ); 49 | } 50 | 51 | @Override 52 | public boolean matchesSafely(final MkAnswer item) { 53 | return item.headers().containsKey(this.header) 54 | && this.matcher.matches(item.headers().get(this.header)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkAnswerMatchers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import org.hamcrest.Matcher; 8 | 9 | /** 10 | * Convenient set of matchers for {@link MkAnswer}. 11 | * @since 1.5 12 | */ 13 | @SuppressWarnings("PMD.ProhibitPublicStaticMethods") 14 | public final class MkAnswerMatchers { 15 | /** 16 | * Private ctor. 17 | */ 18 | private MkAnswerMatchers() { 19 | // Utility class - cannot instantiate. 20 | } 21 | 22 | /** 23 | * Matches the value of the MkAnswer's body against the given matcher. 24 | * 25 | * @param matcher The matcher to use. 26 | * @return Matcher for checking the body of MkAnswer 27 | */ 28 | public static Matcher hasBody(final Matcher matcher) { 29 | return new MkAnswerBodyMatcher(matcher); 30 | } 31 | 32 | /** 33 | * Matches the value of the MkAnswer's body bytes against the given 34 | * matcher. 35 | * @param matcher The matcher to use 36 | * @return Matcher for checking the body of MkAnswer 37 | */ 38 | public static Matcher hasBodyBytes( 39 | final Matcher matcher) { 40 | return new MkAnswerBodyBytesMatcher(matcher); 41 | } 42 | 43 | /** 44 | * Matches the content of the MkAnswer's header against the given 45 | * matcher. Note that for a valid match to occur, the header entry must 46 | * exist and its value(s) must match the given matcher. 47 | * 48 | * @param header The header to check. 49 | * @param matcher The matcher to use. 50 | * @return Matcher for checking the body of MkAnswer 51 | */ 52 | public static Matcher hasHeader(final String header, 53 | final Matcher> matcher) { 54 | return new MkAnswerHeaderMatcher(header, matcher); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import java.io.Closeable; 8 | import java.io.IOException; 9 | import java.net.URI; 10 | import java.util.Collection; 11 | import org.hamcrest.Matcher; 12 | 13 | /** 14 | * Mock version of Java Servlet container. 15 | * 16 | *

A convenient tool to test your application classes against a web 17 | * service. For example: 18 | * 19 | *

 MkContainer container = new MkGrizzlyContainer()
 20 |  *   .next(new MkAnswer.Simple(200, "works fine!"))
 21 |  *   .start();
 22 |  * new JdkRequest(container.home())
 23 |  *   .header("Accept", "text/xml")
 24 |  *   .fetch().as(RestResponse.class)
 25 |  *   .assertStatus(200)
 26 |  *   .assertBody(Matchers.equalTo("works fine!"));
 27 |  * MatcherAssert.assertThat(
 28 |  *   container.take().method(),
 29 |  *   Matchers.equalTo("GET")
 30 |  * );
 31 |  * container.stop();
32 | * 33 | *

Keep in mind that container automatically reserves a new free TCP port 34 | * and works until JVM is shut down. The only way to stop it is to call 35 | * {@link #stop()}. 36 | * 37 | *

Since version 0.11 container implements {@link Closeable} and can be 38 | * used in try-with-resource block. 39 | * 40 | * @see Examples 41 | * @since 0.10 42 | */ 43 | @SuppressWarnings("PMD.TooManyMethods") 44 | public interface MkContainer extends Closeable { 45 | 46 | /** 47 | * Give this answer on the next request. 48 | * @param answer Next answer to give 49 | * @return This object 50 | */ 51 | MkContainer next(MkAnswer answer); 52 | 53 | /** 54 | * Give this answer on the next request if the matcher condition is 55 | * satisfied. 56 | * @param answer Next answer to give 57 | * @param condition The condition to match 58 | * @return This object 59 | */ 60 | MkContainer next(MkAnswer answer, Matcher condition); 61 | 62 | /** 63 | * Give this answer on the next request(s) if the matcher condition is 64 | * satisfied up to a certain number of requests. 65 | * @param answer Next answer to give 66 | * @param condition The condition to match 67 | * @param count Number of requests to match 68 | * @return This object 69 | */ 70 | MkContainer next(MkAnswer answer, Matcher condition, int count); 71 | 72 | /** 73 | * Get the oldest request received 74 | * ({@link java.util.NoSuchElementException} 75 | * if no more elements in the list). 76 | * @return Request received 77 | */ 78 | MkQuery take(); 79 | 80 | /** 81 | * Get the oldest request received subject to the matching condition. 82 | * ({@link java.util.NoSuchElementException} if no elements satisfy the 83 | * condition). 84 | * @param matcher The matcher specifying the condition 85 | * @return Request received satisfying the matcher 86 | */ 87 | MkQuery take(Matcher matcher); 88 | 89 | /** 90 | * Get the all requests received satisfying the given matcher. 91 | * ({@link java.util.NoSuchElementException} if no elements satisfy the 92 | * condition). 93 | * @param matcher The matcher specifying the condition 94 | * @return Collection of all requests satisfying the matcher, ordered from 95 | * oldest to newest. 96 | */ 97 | Collection takeAll(Matcher matcher); 98 | 99 | /** 100 | * How many queries we have left. 101 | * @return Total number of queries you can retrieve with {@link #take()} 102 | * @since 1.0 103 | */ 104 | int queries(); 105 | 106 | /** 107 | * Start it on the first available TCP port. 108 | * @return This object 109 | * @throws IOException If fails 110 | */ 111 | MkContainer start() throws IOException; 112 | 113 | /** 114 | * Start it on a provided port. 115 | * @param prt The port where it should start listening 116 | * @return This object 117 | * @throws IOException If fails 118 | */ 119 | MkContainer start(int prt) throws IOException; 120 | 121 | /** 122 | * Stop container. 123 | */ 124 | void stop(); 125 | 126 | /** 127 | * Get its home. 128 | * @return URI of the started container 129 | */ 130 | URI home(); 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkGrizzlyContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import com.jcabi.aspects.Loggable; 8 | import com.jcabi.log.Logger; 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.util.Collection; 12 | import lombok.EqualsAndHashCode; 13 | import org.glassfish.grizzly.http.server.HttpServer; 14 | import org.glassfish.grizzly.http.server.NetworkListener; 15 | import org.hamcrest.Matcher; 16 | import org.hamcrest.core.IsAnything; 17 | 18 | /** 19 | * Implementation of {@link MkContainer} based on Grizzly Server. 20 | * @see MkContainer 21 | * @since 0.10 22 | */ 23 | @SuppressWarnings("PMD.TooManyMethods") 24 | @EqualsAndHashCode(of = {"adapter", "gws", "port"}) 25 | @Loggable(Loggable.DEBUG) 26 | public final class MkGrizzlyContainer implements MkContainer { 27 | 28 | /** 29 | * Grizzly adapter. 30 | */ 31 | private final transient MkGrizzlyAdapter adapter = 32 | new MkGrizzlyAdapter(); 33 | 34 | /** 35 | * Grizzly container. 36 | */ 37 | private transient HttpServer gws; 38 | 39 | /** 40 | * Port where it works. 41 | */ 42 | private transient int port; 43 | 44 | @Override 45 | public MkContainer next(final MkAnswer answer) { 46 | return this.next(answer, new IsAnything()); 47 | } 48 | 49 | @Override 50 | public MkContainer next( 51 | final MkAnswer answer, 52 | final Matcher condition 53 | ) { 54 | return this.next(answer, condition, 1); 55 | } 56 | 57 | @Override 58 | public MkContainer next( 59 | final MkAnswer answer, 60 | final Matcher condition, final int count 61 | ) { 62 | this.adapter.next(answer, condition, count); 63 | return this; 64 | } 65 | 66 | @Override 67 | public MkQuery take() { 68 | return this.adapter.take(); 69 | } 70 | 71 | @Override 72 | public MkQuery take(final Matcher matcher) { 73 | return this.adapter.take(matcher); 74 | } 75 | 76 | @Override 77 | public Collection takeAll(final Matcher matcher) { 78 | return this.adapter.takeAll(matcher); 79 | } 80 | 81 | @Override 82 | public int queries() { 83 | return this.adapter.queries(); 84 | } 85 | 86 | @Override 87 | public MkContainer start() throws IOException { 88 | return this.start(0); 89 | } 90 | 91 | @Override 92 | public MkContainer start(final int prt) throws IOException { 93 | if (this.port != 0) { 94 | throw new IllegalStateException( 95 | String.format( 96 | "already listening on port %d, use #stop() first", 97 | this.port 98 | ) 99 | ); 100 | } 101 | this.gws = new HttpServer(); 102 | final NetworkListener listener = new NetworkListener( 103 | "grizzly", 104 | NetworkListener.DEFAULT_NETWORK_HOST, 105 | prt 106 | ); 107 | this.gws.addListener(listener); 108 | this.gws.getServerConfiguration() 109 | .setAllowPayloadForUndefinedHttpMethods(true); 110 | this.gws.getServerConfiguration().addHttpHandler( 111 | this.adapter, 112 | "/" 113 | ); 114 | this.gws.start(); 115 | this.port = listener.getPort(); 116 | Logger.info(this, "started on port #%s", this.port); 117 | return this; 118 | } 119 | 120 | @Override 121 | public void stop() { 122 | if (this.gws != null) { 123 | this.gws.shutdown(); 124 | } 125 | Logger.info(this, "stopped on port #%s", this.port); 126 | this.port = 0; 127 | } 128 | 129 | @Override 130 | public URI home() { 131 | return URI.create( 132 | String.format("http://localhost:%d/", this.port) 133 | ); 134 | } 135 | 136 | @Override 137 | public void close() { 138 | this.stop(); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkQuery.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import java.net.URI; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * Mock HTTP query/request. 14 | * 15 | * @since 0.10 16 | */ 17 | @Immutable 18 | public interface MkQuery { 19 | 20 | /** 21 | * URI. 22 | * @return URI 23 | */ 24 | URI uri(); 25 | 26 | /** 27 | * HTTP method. 28 | * @return Method 29 | */ 30 | String method(); 31 | 32 | /** 33 | * Headers. 34 | * @return Headers 35 | */ 36 | Map> headers(); 37 | 38 | /** 39 | * HTTP request body as String. 40 | * @return Body 41 | */ 42 | String body(); 43 | 44 | /** 45 | * HTTP request body as byte array. 46 | * @return Body 47 | * @since 1.13 48 | */ 49 | byte[] binary(); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkQueryBodyMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import lombok.EqualsAndHashCode; 8 | import lombok.ToString; 9 | import org.hamcrest.Description; 10 | import org.hamcrest.Matcher; 11 | import org.hamcrest.TypeSafeMatcher; 12 | 13 | /** 14 | * Matcher for checking {@link MkQuery#body()} result. 15 | * @since 1.5 16 | */ 17 | @ToString 18 | @EqualsAndHashCode(callSuper = false, of = "matcher") 19 | final class MkQueryBodyMatcher extends TypeSafeMatcher { 20 | 21 | /** 22 | * The Matcher to use against the body. 23 | */ 24 | private final transient Matcher matcher; 25 | 26 | /** 27 | * Ctor. 28 | * @param match The matcher to use for the body 29 | */ 30 | MkQueryBodyMatcher(final Matcher match) { 31 | super(); 32 | this.matcher = match; 33 | } 34 | 35 | @Override 36 | public void describeTo(final Description description) { 37 | this.matcher.describeTo( 38 | description.appendText("MkQuery body matching: ") 39 | ); 40 | } 41 | 42 | @Override 43 | public boolean matchesSafely(final MkQuery item) { 44 | return this.matcher.matches(item.body()); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkQueryHeaderMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import lombok.EqualsAndHashCode; 8 | import lombok.ToString; 9 | import org.hamcrest.Description; 10 | import org.hamcrest.Matcher; 11 | import org.hamcrest.TypeSafeMatcher; 12 | 13 | /** 14 | * Matcher for checking {@link MkQuery#headers()} contents. 15 | * @since 1.5 16 | */ 17 | @ToString 18 | @EqualsAndHashCode(callSuper = false, of = {"header", "matcher"}) 19 | final class MkQueryHeaderMatcher extends TypeSafeMatcher { 20 | 21 | /** 22 | * The header to match. 23 | */ 24 | private final transient String header; 25 | 26 | /** 27 | * The Matcher to use against the header. 28 | */ 29 | private final transient Matcher> matcher; 30 | 31 | /** 32 | * Ctor. 33 | * @param hdr The header to match 34 | * @param match The matcher to use for the header 35 | */ 36 | MkQueryHeaderMatcher(final String hdr, 37 | final Matcher> match) { 38 | super(); 39 | this.header = hdr; 40 | this.matcher = match; 41 | } 42 | 43 | @Override 44 | public void describeTo(final Description description) { 45 | this.matcher.describeTo( 46 | description.appendText("MkQuery containing header ") 47 | .appendText(this.header) 48 | .appendText(" with matching value(s) of: ") 49 | ); 50 | } 51 | 52 | @Override 53 | public boolean matchesSafely(final MkQuery item) { 54 | return item.headers().containsKey(this.header) 55 | && this.matcher.matches(item.headers().get(this.header)); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkQueryMatchers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import java.net.URI; 8 | import org.hamcrest.Matcher; 9 | import org.hamcrest.Matchers; 10 | 11 | /** 12 | * Convenient set of matchers for {@link MkQuery}. 13 | * 14 | * @since 1.5 15 | */ 16 | @SuppressWarnings("PMD.ProhibitPublicStaticMethods") 17 | public final class MkQueryMatchers { 18 | 19 | /** 20 | * Private ctor. 21 | */ 22 | private MkQueryMatchers() { 23 | // Utility class - cannot instantiate 24 | } 25 | 26 | /** 27 | * Matches the value of the MkQuery's body against the given matcher. 28 | * 29 | * @param matcher The matcher to use. 30 | * @return Matcher for checking the body of MkQuery 31 | */ 32 | public static Matcher hasBody(final Matcher matcher) { 33 | return new MkQueryBodyMatcher(matcher); 34 | } 35 | 36 | /** 37 | * Matches the content of the MkQuery's header against the given matcher. 38 | * Note that for a valid match to occur, the header entry must exist 39 | * and its value(s) must match the given matcher. 40 | * 41 | * @param header The header to check. 42 | * @param matcher The matcher to use. 43 | * @return Matcher for checking the body of MkQuery 44 | */ 45 | public static Matcher hasHeader( 46 | final String header, 47 | final Matcher> matcher 48 | ) { 49 | return new MkQueryHeaderMatcher(header, matcher); 50 | } 51 | 52 | /** 53 | * Matches the path of the MkQuery. 54 | * 55 | * @param path The path to check. 56 | * @return Matcher for checking the path of MkQuery 57 | */ 58 | public static Matcher hasPath(final Matcher path) { 59 | return new MkQueryUriMatcher( 60 | Matchers.hasProperty("rawPath", path) 61 | ); 62 | } 63 | 64 | /** 65 | * Matches the query of the MkQuery. 66 | * 67 | * @param query The query to check. 68 | * @return Matcher for checking the query of MkQuery 69 | */ 70 | public static Matcher hasQuery(final Matcher query) { 71 | return new MkQueryUriMatcher( 72 | Matchers.hasProperty("rawQuery", query) 73 | ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/MkQueryUriMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import java.net.URI; 8 | import org.hamcrest.Description; 9 | import org.hamcrest.Matcher; 10 | import org.hamcrest.TypeSafeDiagnosingMatcher; 11 | 12 | /** 13 | * Matcher for checking {@link MkQuery#uri()} contents. 14 | * 15 | * @since 1.17.4 16 | * @checkstyle ProtectedMethodInFinalClassCheck (50 lines) 17 | */ 18 | public final class MkQueryUriMatcher 19 | extends TypeSafeDiagnosingMatcher { 20 | /** 21 | * Path to match. 22 | */ 23 | private final transient Matcher matcher; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @param mtrch Path to match. 29 | */ 30 | MkQueryUriMatcher(final Matcher mtrch) { 31 | super(); 32 | this.matcher = mtrch; 33 | } 34 | 35 | @Override 36 | public void describeTo(final Description desc) { 37 | desc.appendDescriptionOf(this.matcher); 38 | } 39 | 40 | @Override 41 | protected boolean matchesSafely( 42 | final MkQuery item, final Description desc 43 | ) { 44 | final URI uri = item.uri(); 45 | desc.appendText("actual uri ").appendValue(uri); 46 | this.matcher.describeMismatch(uri, desc); 47 | return this.matcher.matches(uri); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/mock/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Mock of Servlet Container. 8 | * 9 | * @since 0.10 10 | * @see Examples 11 | */ 12 | package com.jcabi.http.mock; 13 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * RESTful client, for tests (but not only). 8 | * 9 | *

The only dependency you need is (check our latest version available 10 | * at www.rexsl.com): 11 | * 12 | *

<depedency>
13 |  *   <groupId>com.rexsl</groupId>
14 |  *   <artifactId>rexsl-test</artifactId>
15 |  * </dependency>
16 | * 17 | * @see project site 18 | */ 19 | package com.jcabi.http; 20 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/request/Boundary.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.request; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.aspects.Loggable; 9 | import java.util.Random; 10 | 11 | /** 12 | * Boundary for content-type multipart/form-data. 13 | * This is a copy of boundary created by Apache HttpComponents HttpClient 4.5. 14 | * 15 | * @since 1.0 16 | */ 17 | @Immutable 18 | @Loggable(Loggable.DEBUG) 19 | public final class Boundary { 20 | /** 21 | * The pool of ASCII chars to be used for generating a multipart boundary. 22 | */ 23 | private static final char[] MULTIPART_CHARS = 24 | "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 25 | .toCharArray(); 26 | 27 | /** 28 | * Generation of pseudorandom numbers. 29 | */ 30 | private final transient Random rand; 31 | 32 | /** 33 | * Constructor with new random number generation. 34 | */ 35 | public Boundary() { 36 | this(new Random()); 37 | } 38 | 39 | /** 40 | * Ctor. 41 | * @param random Random number generation. 42 | */ 43 | public Boundary(final Random random) { 44 | this.rand = random; 45 | } 46 | 47 | /** 48 | * Generates random boundary with random size from 30 to 40. 49 | * @return Boundary value. 50 | */ 51 | public String value() { 52 | final StringBuilder buffer = new StringBuilder(); 53 | final int count = this.rand.nextInt(11) + 30; 54 | for (int index = 0; index < count; ++index) { 55 | buffer.append( 56 | Boundary.MULTIPART_CHARS[ 57 | this.rand.nextInt(MULTIPART_CHARS.length) 58 | ] 59 | ); 60 | } 61 | return buffer.toString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/request/DefaultResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.request; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.aspects.Loggable; 9 | import com.jcabi.http.Request; 10 | import com.jcabi.http.RequestBody; 11 | import com.jcabi.http.Response; 12 | import com.jcabi.immutable.Array; 13 | import com.jcabi.log.Logger; 14 | import java.lang.reflect.InvocationTargetException; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.LinkedList; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.concurrent.ConcurrentMap; 21 | import lombok.EqualsAndHashCode; 22 | 23 | /** 24 | * Default implementation of {@link com.jcabi.http.Response}. 25 | * 26 | * @since 1.0 27 | */ 28 | @Immutable 29 | @EqualsAndHashCode(of = { "req", "code", "phrase", "hdrs", "content" }) 30 | @Loggable(Loggable.DEBUG) 31 | public final class DefaultResponse implements Response { 32 | 33 | /** 34 | * UTF-8 error marker. 35 | */ 36 | private static final String ERR = "\uFFFD"; 37 | 38 | /** 39 | * Request. 40 | */ 41 | private final transient Request req; 42 | 43 | /** 44 | * Status code. 45 | */ 46 | private final transient int code; 47 | 48 | /** 49 | * Reason phrase. 50 | */ 51 | private final transient String phrase; 52 | 53 | /** 54 | * Headers. 55 | */ 56 | private final transient Array> hdrs; 57 | 58 | /** 59 | * Content received. 60 | */ 61 | @Immutable.Array 62 | //@checkstyle ParameterNumber (15 lines) 63 | private final transient byte[] content; 64 | 65 | /** 66 | * Public ctor. 67 | * @param request The request 68 | * @param status HTTP status 69 | * @param reason HTTP reason phrase 70 | * @param headers HTTP headers 71 | * @param body Body of HTTP response 72 | */ 73 | public DefaultResponse(final Request request, final int status, 74 | final String reason, final Array> headers, 75 | final byte[] body) { 76 | this.req = request; 77 | this.code = status; 78 | this.phrase = reason; 79 | this.hdrs = headers; 80 | this.content = body.clone(); 81 | } 82 | 83 | @Override 84 | public Request back() { 85 | return this.req; 86 | } 87 | 88 | @Override 89 | public int status() { 90 | return this.code; 91 | } 92 | 93 | @Override 94 | public String reason() { 95 | return this.phrase; 96 | } 97 | 98 | @Override 99 | @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 100 | public Map> headers() { 101 | final ConcurrentMap> map = 102 | new ConcurrentHashMap<>(0); 103 | for (final Map.Entry header : this.hdrs) { 104 | map.putIfAbsent(header.getKey(), new LinkedList()); 105 | map.get(header.getKey()).add(header.getValue()); 106 | } 107 | return map; 108 | } 109 | 110 | @Override 111 | public String body() { 112 | final String body = new String(this.content, StandardCharsets.UTF_8); 113 | if (body.contains(DefaultResponse.ERR)) { 114 | throw new IllegalStateException( 115 | Logger.format( 116 | "broken Unicode text at line #%d in '%[text]s' (%d bytes)", 117 | body.length() - body.replace("\n", "").length(), 118 | body, 119 | this.content.length 120 | ) 121 | ); 122 | } 123 | return body; 124 | } 125 | 126 | @Override 127 | public byte[] binary() { 128 | return this.content.clone(); 129 | } 130 | 131 | // @checkstyle MethodName (4 lines) 132 | @Override 133 | @SuppressWarnings("PMD.ShortMethodName") 134 | public T as(final Class type) { 135 | try { 136 | return type.getDeclaredConstructor(Response.class) 137 | .newInstance(this); 138 | } catch (final InstantiationException 139 | | IllegalAccessException | NoSuchMethodException 140 | | InvocationTargetException ex) { 141 | throw new IllegalStateException(ex); 142 | } 143 | } 144 | 145 | @Override 146 | @SuppressWarnings("PMD.ConsecutiveLiteralAppends") 147 | public String toString() { 148 | final StringBuilder text = new StringBuilder(0) 149 | .append(this.code).append(' ') 150 | .append(this.phrase) 151 | .append(" [") 152 | .append(this.back().uri().get()) 153 | .append("]\n"); 154 | for (final Map.Entry header : this.hdrs) { 155 | text.append( 156 | Logger.format( 157 | "%s: %s\n", 158 | header.getKey(), 159 | header.getValue() 160 | ) 161 | ); 162 | } 163 | return text.append('\n') 164 | .append(new RequestBody.Printable(this.content)) 165 | .toString(); 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/request/MultipartBodyBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.request; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.aspects.Loggable; 9 | import java.util.Arrays; 10 | 11 | /** 12 | * Byte builder for multipart body. 13 | * 14 | * @since 1.0 15 | */ 16 | @Immutable 17 | @Loggable(Loggable.DEBUG) 18 | public final class MultipartBodyBuilder { 19 | /** 20 | * Carriage return constant. 21 | */ 22 | private static final byte[] CRLF = {13, 10}; 23 | 24 | /** 25 | * Byte array. 26 | */ 27 | private final transient byte[] values; 28 | 29 | /** 30 | * Ctor. 31 | */ 32 | public MultipartBodyBuilder() { 33 | this(new byte[0]); 34 | } 35 | 36 | /** 37 | * Ctor. 38 | * @param values Initial byte array. 39 | */ 40 | public MultipartBodyBuilder(final byte[] values) { 41 | this.values = values.clone(); 42 | } 43 | 44 | /** 45 | * Append byte array to this multipart body including carriage return. 46 | * @param bytes Byte array to append. 47 | * @return New multipart body. 48 | */ 49 | public MultipartBodyBuilder appendLine(final byte[] bytes) { 50 | return this.append(bytes).append(MultipartBodyBuilder.CRLF); 51 | } 52 | 53 | /** 54 | * Bytes of multipart body. 55 | * @return Bytes array. 56 | */ 57 | public byte[] asBytes() { 58 | return this.values.clone(); 59 | } 60 | 61 | /** 62 | * Append byte array to this multipart body. 63 | * @param bytes Byte array to append. 64 | * @return New multipart body. 65 | */ 66 | public MultipartBodyBuilder append(final byte[] bytes) { 67 | final int offset = this.values.length; 68 | final byte[] neww = Arrays.copyOf(this.values, offset + bytes.length); 69 | System.arraycopy(bytes, 0, neww, offset, bytes.length); 70 | return new MultipartBodyBuilder(neww); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/request/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Requests. 8 | * 9 | *

This package contains implementations of class Request. 10 | * The most popular and easy to use it {@link JdkRequest}. 11 | * 12 | *

However, in some situations {@link JdkRequest} falls short and 13 | * {@link ApacheRequest} should be used instead. For example, 14 | * {@link JdkRequest} doesn't support {@code PATCH} HTTP method due to 15 | * a bug in HttpURLConnection. 16 | * 17 | * @since 0.10 18 | */ 19 | package com.jcabi.http.request; 20 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/response/AbstractResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Request; 9 | import com.jcabi.http.Response; 10 | import java.util.List; 11 | import java.util.Map; 12 | import lombok.EqualsAndHashCode; 13 | 14 | /** 15 | * Abstract response. 16 | * 17 | * @since 0.8 18 | */ 19 | @Immutable 20 | @EqualsAndHashCode(of = "response") 21 | abstract class AbstractResponse implements Response { 22 | 23 | /** 24 | * Encapsulated response. 25 | */ 26 | private final transient Response response; 27 | 28 | /** 29 | * Ctor. 30 | * @param resp Response 31 | */ 32 | AbstractResponse(final Response resp) { 33 | this.response = resp; 34 | } 35 | 36 | @Override 37 | public final String toString() { 38 | return this.response.toString(); 39 | } 40 | 41 | @Override 42 | public final Request back() { 43 | return this.response.back(); 44 | } 45 | 46 | @Override 47 | public final int status() { 48 | return this.response.status(); 49 | } 50 | 51 | @Override 52 | public final String reason() { 53 | return this.response.reason(); 54 | } 55 | 56 | @Override 57 | public final Map> headers() { 58 | return this.response.headers(); 59 | } 60 | 61 | @Override 62 | public String body() { 63 | return this.response.body(); 64 | } 65 | 66 | @Override 67 | public final byte[] binary() { 68 | return this.response.binary(); 69 | } 70 | 71 | // @checkstyle MethodName (4 lines) 72 | @Override 73 | @SuppressWarnings("PMD.ShortMethodName") 74 | public final T as(final Class type) { 75 | return this.response.as(type); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/response/JacksonResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.fasterxml.jackson.core.json.JsonReadFeature; 8 | import com.fasterxml.jackson.databind.JsonNode; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import com.fasterxml.jackson.databind.node.ArrayNode; 11 | import com.fasterxml.jackson.databind.node.ObjectNode; 12 | import com.jcabi.aspects.Immutable; 13 | import com.jcabi.http.Response; 14 | import java.io.IOException; 15 | import java.util.Arrays; 16 | import lombok.EqualsAndHashCode; 17 | 18 | /** 19 | * A JSON response provided by the Jackson Project. 20 | * 21 | * @since 1.17 22 | */ 23 | @Immutable 24 | @EqualsAndHashCode(callSuper = true) 25 | public final class JacksonResponse extends AbstractResponse { 26 | /** 27 | * Ctor. 28 | * 29 | * @param resp Response 30 | */ 31 | public JacksonResponse(final Response resp) { 32 | super(resp); 33 | } 34 | 35 | /** 36 | * Read the body as JSON. 37 | * 38 | * @return JSON reader. 39 | */ 40 | public JsonReader json() { 41 | return new JsonReader( 42 | this.binary() 43 | ); 44 | } 45 | 46 | /** 47 | * A tree representation views of JSON documents. 48 | * 49 | * @since 1.17.1 50 | */ 51 | public static final class JsonReader { 52 | /** 53 | * Jackson's ObjectMapper. Allow unquoted control characters when 54 | * parsing by default. 55 | */ 56 | private static final ObjectMapper MAPPER = new ObjectMapper() 57 | .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()); 58 | 59 | /** 60 | * Response body. 61 | */ 62 | private final transient byte[] body; 63 | 64 | /** 65 | * Public constructor. 66 | * 67 | * @param bytes The HTTP response body as an array of bytes. 68 | */ 69 | public JsonReader(final byte[] bytes) { 70 | this.body = Arrays.copyOf(bytes, bytes.length); 71 | } 72 | 73 | /** 74 | * Returns a mutable JSON array node, if the parsed JSON is a valid 75 | * array. 76 | * 77 | * @return JSON array node. 78 | * @throws IOException If the body is not a valid JSON or JSON array. 79 | */ 80 | public ArrayNode readArray() throws IOException { 81 | final JsonNode node = this.read(); 82 | if (!node.isArray()) { 83 | throw new IOException( 84 | "Cannot read as an array. The JSON is not a valid array." 85 | ); 86 | } 87 | return (ArrayNode) node; 88 | } 89 | 90 | /** 91 | * Returns a mutable JSON object node, if the parsed JSON is a valid 92 | * object. 93 | * 94 | * @return JSON object node. 95 | * @throws IOException If the body is not a valid JSON or JSON object. 96 | */ 97 | public ObjectNode readObject() throws IOException { 98 | final JsonNode node = this.read(); 99 | if (!node.isObject()) { 100 | throw new IOException( 101 | "Cannot read as an object. The JSON is not a valid object." 102 | ); 103 | } 104 | return (ObjectNode) node; 105 | } 106 | 107 | /** 108 | * Returns an immutable JSON node. 109 | * 110 | * @return JSON node. 111 | * @throws IOException If the body is not a valid JSON. 112 | */ 113 | public JsonNode read() throws IOException { 114 | return MAPPER.readTree(this.body); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/response/JsoupResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Response; 9 | import lombok.EqualsAndHashCode; 10 | import org.jsoup.Jsoup; 11 | import org.jsoup.nodes.Document; 12 | import org.jsoup.nodes.Entities; 13 | 14 | /** 15 | * Jsoup response. 16 | * 17 | *

This response decorator is able to parse HTTP response body as an HTML 18 | * document. Example usage: 19 | * 20 | *

 String body = new JdkRequest("http://my.example.com")
21 |  *   .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
22 |  *   .fetch()
23 |  *   .as(JsoupResponse.class)
24 |  *   .body();
25 | * 26 | *

{@link #body()} will try to output clean HTML even for 27 | * malformed responses. For example: 28 | *

    29 | *
  • Unclosed tags will be closed ("<p>Hello" will become 30 | * "<p>Hello</p>") 31 | *
  • Implicit tags will be made explicit (e.g. a naked <td> will be 32 | * wrapped in a <table><tr><td>) 33 | *
  • Basic structure is guaranteed (i.e. html, head, body elements) 34 | *
35 | * 36 | *

The class is immutable and thread-safe. 37 | * 38 | * @see Jsoup website 39 | * @since 1.4 40 | */ 41 | @Immutable 42 | @EqualsAndHashCode(callSuper = true) 43 | public final class JsoupResponse extends AbstractResponse { 44 | 45 | /** 46 | * Public ctor. 47 | * @param resp Response 48 | */ 49 | public JsoupResponse(final Response resp) { 50 | super(resp); 51 | } 52 | 53 | @Override 54 | public String body() { 55 | final Document html = Jsoup.parse(super.body()); 56 | html.outputSettings().syntax(Document.OutputSettings.Syntax.xml); 57 | html.outputSettings().escapeMode(Entities.EscapeMode.xhtml); 58 | return html.html(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/response/XmlResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Request; 9 | import com.jcabi.http.Response; 10 | import com.jcabi.immutable.ArrayMap; 11 | import com.jcabi.matchers.XhtmlMatchers; 12 | import com.jcabi.xml.XML; 13 | import com.jcabi.xml.XMLDocument; 14 | import com.jcabi.xml.XPathContext; 15 | import java.net.URI; 16 | import java.util.Map; 17 | import javax.xml.namespace.NamespaceContext; 18 | import lombok.EqualsAndHashCode; 19 | 20 | /** 21 | * XML response. 22 | * 23 | *

This response decorator is able to parse HTTP response body as 24 | * an XML document and manipulate with it afterwords, for example: 25 | * 26 | *

 String name = new JdkRequest("http://my.example.com")
 27 |  *   .header(HttpHeaders.ACCEPT, MediaType.TEXT_XML)
 28 |  *   .fetch()
 29 |  *   .as(XmlResponse.class)
 30 |  *   .assertXPath("/user/name")
 31 |  *   .xml()
 32 |  *   .xpath("/user/name/text()")
 33 |  *   .get(0);
34 | * 35 | *

In HATEOAS 36 | * responses it is convenient to use this decorator's 37 | * method {@link #rel(String)} 38 | * in order to follow the link provided in {@code <link>} XML element, 39 | * for example: 40 | * 41 | *

 String data = new JdkRequest("http://my.example.com")
 42 |  *   .header(HttpHeaders.ACCEPT, MediaType.TEXT_XML)
 43 |  *   .fetch()
 44 |  *   .as(XmlResponse.class)
 45 |  *   .rel("/user/links/link[@rel='next']/@href")
 46 |  *   .fetch()
 47 |  *   .body();
48 | * 49 | *

The class is immutable and thread-safe. 50 | * 51 | * @since 0.8 52 | */ 53 | @Immutable 54 | @EqualsAndHashCode(callSuper = true) 55 | public final class XmlResponse extends AbstractResponse { 56 | 57 | /** 58 | * Map of namespaces. 59 | */ 60 | private final transient ArrayMap namespaces; 61 | 62 | /** 63 | * Public ctor. 64 | * @param resp Response 65 | */ 66 | public XmlResponse(final Response resp) { 67 | this(resp, new ArrayMap()); 68 | } 69 | 70 | /** 71 | * Public ctor. 72 | * @param resp Response 73 | * @param map Map of namespaces 74 | */ 75 | private XmlResponse(final Response resp, 76 | final ArrayMap map) { 77 | super(resp); 78 | this.namespaces = map; 79 | } 80 | 81 | /** 82 | * Get XML body. 83 | * @return XML body 84 | */ 85 | public XML xml() { 86 | return new XMLDocument(this.body()).merge(this.context()); 87 | } 88 | 89 | /** 90 | * Register this new namespace. 91 | * @param prefix Prefix to use 92 | * @param uri Namespace URI 93 | * @return This object 94 | */ 95 | public XmlResponse registerNs(final String prefix, final String uri) { 96 | return new XmlResponse(this, this.namespaces.with(prefix, uri)); 97 | } 98 | 99 | /** 100 | * Verifies HTTP response body XHTML/XML content against XPath query, 101 | * and throws {@link AssertionError} in case of mismatch. 102 | * @param xpath Query to use 103 | * @return This object 104 | */ 105 | public XmlResponse assertXPath(final String xpath) { 106 | final String body = this.body(); 107 | if (!XhtmlMatchers.hasXPath(xpath, this.context()).matches(body)) { 108 | throw new AssertionError( 109 | String.format( 110 | "XML doesn't contain required XPath '%s':%n%s", 111 | xpath, body 112 | ) 113 | ); 114 | } 115 | return this; 116 | } 117 | 118 | /** 119 | * Follow XML link. 120 | * @param query XPath query to fetch new URI 121 | * @return New request 122 | */ 123 | public Request rel(final String query) { 124 | this.assertXPath(query); 125 | return new RestResponse(this).jump( 126 | URI.create(this.xml().xpath(query).get(0)) 127 | ); 128 | } 129 | 130 | /** 131 | * Create XPath context. 132 | * @return Context 133 | */ 134 | private NamespaceContext context() { 135 | XPathContext context = new XPathContext(); 136 | for (final Map.Entry entry 137 | : this.namespaces.entrySet()) { 138 | context = context.add(entry.getKey(), entry.getValue()); 139 | } 140 | return context; 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/response/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Responses. 8 | * 9 | * @since 0.10 10 | */ 11 | package com.jcabi.http.response; 12 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/AutoRedirectingWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Request; 9 | import com.jcabi.http.Response; 10 | import com.jcabi.http.Wire; 11 | import jakarta.ws.rs.core.HttpHeaders; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.net.HttpURLConnection; 15 | import java.net.URI; 16 | import java.util.Collection; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.concurrent.TimeUnit; 20 | import lombok.EqualsAndHashCode; 21 | import lombok.ToString; 22 | 23 | /** 24 | * Auto Redirecting Wire. 25 | * 26 | *

This wire will retry a request a certain number of times (default: 5) 27 | * after a short delay when a HTTP response with a status code of 300-399 is 28 | * received. On every next attempt a new URL will be used, according 29 | * to the value of {@code Location} HTTP header of the response. 30 | * 31 | *

If the maximum number of retries are reached, the last response 32 | * received is returned to the caller, regardless of its status code. 33 | * 34 | *

 String html = new JdkRequest("http://goggle.com")
 35 |  *   .through(AutoRedirectingWire.class)
 36 |  *   .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN)
 37 |  *   .fetch()
 38 |  *   .body();
39 | * 40 | *

The class is immutable and thread-safe. 41 | * 42 | * @since 1.6 43 | */ 44 | @Immutable 45 | @ToString(of = "origin") 46 | @EqualsAndHashCode(of = { "origin", "max" }) 47 | public final class AutoRedirectingWire implements Wire { 48 | /** 49 | * Original wire. 50 | */ 51 | private final transient Wire origin; 52 | 53 | /** 54 | * Maximum number of retries to be made. 55 | */ 56 | private final transient int max; 57 | 58 | /** 59 | * Public ctor. 60 | * @param wire Original wire 61 | */ 62 | public AutoRedirectingWire(final Wire wire) { 63 | this(wire, 5); 64 | } 65 | 66 | /** 67 | * Public ctor. 68 | * @param wire Original wire 69 | * @param retries Maximum number of retries 70 | */ 71 | public AutoRedirectingWire(final Wire wire, final int retries) { 72 | this.origin = wire; 73 | this.max = retries; 74 | } 75 | 76 | // @checkstyle ParameterNumber (5 lines) 77 | @Override 78 | public Response send(final Request req, final String home, 79 | final String method, 80 | final Collection> headers, 81 | final InputStream content, 82 | final int connect, 83 | final int read) throws IOException { 84 | Response response = this.origin.send( 85 | req, home, method, headers, content, connect, read 86 | ); 87 | int attempt = 1; 88 | final URI uri = URI.create(home); 89 | while (attempt < this.max) { 90 | if (response.status() < HttpURLConnection.HTTP_MULT_CHOICE 91 | || response.status() >= HttpURLConnection.HTTP_BAD_REQUEST) { 92 | break; 93 | } 94 | final List locations = response.headers().get( 95 | HttpHeaders.LOCATION 96 | ); 97 | if (locations == null || locations.size() != 1) { 98 | break; 99 | } 100 | URI location = URI.create(locations.get(0)); 101 | if (!location.isAbsolute()) { 102 | location = uri.resolve(location); 103 | } 104 | response = this.origin.send( 105 | req, location.toString(), 106 | method, headers, content, connect, read 107 | ); 108 | try { 109 | TimeUnit.SECONDS.sleep((long) attempt); 110 | } catch (final InterruptedException ex) { 111 | throw new IOException(ex); 112 | } 113 | ++attempt; 114 | } 115 | return response; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/BasicAuthWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.ImmutableHeader; 9 | import com.jcabi.http.Request; 10 | import com.jcabi.http.Response; 11 | import com.jcabi.http.Wire; 12 | import com.jcabi.log.Logger; 13 | import jakarta.ws.rs.core.HttpHeaders; 14 | import jakarta.xml.bind.DatatypeConverter; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.net.URI; 18 | import java.nio.charset.Charset; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.Collection; 21 | import java.util.LinkedList; 22 | import java.util.Map; 23 | import lombok.EqualsAndHashCode; 24 | import lombok.ToString; 25 | 26 | /** 27 | * Wire with HTTP basic authentication based on user info of URI. 28 | * 29 | *

This wire converts user info from URI into 30 | * {@code "Authorization"} HTTP header, for example: 31 | * 32 | *

 String html = new JdkRequest("http://jeff:12345@example.com")
 33 |  *   .through(BasicAuthWire.class)
 34 |  *   .fetch()
 35 |  *   .body();
36 | * 37 | *

In this example, an additional HTTP header {@code Authorization} 38 | * will be added with a value {@code Basic amVmZjoxMjM0NQ==}. 39 | * 40 | *

The class is immutable and thread-safe. 41 | * 42 | * @see RFC 2617 "HTTP Authentication: Basic and Digest Access Authentication" 43 | * @since 0.10 44 | */ 45 | @Immutable 46 | @ToString(of = "origin") 47 | @EqualsAndHashCode(of = "origin") 48 | public final class BasicAuthWire implements Wire { 49 | 50 | /** 51 | * The Charset to use. 52 | */ 53 | private static final Charset CHARSET = StandardCharsets.UTF_8; 54 | 55 | /** 56 | * Original wire. 57 | */ 58 | private final transient Wire origin; 59 | 60 | /** 61 | * Public ctor. 62 | * @param wire Original wire 63 | */ 64 | public BasicAuthWire(final Wire wire) { 65 | this.origin = wire; 66 | } 67 | 68 | // @checkstyle ParameterNumber (7 lines) 69 | @Override 70 | public Response send(final Request req, final String home, 71 | final String method, 72 | final Collection> headers, 73 | final InputStream content, 74 | final int connect, 75 | final int read) throws IOException { 76 | final Collection> hdrs = 77 | new LinkedList<>(); 78 | boolean absent = true; 79 | for (final Map.Entry header : headers) { 80 | if (header.getKey().equals(HttpHeaders.AUTHORIZATION)) { 81 | Logger.warn( 82 | this, 83 | "Request already contains %s header", 84 | HttpHeaders.AUTHORIZATION 85 | ); 86 | absent = false; 87 | } 88 | hdrs.add(header); 89 | } 90 | final String info = URI.create(home).getUserInfo(); 91 | if (absent && info != null) { 92 | final String[] parts = info.split(":", 2); 93 | hdrs.add( 94 | new ImmutableHeader( 95 | HttpHeaders.AUTHORIZATION, 96 | Logger.format( 97 | "Basic %s", 98 | DatatypeConverter.printBase64Binary( 99 | Logger.format( 100 | "%s:%s", 101 | parts[0], 102 | parts[1] 103 | ).getBytes(BasicAuthWire.CHARSET) 104 | ) 105 | ) 106 | ) 107 | ); 108 | } 109 | return this.origin.send( 110 | req.uri().userInfo(null).back(), 111 | home, 112 | method, 113 | hdrs, 114 | content, 115 | connect, 116 | read 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/CookieOptimizingWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.ImmutableHeader; 9 | import com.jcabi.http.Request; 10 | import com.jcabi.http.Response; 11 | import com.jcabi.http.Wire; 12 | import jakarta.ws.rs.core.HttpHeaders; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.util.Collection; 16 | import java.util.LinkedList; 17 | import java.util.Map; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.concurrent.ConcurrentMap; 20 | import java.util.stream.Collectors; 21 | import lombok.EqualsAndHashCode; 22 | import lombok.ToString; 23 | 24 | /** 25 | * Wire that compresses cookies before sending. 26 | * 27 | *

This wire compresses all provided {@code Cookie} headers into one 28 | * and removes empty cookies, for example: 29 | * 30 | *

 String html = new JdkRequest("http://goggle.com")
 31 |  *   .through(CookieOptimizingWire.class)
 32 |  *   .header(HttpHeaders.Cookie, "alpha=test")
 33 |  *   .header(HttpHeaders.Cookie, "beta=")
 34 |  *   .header(HttpHeaders.Cookie, "gamma=foo")
 35 |  *   .fetch()
 36 |  *   .body();
37 | * 38 | *

An actual HTTP request will be sent with just one {@code Cookie} 39 | * header with a value {@code alpha=test; gamma=foo}. 40 | * 41 | *

It is highly recommended to use this wire decorator when you're 42 | * working with cookies. 43 | * 44 | *

The class is immutable and thread-safe. 45 | * 46 | * @see RFC 2965 "HTTP State Management Mechanism" 47 | * @since 0.10 48 | */ 49 | @Immutable 50 | @ToString(of = "origin") 51 | @EqualsAndHashCode(of = "origin") 52 | public final class CookieOptimizingWire implements Wire { 53 | 54 | /** 55 | * Original wire. 56 | */ 57 | private final transient Wire origin; 58 | 59 | /** 60 | * Public ctor. 61 | * @param wire Original wire 62 | */ 63 | public CookieOptimizingWire(final Wire wire) { 64 | this.origin = wire; 65 | } 66 | 67 | // @checkstyle ParameterNumber (7 lines) 68 | @Override 69 | public Response send(final Request req, final String home, 70 | final String method, 71 | final Collection> headers, 72 | final InputStream content, 73 | final int connect, 74 | final int read) throws IOException { 75 | final Collection> hdrs = 76 | new LinkedList<>(); 77 | final ConcurrentMap cookies = 78 | new ConcurrentHashMap<>(0); 79 | headers.forEach( 80 | header -> { 81 | if (header.getKey().equals(HttpHeaders.COOKIE)) { 82 | final String cookie = header.getValue(); 83 | final int split = cookie.indexOf('='); 84 | final String name = cookie.substring(0, split); 85 | final String value = cookie.substring(split + 1); 86 | if (value.isEmpty()) { 87 | cookies.remove(name); 88 | } else { 89 | cookies.put(name, value); 90 | } 91 | } else { 92 | hdrs.add(header); 93 | } 94 | }); 95 | if (!cookies.isEmpty()) { 96 | final String text = cookies.entrySet().stream().filter( 97 | cookie -> !cookie.getValue().isEmpty() 98 | ).map( 99 | cookie -> cookie.getKey() + '=' + cookie.getValue() 100 | ).collect( 101 | Collectors.joining("; ") 102 | ); 103 | hdrs.add( 104 | new ImmutableHeader( 105 | HttpHeaders.COOKIE, 106 | text 107 | ) 108 | ); 109 | } 110 | return this.origin.send( 111 | req, home, method, hdrs, content, connect, read 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/ETagCachingWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Wire; 9 | import jakarta.ws.rs.core.HttpHeaders; 10 | import lombok.ToString; 11 | 12 | /** 13 | * Wire that caches requests with ETags. 14 | * 15 | *

This decorator can be used when you want to avoid duplicate 16 | * requests to load-sensitive resources and server supports ETags, for example: 17 | * 18 | *

{@code
19 |  *    String html = new JdkRequest("http://goggle.com")
20 |  *        .through(ETagCachingWire.class)
21 |  *        .fetch()
22 |  *        .body();
23 |  * }
24 | * 25 | *

Client will automatically detect if server uses ETags and start adding 26 | * corresponding If-None-Match to outgoing requests 27 | * 28 | *

Client will take response from the cache if it is present 29 | * or will query resource for that. 30 | * 31 | *

The class is immutable and thread-safe. 32 | * 33 | * @since 2.0 34 | */ 35 | @ToString 36 | @Immutable 37 | public final class ETagCachingWire extends AbstractHeaderBasedCachingWire { 38 | 39 | /** 40 | * Public ctor. 41 | * @param wire Original wire 42 | */ 43 | public ETagCachingWire(final Wire wire) { 44 | super(HttpHeaders.ETAG, HttpHeaders.IF_NONE_MATCH, wire); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/FcWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Request; 9 | import com.jcabi.http.Response; 10 | import com.jcabi.http.Wire; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.net.URI; 14 | import java.util.Collection; 15 | import java.util.Map; 16 | import lombok.EqualsAndHashCode; 17 | import lombok.ToString; 18 | 19 | /** 20 | * Wire that caches GET requests. 21 | * 22 | *

This decorator can be used when you want to avoid duplicate 23 | * GET requests to load-sensitive resources, for example: 24 | * 25 | *

 String html = new JdkRequest("http://goggle.com")
 26 |  *   .through(FileCachingWire.class)
 27 |  *   .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN)
 28 |  *   .fetch()
 29 |  *   .body();
30 | * 31 | *

You can also configure it to flush the entire cache 32 | * on certain request URI's, for example: 33 | * 34 | *

new JdkRequest(uri)
 35 |  *   .through(CachingWire.class, "GET /save/.*")
 36 |  *   .uri().path("/save/123").back()
 37 |  *   .fetch();
38 | * 39 | *

The regular expression provided will be used against a string 40 | * constructed as an HTTP method, space, path of the URI together with 41 | * query part. 42 | * 43 | *

The class is immutable and thread-safe. 44 | * 45 | * @since 1.16 46 | */ 47 | @Immutable 48 | @ToString 49 | @EqualsAndHashCode(of = { "origin", "regex" }) 50 | public final class FcWire implements Wire { 51 | 52 | /** 53 | * Cache in files. 54 | */ 55 | private final transient FcCache cache; 56 | 57 | /** 58 | * Original wire. 59 | */ 60 | private final transient Wire origin; 61 | 62 | /** 63 | * Flushing regular expression. 64 | */ 65 | private final transient String regex; 66 | 67 | /** 68 | * Public ctor. 69 | * @param wire Original wire 70 | */ 71 | public FcWire(final Wire wire) { 72 | this(wire, "$never"); 73 | } 74 | 75 | /** 76 | * Public ctor. 77 | * @param wire Original wire 78 | * @param flsh Flushing regular expression 79 | */ 80 | public FcWire(final Wire wire, final String flsh) { 81 | this(wire, flsh, new FcCache()); 82 | } 83 | 84 | /** 85 | * Public ctor. 86 | * @param wire Original wire 87 | * @param flsh Flushing regular expression 88 | * @param path Path for the files 89 | */ 90 | public FcWire(final Wire wire, final String flsh, final String path) { 91 | this(wire, flsh, new FcCache(path)); 92 | } 93 | 94 | /** 95 | * Public ctor. 96 | * @param wire Original wire 97 | * @param flsh Flushing regular expression 98 | * @param fcc Cache 99 | */ 100 | public FcWire(final Wire wire, final String flsh, final FcCache fcc) { 101 | this.origin = wire; 102 | this.regex = flsh; 103 | this.cache = fcc; 104 | } 105 | 106 | // @checkstyle ParameterNumber (5 lines) 107 | @Override 108 | public Response send(final Request req, final String home, 109 | final String method, 110 | final Collection> headers, 111 | final InputStream content, 112 | final int connect, 113 | final int read) throws IOException { 114 | final URI uri = req.uri().get(); 115 | final StringBuilder label = new StringBuilder(100) 116 | .append(method).append(' ').append(uri.getPath()); 117 | if (uri.getQuery() != null) { 118 | label.append('?').append(uri.getQuery()); 119 | } 120 | if (label.toString().matches(this.regex)) { 121 | this.cache.invalidate(); 122 | } 123 | final Response rsp; 124 | if (method.equals(Request.GET)) { 125 | rsp = this.cache.get( 126 | label.toString(), this.origin, req, 127 | home, method, headers, content, connect, read 128 | ); 129 | } else { 130 | rsp = this.origin.send( 131 | req, home, method, headers, content, 132 | connect, read 133 | ); 134 | } 135 | return rsp; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/LastModifiedCachingWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.Wire; 8 | import jakarta.ws.rs.core.HttpHeaders; 9 | import lombok.ToString; 10 | 11 | /** 12 | * Wire that caches requests based on Last-Modified 13 | * and If-Modified-Since headers. 14 | * @since 1.15 15 | */ 16 | @ToString 17 | public final class LastModifiedCachingWire 18 | extends AbstractHeaderBasedCachingWire { 19 | 20 | /** 21 | * Public ctor. 22 | * @param origin Original wire 23 | */ 24 | public LastModifiedCachingWire(final Wire origin) { 25 | super(HttpHeaders.LAST_MODIFIED, HttpHeaders.IF_MODIFIED_SINCE, origin); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/OneMinuteWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.aspects.Timeable; 9 | import com.jcabi.http.Request; 10 | import com.jcabi.http.Response; 11 | import com.jcabi.http.Wire; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.util.Collection; 15 | import java.util.Map; 16 | import java.util.concurrent.TimeUnit; 17 | import lombok.EqualsAndHashCode; 18 | import lombok.ToString; 19 | 20 | /** 21 | * Wire that throws an {@link IOException} if a request takes longer 22 | * than a minute. 23 | * 24 | *

It's recommended to use this decorator in production, in order 25 | * to avoid stuck requests: 26 | * 27 | *

 String html = new JdkRequest("http://goggle.com")
28 |  *   .through(OneMinuteWire.class)
29 |  *   .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN)
30 |  *   .fetch()
31 |  *   .body();
32 | * 33 | *

The class is immutable and thread-safe. 34 | * 35 | * @since 0.10 36 | */ 37 | @Immutable 38 | @ToString(of = "origin") 39 | @EqualsAndHashCode(of = "origin") 40 | public final class OneMinuteWire implements Wire { 41 | 42 | /** 43 | * Original wire. 44 | */ 45 | private final transient Wire origin; 46 | 47 | /** 48 | * Public ctor. 49 | * @param wire Original wire 50 | */ 51 | public OneMinuteWire(final Wire wire) { 52 | this.origin = wire; 53 | } 54 | 55 | // @checkstyle ParameterNumber (5 lines) 56 | @Override 57 | @Timeable(limit = 1, unit = TimeUnit.MINUTES) 58 | public Response send(final Request req, final String home, 59 | final String method, 60 | final Collection> headers, 61 | final InputStream content, 62 | final int connect, 63 | final int read) throws IOException { 64 | return this.origin.send( 65 | req, home, method, headers, content, connect, read 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/RetryWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Request; 9 | import com.jcabi.http.Response; 10 | import com.jcabi.http.Wire; 11 | import com.jcabi.log.Logger; 12 | import jakarta.ws.rs.core.UriBuilder; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.net.HttpURLConnection; 16 | import java.net.URI; 17 | import java.util.Collection; 18 | import java.util.Map; 19 | import lombok.EqualsAndHashCode; 20 | import lombok.ToString; 21 | 22 | /** 23 | * Wire that retries a few times before giving up and throwing exception. 24 | * 25 | *

This wire retries again (at least three times) if an original one throws 26 | * {@link IOException}: 27 | * 28 | *

 String html = new JdkRequest("http://goggle.com")
 29 |  *   .through(RetryWire.class)
 30 |  *   .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN)
 31 |  *   .fetch()
 32 |  *   .body();
33 | * 34 | *

Since version 1.9 this wire retries also if HTTP status code 35 | * is between 500 and 599. 36 | * 37 | *

The class is immutable and thread-safe. 38 | * 39 | * @since 0.10 40 | */ 41 | @Immutable 42 | @ToString(of = "origin") 43 | @EqualsAndHashCode(of = "origin") 44 | public final class RetryWire implements Wire { 45 | 46 | /** 47 | * Original wire. 48 | */ 49 | private final transient Wire origin; 50 | 51 | /** 52 | * Public ctor. 53 | * @param wire Original wire 54 | */ 55 | public RetryWire(final Wire wire) { 56 | this.origin = wire; 57 | } 58 | 59 | // @checkstyle ParameterNumber (13 lines) 60 | @Override 61 | public Response send(final Request req, final String home, final String method, 62 | final Collection> hdrs, final InputStream cont, 63 | final int conn, final int read) throws IOException { 64 | int attempt = 0; 65 | while (true) { 66 | if (attempt > 3) { 67 | throw new IOException( 68 | String.format("failed after %d attempts", attempt) 69 | ); 70 | } 71 | try { 72 | final Response rsp = this.sendRequest(req, home, method, hdrs, cont, conn, read); 73 | if (rsp.status() < HttpURLConnection.HTTP_INTERNAL_ERROR) { 74 | return rsp; 75 | } 76 | this.logWarning(method, home, rsp.status(), attempt); 77 | } catch (final IOException ex) { 78 | this.logWarning(ex); 79 | } 80 | ++attempt; 81 | } 82 | } 83 | 84 | // @checkstyle ParameterNumber (4 lines) 85 | private Response sendRequest(final Request req, final String home, final String method, 86 | final Collection> headers, final InputStream content, 87 | final int connect, final int read) throws IOException { 88 | return this.origin.send(req, home, method, headers, content, connect, read); 89 | } 90 | 91 | // @checkstyle ParameterNumber (17 lines) 92 | private void logWarning(final String method, final String home, final int status, 93 | final int attempt) { 94 | if (Logger.isWarnEnabled(this)) { 95 | final URI uri = URI.create(home); 96 | final String noauth = UriBuilder.fromUri(uri).userInfo("").toString(); 97 | String authinfo = ""; 98 | if (uri.getUserInfo() != null) { 99 | authinfo = Logger.format( 100 | " (auth: %[secret]s)", 101 | uri.getUserInfo() 102 | ); 103 | } 104 | Logger.warn( 105 | this, "%s %s%s returns %d status (attempt #%d)", 106 | method, noauth, authinfo, status, attempt + 1 107 | ); 108 | } 109 | } 110 | 111 | private void logWarning(final IOException exp) { 112 | if (Logger.isWarnEnabled(this)) { 113 | Logger.warn( 114 | this, "%s: %s", 115 | exp.getClass().getName(), exp.getLocalizedMessage() 116 | ); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/TrustedWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Request; 9 | import com.jcabi.http.Response; 10 | import com.jcabi.http.Wire; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.security.KeyManagementException; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.security.SecureRandom; 16 | import java.security.cert.X509Certificate; 17 | import java.util.Collection; 18 | import java.util.Map; 19 | import javax.net.ssl.HttpsURLConnection; 20 | import javax.net.ssl.SSLContext; 21 | import javax.net.ssl.SSLSocketFactory; 22 | import javax.net.ssl.TrustManager; 23 | import javax.net.ssl.X509TrustManager; 24 | import lombok.EqualsAndHashCode; 25 | import lombok.ToString; 26 | 27 | /** 28 | * Wire that ignores SSL PKIX verifications. 29 | * 30 | *

This wire ignores : 31 | * 32 | *

 String html = new JdkRequest("http://goggle.com")
 33 |  *   .through(TrustedWire.class)
 34 |  *   .fetch()
 35 |  *   .body();
36 | * 37 | *

The class is immutable and thread-safe. 38 | * 39 | * @since 1.10 40 | */ 41 | @Immutable 42 | @ToString(of = "origin") 43 | @EqualsAndHashCode(of = "origin") 44 | public final class TrustedWire implements Wire { 45 | 46 | /** 47 | * Trust manager. 48 | */ 49 | private static final TrustManager MANAGER = new X509TrustManager() { 50 | @Override 51 | public X509Certificate[] getAcceptedIssuers() { 52 | return new X509Certificate[0]; 53 | } 54 | 55 | @Override 56 | public void checkClientTrusted(final X509Certificate[] certs, 57 | final String type) { 58 | // nothing to check here 59 | } 60 | 61 | @Override 62 | public void checkServerTrusted(final X509Certificate[] certs, 63 | final String types) { 64 | // nothing to check here 65 | } 66 | }; 67 | 68 | /** 69 | * Original wire. 70 | */ 71 | private final transient Wire origin; 72 | 73 | /** 74 | * Public ctor. 75 | * @param wire Original wire 76 | */ 77 | public TrustedWire(final Wire wire) { 78 | this.origin = wire; 79 | } 80 | 81 | // @checkstyle ParameterNumber (13 lines) 82 | @Override 83 | public Response send(final Request req, final String home, 84 | final String method, 85 | final Collection> headers, 86 | final InputStream content, 87 | final int connect, final int read) throws IOException { 88 | synchronized (TrustedWire.class) { 89 | final SSLSocketFactory def = 90 | HttpsURLConnection.getDefaultSSLSocketFactory(); 91 | try { 92 | HttpsURLConnection.setDefaultSSLSocketFactory( 93 | TrustedWire.context().getSocketFactory() 94 | ); 95 | return this.origin.send( 96 | req, home, method, headers, content, 97 | connect, read 98 | ); 99 | } finally { 100 | HttpsURLConnection.setDefaultSSLSocketFactory(def); 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * Create context. 107 | * @return Context 108 | */ 109 | private static SSLContext context() { 110 | try { 111 | final SSLContext ctx = SSLContext.getInstance("SSL"); 112 | ctx.init( 113 | null, 114 | new TrustManager[]{TrustedWire.MANAGER}, 115 | new SecureRandom() 116 | ); 117 | return ctx; 118 | } catch (final KeyManagementException | NoSuchAlgorithmException ex) { 119 | throw new IllegalStateException(ex); 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/UserAgentWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.ImmutableHeader; 9 | import com.jcabi.http.Request; 10 | import com.jcabi.http.Response; 11 | import com.jcabi.http.Wire; 12 | import com.jcabi.manifests.Manifests; 13 | import jakarta.ws.rs.core.HttpHeaders; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.util.Collection; 17 | import java.util.LinkedList; 18 | import java.util.Map; 19 | import lombok.EqualsAndHashCode; 20 | import lombok.RequiredArgsConstructor; 21 | import lombok.ToString; 22 | 23 | /** 24 | * Wire with default user agent. 25 | * 26 | *

This wire adds an extra HTTP header {@code User-Agent} to the request, 27 | * if it's not yet provided, for example: 28 | * 29 | *

 String html = new JdkRequest("http://goggle.com")
 30 |  *   .through(UserAgentWire.class)
 31 |  *   .fetch()
 32 |  *   .body();
33 | * 34 | *

An actual HTTP request will be sent with {@code User-Agent} 35 | * header with a value {@code ReXSL-0.1/abcdef0 Java/1.6} (for example). It 36 | * is recommended to use this wire decorator when you're working with 37 | * third party RESTful services, to properly identify yourself and avoid 38 | * troubles. 39 | * 40 | *

The class is immutable and thread-safe. 41 | * 42 | * @see RFC 2616 section 14.43 "User-Agent" 43 | * @since 0.10 44 | */ 45 | @Immutable 46 | @ToString(of = "origin") 47 | @EqualsAndHashCode(of = "origin") 48 | @RequiredArgsConstructor 49 | public final class UserAgentWire implements Wire { 50 | 51 | /** 52 | * Original wire. 53 | */ 54 | private final Wire origin; 55 | 56 | /** 57 | * Agent. 58 | */ 59 | private final String agent; 60 | 61 | /** 62 | * Public ctor. 63 | * @param wire Original wire 64 | */ 65 | public UserAgentWire(final Wire wire) { 66 | this( 67 | wire, 68 | String.format( 69 | "jcabi-%s/%s Java/%s", 70 | Manifests.read("JCabi-Version"), 71 | Manifests.read("JCabi-Build"), 72 | System.getProperty("java.version") 73 | ) 74 | ); 75 | } 76 | 77 | // @checkstyle ParameterNumber (7 lines) 78 | @Override 79 | public Response send(final Request req, final String home, 80 | final String method, 81 | final Collection> headers, 82 | final InputStream content, 83 | final int connect, 84 | final int read) throws IOException { 85 | final Collection> hdrs = 86 | new LinkedList<>(); 87 | boolean absent = true; 88 | for (final Map.Entry header : headers) { 89 | hdrs.add(header); 90 | if (header.getKey().equals(HttpHeaders.USER_AGENT)) { 91 | absent = false; 92 | } 93 | } 94 | if (absent) { 95 | hdrs.add( 96 | new ImmutableHeader( 97 | HttpHeaders.USER_AGENT, 98 | this.agent 99 | ) 100 | ); 101 | } 102 | return this.origin.send( 103 | req, home, method, hdrs, content, connect, read 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/VerboseWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.http.Request; 9 | import com.jcabi.http.RequestBody; 10 | import com.jcabi.http.Response; 11 | import com.jcabi.http.Wire; 12 | import com.jcabi.log.Logger; 13 | import java.io.ByteArrayInputStream; 14 | import java.io.ByteArrayOutputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.util.Collection; 18 | import java.util.Map; 19 | import lombok.EqualsAndHashCode; 20 | import lombok.ToString; 21 | 22 | /** 23 | * Verbose wire. 24 | * 25 | *

This wire makes HTTP request and response details visible in 26 | * log (we're using SLF4J logging facility), for example: 27 | * 28 | *

 String html = new JdkRequest("http://goggle.com")
 29 |  *   .through(VerboseWire.class)
 30 |  *   .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN)
 31 |  *   .fetch()
 32 |  *   .body();
33 | * 34 | *

The class is immutable and thread-safe. 35 | * 36 | * @since 0.10 37 | */ 38 | @Immutable 39 | @ToString(of = "origin") 40 | @EqualsAndHashCode(of = "origin") 41 | public final class VerboseWire implements Wire { 42 | 43 | /** 44 | * Original wire. 45 | */ 46 | private final transient Wire origin; 47 | 48 | /** 49 | * Public ctor. 50 | * @param wire Original wire 51 | */ 52 | public VerboseWire(final Wire wire) { 53 | this.origin = wire; 54 | } 55 | 56 | // @checkstyle ParameterNumber (7 lines) 57 | @Override 58 | public Response send(final Request req, final String home, 59 | final String method, 60 | final Collection> headers, 61 | final InputStream content, 62 | final int connect, 63 | final int read) throws IOException { 64 | final ByteArrayOutputStream output = new ByteArrayOutputStream(); 65 | final byte[] buffer = new byte[1000]; 66 | for (int bytes = content.read(buffer); bytes != -1; 67 | bytes = content.read(buffer)) { 68 | output.write(buffer, 0, bytes); 69 | } 70 | output.flush(); 71 | final Response response = this.origin.send( 72 | req, home, method, headers, 73 | new ByteArrayInputStream(output.toByteArray()), 74 | connect, read 75 | ); 76 | final StringBuilder text = new StringBuilder(0); 77 | for (final Map.Entry header : headers) { 78 | text.append(header.getKey()) 79 | .append(": ") 80 | .append(header.getValue()) 81 | .append('\n'); 82 | } 83 | text.append('\n').append( 84 | new RequestBody.Printable(output.toByteArray()).toString() 85 | ); 86 | Logger.info( 87 | this, 88 | "#send(%s %s):\nHTTP Request (%s):\n%s\nHTTP Response (%s):\n%s", 89 | method, home, 90 | req.getClass().getName(), 91 | VerboseWire.indent(text.toString()), 92 | response.getClass().getName(), 93 | VerboseWire.indent(response.toString()) 94 | ); 95 | return response; 96 | } 97 | 98 | /** 99 | * Indent provided text. 100 | * @param text Text to indent 101 | * @return Indented text 102 | */ 103 | private static String indent(final String text) { 104 | return new StringBuilder(" ") 105 | .append(text.replaceAll("(\n|\n\r)", "$1 ")) 106 | .toString(); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/http/wire/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Wires. 8 | * 9 | * @since 0.10 10 | */ 11 | package com.jcabi.http.wire; 12 | -------------------------------------------------------------------------------- /src/site/apt/example-auto-redirecting.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | Retrying for Redirect Responses 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | Retrying for Redirect Responses 10 | 11 | {{{./apidocs-${project.version}/com/jcabi/http/wire/AutoRedirectingWire.html}<<>>}} 12 | is a decorator of 13 | {{{./apidocs-${project.version}/com/jcabi/http/Wire.html}<<>>}} 14 | that retries when an HTTP Response code between 300 and 399 is received 15 | (e.g. <<>>): 16 | 17 | +-- 18 | String body = new JdkRequest("http://www.google.com") 19 | .through(AutoRedirectingWire.class); 20 | .fetch() 21 | .body(); 22 | +-- 23 | 24 | By default, it will retry up to 5 times. If the retry limit is exceeded, the 25 | last response to be received will be returned. You may specify the number of 26 | retries with an optional parameter: 27 | 28 | +-- 29 | String body = new JdkRequest("http://www.google.com") 30 | .through(AutoRedirectingWire.class, 10) // Retry up to 10 times 31 | .fetch() 32 | .body(); 33 | +-- 34 | -------------------------------------------------------------------------------- /src/site/apt/example-json-response.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | Managing JSON Responses 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | Managing JSON Responses 10 | 11 | When your HTTP 12 | {{{./apidocs-${project.version}/com/jcabi/http/Response.html}<<>>}} 13 | contains a JSON document, it is convenient to use 14 | {{{./apidocs-${project.version}/com/jcabi/http/response/JsonResponse.html}<<>>}} 15 | decorator to manipulate it, for example: 16 | 17 | +-- 18 | String name = new JdkRequest("http://my.example.com") 19 | .fetch() 20 | .as(JsonResponse.class) 21 | .json() // convert HTTP body to javax.json.JsonReader 22 | .readJsonObject().getString("name"); 23 | +-- 24 | 25 | Method {{{./apidocs-${project.version}/com/jcabi/http/response/JsonResponse.html}<<>>}} 26 | converts HTTP body to an instance of class 27 | {{{http://docs.oracle.com/javaee/7/api/javax/json/JsonReader.html}<<>>}}. 28 | 29 | Don't forget to add these two dependencies to your classpath: 30 | 31 | +-- 32 | 33 | javax.json 34 | javax.json-api 35 | 1.0 36 | provided 37 | 38 | 39 | org.glassfish 40 | javax.json 41 | 1.0.3 42 | runtime 43 | 44 | +-- 45 | 46 | Read also about 47 | {{{./apidocs-${project.version}/com/jcabi/http/response/RestResponse.html}<<>>}}, 48 | which helps you to manage default HTTP response concepts. 49 | -------------------------------------------------------------------------------- /src/site/apt/example-mock-container.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | Mock Servlet Container 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | Mock Servlet Container 10 | 11 | {{{./apidocs-${project.version}/com/jcabi/http/mock/MkContainer.html}<<>>}} 12 | is a simple lightweight Java servlet container that can act 13 | as a mock server-side of your application, in order to test your 14 | RESTful client, for example: 15 | 16 | +-- 17 | public class ClientTest { 18 | @Test 19 | public void sendsHttpHeadersCorrectly() { 20 | MkContainer container = new MkGrizzlyContainer(); 21 | container.next( 22 | new MkAnswer.Simple("hello, world!") 23 | .withHeader("Content-Type", "text/plain") 24 | .withHeader("Server", "mock") 25 | ); 26 | container.start(); 27 | new JdkRequest(container.home()) 28 | .through(VerboseWire.class) 29 | .header("User-Agent", "My Super HTTP Client") 30 | .fetch(); 31 | container.stop(); 32 | MkQuery query = container.take(); 33 | MatcherAssert.assertThat( 34 | query.headers(), 35 | Matchers.hasEntry( 36 | Matchers.equalTo("User=Agent"), 37 | Matchers.hasItem("My Super HTTP Client") 38 | ) 39 | ); 40 | } 41 | } 42 | +-- 43 | 44 | Let's see what's happening here. First, a new instance of 45 | {{{./apidocs-${project.version}/com/jcabi/http/mock/MkContainer.html}<<>>}} 46 | is created, with a Grizzly-based implementation 47 | {{{./apidocs-${project.version}/com/jcabi/http/mock/MkGrizzlyContainer.html}<<>>}}: 48 | 49 | +-- 50 | MkContainer container = new MkGrizzlyContainer(); 51 | +-- 52 | 53 | The container is not yet started and doesn't know what to reply when 54 | a new HTTP request comes in. We should add at least one instance of 55 | {{{./apidocs-${project.version}/com/jcabi/http/mock/MkAnswer.html}<<>>}} 56 | to its queue of ready-to-be-dispached answers: 57 | 58 | +-- 59 | container.next( 60 | new MkAnswer.Simple("hello, world!") 61 | .withHeader("Content-Type", "text/plain") 62 | .withHeader("Server", "mock") 63 | ); 64 | +-- 65 | 66 | It's possible to add more instances of 67 | {{{./apidocs-${project.version}/com/jcabi/http/mock/MkAnswer.html}<<>>}}, 68 | using this method <<>>. They will be returned by a running container 69 | one by one in response to incoming HTTP "queries" 70 | (which will be recorded by the container as instances of 71 | {{{./apidocs-${project.version}/com/jcabi/http/mock/MkQuery.html}<<>>}} 72 | and available later through method <<>>). 73 | 74 | Now it's time to start the container on a randomly allocated 75 | TCP port (you can also use a fixed port number, but random allocation 76 | is preferred): 77 | 78 | +-- 79 | container.start(); 80 | +-- 81 | 82 | Then, we're making a request to a running container, getting its 83 | main entry URI by <<>> 84 | ({{{./apidocs-${project.version}/com/jcabi/http/wire/VerboseWire.html}<<>>}} 85 | is used in order to make HTTP request and response details visible in log): 86 | 87 | +-- 88 | new JdkRequest(container.home()) 89 | .through(VerboseWire.class) 90 | .header("User-Agent", "My Super HTTP Client") 91 | .fetch(); 92 | +-- 93 | 94 | Then, we stop the container: 95 | 96 | +-- 97 | container.stop(); 98 | +-- 99 | 100 | Now it's time to check what was received by the container before it 101 | was stopped. All "queries" received are recorded in the container in 102 | chronological order. They may be retrieved using <<>> method: 103 | 104 | +-- 105 | MkQuery query = container.take(); 106 | +-- 107 | 108 | Finally, we can make assertions about this query: 109 | 110 | +-- 111 | MatcherAssert.assertThat( 112 | query.headers(), 113 | Matchers.hasEntry( 114 | Matchers.equalTo("User=Agent"), 115 | Matchers.hasItem("My Super HTTP Client") 116 | ) 117 | ); 118 | +-- 119 | 120 | {{{./apidocs-${project.version}/com/jcabi/http/mock/MkGrizzlyContainer.html}<<>>}} 121 | depends on 122 | {{{https://java.net/projects/grizzly-servlet-container}Grizzly Servlet Webserver}}, 123 | and this dependency is required in your <<>>: 124 | 125 | +-- 126 | 127 | com.sun.grizzly 128 | grizzly-servlet-webserver 129 | 1.9.10 130 | test 131 | 132 | +-- 133 | -------------------------------------------------------------------------------- /src/site/apt/example-request.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | Making HTTP Request 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | Making HTTP Request 10 | 11 | There is one interface 12 | {{{./apidocs-${project.version}/com/jcabi/http/Request.html}<<>>}} 13 | that represents an HTTP request. There are a few implementations 14 | of the interface, and the most popular and easy to use is 15 | {{{./apidocs-${project.version}/com/jcabi/http/request/JdkRequest.html}<<>>}}. 16 | It makes a request using native JDK class 17 | {{{http://docs.oracle.com/javase/7/docs/api/java/net/HttpURLConnection.html}<<>>}}. 18 | 19 | First, make an instance of {{{./apidocs-${project.version}/com/jcabi/http/request/JdkRequest.html}<<>>}}, 20 | providing target URI as the first and the only constructor argument 21 | (<<>>, <<>>, and <<>> types are accepted): 22 | 23 | +-- 24 | Request request = new JdkRequest("http://my.example.com"); 25 | +-- 26 | 27 | The object is ready to make an HTTP request using the URI provided, 28 | <<>> HTTP method, no custom HTTP headers and no request body. To fire 29 | an HTTP request and obtain a response use method <<>>: 30 | 31 | +-- 32 | Response response = request.fetch(); 33 | int status = response.status(); 34 | String body = response.body(); 35 | +-- 36 | 37 | {{{./apidocs-${project.version}/com/jcabi/http/Request.html}<<>>}} 38 | is a trully immutable class, which means that you 39 | can't change any of its properties. However, 40 | you can create new requests. For example, you want to make a similar 41 | request, but with a <<>> HTTP method: 42 | 43 | +-- 44 | Request post = request.method(Request.POST); 45 | post.fetch(); 46 | +-- 47 | 48 | This is a bigger example of 49 | {{{./apidocs-${project.version}/com/jcabi/http/Request.html}<<>>}} 50 | usage: 51 | 52 | +-- 53 | Response response = new JdkRequest("http://my.example.com") 54 | .uri().path("/users").queryParam("id", "456").back() 55 | .method(Request.POST) 56 | .body() 57 | .formParam("salary", "55000") 58 | .formParam("department", "IT") 59 | .back() 60 | .header("Content-Type", "application/json") 61 | .header("User-Agent", "My Custom App") 62 | .fetch(); 63 | +-- 64 | 65 | This code will make a <<>> HTTP request to 66 | <<>>. Request body 67 | will be set to <<>>. There will be 68 | two HTTP headers in the request: <<>> and <<>>. 69 | 70 | {{{./apidocs-${project.version}/com/jcabi/http/Response.html}<<>>}} 71 | has a few methods that return its data, including <<>>, 72 | <<>>, <<>>, <<>>, and <<>>. Besides that, 73 | there is a method <<>>, which allows you to wrap an existing 74 | response with a new custom decorator, that implements more methods, suitable 75 | for a particular use case. 76 | 77 | The simpliest decorator is 78 | {{{./apidocs-${project.version}/com/jcabi/http/response/RestResponse.html}<<>>}}. 79 | It helps you to make assertions on the response and follow redirects, 80 | for example: 81 | 82 | +-- 83 | String page = new JdkRequest("http://my.example.com") 84 | .fetch() 85 | .as(RestResponse.class) 86 | .assertStatus(301) // AssertionError if status is not equal to 301 87 | .follow() // new request, pointing to the URL from Location header 88 | .fetch() 89 | .as(RestResponse.class) 90 | .assertStatus(200) 91 | .body(); 92 | +-- 93 | 94 | {{{./apidocs-${project.version}/com/jcabi/http/response/RestResponse.html}<<>>}} 95 | also helps you to retrieve cookies from a response: 96 | 97 | +-- 98 | String cookie = new JdkRequest("http://my.example.com") 99 | .fetch() 100 | .as(RestResponse.class) 101 | .cookie("SessionId"); 102 | +-- 103 | 104 | Read also about {{{./example-xml-response.html}<<>>}} and 105 | {{{./example-json-response.html}<<>>}}. 106 | -------------------------------------------------------------------------------- /src/site/apt/example-retry-request.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | HTTP Request that Retries on Failure 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | HTTP Request that Retries on Failure 10 | 11 | {{{./apidocs-${project.version}/com/jcabi/http/wire/RetryWire.html}<<>>}}. 12 | is a decorator of 13 | {{{./apidocs-${project.version}/com/jcabi/http/Wire.html}<<>>}} 14 | that retries to fetch a response a few times before giving up and 15 | throwing <<>>, for example: 16 | 17 | +-- 18 | String body = new JdkRequest("http://www.google.com") 19 | .through(RetryWire.class); 20 | .fetch() 21 | .body(); 22 | +-- 23 | 24 | If it recommended to use this decorator in production, to avoid 25 | accidental exceptions due to unstable connection. 26 | -------------------------------------------------------------------------------- /src/site/apt/example-web-linking-response.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | Web Linking Response 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | Web Linking Response 10 | 11 | {{{http://tools.ietf.org/html/rfc5988}RFC 5988 "Web Linking"}} recommends to use 12 | <<>> HTTP header for linking pages. 13 | {{{./apidocs-${project.version}/com/jcabi/http/response/WebLinkingResponse.html}<<>>}} 14 | implements parsing of such header. This is how you can use it: 15 | 16 | +-- 17 | URI destination = new JdkRequest("http://my.example.com") 18 | .fetch() 19 | .as(WebLinkingResponse.class) 20 | .link("next") // IOException here if such a link doesn't exist 21 | .uri(); 22 | +-- 23 | -------------------------------------------------------------------------------- /src/site/apt/example-xml-response.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | Managing XML Responses 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | Managing XML Responses 10 | 11 | When your HTTP 12 | {{{./apidocs-${project.version}/com/jcabi/http/Response.html}<<>>}} 13 | contains an XML document, it is convenient to use 14 | {{{./apidocs-${project.version}/com/jcabi/http/response/XmlResponse.html}<<>>}} 15 | decorator to manipulate it, for example: 16 | 17 | +-- 18 | String name = new JdkRequest("http://my.example.com") 19 | .fetch() 20 | .as(XmlResponse.class) 21 | .assertXPath("/data/name") // AssertionError if this XPath is absent 22 | .xml() // convert HTTP body to com.jcabi.xml.XML 23 | .xpath("/data/name/text()").get(0); 24 | +-- 25 | 26 | Method {{{./apidocs-${project.version}/com/jcabi/http/response/XmlResponse.html}<<>>}} 27 | converts HTTP body to an instance of class 28 | {{{http://xml.jcabi.com/apidocs-0.6/com/jcabi/xml/XML.html}<<>>}} 29 | from {{{http://xml.jcabi.com}jcabi-xml}} project. 30 | 31 | Don't forget to add this dependency to your classpath: 32 | 33 | +-- 34 | 35 | com.jcabi 36 | jcabi-xml 37 | 0.6 38 | 39 | +-- 40 | 41 | Read also about 42 | {{{./apidocs-${project.version}/com/jcabi/http/response/RestResponse.html}<<>>}}, 43 | which helps you to manage default HTTP response concepts. 44 | -------------------------------------------------------------------------------- /src/site/apt/index.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | Fluent HTTP Client 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-11-02 7 | --- 8 | 9 | Fluent HTTP Client 10 | 11 | For example: 12 | 13 | +-- 14 | import com.jcabi.http.Request; 15 | import com.jcabi.http.request.ApacheRequest; 16 | public class Main { 17 | public static void main(String[] args) { 18 | String html = new ApacheRequest("https://www.google.com") 19 | .uri().path("/users").queryParam("id", 333).back() 20 | .method(Request.GET) 21 | .header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML) 22 | .fetch() 23 | .as(RestResponse.class) 24 | .assertStatus(HttpURLConnection.HTTP_OK) 25 | .body(); 26 | } 27 | } 28 | +-- 29 | 30 | {{{http://www.yegor256.com/2014/04/11/jcabi-http-intro.html}This blog post}} 31 | explains the 32 | benefits of this client comparing to Apache, Jersey, and others. 33 | 34 | Check {{{./apidocs-${project.version}/index.html}JavaDoc}} 35 | for more details. 36 | 37 | The main dependency that you need is 38 | (you can also download 39 | {{{http://repo1.maven.org/maven2/com/jcabi/jcabi-http/${project.version}/jcabi-http-${project.version}.jar}jcabi-http-${project.version}.jar}} 40 | and add it to the classpath): 41 | 42 | +-- 43 | 44 | com.jcabi 45 | jcabi-http 46 | ${project.version} 47 | 48 | +-- 49 | 50 | Other dependencies may be required in order to use all features of this library. 51 | See {{{./optional-dependencies.html}here}} for a full list of the dependencies 52 | that you may need. 53 | 54 | * Cutting Edge Version 55 | 56 | If you want to use current version of the product, you can do it with 57 | this configuration in your <<>>: 58 | 59 | +-- 60 | 61 | 62 | oss.sonatype.org 63 | https://oss.sonatype.org/content/repositories/snapshots/ 64 | 65 | 66 | 67 | 68 | com.jcabi 69 | jcabi-http 70 | 2.0-SNAPSHOT 71 | 72 | 73 | +-- 74 | -------------------------------------------------------------------------------- /src/site/apt/pkix-validator.apt.vm: -------------------------------------------------------------------------------- 1 | --- 2 | PKIX Validator Failure 3 | --- 4 | Yegor Bugayenko 5 | --- 6 | 2014-12-26 7 | --- 8 | 9 | How to Solve PKIX Validator Failure 10 | 11 | This is the exception you may get if the SSL certificate 12 | is not a good one: 13 | 14 | +-- 15 | sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 16 | at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385) 17 | at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292) 18 | at sun.security.validator.Validator.validate(Validator.java:260) 19 | at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:326) 20 | at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231) 21 | at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:126) 22 | at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323) 23 | ... 42 more 24 | Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 25 | at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:196) 26 | at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:268) 27 | at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380) 28 | ... 48 more 29 | +-- 30 | 31 | Just use {{{./apidocs-${project.version}/com/jcabi/http/wire/TrustedWire.html}<<>>}} 32 | and the problem is solved: 33 | 34 | +-- 35 | String html = new JdkRequest("https://www.google.com") 36 | .method(Request.GET) 37 | .through(TrustedWire.class) 38 | .fetch(); 39 | +-- 40 | -------------------------------------------------------------------------------- /src/site/resources/CNAME: -------------------------------------------------------------------------------- 1 | http.jcabi.com 2 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 33 | com.jcabi 34 | jcabi-maven-skin 35 | 1.7.1 36 | 37 | 38 | jcabi 39 | https://www.jcabi.com/logo-square.svg 40 | https://www.jcabi.com/ 41 | 64 42 | 64 43 | 44 | UA-1963507-23 45 | 46 | 47 | <link href="https://www.jcabi.com/favicon.ico" rel="shortcut icon"/> 48 | <link href="https://plus.google.com/u/0/114792568016408327418?rel=author" rel="author"/> 49 | 50 |

51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/ImmutableHeaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import java.util.Map; 8 | import org.hamcrest.MatcherAssert; 9 | import org.hamcrest.Matchers; 10 | import org.junit.jupiter.api.Test; 11 | 12 | /** 13 | * Test case for {@link ImmutableHeader}. 14 | * @since 1.1 15 | */ 16 | final class ImmutableHeaderTest { 17 | 18 | /** 19 | * ImmutableHeader can normalize headers. 20 | * @throws Exception If something goes wrong inside 21 | */ 22 | @Test 23 | void normalizesHeaderKey() throws Exception { 24 | final Map.Entry header = 25 | new ImmutableHeader("content-type", "text/plain"); 26 | MatcherAssert.assertThat( 27 | "should be 'Content-Type'", 28 | header.getKey(), 29 | Matchers.equalTo("Content-Type") 30 | ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/MockWire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.Collection; 10 | import java.util.Map.Entry; 11 | 12 | /** 13 | * Utility wire used for injecting a mock object into a {@link Request} 14 | * implementation. 15 | *

16 | * NOTE: This is not threadsafe and access to it should be synchronized. 17 | * 18 | * @since 1.17.1 19 | * @checkstyle ParameterNumberCheck (50 lines) 20 | */ 21 | public class MockWire implements Wire { 22 | 23 | /** 24 | * The actual mock object we delegate the Wire.send call to. 25 | */ 26 | private static Wire mockDelegate; 27 | 28 | /** 29 | * Creates a new mock wire instance. 30 | *

31 | * The given target wire is ignored and Wire.send is delegated 32 | * to the static mock delegate. 33 | * 34 | * @param wire The original wire which is ignored 35 | */ 36 | @SuppressWarnings("PMD.UnusedFormalParameter") 37 | public MockWire(final Wire wire) { 38 | // Instantiated by a Request implementation, wire is ignored 39 | } 40 | 41 | @Override 42 | public final Response send(final Request req, final String home, 43 | final String method, final Collection> headers, 44 | final InputStream content, final int connect, final int read) 45 | throws IOException { 46 | return mockDelegate.send( 47 | req, 48 | home, 49 | method, 50 | headers, 51 | content, 52 | connect, 53 | read 54 | ); 55 | } 56 | 57 | /** 58 | * Sets the mock the Request.send method is delegated to. 59 | * 60 | * @param mock The mock to assert variables passed by the request 61 | * implementation 62 | */ 63 | @SuppressWarnings("PMD.DefaultPackage") 64 | static void setMockDelegate(final Wire mock) { 65 | MockWire.mockDelegate = mock; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/RequestSecondITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import com.jcabi.http.request.ApacheRequest; 8 | import com.jcabi.http.request.JdkRequest; 9 | import java.net.URI; 10 | import org.junit.jupiter.api.AfterAll; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.TestInstance; 14 | import org.testcontainers.containers.GenericContainer; 15 | import org.testcontainers.junit.jupiter.Testcontainers; 16 | import org.testcontainers.utility.DockerImageName; 17 | 18 | /** 19 | * Integration test for {@link Request}. 20 | * 21 | * @since 1.17.8 22 | */ 23 | @SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod") 24 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 25 | @Testcontainers(disabledWithoutDocker = true) 26 | final class RequestSecondITCase { 27 | 28 | /** 29 | * Container with HttpBin. 30 | */ 31 | private final GenericContainer container = new GenericContainer<>( 32 | DockerImageName.parse("kennethreitz/httpbin") 33 | ).withExposedPorts(80); 34 | 35 | @BeforeAll 36 | void beforeAll() { 37 | this.container.start(); 38 | } 39 | 40 | @AfterAll 41 | void tearDown() { 42 | this.container.stop(); 43 | } 44 | 45 | /** 46 | * URI of the container. 47 | * @return URI. 48 | */ 49 | private URI uri() { 50 | return URI.create( 51 | String.format( 52 | "http://%s:%d", 53 | this.container.getHost(), 54 | this.container.getFirstMappedPort() 55 | ) 56 | ); 57 | } 58 | 59 | /** 60 | * Test for {@link JdkRequest}. 61 | * @since 1.17.8 62 | */ 63 | @Nested 64 | final class JdkRequestITCase extends RequestITCaseTemplate { 65 | JdkRequestITCase() { 66 | super(JdkRequest.class, RequestSecondITCase.this.uri()); 67 | } 68 | } 69 | 70 | /** 71 | * Test for {@link ApacheRequest}. 72 | * @since 1.17.8 73 | */ 74 | @Nested 75 | final class ApacheRequestITCase extends RequestITCaseTemplate { 76 | ApacheRequestITCase() { 77 | super(ApacheRequest.class, RequestSecondITCase.this.uri()); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/RequestTestTemplate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http; 6 | 7 | import com.jcabi.http.request.ApacheRequest; 8 | import com.jcabi.http.request.JdkRequest; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | import java.net.URI; 12 | import lombok.SneakyThrows; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | 15 | /** 16 | * Template for generic tests for {@link Request}. 17 | * 18 | * @since 1.17.4 19 | */ 20 | @SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod") 21 | abstract class RequestTestTemplate { 22 | /** 23 | * Annotation for a parameterized test case. 24 | * 25 | * @since 1.17.4 26 | */ 27 | @Retention(RetentionPolicy.RUNTIME) 28 | @ValueSource(classes = {ApacheRequest.class, JdkRequest.class}) 29 | protected @interface Values { 30 | } 31 | 32 | /** 33 | * Make a request. 34 | * @param uri URI to start with 35 | * @param type Type of the request 36 | * @return Request 37 | */ 38 | @SneakyThrows 39 | static Request request(final URI uri, final Class type) { 40 | return type.getDeclaredConstructor(URI.class).newInstance(uri); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/mock/GrizzlyQueryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.util.Collections; 9 | import org.glassfish.grizzly.http.server.Request; 10 | import org.hamcrest.MatcherAssert; 11 | import org.hamcrest.Matchers; 12 | import org.junit.jupiter.api.Test; 13 | import org.mockito.Mockito; 14 | 15 | /** 16 | * Test case for {@link GrizzlyQuery}. 17 | * @since 1.13 18 | */ 19 | final class GrizzlyQueryTest { 20 | 21 | /** 22 | * GrizzlyQuery can return a body as a byte array. 23 | * @throws Exception if something goes wrong. 24 | */ 25 | @Test 26 | void returnsBinaryBody() throws Exception { 27 | final Request request = Mockito.mock(Request.class); 28 | Mockito.when(request.getRequestURI()).thenReturn("http://fake.com"); 29 | Mockito.when(request.getHeaderNames()).thenReturn( 30 | Collections.emptyList() 31 | ); 32 | final byte[] body = "body".getBytes(); 33 | Mockito.when(request.getInputStream()).thenReturn( 34 | new ByteArrayInputStream(body) 35 | ); 36 | MatcherAssert.assertThat( 37 | "should match the body", 38 | new GrizzlyQuery(request).binary(), 39 | Matchers.is(body) 40 | ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/mock/MkAnswerMatchersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import java.util.Collections; 8 | import org.hamcrest.MatcherAssert; 9 | import org.hamcrest.Matchers; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.Mockito; 12 | 13 | /** 14 | * Test case for {@link MkAnswerMatchers}. 15 | * @since 1.5 16 | */ 17 | final class MkAnswerMatchersTest { 18 | 19 | /** 20 | * MkAnswerMatchers should be able to match MkAnswer body. 21 | */ 22 | @Test 23 | void canMatchBody() { 24 | final String body = "Hello \u20ac!"; 25 | final MkAnswer query = Mockito.mock(MkAnswer.class); 26 | Mockito.doReturn(body).when(query).body(); 27 | MatcherAssert.assertThat( 28 | "should match the answer body", 29 | query, 30 | MkAnswerMatchers.hasBody( 31 | Matchers.is(body) 32 | ) 33 | ); 34 | } 35 | 36 | /** 37 | * MkAnswerMatchers can match MkAnswer body bytes. 38 | */ 39 | @Test 40 | void canMatchBodyBytes() { 41 | final byte[] body = {0x01, 0x45, 0x21}; 42 | final MkAnswer query = Mockito.mock(MkAnswer.class); 43 | Mockito.doReturn(body).when(query).bodyBytes(); 44 | MatcherAssert.assertThat( 45 | "should match the answer body bytes", 46 | query, 47 | MkAnswerMatchers.hasBodyBytes( 48 | Matchers.is(body) 49 | ) 50 | ); 51 | } 52 | 53 | /** 54 | * MkAnswerMatchers should be able to match MkAnswer header. 55 | */ 56 | @Test 57 | void canMatchHeader() { 58 | final String header = "Content-Type"; 59 | final String value = "application/json"; 60 | final MkAnswer query = Mockito.mock(MkAnswer.class); 61 | Mockito.doReturn( 62 | Collections.singletonMap(header, Collections.singletonList(value)) 63 | ).when(query).headers(); 64 | MatcherAssert.assertThat( 65 | "should match the answer header", 66 | query, 67 | MkAnswerMatchers.hasHeader( 68 | header, 69 | Matchers.contains(value) 70 | ) 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/mock/MkQueryMatchersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.mock; 6 | 7 | import java.net.URI; 8 | import java.util.Collections; 9 | import org.hamcrest.MatcherAssert; 10 | import org.hamcrest.Matchers; 11 | import org.junit.jupiter.api.Test; 12 | import org.mockito.Mockito; 13 | 14 | /** 15 | * Test case for {@link MkQueryMatchers}. 16 | * 17 | * @since 1.5 18 | */ 19 | final class MkQueryMatchersTest { 20 | 21 | /** 22 | * MkQueryMatchers should be able to match MkQuery body. 23 | */ 24 | @Test 25 | void canMatchBody() { 26 | final String body = "Hello \u20ac!"; 27 | final MkQuery query = Mockito.mock(MkQuery.class); 28 | Mockito.doReturn(body).when(query).body(); 29 | MatcherAssert.assertThat( 30 | "should match the query body", 31 | query, 32 | MkQueryMatchers.hasBody( 33 | Matchers.is(body) 34 | ) 35 | ); 36 | } 37 | 38 | /** 39 | * MkQueryMatchers should be able to match MkQuery header. 40 | */ 41 | @Test 42 | void canMatchHeader() { 43 | final String header = "Content-Type"; 44 | final String value = "application/json"; 45 | final MkQuery query = Mockito.mock(MkQuery.class); 46 | Mockito.doReturn( 47 | Collections.singletonMap(header, Collections.singletonList(value)) 48 | ).when(query).headers(); 49 | MatcherAssert.assertThat( 50 | "should match the query header", 51 | query, 52 | MkQueryMatchers.hasHeader( 53 | header, 54 | Matchers.contains(value) 55 | ) 56 | ); 57 | } 58 | 59 | /** 60 | * MkQueryMatchers should be able to match MkQuery raw path. 61 | */ 62 | @Test 63 | void canMatchPath() { 64 | final URI body = URI.create("http://example.com/index.html?y=x"); 65 | final MkQuery query = Mockito.mock(MkQuery.class); 66 | Mockito.doReturn(body).when(query).uri(); 67 | MatcherAssert.assertThat( 68 | "should match the raw path", 69 | query, 70 | MkQueryMatchers.hasPath( 71 | Matchers.is("/index.html") 72 | ) 73 | ); 74 | } 75 | 76 | /** 77 | * MkQueryMatchers should be able to match MkQuery raw query. 78 | */ 79 | @Test 80 | void canMatchQuery() { 81 | final URI body = URI.create("http://example.com/?x=10"); 82 | final MkQuery query = Mockito.mock(MkQuery.class); 83 | Mockito.doReturn(body).when(query).uri(); 84 | MatcherAssert.assertThat( 85 | "should match the raw query", 86 | query, 87 | MkQueryMatchers.hasQuery( 88 | Matchers.is("x=10") 89 | ) 90 | ); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/mock/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Mock of Servlet Container, tests. 8 | * 9 | * @since 0.10 10 | */ 11 | package com.jcabi.http.mock; 12 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Test package, tests for it. 8 | */ 9 | package com.jcabi.http; 10 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/request/BoundaryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.request; 6 | 7 | import java.util.Random; 8 | import org.hamcrest.MatcherAssert; 9 | import org.hamcrest.Matchers; 10 | import org.junit.jupiter.api.Test; 11 | 12 | /** 13 | * Test case {@link Boundary}. 14 | * @since 1.17.3 15 | */ 16 | final class BoundaryTest { 17 | 18 | /** 19 | * Boundary builds valid string. 20 | * @throws Exception If something goes wrong inside. 21 | */ 22 | @Test 23 | void buildsExpectedBoundary() throws Exception { 24 | MatcherAssert.assertThat( 25 | "should be match", 26 | new Boundary(new Random(0L)).value(), 27 | Matchers.is("PdAChx6AMjemBZYS_W0fi7l8H_-w-X") 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/request/DefaultResponseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.request; 6 | 7 | import com.jcabi.http.Request; 8 | import com.jcabi.immutable.Array; 9 | import java.net.HttpURLConnection; 10 | import java.util.Map; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.function.Executable; 14 | import org.mockito.Mockito; 15 | 16 | /** 17 | * Test case for {@link DefaultResponse}. 18 | * @since 1.0 19 | */ 20 | final class DefaultResponseTest { 21 | 22 | /** 23 | * DefaultResponse can throw when entity is not a Unicode text. 24 | */ 25 | @Test 26 | void throwsWhenEntityIsNotAUnicodeString() { 27 | Assertions.assertThrows( 28 | RuntimeException.class, 29 | new Executable() { 30 | @Override 31 | public void execute() { 32 | new DefaultResponse( 33 | Mockito.mock(Request.class), 34 | HttpURLConnection.HTTP_OK, 35 | "some text", 36 | new Array>(), 37 | // @checkstyle MagicNumber (1 line) 38 | new byte[]{(byte) 0xC0, (byte) 0xC0} 39 | ).body(); 40 | } 41 | } 42 | ); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/request/JdkRequestITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.request; 6 | 7 | import jakarta.ws.rs.HttpMethod; 8 | import java.io.IOException; 9 | import java.net.MalformedURLException; 10 | import java.net.URI; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.hamcrest.MatcherAssert; 13 | import org.hamcrest.Matchers; 14 | import org.junit.jupiter.api.Assertions; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.function.Executable; 17 | 18 | /** 19 | * Integration case for {@link JdkRequest}. 20 | * @since 1.4.1 21 | */ 22 | final class JdkRequestITCase { 23 | 24 | /** 25 | * Property name of Exception. 26 | */ 27 | private static final String MESSAGE = "message"; 28 | 29 | /** 30 | * BaseRequest throws an exception with a descriptive message showing the 31 | * URI and method when an error occurs. 32 | */ 33 | @Test 34 | void throwsDescriptiveException() { 35 | final String uri = "http://localhost:6789"; 36 | final String method = HttpMethod.POST; 37 | MatcherAssert.assertThat( 38 | "should be error with a descriptive message", 39 | Assertions.assertThrows( 40 | IOException.class, 41 | new Executable() { 42 | @Override 43 | public void execute() throws Throwable { 44 | new JdkRequest(new URI(uri)).method(method).fetch(); 45 | } 46 | }), 47 | Matchers.hasProperty( 48 | JdkRequestITCase.MESSAGE, 49 | Matchers.allOf( 50 | Matchers.containsString(uri), 51 | Matchers.containsString(method) 52 | ) 53 | ) 54 | ); 55 | } 56 | 57 | /** 58 | * BaseRequest throws an exception with a descriptive message if there is no 59 | * port and no protocol mentioned in the uri. 60 | */ 61 | @Test 62 | void failsNoProtocolNoPort() { 63 | final String uri = "localhost"; 64 | MatcherAssert.assertThat( 65 | "should be error with a descriptive message", 66 | Assertions.assertThrows( 67 | IOException.class, 68 | new Executable() { 69 | @Override 70 | public void execute() throws Throwable { 71 | new JdkRequest(uri).fetch(); 72 | } 73 | }), 74 | Matchers.hasProperty( 75 | JdkRequestITCase.MESSAGE, 76 | Matchers.allOf( 77 | Matchers.containsString("is incorrect"), 78 | Matchers.containsString(uri) 79 | ) 80 | ) 81 | ); 82 | } 83 | 84 | /** 85 | * BaseRequest throws an exception with a descriptive message if there is no 86 | * protocol mentioned in the uri. 87 | */ 88 | @Test 89 | void failsWithPortButNoProtocol() { 90 | final String url = "test.com"; 91 | final String colon = ":"; 92 | MatcherAssert.assertThat( 93 | "should be error with a descriptive message", 94 | Assertions.assertThrows( 95 | MalformedURLException.class, 96 | new Executable() { 97 | 98 | @Override 99 | public void execute() throws Throwable { 100 | new JdkRequest( 101 | StringUtils.join( 102 | url, 103 | colon, 104 | "80" 105 | ) 106 | ).fetch(); 107 | } 108 | } 109 | ), 110 | Matchers.hasProperty( 111 | JdkRequestITCase.MESSAGE, 112 | Matchers.allOf( 113 | Matchers.containsString("unknown protocol: "), 114 | Matchers.containsString(url) 115 | ) 116 | ) 117 | ); 118 | } 119 | 120 | /** 121 | * BaseRequest throws an exception with a descriptive message 122 | * if the uri is completely wrong (e.g. bla bla1) 123 | */ 124 | @Test 125 | void failsMalformedEntirely() { 126 | final String uri = "bla bla url"; 127 | MatcherAssert.assertThat( 128 | "should be error with a descriptive message", 129 | Assertions.assertThrows( 130 | IllegalArgumentException.class, 131 | new Executable() { 132 | 133 | @Override 134 | public void execute() throws Throwable { 135 | new JdkRequest(uri).fetch(); 136 | } 137 | }), 138 | Matchers.hasProperty( 139 | JdkRequestITCase.MESSAGE, 140 | Matchers.allOf( 141 | Matchers.containsString("Illegal character in path"), 142 | Matchers.containsString(uri) 143 | ) 144 | ) 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/request/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Requests, tests. 8 | * 9 | * @since 0.10 10 | */ 11 | package com.jcabi.http.request; 12 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/response/JsonResponseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.http.Response; 8 | import com.jcabi.http.request.FakeRequest; 9 | import jakarta.json.stream.JsonParsingException; 10 | import org.hamcrest.MatcherAssert; 11 | import org.hamcrest.Matchers; 12 | import org.junit.jupiter.api.Assertions; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.function.Executable; 15 | 16 | /** 17 | * Test case for {@link JsonResponse}. 18 | * @since 1.1 19 | */ 20 | final class JsonResponseTest { 21 | 22 | /** 23 | * JsonResponse can read and return a JSON document. 24 | * @throws Exception If something goes wrong inside 25 | */ 26 | @Test 27 | void readsJsonDocument() throws Exception { 28 | final Response resp = new FakeRequest() 29 | .withBody("{\n\t\r\"foo-foo\":2,\n\"bar\":\"\u20ac\"}") 30 | .fetch(); 31 | final JsonResponse response = new JsonResponse(resp); 32 | MatcherAssert.assertThat( 33 | "should be equal 2", 34 | response.json().readObject().getInt("foo-foo"), 35 | Matchers.equalTo(2) 36 | ); 37 | MatcherAssert.assertThat( 38 | "should be equal \u20ac", 39 | response.json().readObject().getString("bar"), 40 | Matchers.equalTo("\u20ac") 41 | ); 42 | } 43 | 44 | /** 45 | * JsonResponse can read control characters. 46 | * 47 | * @throws Exception If something goes wrong inside 48 | */ 49 | @Test 50 | void readsControlCharacters() throws Exception { 51 | final Response resp = new FakeRequest() 52 | .withBody("{\"test\":\n\"\u001Fblah\uFFFDcwhoa\u0000!\"}").fetch(); 53 | final JsonResponse response = new JsonResponse(resp); 54 | MatcherAssert.assertThat( 55 | "should be \u001Fblah\uFFFDcwhoa\u0000!", 56 | response.json().readObject().getString("test"), 57 | Matchers.is("\u001Fblah\uFFFDcwhoa\u0000!") 58 | ); 59 | } 60 | 61 | /** 62 | * JsonResponse logs the JSON body for JSON object parse errors. 63 | * 64 | * @throws Exception If something goes wrong inside 65 | */ 66 | @Test 67 | void logsForInvalidJsonObject() throws Exception { 68 | final String body = "{\"test\": \"logged!\"$@%#^&%@$#}"; 69 | final Response resp = new FakeRequest().withBody(body).fetch(); 70 | MatcherAssert.assertThat( 71 | "should contains json body", 72 | Assertions.assertThrows( 73 | JsonParsingException.class, 74 | new Executable() { 75 | @Override 76 | public void execute() throws Throwable { 77 | new JsonResponse(resp).json().readObject(); 78 | } 79 | }, 80 | "readObject() should have thrown JsonParsingException" 81 | ), 82 | Matchers.hasToString(Matchers.containsString(body)) 83 | ); 84 | } 85 | 86 | /** 87 | * JsonResponse logs the JSON body for JSON array parse errors. 88 | * 89 | * @throws Exception If something goes wrong inside 90 | */ 91 | @Test 92 | void logsForInvalidJsonArray() throws Exception { 93 | final String body = "[\"test\": \"logged!\"$@%#^&%@$#]"; 94 | final Response resp = new FakeRequest().withBody(body).fetch(); 95 | MatcherAssert.assertThat( 96 | "should contains json body", 97 | Assertions.assertThrows( 98 | JsonParsingException.class, 99 | new Executable() { 100 | @Override 101 | public void execute() throws Throwable { 102 | new JsonResponse(resp).json().readArray(); 103 | } 104 | }, 105 | "readArray() should have thrown JsonParsingException" 106 | ), 107 | Matchers.hasToString( 108 | Matchers.containsString( 109 | body 110 | ) 111 | ) 112 | ); 113 | } 114 | 115 | /** 116 | * JsonResponse logs the JSON body for JSON read() parse errors. 117 | * 118 | * @throws Exception If something goes wrong inside 119 | */ 120 | @Test 121 | void logsForInvalidJson() throws Exception { 122 | final String body = "{test:[]}}}"; 123 | final Response resp = new FakeRequest().withBody(body).fetch(); 124 | MatcherAssert.assertThat( 125 | "should contains json body", 126 | Assertions.assertThrows( 127 | JsonParsingException.class, 128 | new Executable() { 129 | @Override 130 | public void execute() throws Throwable { 131 | new JsonResponse(resp).json().read(); 132 | } 133 | }, 134 | "readStructure() should have thrown JsonParsingException" 135 | ), 136 | Matchers.hasToString( 137 | Matchers.containsString( 138 | body 139 | ) 140 | ) 141 | ); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/response/JsoupResponseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.google.common.base.Joiner; 8 | import com.jcabi.http.Response; 9 | import com.jcabi.http.request.FakeRequest; 10 | import com.jcabi.matchers.XhtmlMatchers; 11 | import org.hamcrest.MatcherAssert; 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** 15 | * Test case for {@link JsoupResponse}. 16 | * 17 | * @since 1.4 18 | */ 19 | final class JsoupResponseTest { 20 | 21 | /** 22 | * JsoupResponse normalizes malformed HTML responses. 23 | * @throws Exception If a problem occurs. 24 | */ 25 | @Test 26 | void normalizesHtml() throws Exception { 27 | final Response resp = new FakeRequest().withBody( 28 | Joiner.on(' ').join( 29 | "", 30 | "", 31 | "

Hello world" 32 | ) 33 | ).fetch(); 34 | MatcherAssert.assertThat( 35 | "should contains normalized response", 36 | new JsoupResponse(resp).body(), 37 | XhtmlMatchers.hasXPaths( 38 | "/xhtml:html/xhtml:head", 39 | "/xhtml:html/xhtml:body/xhtml:p[.=\"Hello world\"]" 40 | ) 41 | ); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/response/RestResponseITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.http.request.JdkRequest; 8 | import java.io.IOException; 9 | import org.hamcrest.MatcherAssert; 10 | import org.hamcrest.Matchers; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.function.Executable; 14 | 15 | /** 16 | * Integration test for {@link RestResponse}. 17 | * 18 | * @since 1.17.5 19 | */ 20 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") final class RestResponseITCase { 21 | @Test 22 | void readsCookiesSeveralValues() throws IOException { 23 | final RestResponse resp = new JdkRequest( 24 | "https://httpbin.org/cookies/set?ijk=efg&xyz=abc" 25 | ) 26 | .fetch() 27 | .as(RestResponse.class); 28 | Assertions.assertAll( 29 | new Executable() { 30 | @Override 31 | public void execute() { 32 | MatcherAssert.assertThat( 33 | "should contains value 'efg'", 34 | resp.cookie("ijk"), 35 | Matchers.hasProperty("value", Matchers.is("efg")) 36 | ); 37 | } 38 | }, 39 | new Executable() { 40 | @Override 41 | public void execute() { 42 | MatcherAssert.assertThat( 43 | "should contains value 'abc'", 44 | resp.cookie("xyz"), 45 | Matchers.hasProperty("value", Matchers.is("abc")) 46 | ); 47 | } 48 | } 49 | ); 50 | } 51 | 52 | @Test 53 | void readsCookies() throws IOException { 54 | MatcherAssert.assertThat( 55 | "should contains value 'bar'", 56 | new JdkRequest("https://httpbin.org/cookies/set?foo=bar") 57 | .fetch() 58 | .as(RestResponse.class) 59 | .cookie("foo"), 60 | Matchers.hasProperty("value", Matchers.is("bar")) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/response/RestResponseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.http.Response; 8 | import com.jcabi.http.request.FakeRequest; 9 | import jakarta.ws.rs.core.HttpHeaders; 10 | import java.net.HttpURLConnection; 11 | import java.net.URI; 12 | import org.hamcrest.Matcher; 13 | import org.hamcrest.MatcherAssert; 14 | import org.hamcrest.Matchers; 15 | import org.junit.jupiter.api.Assertions; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.function.Executable; 18 | 19 | /** 20 | * Test case for {@link RestResponse}. 21 | * @since 1.1 22 | */ 23 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 24 | final class RestResponseTest { 25 | 26 | /** 27 | * RestResponse can assert HTTP status. 28 | */ 29 | @Test 30 | void assertsHttpStatusCode() { 31 | Assertions.assertThrows( 32 | AssertionError.class, 33 | new Executable() { 34 | @Override 35 | public void execute() throws Throwable { 36 | new RestResponse( 37 | new FakeRequest() 38 | .withStatus(HttpURLConnection.HTTP_OK) 39 | .fetch() 40 | ).assertStatus(HttpURLConnection.HTTP_NOT_FOUND); 41 | } 42 | } 43 | ); 44 | } 45 | 46 | /** 47 | * RestResponse can assert HTTP header. 48 | * @throws Exception If something goes wrong inside 49 | */ 50 | @Test 51 | @SuppressWarnings("unchecked") 52 | void assertsHttpHeaders() throws Exception { 53 | final String name = "Abc"; 54 | final String value = "t66"; 55 | final Response rsp = new FakeRequest().withHeader(name, value).fetch(); 56 | new RestResponse(rsp).assertHeader( 57 | name, 58 | Matchers.allOf( 59 | Matchers.hasItems(value), 60 | Matcher.class.cast(Matchers.hasSize(1)) 61 | ) 62 | ); 63 | new RestResponse(rsp).assertHeader( 64 | "Something-Else-Which-Is-Absent", 65 | Matcher.class.cast(Matchers.empty()) 66 | ); 67 | } 68 | 69 | /** 70 | * RestResponse can retrieve a cookie by name. 71 | * @throws Exception If something goes wrong inside 72 | */ 73 | @Test 74 | void retrievesCookieByName() throws Exception { 75 | final RestResponse response = new RestResponse( 76 | new FakeRequest() 77 | .withBody("") 78 | .withHeader( 79 | HttpHeaders.SET_COOKIE, 80 | "cookie1=foo1;Path=/;Comment=\"\", bar=1;" 81 | ) 82 | .fetch() 83 | ); 84 | MatcherAssert.assertThat( 85 | "should contains value & path", 86 | response.cookie("cookie1"), 87 | Matchers.allOf( 88 | Matchers.hasProperty("value", Matchers.equalTo("foo1")), 89 | Matchers.hasProperty("path", Matchers.equalTo("/")) 90 | ) 91 | ); 92 | } 93 | 94 | /** 95 | * RestResponse can retrieve a cookie by name if header occurs several 96 | * times. 97 | * @throws Exception If something goes wrong inside 98 | */ 99 | @Test 100 | void retrievesCookieByNameSeveralValues() throws Exception { 101 | final RestResponse response = new RestResponse( 102 | new FakeRequest() 103 | .withHeader(HttpHeaders.SET_COOKIE, "foo=bar; path=/i;") 104 | .withHeader(HttpHeaders.SET_COOKIE, "baz=goo; path=/l;") 105 | .fetch() 106 | ); 107 | MatcherAssert.assertThat( 108 | "should contains value & path", 109 | response.cookie("baz"), 110 | Matchers.allOf( 111 | Matchers.hasProperty("value", Matchers.equalTo("goo")), 112 | Matchers.hasProperty("path", Matchers.equalTo("/l")) 113 | ) 114 | ); 115 | MatcherAssert.assertThat( 116 | "should contains value & path", 117 | response.cookie("foo"), 118 | Matchers.allOf( 119 | Matchers.hasProperty("value", Matchers.equalTo("bar")), 120 | Matchers.hasProperty("path", Matchers.equalTo("/i")) 121 | ) 122 | ); 123 | } 124 | 125 | /** 126 | * RestResponse can jump to a relative URL. 127 | * @throws Exception If something goes wrong inside 128 | */ 129 | @Test 130 | void jumpsToRelativeUrls() throws Exception { 131 | MatcherAssert.assertThat( 132 | "should contains value & path", 133 | new RestResponse( 134 | new FakeRequest() 135 | .uri().set(new URI("http://locahost:888/tt")).back() 136 | .fetch() 137 | ).jump(new URI("/foo/bar?hey")).uri().get(), 138 | Matchers.hasToString("http://locahost:888/foo/bar?hey") 139 | ); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/response/WebLinkingResponseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.http.request.FakeRequest; 8 | import java.net.URI; 9 | import org.hamcrest.MatcherAssert; 10 | import org.hamcrest.Matchers; 11 | import org.junit.jupiter.api.Test; 12 | 13 | /** 14 | * Test case for {@link WebLinkingResponse}. 15 | * @since 0.9 16 | */ 17 | final class WebLinkingResponseTest { 18 | 19 | /** 20 | * The Link header. 21 | */ 22 | private static final String LINK = "Link"; 23 | 24 | /** 25 | * WebLinkingResponse can recognize Links in headers. 26 | * @throws Exception If something goes wrong inside 27 | */ 28 | @Test 29 | @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 30 | void parsesLinksInHeaders() throws Exception { 31 | final String[] headers = { 32 | "; title=\"Hi!\"; rel=foo", 33 | "; title=\"\u20ac\"; rel=\"foo\"; media=\"text/xml\"", 34 | }; 35 | for (final String header : headers) { 36 | final WebLinkingResponse response = new WebLinkingResponse( 37 | new FakeRequest() 38 | .withHeader(WebLinkingResponseTest.LINK, header) 39 | .fetch() 40 | ); 41 | final WebLinkingResponse.Link link = response.links().get("foo"); 42 | MatcherAssert.assertThat( 43 | "should contains '/hey/foo'", 44 | link.uri(), 45 | Matchers.hasToString("/hey/foo") 46 | ); 47 | MatcherAssert.assertThat( 48 | "should contains key 'title'", 49 | link, 50 | Matchers.hasKey("title") 51 | ); 52 | MatcherAssert.assertThat( 53 | "should not contains key 'something else'", 54 | response.links(), 55 | Matchers.not(Matchers.hasKey("something else")) 56 | ); 57 | } 58 | } 59 | 60 | /** 61 | * WebLinkingResponse can follow a link. 62 | * @throws Exception If something goes wrong inside 63 | */ 64 | @Test 65 | void followsLinksInHeaders() throws Exception { 66 | final WebLinkingResponse response = new WebLinkingResponse( 67 | new FakeRequest().withHeader( 68 | WebLinkingResponseTest.LINK, 69 | "; rel=\"first\", ; rel=\"second\"" 70 | ).uri().set(new URI("http://localhost/test")).back().fetch() 71 | ); 72 | MatcherAssert.assertThat( 73 | "should equals 'http://localhost/a'", 74 | response.follow("first").uri().get(), 75 | Matchers.equalTo(new URI("http://localhost/a")) 76 | ); 77 | MatcherAssert.assertThat( 78 | "should equals 'http://localhost/o'", 79 | response.follow("second").uri().get(), 80 | Matchers.equalTo(new URI("http://localhost/o")) 81 | ); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/response/XmlResponseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.response; 6 | 7 | import com.jcabi.http.Response; 8 | import com.jcabi.http.request.FakeRequest; 9 | import com.jcabi.xml.XML; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.hamcrest.MatcherAssert; 12 | import org.hamcrest.Matchers; 13 | import org.junit.jupiter.api.Test; 14 | 15 | /** 16 | * Test case for {@link XmlResponse}. 17 | * @since 1.1 18 | * @checkstyle ClassDataAbstractionCoupling (500 lines) 19 | */ 20 | @SuppressWarnings("PMD.TooManyMethods") 21 | final class XmlResponseTest { 22 | 23 | /** 24 | * XmlResponse can find nodes with XPath. 25 | * @throws Exception If something goes wrong inside 26 | */ 27 | @Test 28 | void findsDocumentNodesWithXpath() throws Exception { 29 | final XmlResponse response = new XmlResponse( 30 | new FakeRequest() 31 | .withBody("\u0443\u0440\u0430!B") 32 | .fetch() 33 | ); 34 | MatcherAssert.assertThat( 35 | "should be equal 2", 36 | response.xml().xpath("//a/text()"), 37 | Matchers.hasSize(2) 38 | ); 39 | MatcherAssert.assertThat( 40 | "should contains '\u0443\u0440\u0430'", 41 | response.xml().xpath("/r/a/text()"), 42 | Matchers.hasItem("\u0443\u0440\u0430!") 43 | ); 44 | } 45 | 46 | /** 47 | * XmlResponse can assert with XPath. 48 | * @throws Exception If something goes wrong inside 49 | */ 50 | @Test 51 | void assertsWithXpath() throws Exception { 52 | final Response resp = new FakeRequest() 53 | .withBody("\u0443\u0440\u0430!") 54 | .fetch(); 55 | new XmlResponse(resp) 56 | .assertXPath("//y[.='\u0443\u0440\u0430!']") 57 | .assertXPath("/x/@a") 58 | .assertXPath("/x/comment()") 59 | .assertXPath("/x/y[contains(.,'\u0430')]"); 60 | } 61 | 62 | /** 63 | * XmlResponse can assert with XPath and namespaces. 64 | * @throws Exception If something goes wrong inside 65 | */ 66 | @Test 67 | void assertsWithXpathAndNamespaces() throws Exception { 68 | final Response resp = new FakeRequest().withBody( 69 | StringUtils.join( 70 | "", 71 | "

\u0443\u0440\u0430!
" 72 | ) 73 | ).fetch(); 74 | new XmlResponse(resp) 75 | .assertXPath("/xhtml:html/xhtml:div") 76 | .assertXPath("//xhtml:div[.='\u0443\u0440\u0430!']"); 77 | } 78 | 79 | /** 80 | * XmlResponse can assert with XPath with custom namespaces. 81 | * @throws Exception If something goes wrong inside 82 | */ 83 | @Test 84 | void assertsWithXpathWithCustomNamespace() throws Exception { 85 | final XmlResponse response = new XmlResponse( 86 | new FakeRequest() 87 | .withBody("yes!") 88 | .fetch() 89 | ).registerNs("foo", "urn:foo"); 90 | final XML xml = response.xml(); 91 | MatcherAssert.assertThat( 92 | "should be equal to 'yes!'", 93 | xml.xpath("//foo:b/text()").get(0), 94 | Matchers.equalTo("yes!") 95 | ); 96 | MatcherAssert.assertThat( 97 | "should be empty", 98 | xml.nodes("/foo:a/foo:b"), 99 | Matchers.not(Matchers.empty()) 100 | ); 101 | } 102 | 103 | /** 104 | * XmlResponse can find and return nodes with XPath. 105 | * @throws Exception If something goes wrong inside 106 | */ 107 | @Test 108 | void findsDocumentNodesWithXpathAndReturnsThem() throws Exception { 109 | final XmlResponse response = new XmlResponse( 110 | new FakeRequest() 111 | .withBody("12") 112 | .fetch() 113 | ); 114 | MatcherAssert.assertThat( 115 | "should be equal 2", 116 | response.xml().nodes("//a"), 117 | Matchers.hasSize(2) 118 | ); 119 | MatcherAssert.assertThat( 120 | "should be equal 1", 121 | response.xml().nodes("/root/a").get(0).xpath("x/text()").get(0), 122 | Matchers.equalTo("1") 123 | ); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/response/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Responses, tests. 8 | * 9 | * @since 0.10 10 | */ 11 | package com.jcabi.http.response; 12 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/AutoRedirectingWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.mock.MkAnswer; 8 | import com.jcabi.http.mock.MkContainer; 9 | import com.jcabi.http.mock.MkGrizzlyContainer; 10 | import com.jcabi.http.mock.MkQuery; 11 | import com.jcabi.http.request.JdkRequest; 12 | import com.jcabi.http.response.RestResponse; 13 | import jakarta.ws.rs.core.HttpHeaders; 14 | import org.apache.http.HttpStatus; 15 | import org.hamcrest.MatcherAssert; 16 | import org.hamcrest.Matchers; 17 | import org.junit.jupiter.api.Test; 18 | 19 | /** 20 | * Test case for {@link AutoRedirectingWire}. 21 | * 22 | * @since 1.7 23 | */ 24 | final class AutoRedirectingWireTest { 25 | 26 | /** 27 | * AutoRedirectingWire retries up to the specified number of times for 28 | * HTTP Status 3xx responses. 29 | * 30 | * @throws Exception If something goes wrong inside 31 | */ 32 | @Test 33 | void retriesForHttpRedirectStatus() throws Exception { 34 | final MkContainer container = new MkGrizzlyContainer().next( 35 | new MkAnswer.Simple(HttpStatus.SC_MOVED_TEMPORARILY, "") 36 | // @checkstyle MultipleStringLiteralsCheck (1 line) 37 | .withHeader(HttpHeaders.LOCATION, "/"), 38 | Matchers.any(MkQuery.class), 39 | Integer.MAX_VALUE 40 | ).start(); 41 | try { 42 | final int retries = 3; 43 | new JdkRequest(container.home()) 44 | .through(AutoRedirectingWire.class, retries) 45 | .fetch().as(RestResponse.class) 46 | .assertStatus(HttpStatus.SC_MOVED_TEMPORARILY); 47 | MatcherAssert.assertThat( 48 | "should retries 3 times", 49 | container.takeAll(Matchers.any(MkAnswer.class)), 50 | Matchers.iterableWithSize(retries) 51 | ); 52 | } finally { 53 | container.stop(); 54 | } 55 | } 56 | 57 | /** 58 | * AutoRedirectingWire will retry a few times and immediately return if 59 | * a valid response is obtained. 60 | * @throws Exception If something goes wrong inside 61 | */ 62 | @Test 63 | void returnsValidResponseAfterRetry() throws Exception { 64 | final String body = "success"; 65 | final MkContainer container = new MkGrizzlyContainer().next( 66 | new MkAnswer.Simple(HttpStatus.SC_MOVED_TEMPORARILY, "") 67 | .withHeader(HttpHeaders.LOCATION, "/"), 68 | Matchers.any(MkQuery.class), 69 | 2 70 | ).next(new MkAnswer.Simple(body)).start(); 71 | try { 72 | new JdkRequest(container.home()) 73 | .through(AutoRedirectingWire.class) 74 | .fetch().as(RestResponse.class) 75 | .assertBody(Matchers.is(body)) 76 | .assertStatus(HttpStatus.SC_OK); 77 | MatcherAssert.assertThat( 78 | "should retries 3 times", 79 | container.takeAll(Matchers.any(MkAnswer.class)), 80 | Matchers.iterableWithSize(3) 81 | ); 82 | } finally { 83 | container.stop(); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/BasicAuthWireITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.request.JdkRequest; 8 | import com.jcabi.http.response.XmlResponse; 9 | import java.io.IOException; 10 | import org.hamcrest.MatcherAssert; 11 | import org.hamcrest.Matchers; 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** 15 | * Integration case for {@link BasicAuthWire}. 16 | * 17 | * @since 1.17.4 18 | */ 19 | final class BasicAuthWireITCase { 20 | @Test 21 | void basicAuthWorks() throws IOException { 22 | final XmlResponse res = new JdkRequest( 23 | "https://User:Pass@authenticationtest.com/HTTPAuth/" 24 | ) 25 | .through(BasicAuthWire.class) 26 | .through(AutoRedirectingWire.class) 27 | .fetch() 28 | .as(XmlResponse.class); 29 | MatcherAssert.assertThat( 30 | "should be success", 31 | res.body(), 32 | Matchers.containsString("Success!") 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/CachingWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.google.common.cache.CacheBuilder; 8 | import com.google.common.cache.CacheLoader; 9 | import com.google.common.cache.LoadingCache; 10 | import com.jcabi.http.Request; 11 | import com.jcabi.http.Response; 12 | import com.jcabi.http.mock.MkAnswer; 13 | import com.jcabi.http.mock.MkContainer; 14 | import com.jcabi.http.mock.MkGrizzlyContainer; 15 | import com.jcabi.http.request.JdkRequest; 16 | import com.jcabi.http.response.RestResponse; 17 | import java.net.HttpURLConnection; 18 | import java.util.concurrent.Callable; 19 | import org.hamcrest.MatcherAssert; 20 | import org.hamcrest.Matchers; 21 | import org.junit.jupiter.api.Test; 22 | 23 | /** 24 | * Test case for {@link CachingWire}. 25 | * @since 1.0 26 | */ 27 | final class CachingWireTest { 28 | 29 | /** 30 | * CachingWire can cache GET requests. 31 | * @throws Exception If something goes wrong inside 32 | */ 33 | @Test 34 | void cachesGetRequest() throws Exception { 35 | final MkContainer container = new MkGrizzlyContainer().next( 36 | new MkAnswer.Simple("") 37 | ).start(); 38 | final Request req = new JdkRequest(container.home()) 39 | .through(CachingWire.class); 40 | for (int idx = 0; idx < 10; ++idx) { 41 | req.fetch().as(RestResponse.class) 42 | .assertStatus(HttpURLConnection.HTTP_OK); 43 | } 44 | container.stop(); 45 | MatcherAssert.assertThat("should be equal 1", container.queries(), Matchers.equalTo(1)); 46 | } 47 | 48 | /** 49 | * CachingWire can ignore PUT requests. 50 | * @throws Exception If something goes wrong inside 51 | */ 52 | @Test 53 | void ignoresPutRequest() throws Exception { 54 | final MkContainer container = new MkGrizzlyContainer() 55 | .next(new MkAnswer.Simple("")) 56 | .next(new MkAnswer.Simple("")) 57 | .start(); 58 | final Request req = new JdkRequest(container.home()) 59 | .through(CachingWire.class).method(Request.PUT); 60 | for (int idx = 0; idx < 2; ++idx) { 61 | req.fetch().as(RestResponse.class) 62 | .assertStatus(HttpURLConnection.HTTP_OK); 63 | } 64 | container.stop(); 65 | MatcherAssert.assertThat("should be equal 1", container.queries(), Matchers.equalTo(2)); 66 | } 67 | 68 | /** 69 | * CachingWire can flush on regular expression match. 70 | * @throws Exception If something goes wrong inside 71 | */ 72 | @Test 73 | void flushesOnRegularExpressionMatch() throws Exception { 74 | final MkContainer container = new MkGrizzlyContainer() 75 | .next(new MkAnswer.Simple("first response")) 76 | .next(new MkAnswer.Simple("second response")) 77 | .next(new MkAnswer.Simple("third response")) 78 | .start(); 79 | final Request req = new JdkRequest(container.home()) 80 | .through(CachingWire.class, "POST /flush\\?a=1"); 81 | req.fetch() 82 | .as(RestResponse.class) 83 | .assertBody(Matchers.containsString("first")); 84 | req.fetch() 85 | .as(RestResponse.class) 86 | .assertBody(Matchers.containsString("first re")); 87 | req.method(Request.POST).uri().path("flush") 88 | .queryParam("a", "1").back().fetch(); 89 | req.fetch() 90 | .as(RestResponse.class) 91 | .assertBody(Matchers.containsString("third")); 92 | container.stop(); 93 | MatcherAssert.assertThat( 94 | "should be equal 3", 95 | container.queries(), 96 | Matchers.equalTo(3) 97 | ); 98 | } 99 | 100 | /** 101 | * CachingWire can use custom cache. 102 | * @throws Exception If something goes wrong inside 103 | */ 104 | @Test 105 | void cachesGetRequestWithCustomCache() throws Exception { 106 | final MkContainer container = new MkGrizzlyContainer().next( 107 | new MkAnswer.Simple("") 108 | ).next( 109 | new MkAnswer.Simple(HttpURLConnection.HTTP_BAD_GATEWAY) 110 | ).start(); 111 | final LoadingCache, Response> cache = 112 | CacheBuilder 113 | .newBuilder() 114 | .build( 115 | new CacheLoader, Response>() { 116 | @Override 117 | public Response load(final Callable query) 118 | throws Exception { 119 | return query.call(); 120 | } 121 | } 122 | ); 123 | final Request req = new JdkRequest(container.home()) 124 | .through(CachingWire.class, cache); 125 | for (int idx = 0; idx < 10; ++idx) { 126 | req.fetch().as(RestResponse.class) 127 | .assertStatus(HttpURLConnection.HTTP_OK); 128 | } 129 | container.stop(); 130 | MatcherAssert.assertThat("should be equal 1", container.queries(), Matchers.equalTo(1)); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/CookieOptimizingWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.mock.MkAnswer; 8 | import com.jcabi.http.mock.MkContainer; 9 | import com.jcabi.http.mock.MkGrizzlyContainer; 10 | import com.jcabi.http.mock.MkQuery; 11 | import com.jcabi.http.request.JdkRequest; 12 | import com.jcabi.http.response.RestResponse; 13 | import jakarta.ws.rs.core.HttpHeaders; 14 | import java.net.HttpURLConnection; 15 | import org.hamcrest.MatcherAssert; 16 | import org.hamcrest.Matchers; 17 | import org.junit.jupiter.api.Test; 18 | 19 | /** 20 | * Test case for {@link CookieOptimizingWire}. 21 | * @since 1.0 22 | */ 23 | final class CookieOptimizingWireTest { 24 | 25 | /** 26 | * CookieOptimizingWire can transfer cookies. 27 | * @throws Exception If something goes wrong inside 28 | */ 29 | @Test 30 | void transfersCookiesOnFollow() throws Exception { 31 | final MkContainer container = new MkGrizzlyContainer().next( 32 | new MkAnswer.Simple("") 33 | .withHeader(HttpHeaders.SET_COOKIE, "beta=something; path=/") 34 | .withHeader(HttpHeaders.SET_COOKIE, "alpha=boom1; path=/") 35 | .withHeader(HttpHeaders.SET_COOKIE, "gamma=something; path=/") 36 | .withHeader(HttpHeaders.LOCATION, "/") 37 | ).next(new MkAnswer.Simple("")).start(); 38 | new JdkRequest(container.home()) 39 | .through(VerboseWire.class) 40 | .through(CookieOptimizingWire.class) 41 | .header(HttpHeaders.COOKIE, "alpha=boom5") 42 | .fetch() 43 | .as(RestResponse.class) 44 | .follow() 45 | .fetch().as(RestResponse.class) 46 | .assertStatus(HttpURLConnection.HTTP_OK); 47 | container.stop(); 48 | container.take(); 49 | final MkQuery query = container.take(); 50 | MatcherAssert.assertThat( 51 | "should be size 1", 52 | query.headers().get(HttpHeaders.COOKIE), 53 | Matchers.hasSize(1) 54 | ); 55 | MatcherAssert.assertThat( 56 | "should contains 3 items", 57 | query.headers(), 58 | Matchers.hasEntry( 59 | Matchers.equalTo(HttpHeaders.COOKIE), 60 | Matchers.everyItem( 61 | Matchers.allOf( 62 | Matchers.containsString("beta=something"), 63 | Matchers.containsString("gamma=something"), 64 | Matchers.containsString("alpha=boom1") 65 | ) 66 | ) 67 | ) 68 | ); 69 | } 70 | 71 | /** 72 | * CookieOptimizingWire can avoid transferring of empty cookies. 73 | * @throws Exception If something goes wrong inside 74 | */ 75 | @Test 76 | void avoidsTransferringOfEmptyCookiesOnFollow() throws Exception { 77 | final MkContainer container = new MkGrizzlyContainer().next( 78 | new MkAnswer.Simple("") 79 | .withHeader(HttpHeaders.SET_COOKIE, "first=A; path=/") 80 | .withHeader(HttpHeaders.SET_COOKIE, "second=; path=/") 81 | .withHeader(HttpHeaders.SET_COOKIE, "third=B; path=/") 82 | .withHeader(HttpHeaders.LOCATION, "/a") 83 | ).next(new MkAnswer.Simple("")).start(); 84 | new JdkRequest(container.home()) 85 | .through(VerboseWire.class) 86 | .through(CookieOptimizingWire.class) 87 | .header(HttpHeaders.COOKIE, "second=initial-value") 88 | .fetch() 89 | .as(RestResponse.class) 90 | .follow() 91 | .fetch() 92 | .as(RestResponse.class) 93 | .assertStatus(HttpURLConnection.HTTP_OK); 94 | container.stop(); 95 | container.take(); 96 | final MkQuery query = container.take(); 97 | MatcherAssert.assertThat( 98 | "should be size 1", 99 | query.headers().get(HttpHeaders.COOKIE), 100 | Matchers.hasSize(1) 101 | ); 102 | MatcherAssert.assertThat( 103 | "should contains 2 items & not contains 1 item", 104 | query.headers(), 105 | Matchers.hasEntry( 106 | Matchers.equalTo(HttpHeaders.COOKIE), 107 | Matchers.hasItem( 108 | Matchers.allOf( 109 | Matchers.containsString("first=A"), 110 | Matchers.containsString("third=B"), 111 | Matchers.not(Matchers.containsString("second")) 112 | ) 113 | ) 114 | ) 115 | ); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/ETagCachingWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.Request; 8 | import com.jcabi.http.mock.MkAnswer; 9 | import com.jcabi.http.mock.MkContainer; 10 | import com.jcabi.http.mock.MkGrizzlyContainer; 11 | import com.jcabi.http.request.JdkRequest; 12 | import com.jcabi.http.response.RestResponse; 13 | import jakarta.ws.rs.core.HttpHeaders; 14 | import java.io.IOException; 15 | import java.net.HttpURLConnection; 16 | import org.hamcrest.Matchers; 17 | import org.junit.jupiter.api.Test; 18 | 19 | /** 20 | * Test case for {@link ETagCachingWire}. 21 | * @since 2.0 22 | */ 23 | final class ETagCachingWireTest { 24 | 25 | /** 26 | * ETagCachingWire can take content from cache. 27 | * @throws IOException If something goes wrong inside 28 | */ 29 | @Test 30 | void takesContentFromCache() throws IOException { 31 | final String body = "sample content"; 32 | final MkContainer container = new MkGrizzlyContainer() 33 | .next( 34 | new MkAnswer.Simple(body) 35 | .withHeader(HttpHeaders.ETAG, "3e25") 36 | ) 37 | .next( 38 | new MkAnswer.Simple("") 39 | .withStatus(HttpURLConnection.HTTP_NOT_MODIFIED) 40 | ) 41 | .start(); 42 | final Request req = 43 | new JdkRequest(container.home()).through(ETagCachingWire.class); 44 | req 45 | .fetch() 46 | .as(RestResponse.class) 47 | .assertStatus(HttpURLConnection.HTTP_OK) 48 | .assertBody(Matchers.equalTo(body)); 49 | req 50 | .fetch() 51 | .as(RestResponse.class) 52 | .assertStatus(HttpURLConnection.HTTP_OK) 53 | .assertBody(Matchers.equalTo(body)); 54 | container.stop(); 55 | } 56 | 57 | /** 58 | * ETagCachingWire can detect content modification. 59 | * @throws IOException If something goes wrong inside 60 | */ 61 | @Test 62 | void detectsContentModification() throws IOException { 63 | final String before = "before change"; 64 | final String after = "after change"; 65 | final MkContainer container = new MkGrizzlyContainer() 66 | .next( 67 | new MkAnswer.Simple(before) 68 | .withHeader(HttpHeaders.ETAG, "3e26") 69 | ) 70 | .next( 71 | new MkAnswer.Simple(after) 72 | .withHeader(HttpHeaders.ETAG, "3e27") 73 | ) 74 | .start(); 75 | final Request req = 76 | new JdkRequest(container.home()) 77 | .through(ETagCachingWire.class); 78 | req 79 | .fetch() 80 | .as(RestResponse.class) 81 | .assertStatus(HttpURLConnection.HTTP_OK) 82 | .assertBody(Matchers.equalTo(before)); 83 | req 84 | .fetch() 85 | .as(RestResponse.class) 86 | .assertStatus(HttpURLConnection.HTTP_OK) 87 | .assertBody(Matchers.equalTo(after)); 88 | container.stop(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/FcWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.Request; 8 | import com.jcabi.http.mock.MkAnswer; 9 | import com.jcabi.http.mock.MkContainer; 10 | import com.jcabi.http.mock.MkGrizzlyContainer; 11 | import com.jcabi.http.request.JdkRequest; 12 | import com.jcabi.http.response.RestResponse; 13 | import java.net.HttpURLConnection; 14 | import org.hamcrest.MatcherAssert; 15 | import org.hamcrest.Matchers; 16 | import org.junit.jupiter.api.Test; 17 | 18 | /** 19 | * Test case for {@link FcWire}. 20 | * @since 1.0 21 | */ 22 | final class FcWireTest { 23 | 24 | /** 25 | * FileCachingWire can cache GET requests. 26 | * @throws Exception If something goes wrong inside 27 | */ 28 | @Test 29 | void cachesGetRequest() throws Exception { 30 | final MkContainer container = new MkGrizzlyContainer().next( 31 | new MkAnswer.Simple("") 32 | ).start(); 33 | final Request req = new JdkRequest(container.home()) 34 | .through(FcWire.class); 35 | for (int idx = 0; idx < 10; ++idx) { 36 | req.fetch().as(RestResponse.class) 37 | .assertStatus(HttpURLConnection.HTTP_OK); 38 | } 39 | container.stop(); 40 | MatcherAssert.assertThat("should be equal 1", container.queries(), Matchers.equalTo(1)); 41 | } 42 | 43 | /** 44 | * CachingWire can ignore PUT requests. 45 | * @throws Exception If something goes wrong inside 46 | */ 47 | @Test 48 | void ignoresPutRequest() throws Exception { 49 | final MkContainer container = new MkGrizzlyContainer() 50 | .next(new MkAnswer.Simple("")) 51 | .next(new MkAnswer.Simple("")) 52 | .start(); 53 | final Request req = new JdkRequest(container.home()) 54 | .through(FcWire.class).method(Request.PUT); 55 | for (int idx = 0; idx < 2; ++idx) { 56 | req.fetch().as(RestResponse.class) 57 | .assertStatus(HttpURLConnection.HTTP_OK); 58 | } 59 | container.stop(); 60 | MatcherAssert.assertThat("should be equal 2", container.queries(), Matchers.equalTo(2)); 61 | } 62 | 63 | /** 64 | * CachingWire can flush on regular expression match. 65 | * @throws Exception If something goes wrong inside 66 | */ 67 | @Test 68 | void flushesOnRegularExpressionMatch() throws Exception { 69 | final MkContainer container = new MkGrizzlyContainer() 70 | .next(new MkAnswer.Simple("first response")) 71 | .next(new MkAnswer.Simple("second response")) 72 | .next(new MkAnswer.Simple("third response")) 73 | .start(); 74 | final Request req = new JdkRequest(container.home()) 75 | .through(FcWire.class, "POST /flush\\?a=1"); 76 | req.fetch() 77 | .as(RestResponse.class) 78 | .assertBody(Matchers.containsString("first")); 79 | req.fetch() 80 | .as(RestResponse.class) 81 | .assertBody(Matchers.containsString("first re")); 82 | req.method(Request.POST).uri().path("flush") 83 | .queryParam("a", "1").back().fetch(); 84 | req.fetch() 85 | .as(RestResponse.class) 86 | .assertBody(Matchers.containsString("third")); 87 | container.stop(); 88 | MatcherAssert.assertThat( 89 | "should be equal 3", 90 | container.queries(), 91 | Matchers.equalTo(3) 92 | ); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/RetryWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import ch.qos.logback.classic.Logger; 8 | import ch.qos.logback.classic.spi.ILoggingEvent; 9 | import ch.qos.logback.core.read.ListAppender; 10 | import com.jcabi.http.mock.MkAnswer; 11 | import com.jcabi.http.mock.MkContainer; 12 | import com.jcabi.http.mock.MkGrizzlyContainer; 13 | import com.jcabi.http.request.JdkRequest; 14 | import com.jcabi.http.response.RestResponse; 15 | import jakarta.ws.rs.core.UriBuilder; 16 | import java.net.HttpURLConnection; 17 | import java.net.URI; 18 | import org.hamcrest.MatcherAssert; 19 | import org.hamcrest.Matchers; 20 | import org.junit.jupiter.api.Test; 21 | import org.slf4j.LoggerFactory; 22 | 23 | /** 24 | * Test case for {@link RetryWire}. 25 | * 26 | * @since 1.2 27 | */ 28 | final class RetryWireTest { 29 | 30 | /** 31 | * RetryWire can make a few requests before giving up. 32 | * 33 | * @throws Exception If something goes wrong inside 34 | */ 35 | @Test 36 | void makesMultipleRequests() throws Exception { 37 | final MkContainer container = new MkGrizzlyContainer() 38 | .next(new MkAnswer.Simple(HttpURLConnection.HTTP_INTERNAL_ERROR)) 39 | .next(new MkAnswer.Simple(HttpURLConnection.HTTP_INTERNAL_ERROR)) 40 | .next(new MkAnswer.Simple(HttpURLConnection.HTTP_OK)) 41 | .start(); 42 | new JdkRequest(container.home()) 43 | .through(RetryWire.class) 44 | .fetch() 45 | .as(RestResponse.class) 46 | .assertStatus(HttpURLConnection.HTTP_OK); 47 | container.stop(); 48 | } 49 | 50 | /** 51 | * RetryWire should strip user info when logging URL. 52 | * 53 | * @throws Exception If something goes wrong inside 54 | */ 55 | @Test 56 | void stripsUserInfoWhenLogging() throws Exception { 57 | final Logger logger = (Logger) LoggerFactory.getLogger(RetryWire.class); 58 | final ListAppender appender = new ListAppender<>(); 59 | appender.start(); 60 | logger.addAppender(appender); 61 | try (MkContainer container = new MkGrizzlyContainer() 62 | .next(new MkAnswer.Simple(HttpURLConnection.HTTP_INTERNAL_ERROR)) 63 | .next(new MkAnswer.Simple(HttpURLConnection.HTTP_OK)) 64 | .start()) { 65 | final URI home = container.home(); 66 | new JdkRequest(UriBuilder.fromUri(home).userInfo("jeff:ffej").toString()) 67 | .through(RetryWire.class) 68 | .fetch() 69 | .as(RestResponse.class) 70 | .assertStatus(HttpURLConnection.HTTP_OK); 71 | MatcherAssert.assertThat( 72 | "should strips user info", 73 | appender.list, 74 | Matchers.hasItem( 75 | Matchers.hasProperty( 76 | "message", 77 | Matchers.containsString( 78 | String.format("GET %s (auth: j***j)", home) 79 | ) 80 | ) 81 | ) 82 | ); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/TrustedWireITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.request.JdkRequest; 8 | import com.jcabi.http.response.RestResponse; 9 | import java.net.HttpURLConnection; 10 | import org.junit.jupiter.params.ParameterizedTest; 11 | import org.junit.jupiter.params.provider.CsvSource; 12 | 13 | /** 14 | * Integration case for {@link TrustedWire}. 15 | * 16 | * @since 1.10.1 17 | */ 18 | final class TrustedWireITCase { 19 | 20 | /** 21 | * TrustedWire can ignore SSL verifications. 22 | * @param url URL with SSL problems 23 | * @throws Exception If something goes wrong inside 24 | */ 25 | @ParameterizedTest 26 | @CsvSource({ 27 | "https://expired.badssl.com/", 28 | "https://self-signed.badssl.com/", 29 | "https://untrusted-root.badssl.com/" 30 | }) 31 | void ignoresSslCertProblems(final String url) throws Exception { 32 | new JdkRequest(url) 33 | .through(TrustedWire.class) 34 | .fetch() 35 | .as(RestResponse.class) 36 | .assertStatus(HttpURLConnection.HTTP_OK); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/TrustedWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.mock.MkAnswer; 8 | import com.jcabi.http.mock.MkContainer; 9 | import com.jcabi.http.mock.MkGrizzlyContainer; 10 | import com.jcabi.http.request.JdkRequest; 11 | import com.jcabi.http.response.RestResponse; 12 | import java.net.HttpURLConnection; 13 | import org.junit.jupiter.api.Test; 14 | 15 | /** 16 | * Test case for {@link TrustedWire}. 17 | * @since 1.10 18 | */ 19 | final class TrustedWireTest { 20 | 21 | /** 22 | * TrustedWire can ignore PKIX errors. 23 | * @throws Exception If something goes wrong inside 24 | */ 25 | @Test 26 | void ignoresPkixErrors() throws Exception { 27 | final MkContainer container = new MkGrizzlyContainer().next( 28 | new MkAnswer.Simple("") 29 | ).start(); 30 | try { 31 | new JdkRequest(container.home()) 32 | .through(TrustedWire.class) 33 | .fetch() 34 | .as(RestResponse.class) 35 | .assertStatus(HttpURLConnection.HTTP_OK); 36 | } finally { 37 | container.stop(); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/UserAgentWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.mock.MkAnswer; 8 | import com.jcabi.http.mock.MkContainer; 9 | import com.jcabi.http.mock.MkGrizzlyContainer; 10 | import com.jcabi.http.request.JdkRequest; 11 | import com.jcabi.http.response.RestResponse; 12 | import jakarta.ws.rs.core.HttpHeaders; 13 | import java.net.HttpURLConnection; 14 | import org.hamcrest.MatcherAssert; 15 | import org.hamcrest.Matchers; 16 | import org.junit.jupiter.api.Test; 17 | 18 | /** 19 | * Test case for {@link UserAgentWire}. 20 | * @since 1.2 21 | */ 22 | final class UserAgentWireTest { 23 | 24 | @Test 25 | void addsDefaultUserAgentHeader() throws Exception { 26 | final MkContainer container = new MkGrizzlyContainer().next( 27 | new MkAnswer.Simple("") 28 | ).start(); 29 | new JdkRequest(container.home()) 30 | .through(UserAgentWire.class) 31 | .fetch() 32 | .as(RestResponse.class) 33 | .assertStatus(HttpURLConnection.HTTP_OK); 34 | container.stop(); 35 | MatcherAssert.assertThat( 36 | "must add default User-Agent HTTP header", 37 | container.take().headers(), 38 | Matchers.hasEntry( 39 | Matchers.is(HttpHeaders.USER_AGENT), 40 | Matchers.contains( 41 | Matchers.startsWith("jcabi-") 42 | ) 43 | ) 44 | ); 45 | } 46 | 47 | @Test 48 | void addsCustomUserAgentHeader() throws Exception { 49 | final MkContainer container = new MkGrizzlyContainer().next( 50 | new MkAnswer.Simple("") 51 | ).start(); 52 | final String agent = "Mozilla/5.0"; 53 | new JdkRequest(container.home()) 54 | .through(UserAgentWire.class, agent) 55 | .fetch() 56 | .as(RestResponse.class) 57 | .assertStatus(HttpURLConnection.HTTP_OK); 58 | container.stop(); 59 | MatcherAssert.assertThat( 60 | "must add custom User-Agent HTTP header", 61 | container.take().headers(), 62 | Matchers.hasEntry( 63 | Matchers.is(HttpHeaders.USER_AGENT), 64 | Matchers.contains(agent) 65 | ) 66 | ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/VerboseWireTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.http.wire; 6 | 7 | import com.jcabi.http.Request; 8 | import com.jcabi.http.mock.MkAnswer; 9 | import com.jcabi.http.mock.MkContainer; 10 | import com.jcabi.http.mock.MkGrizzlyContainer; 11 | import com.jcabi.http.mock.MkQuery; 12 | import com.jcabi.http.request.JdkRequest; 13 | import com.jcabi.http.response.RestResponse; 14 | import jakarta.ws.rs.core.HttpHeaders; 15 | import java.net.HttpURLConnection; 16 | import org.hamcrest.MatcherAssert; 17 | import org.hamcrest.Matchers; 18 | import org.junit.jupiter.api.Test; 19 | 20 | /** 21 | * Test case for {@link VerboseWire}. 22 | * @since 1.0 23 | */ 24 | final class VerboseWireTest { 25 | 26 | /** 27 | * VerboseWire can log requests. 28 | * @throws Exception If something goes wrong inside 29 | */ 30 | @Test 31 | void logsRequest() throws Exception { 32 | final MkContainer container = new MkGrizzlyContainer().next( 33 | new MkAnswer.Simple("") 34 | ).start(); 35 | new JdkRequest(container.home()) 36 | .through(VerboseWire.class) 37 | .header(HttpHeaders.USER_AGENT, "it's me") 38 | .fetch() 39 | .as(RestResponse.class) 40 | .assertStatus(HttpURLConnection.HTTP_OK); 41 | container.stop(); 42 | } 43 | 44 | /** 45 | * VerboseWire can log request body. 46 | * @throws Exception If something goes wrong inside 47 | */ 48 | @Test 49 | void logsRequestBody() throws Exception { 50 | final MkContainer container = new MkGrizzlyContainer().next( 51 | new MkAnswer.Simple("") 52 | ).start(); 53 | try { 54 | new JdkRequest(container.home()) 55 | .through(VerboseWire.class) 56 | .method(Request.POST) 57 | .body().set("hello, world!").back() 58 | .fetch() 59 | .as(RestResponse.class) 60 | .assertStatus(HttpURLConnection.HTTP_OK); 61 | final MkQuery query = container.take(); 62 | MatcherAssert.assertThat( 63 | "should starts with 'hello,'", 64 | query.body(), 65 | Matchers.startsWith("hello,") 66 | ); 67 | } finally { 68 | container.stop(); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/http/wire/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Wires, tests. 8 | * 9 | * @since 0.10 10 | */ 11 | package com.jcabi.http.wire; 12 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Set root logger level to DEBUG and its only appender to CONSOLE 5 | log4j.rootLogger=WARN, CONSOLE 6 | 7 | # "Console" is set to be a ConsoleAppender. 8 | log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 9 | log4j.appender.CONSOLE.layout=com.jcabi.log.MulticolorLayout 10 | log4j.appender.CONSOLE.layout.ConversionPattern=[%color{%p}] %t %c: %m%n 11 | 12 | # Application-specific logging 13 | log4j.logger.com.jcabi.http=DEBUG 14 | --------------------------------------------------------------------------------