├── src ├── site │ ├── resources │ │ └── CNAME │ ├── apt │ │ ├── broken-links.apt.vm │ │ ├── regex-matchers.apt.vm │ │ ├── xhtml-matchers.apt.vm │ │ ├── w3c-matchers.apt.vm │ │ ├── index.apt.vm │ │ └── jaxb-converter.apt.vm │ └── site.xml ├── test │ ├── java │ │ └── com │ │ │ └── jcabi │ │ │ └── matchers │ │ │ ├── package-info.java │ │ │ ├── NoBrokenLinksITCase.java │ │ │ ├── StringSourceTest.java │ │ │ ├── W3CMatchersTest.java │ │ │ ├── RegexMatchersTest.java │ │ │ ├── NoBrokenLinksTest.java │ │ │ ├── JaxbConverterTest.java │ │ │ └── XhtmlMatchersTest.java │ └── resources │ │ └── log4j.properties └── main │ ├── resources │ └── com │ │ └── jcabi │ │ └── matchers │ │ └── jaxb.properties │ └── java │ └── com │ └── jcabi │ └── matchers │ ├── package-info.java │ ├── RegexMatchingPatternMatcher.java │ ├── RegexContainingPatternMatcher.java │ ├── W3CMatchers.java │ ├── XPathMatcher.java │ ├── W3CValidatorMatcher.java │ ├── AllOfThatPrintsOnlyWrongMatchers.java │ ├── StringSource.java │ ├── RegexMatchers.java │ ├── NoBrokenLinks.java │ ├── JaxbConverter.java │ └── XhtmlMatchers.java ├── renovate.json ├── .gitignore ├── .pdd ├── .0pdd.yml ├── .gitattributes ├── .github ├── yamllint.yml └── workflows │ ├── reuse.yml │ ├── typos.yml │ ├── xcop.yml │ ├── pdd.yml │ ├── yamllint.yml │ ├── markdown-lint.yml │ ├── actionlint.yml │ ├── copyrights.yml │ ├── up.yml │ ├── codecov.yml │ └── mvn.yml ├── REUSE.toml ├── .rultor.yml ├── LICENSE.txt ├── LICENSES └── MIT.txt ├── README.md └── pom.xml /src/site/resources/CNAME: -------------------------------------------------------------------------------- 1 | matchers.jcabi.com 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.0pdd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | errors: 5 | - yegor256@gmail.com 6 | tags: 7 | - pdd 8 | - bug 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Hamcrest matchers. 8 | * 9 | * @since 1.0 10 | */ 11 | package com.jcabi.matchers; 12 | -------------------------------------------------------------------------------- /src/main/resources/com/jcabi/matchers/jaxb.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | # JAXB 2.0 reference implementation from com.sun.xml.bind:jaxb-impl:2.2.* 5 | javax.xml.bind.context.factory=com.sun.xml.bind.v2.ContextFactory 6 | -------------------------------------------------------------------------------- /.github/yamllint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | extends: default 5 | rules: 6 | line-length: disable 7 | brackets: 8 | min-spaces-inside: 1 9 | max-spaces-inside: 2 10 | truthy: 11 | level: warning 12 | allowed-values: [ 'on', 'true', 'false', 'yes', 'no' ] 13 | -------------------------------------------------------------------------------- /.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@v5 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@v5 19 | - uses: crate-ci/typos@v1.35.4 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@v5 19 | - uses: g4s8/xcop-action@master 20 | -------------------------------------------------------------------------------- /.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@v5 19 | - uses: volodya-lombrozo/pdd-action@master 20 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | log4j.rootLogger=WARN, CONSOLE 5 | log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 6 | log4j.appender.CONSOLE.layout=com.jcabi.log.MulticolorLayout 7 | log4j.appender.CONSOLE.layout.ConversionPattern=[%color{%p}] %t %c: %m%n 8 | 9 | # Application-specific logging 10 | log4j.logger.com.jcabi.w3c=DEBUG 11 | log4j.logger.com.jcabi.matchers=DEBUG 12 | -------------------------------------------------------------------------------- /.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@v5 19 | - uses: ibiqlik/action-yamllint@v3 20 | with: 21 | config_file: .github/yamllint.yml 22 | -------------------------------------------------------------------------------- /.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 | concurrency: 14 | group: markdown-lint-${{ github.ref }} 15 | cancel-in-progress: true 16 | jobs: 17 | markdown-lint: 18 | timeout-minutes: 15 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - uses: actions/checkout@v5 22 | - uses: DavidAnson/markdownlint-cli2-action@v20.0.0 23 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Hamcrest matchers. 8 | * 9 | *

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

<dependency>
13 |  *   <groupId>com.jcabi</groupId>
14 |  *   <artifactId>jcabi-matchers</artifactId>
15 |  * </dependency>
16 | * 17 | * @since 1.0 18 | * @see project website 19 | */ 20 | package com.jcabi.matchers; 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@v5 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/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 | pull_request: 9 | jobs: 10 | copyrights: 11 | timeout-minutes: 15 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - uses: actions/checkout@v5 15 | - uses: yegor256/copyrights-action@0.0.12 16 | with: 17 | globs: >- 18 | **/LICENSE.txt 19 | **/*.sh 20 | **/*.yml 21 | **/*.yaml 22 | **/*.eo 23 | **/*.xmir 24 | **/*.xml 25 | **/*.xsl 26 | **/*.xsd 27 | **/*.ini 28 | **/*.java 29 | **/*.g4 30 | **/*.properties 31 | **/*.groovy 32 | -------------------------------------------------------------------------------- /.github/workflows/up.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: up 6 | 'on': 7 | push: 8 | jobs: 9 | up: 10 | timeout-minutes: 15 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - uses: actions/checkout@v5 14 | - run: |- 15 | git fetch --tags --force && \ 16 | latest=$(git tag --sort=creatordate | tail -1) && \ 17 | sed -E -i "s/[^<]+/${latest}/g" README.md 18 | - uses: peter-evans/create-pull-request@v7 19 | with: 20 | sign-commits: true 21 | commit-message: 'new version in README' 22 | delete-branch: true 23 | title: 'New version in README' 24 | assignees: yegor256 25 | branch: up 26 | base: master 27 | -------------------------------------------------------------------------------- /.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@v5 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 | token: ${{ secrets.CODECOV_TOKEN }} 30 | files: ./target/site/jacoco/jacoco.xml 31 | fail_ci_if_error: true 32 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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, 21 ] 21 | steps: 22 | - uses: actions/checkout@v5 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 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/NoBrokenLinksITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.http.request.FakeRequest; 8 | import java.net.URI; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.hamcrest.MatcherAssert; 11 | import org.hamcrest.Matchers; 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** 15 | * Integration case for {@link NoBrokenLinks}. 16 | * @since 0.1 17 | */ 18 | final class NoBrokenLinksITCase { 19 | 20 | @Test 21 | void findsBrokenLinksInHtml() throws Exception { 22 | MatcherAssert.assertThat( 23 | "should finds the broken links", 24 | new FakeRequest().withBody( 25 | StringUtils.join( 26 | "", 27 | "", 28 | "" 29 | ) 30 | ).fetch(), 31 | Matchers.not( 32 | new NoBrokenLinks(new URI("http://www.google.com/")) 33 | ) 34 | ); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/RegexMatchingPatternMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | import org.hamcrest.Description; 11 | import org.hamcrest.TypeSafeMatcher; 12 | 13 | /** 14 | * Matcher of Regex patterns against a String, similar to 15 | * {@link String#matches(String)}. 16 | * 17 | *

Objects of this class are immutable and thread-safe. 18 | * 19 | * @since 1.3 20 | */ 21 | @Immutable 22 | @ToString 23 | @EqualsAndHashCode(callSuper = false, of = "pattern") 24 | final class RegexMatchingPatternMatcher extends TypeSafeMatcher { 25 | 26 | /** 27 | * The Regex pattern. 28 | */ 29 | private final transient String pattern; 30 | 31 | /** 32 | * Public ctor. 33 | * @param regex The regular expression to match against. 34 | */ 35 | RegexMatchingPatternMatcher(final String regex) { 36 | super(); 37 | this.pattern = regex; 38 | } 39 | 40 | @Override 41 | public void describeTo(final Description description) { 42 | description.appendText("a String matching the regular expression ") 43 | .appendText(this.pattern); 44 | } 45 | 46 | @Override 47 | public boolean matchesSafely(final String item) { 48 | return item.matches(this.pattern); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/RegexContainingPatternMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import java.util.regex.Pattern; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | import org.hamcrest.Description; 11 | import org.hamcrest.TypeSafeMatcher; 12 | 13 | /** 14 | * Checks if a given string contains a subsequence matching the given pattern, 15 | * similar to {@link java.util.regex.Matcher#find()}. 16 | * 17 | *

Objects of this class are immutable and thread-safe. 18 | * 19 | * @since 1.3 20 | */ 21 | @ToString 22 | @EqualsAndHashCode(callSuper = false, of = "pattern") 23 | final class RegexContainingPatternMatcher extends TypeSafeMatcher { 24 | 25 | /** 26 | * The Regex pattern. 27 | */ 28 | private final transient Pattern pattern; 29 | 30 | /** 31 | * Public ctor. 32 | * @param regex The regular expression to match against. 33 | */ 34 | RegexContainingPatternMatcher(final String regex) { 35 | super(); 36 | this.pattern = Pattern.compile(regex); 37 | } 38 | 39 | @Override 40 | public void describeTo(final Description description) { 41 | description.appendText("a String containing the regular expression ") 42 | .appendText(this.pattern.toString()); 43 | } 44 | 45 | @Override 46 | public boolean matchesSafely(final String item) { 47 | return this.pattern.matcher(item).find(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/W3CMatchers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.w3c.ValidatorBuilder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | import org.hamcrest.Matcher; 11 | 12 | /** 13 | * Matchers for validating HTML and CSS content. 14 | * 15 | * @since 0.1 16 | */ 17 | @ToString 18 | @EqualsAndHashCode 19 | @SuppressWarnings("PMD.ProhibitPublicStaticMethods") 20 | public final class W3CMatchers { 21 | 22 | /** 23 | * Default HTML online validator. 24 | */ 25 | private static final W3CValidatorMatcher HTML = 26 | new W3CValidatorMatcher(ValidatorBuilder.HTML); 27 | 28 | /** 29 | * Default online CSS validator. 30 | */ 31 | private static final W3CValidatorMatcher CSS = 32 | new W3CValidatorMatcher(ValidatorBuilder.CSS); 33 | 34 | /** 35 | * Private ctor, it's a utility class. 36 | */ 37 | private W3CMatchers() { 38 | // intentionally empty 39 | } 40 | 41 | /** 42 | * Matcher for validating HTML content against W3C validation servers. 43 | * @return Matcher for validating HTML content. 44 | */ 45 | public static Matcher validHtml() { 46 | return W3CMatchers.HTML; 47 | } 48 | 49 | /** 50 | * Matcher for validating CSS content against W3C validation servers. 51 | * @return Matcher for validating CSS content. 52 | */ 53 | public static Matcher validCss() { 54 | return W3CMatchers.CSS; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/XPathMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.xml.XMLDocument; 8 | import javax.xml.namespace.NamespaceContext; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | import org.hamcrest.Description; 12 | import org.hamcrest.TypeSafeMatcher; 13 | 14 | /** 15 | * Matcher of XPath against a plain string. 16 | * 17 | *

Objects of this class are immutable and thread-safe. 18 | * 19 | * @param Type of param 20 | * @since 0.3.7 21 | */ 22 | @ToString 23 | @EqualsAndHashCode(callSuper = false, of = "xpath") 24 | public final class XPathMatcher extends TypeSafeMatcher { 25 | 26 | /** 27 | * The XPath to use. 28 | */ 29 | private final transient String xpath; 30 | 31 | /** 32 | * The context to use. 33 | */ 34 | private final transient NamespaceContext context; 35 | 36 | /** 37 | * Public ctor. 38 | * @param query The query 39 | * @param ctx The context 40 | */ 41 | public XPathMatcher(final String query, final NamespaceContext ctx) { 42 | super(); 43 | this.xpath = query; 44 | this.context = ctx; 45 | } 46 | 47 | @Override 48 | public boolean matchesSafely(final T input) { 49 | return !new XMLDocument(XhtmlMatchers.xhtml(input)) 50 | .merge(this.context) 51 | .nodes(this.xpath) 52 | .isEmpty(); 53 | } 54 | 55 | @Override 56 | public void describeTo(final Description description) { 57 | description.appendText("an XML document with XPath ") 58 | .appendText(this.xpath); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/W3CValidatorMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.aspects.Immutable; 8 | import com.jcabi.log.Logger; 9 | import com.jcabi.w3c.Validator; 10 | import java.io.IOException; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.ToString; 13 | import org.hamcrest.Description; 14 | import org.hamcrest.TypeSafeMatcher; 15 | 16 | /** 17 | * Matcher for checking HTML and CSS documents against W3C validation services. 18 | * 19 | *

Objects of this class are immutable and thread-safe. 20 | * 21 | * @since 0.1 22 | */ 23 | @Immutable 24 | @ToString 25 | @EqualsAndHashCode(callSuper = false, of = "validator") 26 | final class W3CValidatorMatcher extends TypeSafeMatcher { 27 | 28 | /** 29 | * The W3C Validator. 30 | */ 31 | private final transient Validator validator; 32 | 33 | /** 34 | * Ctor. 35 | * @param val The Validator to use. 36 | */ 37 | W3CValidatorMatcher(final Validator val) { 38 | super(); 39 | this.validator = val; 40 | } 41 | 42 | @Override 43 | public void describeTo(final Description description) { 44 | description.appendText("W3C validator") 45 | .appendText(this.validator.toString()); 46 | } 47 | 48 | @Override 49 | public boolean matchesSafely(final String content) { 50 | boolean matches = false; 51 | try { 52 | matches = this.validator.validate(content).valid(); 53 | } catch (final IOException ex) { 54 | Logger.warn( 55 | this, 56 | "#matchesSafely('%s'): unable to perform validation: %s", 57 | content, ex.getMessage() 58 | ); 59 | } 60 | return matches; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/StringSourceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import javax.xml.parsers.DocumentBuilder; 9 | import javax.xml.parsers.DocumentBuilderFactory; 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 | import org.w3c.dom.Node; 15 | 16 | /** 17 | * Test case for {@link StringSource}. 18 | * @since 0.1 19 | */ 20 | final class StringSourceTest { 21 | 22 | @Test 23 | void formatsIncomingXmlDocument() { 24 | final String xml = "\u0443\u0440\u0430!"; 25 | MatcherAssert.assertThat( 26 | "should contains a string", 27 | new StringSource(xml).toString(), 28 | Matchers.containsString("ƻ") 29 | ); 30 | } 31 | 32 | @Test 33 | void formatIncomingNode() throws Exception { 34 | final DocumentBuilder builder = DocumentBuilderFactory 35 | .newInstance() 36 | .newDocumentBuilder(); 37 | final String xml = StringUtils.join( 38 | "", 39 | "withText" 40 | ); 41 | final Node node = builder.parse( 42 | new ByteArrayInputStream(xml.getBytes()) 43 | ); 44 | MatcherAssert.assertThat( 45 | "should equals to the node", 46 | new StringSource(node).toString(), 47 | Matchers.equalTo( 48 | StringUtils.join( 49 | "", 50 | "\n ", 51 | "withText\n \n \n", 52 | "\n" 53 | ) 54 | ) 55 | ); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/W3CMatchersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.aspects.RetryOnFailure; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.hamcrest.MatcherAssert; 10 | import org.hamcrest.Matchers; 11 | import org.junit.jupiter.api.Disabled; 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** 15 | * Test case for {@link W3CMatchers}. 16 | * @since 0.1 17 | */ 18 | final class W3CMatchersTest { 19 | 20 | @Test 21 | @RetryOnFailure(verbose = false) 22 | void matchesValidHtml() { 23 | MatcherAssert.assertThat( 24 | "should matches valid html", 25 | StringUtils.join( 26 | "", 27 | "", 28 | "hey", 29 | "" 30 | ), 31 | W3CMatchers.validHtml() 32 | ); 33 | } 34 | 35 | @Test 36 | @RetryOnFailure(verbose = false) 37 | void rejectsInvalidHtml() { 38 | MatcherAssert.assertThat( 39 | "should matches non valid html", 40 | "", 41 | Matchers.not(W3CMatchers.validHtml()) 42 | ); 43 | } 44 | 45 | @Test 46 | @RetryOnFailure(verbose = false) 47 | void matchesValidCss() { 48 | MatcherAssert.assertThat( 49 | "should matches valid css", 50 | "body { background-color:#d0e4fe; }", 51 | W3CMatchers.validCss() 52 | ); 53 | } 54 | 55 | @Test 56 | @Disabled("I have no idea why it doesn't work") 57 | @RetryOnFailure(verbose = false) 58 | void rejectsInvalidCss() { 59 | MatcherAssert.assertThat( 60 | "should matches non valid css", 61 | "div { -- $#^@*&^$&@; }", 62 | Matchers.not(W3CMatchers.validCss()) 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/AllOfThatPrintsOnlyWrongMatchers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import org.hamcrest.Description; 10 | import org.hamcrest.DiagnosingMatcher; 11 | import org.hamcrest.Matcher; 12 | import org.hamcrest.core.AllOf; 13 | 14 | /** 15 | * Matcher that test if all matchers matches, but print info about only that 16 | * ones who failed. 17 | * 18 | * Based in {@link AllOf}. 19 | * 20 | * @param Type of argument 21 | * @since 0.2.6 22 | */ 23 | final class AllOfThatPrintsOnlyWrongMatchers extends DiagnosingMatcher { 24 | 25 | /** 26 | * Matchers that will be tested. 27 | */ 28 | private final transient Iterable> matchers; 29 | 30 | /** 31 | * Matchers that does not matches. 32 | */ 33 | private final transient List> wrong; 34 | 35 | /** 36 | * Construct that accept matchers to test. 37 | * @param iterable Matchers that will be tested. 38 | */ 39 | AllOfThatPrintsOnlyWrongMatchers( 40 | final Iterable> iterable 41 | ) { 42 | super(); 43 | this.matchers = iterable; 44 | this.wrong = new ArrayList<>(3); 45 | } 46 | 47 | @Override 48 | public void describeTo(final Description description) { 49 | description.appendList("(", ",", ")", this.wrong); 50 | } 51 | 52 | @Override 53 | public boolean matches(final Object obj, final Description mismatch) { 54 | boolean matches = true; 55 | for (final Matcher matcher : this.matchers) { 56 | if (!matcher.matches(obj)) { 57 | mismatch.appendDescriptionOf(matcher).appendText(" "); 58 | matcher.describeMismatch(obj, mismatch); 59 | this.wrong.add(matcher); 60 | matches = false; 61 | } 62 | } 63 | return matches; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/RegexMatchersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import org.hamcrest.MatcherAssert; 8 | import org.hamcrest.Matchers; 9 | import org.junit.jupiter.api.Test; 10 | 11 | /** 12 | * Test case for {@link RegexMatchers}. 13 | * @since 1.3 14 | */ 15 | final class RegexMatchersTest { 16 | 17 | @Test 18 | void matchesStringToPatterns() { 19 | MatcherAssert.assertThat( 20 | "should matches any pattern", 21 | "zxc456", 22 | RegexMatchers.matchesAnyPattern("[zxc]+\\d{3}", "[abc]+") 23 | ); 24 | } 25 | 26 | @Test 27 | void matchesStringToPattern() { 28 | MatcherAssert.assertThat( 29 | "should matches all of patterns", 30 | "abc123", 31 | Matchers.allOf( 32 | RegexMatchers.matchesPattern("[a-c]+\\d{3}"), 33 | Matchers.not(RegexMatchers.matchesPattern("[d-f]+\\d{4}")) 34 | ) 35 | ); 36 | } 37 | 38 | @Test 39 | void checksIfStringContainsPattern() { 40 | MatcherAssert.assertThat( 41 | "should matches all of patterns", 42 | "aardvark", 43 | Matchers.allOf( 44 | RegexMatchers.containsPattern("rdva"), 45 | Matchers.not(RegexMatchers.matchesPattern("foo")) 46 | ) 47 | ); 48 | } 49 | 50 | @Test 51 | void checksIfStringContainsAnyPattern() { 52 | MatcherAssert.assertThat( 53 | "should matches all of patterns", 54 | "awrjbvjkb", 55 | Matchers.allOf( 56 | RegexMatchers.containsAnyPattern("aa", "bb", "jbv"), 57 | Matchers.not(RegexMatchers.containsAnyPattern("cc", "dd")) 58 | ) 59 | ); 60 | } 61 | 62 | @Test 63 | void checksIfStringContainsAllPatterns() { 64 | MatcherAssert.assertThat( 65 | "should matches all of patterns", 66 | "asjbclkjbxhui", 67 | Matchers.allOf( 68 | RegexMatchers.containsAllPatterns("asj", "lkj", "jbx"), 69 | Matchers.not(RegexMatchers.containsAllPatterns("bcl", "ff")) 70 | ) 71 | ); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Few Matchers for Hamcrest 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-matchers)](https://www.rultor.com/p/jcabi/jcabi-matchers) 5 | 6 | [![mvn](https://github.com/jcabi/jcabi-matchers/actions/workflows/mvn.yml/badge.svg)](https://github.com/jcabi/jcabi-matchers/actions/workflows/mvn.yml) 7 | [![PDD status](https://www.0pdd.com/svg?name=jcabi/jcabi-matchers)](https://www.0pdd.com/p?name=jcabi/jcabi-matchers) 8 | [![Javadoc](https://javadoc.io/badge/com.jcabi/jcabi-matchers.svg)](https://www.javadoc.io/doc/com.jcabi/jcabi-matchers) 9 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.jcabi/jcabi-matchers/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.jcabi/jcabi-matchers) 10 | [![codecov](https://codecov.io/gh/jcabi/jcabi-matchers/branch/master/graph/badge.svg)](https://codecov.io/gh/jcabi/jcabi-matchers) 11 | 12 | More details are here: 13 | [matchers.jcabi.com](http://matchers.jcabi.com/index.html). 14 | Also, read this blog post: [XML/XPath Matchers for Hamcrest][blog]. 15 | 16 | First, you add this to your `pom.xml`: 17 | 18 | ```xml 19 | 20 | com.jcabi 21 | jcabi-matchers 22 | 1.8.0 23 | 24 | ``` 25 | 26 | The library contains a collection of convenient Hamcrest matchers: 27 | 28 | ```java 29 | import com.jcabi.matchers.XhtmlMatchers; 30 | import org.hamcrest.MatcherAssert; 31 | 32 | MatcherAssert.assertThat( 33 | "Jeff", 34 | XhtmlMatchers.hasXPath("/test/name[.='Jeff']") 35 | ); 36 | ``` 37 | 38 | To match XHTML documents you need to specify namespaces in your 39 | XPath expressions: 40 | 41 | ```java 42 | MatcherAssert.assertThat( 43 | "Hello, world!", 44 | XhtmlMatchers.hasXPath("/xhtml:html/xhtml:body[.='Hello, world!']") 45 | ); 46 | ``` 47 | 48 | Here, we use `xhtml` predefined namespace. There are also 49 | `xsl`, `xs`, `xsi`, and `svg` namespaces 50 | provided off-the-shelf. However, you can define your own too, for example: 51 | 52 | ```java 53 | MatcherAssert.assertThat( 54 | "", 55 | XhtmlMatchers.hasXPath("/ns1:foo/ns1:bar", "my-own-namespace") 56 | ); 57 | ``` 58 | 59 | Here, `my-own-namespace` is called `ns1` inside the XPath expression. 60 | 61 | ## How to contribute? 62 | 63 | Fork the repository, make changes, submit a pull request. 64 | We promise to review your changes same day and apply to 65 | the `master` branch, if they look correct. 66 | 67 | Please run Maven build before submitting a pull request: 68 | 69 | ```bash 70 | mvn clean install -Pqulice 71 | ``` 72 | 73 | [blog]: http://www.yegor256.com/2014/04/28/xml-xpath-hamcrest-matchers.html 74 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/NoBrokenLinksTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.http.request.FakeRequest; 8 | import java.net.URI; 9 | import org.apache.commons.lang3.StringUtils; 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 | 15 | /** 16 | * Test case for {@link NoBrokenLinks}. 17 | * @since 0.1 18 | */ 19 | final class NoBrokenLinksTest { 20 | 21 | @Test 22 | void findsEmptyLinksInHtml() throws Exception { 23 | MatcherAssert.assertThat( 24 | "should finds the empty links", 25 | new FakeRequest().withBody( 26 | StringUtils.join( 27 | "", 28 | "" 29 | ) 30 | ).fetch(), 31 | Matchers.not( 32 | new NoBrokenLinks(new URI("http://www.facebook.com/")) 33 | ) 34 | ); 35 | } 36 | 37 | @Test 38 | void findsBrLinksInHtmlWithNamespace() throws Exception { 39 | MatcherAssert.assertThat( 40 | "should finds the broken links", 41 | new FakeRequest().withBody( 42 | StringUtils.join( 43 | "", 44 | "", 45 | " " 46 | ) 47 | ).fetch(), 48 | Matchers.not( 49 | new NoBrokenLinks(new URI("http://google.com")) 50 | ) 51 | ); 52 | } 53 | 54 | @Test 55 | void passesWithoutBrokenLinks() throws Exception { 56 | MatcherAssert.assertThat( 57 | "should pass without broken links", 58 | new FakeRequest().withBody( 59 | StringUtils.join( 60 | "", 61 | "", 62 | "", 63 | "" 64 | ) 65 | ).fetch(), 66 | new NoBrokenLinks(new URI("http://www.teamed.io/")) 67 | ); 68 | } 69 | 70 | @Test 71 | void throwsWhenHtmlIsBroken() { 72 | Assertions.assertThrows( 73 | IllegalArgumentException.class, 74 | () -> 75 | MatcherAssert.assertThat( 76 | "should matches valid html", 77 | new FakeRequest().withBody("not HTML at all").fetch(), 78 | new NoBrokenLinks(new URI("#")) 79 | ) 80 | ); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/StringSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.xml.XMLDocument; 8 | import java.io.StringWriter; 9 | import java.util.Locale; 10 | import javax.xml.transform.OutputKeys; 11 | import javax.xml.transform.Transformer; 12 | import javax.xml.transform.TransformerException; 13 | import javax.xml.transform.TransformerFactory; 14 | import javax.xml.transform.dom.DOMSource; 15 | import javax.xml.transform.stream.StreamResult; 16 | import lombok.EqualsAndHashCode; 17 | import org.w3c.dom.Node; 18 | 19 | /** 20 | * Private class for DOM to String converting. 21 | * 22 | *

Objects of this class are immutable and thread-safe. 23 | * 24 | * @since 0.1 25 | */ 26 | @EqualsAndHashCode(callSuper = false, of = "xml") 27 | @SuppressWarnings( 28 | { 29 | "PMD.OnlyOneConstructorShouldDoInitialization", 30 | "PMD.ConstructorOnlyInitializesOrCallOtherConstructors" 31 | } 32 | ) 33 | final class StringSource extends DOMSource { 34 | 35 | /** 36 | * The XML itself. 37 | */ 38 | private final transient String xml; 39 | 40 | /** 41 | * Public ctor. 42 | * @param text The content of the document 43 | */ 44 | StringSource(final String text) { 45 | super(); 46 | this.xml = text; 47 | super.setNode(new XMLDocument(text).deepCopy()); 48 | } 49 | 50 | /** 51 | * Public ctor. 52 | * @param node The node 53 | */ 54 | StringSource(final Node node) { 55 | super(); 56 | final StringWriter writer = new StringWriter(); 57 | try { 58 | final Transformer transformer = 59 | TransformerFactory.newInstance().newTransformer(); 60 | final String yes = "yes"; 61 | transformer.setOutputProperty( 62 | OutputKeys.OMIT_XML_DECLARATION, yes 63 | ); 64 | transformer.setOutputProperty(OutputKeys.INDENT, yes); 65 | transformer.transform( 66 | new DOMSource(node), 67 | new StreamResult(writer) 68 | ); 69 | } catch (final TransformerException ex) { 70 | throw new IllegalStateException(ex); 71 | } 72 | this.xml = writer.toString(); 73 | this.setNode(node); 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | final StringBuilder buf = new StringBuilder(); 79 | final int length = this.xml.length(); 80 | for (int pos = 0; pos < length; ++pos) { 81 | final char chr = this.xml.charAt(pos); 82 | // @checkstyle MagicNumber (1 line) 83 | if (chr > 0x7f) { 84 | buf.append("&#").append( 85 | Integer.toHexString(chr).toUpperCase(Locale.ENGLISH) 86 | ).append(';'); 87 | } else { 88 | buf.append(chr); 89 | } 90 | } 91 | return buf.toString(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/site/apt/broken-links.apt.vm: -------------------------------------------------------------------------------- 1 | ------ 2 | Broken Links Matcher 3 | ------ 4 | Yegor Bugayenko 5 | ------ 6 | 2014-05-05 7 | ------ 8 | 9 | ~~ 10 | ~~ Copyright (c) 2014-2025 Yegor Bugayenko 11 | ~~ All rights reserved. 12 | ~~ 13 | ~~ Redistribution and use in source and binary forms, with or without 14 | ~~ modification, are permitted provided that the following conditions 15 | ~~ are met: 1) Redistributions of source code must retain the above 16 | ~~ copyright notice, this list of conditions and the following 17 | ~~ disclaimer. 2) Redistributions in binary form must reproduce the above 18 | ~~ copyright notice, this list of conditions and the following 19 | ~~ disclaimer in the documentation and/or other materials provided 20 | ~~ with the distribution. 3) Neither the name of the jcabi.com nor 21 | ~~ the names of its contributors may be used to endorse or promote 22 | ~~ products derived from this software without specific prior written 23 | ~~ permission. 24 | ~~ 25 | ~~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | ~~ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 27 | ~~ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | ~~ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 29 | ~~ THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 30 | ~~ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | ~~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | ~~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 33 | ~~ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 34 | ~~ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ~~ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 36 | ~~ OF THE POSSIBILITY OF SUCH DAMAGE. 37 | ~~ 38 | 39 | Broken Links Matchers 40 | 41 | {{{./apidocs-${project.version}/com/jcabi/matchers/NoBrokenLinks.html}<<>>}} 42 | is a Hamcrest matchers that checks your XML/HTML document 43 | and catches links that point to incorrect locations, for example: 44 | 45 | +-- 46 | import com.jcabi.matchers.NoBrokenLinks; 47 | import org.hamcrest.MatcherAssert; 48 | public class FooTest { 49 | @Test 50 | public void htmlHasNoBrokenLinks() { 51 | MatcherAssert.assertThat( 52 | "hey", 53 | new NoBrokenLinks() 54 | ); 55 | } 56 | } 57 | +-- 58 | 59 | These dependencies you will need in your <<>>: 60 | 61 | +-- 62 | 63 | org.hamcrest 64 | hamcrest-library 65 | 1.3 66 | test 67 | 68 | 69 | org.hamcrest 70 | hamcrest-core 71 | 1.3 72 | test 73 | 74 | 75 | com.jcabi 76 | jcabi-http 77 | 1.0 78 | test 79 | 80 | 81 | com.sun.jersey 82 | jersey-client 83 | 1.18.1 84 | test 85 | 86 | +-- 87 | -------------------------------------------------------------------------------- /src/site/apt/regex-matchers.apt.vm: -------------------------------------------------------------------------------- 1 | ------ 2 | Regular Expression Hamcrest Matchers 3 | ------ 4 | Carlos Miranda 5 | ------ 6 | 2014-10-07 7 | ------ 8 | 9 | ~~ 10 | ~~ Copyright (c) 2014-2025 Yegor Bugayenko 11 | ~~ All rights reserved. 12 | ~~ 13 | ~~ Redistribution and use in source and binary forms, with or without 14 | ~~ modification, are permitted provided that the following conditions 15 | ~~ are met: 1) Redistributions of source code must retain the above 16 | ~~ copyright notice, this list of conditions and the following 17 | ~~ disclaimer. 2) Redistributions in binary form must reproduce the above 18 | ~~ copyright notice, this list of conditions and the following 19 | ~~ disclaimer in the documentation and/or other materials provided 20 | ~~ with the distribution. 3) Neither the name of the jcabi.com nor 21 | ~~ the names of its contributors may be used to endorse or promote 22 | ~~ products derived from this software without specific prior written 23 | ~~ permission. 24 | ~~ 25 | ~~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | ~~ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 27 | ~~ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | ~~ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 29 | ~~ THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 30 | ~~ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | ~~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | ~~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 33 | ~~ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 34 | ~~ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ~~ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 36 | ~~ OF THE POSSIBILITY OF SUCH DAMAGE. 37 | ~~ 38 | 39 | Regular Expression Hamcrest Matchers 40 | 41 | {{{./apidocs-${project.version}/com/jcabi/matchers/RegexMatchers.html}<<>>}} 42 | is a utility class with static methods that create Hamcrest matchers that can 43 | check Strings against regular expressions, as used in unit tests. 44 | 45 | You can check if an entire string matches a certain regular expression in the 46 | following manner: 47 | 48 | +-- 49 | import com.jcabi.matchers.RegexMatchers; 50 | import org.hamcrest.MatcherAssert; 51 | public class FooTest { 52 | @Test 53 | public void stringMatchesPattern() { 54 | MatcherAssert.assert( 55 | "abc123", 56 | RegexMatchers.matchesPattern("[a-c]+\\d{3}") 57 | ); 58 | } 59 | } 60 | +-- 61 | 62 | You may also check if a string contains a substring matching a regular 63 | expression, as shown below: 64 | 65 | +-- 66 | import com.jcabi.matchers.RegexMatchers; 67 | import org.hamcrest.MatcherAssert; 68 | public class FooTest { 69 | @Test 70 | public void stringContainsPattern() { 71 | MatcherAssert.assert( 72 | "foobar456", 73 | RegexMatchers.containsPattern("ar45") 74 | ); 75 | } 76 | } 77 | +-- 78 | 79 | These dependencies you will need in your <<>>: 80 | 81 | +-- 82 | 83 | org.hamcrest 84 | hamcrest-library 85 | 1.3 86 | test 87 | 88 | 89 | org.hamcrest 90 | hamcrest-core 91 | 1.3 92 | test 93 | 94 | +-- 95 | -------------------------------------------------------------------------------- /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/logo-square.svg" 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 | -------------------------------------------------------------------------------- /src/site/apt/xhtml-matchers.apt.vm: -------------------------------------------------------------------------------- 1 | ------ 2 | XHTML Hamcrest Matchers 3 | ------ 4 | Yegor Bugayenko 5 | ------ 6 | 2014-04-27 7 | ------ 8 | 9 | ~~ 10 | ~~ Copyright (c) 2014-2025 Yegor Bugayenko 11 | ~~ All rights reserved. 12 | ~~ 13 | ~~ Redistribution and use in source and binary forms, with or without 14 | ~~ modification, are permitted provided that the following conditions 15 | ~~ are met: 1) Redistributions of source code must retain the above 16 | ~~ copyright notice, this list of conditions and the following 17 | ~~ disclaimer. 2) Redistributions in binary form must reproduce the above 18 | ~~ copyright notice, this list of conditions and the following 19 | ~~ disclaimer in the documentation and/or other materials provided 20 | ~~ with the distribution. 3) Neither the name of the jcabi.com nor 21 | ~~ the names of its contributors may be used to endorse or promote 22 | ~~ products derived from this software without specific prior written 23 | ~~ permission. 24 | ~~ 25 | ~~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | ~~ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 27 | ~~ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | ~~ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 29 | ~~ THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 30 | ~~ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | ~~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | ~~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 33 | ~~ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 34 | ~~ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ~~ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 36 | ~~ OF THE POSSIBILITY OF SUCH DAMAGE. 37 | ~~ 38 | 39 | XHTML Hamcrest Matchers 40 | 41 | {{{./apidocs-${project.version}/com/jcabi/matchers/XhtmlMatchers.html}<<>>}} 42 | is a utility class with static methods that create 43 | XHTML Hamcrest matchers, to be used in unit tests, for example: 44 | 45 | +-- 46 | import com.jcabi.matchers.XhtmlMatchers; 47 | import org.hamcrest.MatcherAssert; 48 | public class FooTest { 49 | @Test 50 | public void buildsValidXml() { 51 | MatcherAssert.assertThat( 52 | "Jeff", 53 | XhtmlMatchers.hasXPath("/data/employee[@id=33]/name") 54 | ); 55 | } 56 | } 57 | +-- 58 | 59 | Besides that, there is a convenient static method that 60 | converts its input to XML, that correctly renders itself 61 | in unit test output: 62 | 63 | +-- 64 | import com.jcabi.matchers.XhtmlMatchers; 65 | import org.hamcrest.MatcherAssert; 66 | public class FooTest { 67 | @Test 68 | public void buildsValidXml() { 69 | Source source = // ... get it somewhere 70 | MatcherAssert.assertThat( 71 | XhtmlMatchers.xhtml(source), 72 | XhtmlMatchers.hasXPath("/data/employee[@id=33]/name") 73 | ); 74 | } 75 | } 76 | +-- 77 | 78 | These dependencies you will need in your <<>>: 79 | 80 | +-- 81 | 82 | org.hamcrest 83 | hamcrest-library 84 | 1.3 85 | test 86 | 87 | 88 | org.hamcrest 89 | hamcrest-core 90 | 1.3 91 | test 92 | 93 | 94 | com.jcabi 95 | jcabi-xml 96 | 0.7.8 97 | test 98 | 99 | +-- 100 | -------------------------------------------------------------------------------- /src/site/apt/w3c-matchers.apt.vm: -------------------------------------------------------------------------------- 1 | ------ 2 | W3C Hamcrest Matchers 3 | ------ 4 | Yegor Bugayenko 5 | ------ 6 | 2014-05-05 7 | ------ 8 | 9 | ~~ 10 | ~~ Copyright (c) 2014-2025 Yegor Bugayenko 11 | ~~ All rights reserved. 12 | ~~ 13 | ~~ Redistribution and use in source and binary forms, with or without 14 | ~~ modification, are permitted provided that the following conditions 15 | ~~ are met: 1) Redistributions of source code must retain the above 16 | ~~ copyright notice, this list of conditions and the following 17 | ~~ disclaimer. 2) Redistributions in binary form must reproduce the above 18 | ~~ copyright notice, this list of conditions and the following 19 | ~~ disclaimer in the documentation and/or other materials provided 20 | ~~ with the distribution. 3) Neither the name of the jcabi.com nor 21 | ~~ the names of its contributors may be used to endorse or promote 22 | ~~ products derived from this software without specific prior written 23 | ~~ permission. 24 | ~~ 25 | ~~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | ~~ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 27 | ~~ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | ~~ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 29 | ~~ THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 30 | ~~ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | ~~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | ~~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 33 | ~~ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 34 | ~~ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ~~ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 36 | ~~ OF THE POSSIBILITY OF SUCH DAMAGE. 37 | ~~ 38 | 39 | W3C Hamcrest Matchers 40 | 41 | {{{./apidocs-${project.version}/com/jcabi/matchers/W3CMatchers.html}<<>>}} 42 | is a utility class with static methods that create 43 | W3C Hamcrest matchers, to be used in unit tests, for example: 44 | 45 | +-- 46 | import com.jcabi.matchers.W3CMatchers; 47 | import org.hamcrest.MatcherAssert; 48 | public class FooTest { 49 | @Test 50 | public void htmlIsValidAccordingToW3C() { 51 | MatcherAssert.assertThat( 52 | "Hello", 53 | W3CMatchers.validHtml() 54 | ); 55 | } 56 | } 57 | +-- 58 | 59 | The same for CSS: 60 | 61 | +-- 62 | import com.jcabi.matchers.W3CMatchers; 63 | import org.hamcrest.MatcherAssert; 64 | public class FooTest { 65 | @Test 66 | public void htmlIsValidAccordingToW3C() { 67 | MatcherAssert.assertThat( 68 | "body { color: red; }", 69 | W3CMatchers.validCss() 70 | ); 71 | } 72 | } 73 | +-- 74 | 75 | These dependencies you will need in your <<>>: 76 | 77 | +-- 78 | 79 | org.hamcrest 80 | hamcrest-library 81 | 1.3 82 | test 83 | 84 | 85 | org.hamcrest 86 | hamcrest-core 87 | 1.3 88 | test 89 | 90 | 91 | com.jcabi 92 | jcabi-w3c 93 | 1.0 94 | test 95 | 96 | 97 | com.jcabi 98 | jcabi-w3c 99 | 1.0 100 | test 101 | 102 | 103 | org.glassfish 104 | javax.json 105 | 1.0.4 106 | test 107 | 108 | +-- 109 | -------------------------------------------------------------------------------- /src/site/apt/index.apt.vm: -------------------------------------------------------------------------------- 1 | ------ 2 | Hamcrest Matchers 3 | ------ 4 | Yegor Bugayenko 5 | ------ 6 | 2014-04-27 7 | ------ 8 | 9 | ~~ 10 | ~~ Copyright (c) 2014-2025 Yegor Bugayenko 11 | ~~ All rights reserved. 12 | ~~ 13 | ~~ Redistribution and use in source and binary forms, with or without 14 | ~~ modification, are permitted provided that the following conditions 15 | ~~ are met: 1) Redistributions of source code must retain the above 16 | ~~ copyright notice, this list of conditions and the following 17 | ~~ disclaimer. 2) Redistributions in binary form must reproduce the above 18 | ~~ copyright notice, this list of conditions and the following 19 | ~~ disclaimer in the documentation and/or other materials provided 20 | ~~ with the distribution. 3) Neither the name of the jcabi.com nor 21 | ~~ the names of its contributors may be used to endorse or promote 22 | ~~ products derived from this software without specific prior written 23 | ~~ permission. 24 | ~~ 25 | ~~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | ~~ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 27 | ~~ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | ~~ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 29 | ~~ THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 30 | ~~ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | ~~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | ~~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 33 | ~~ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 34 | ~~ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ~~ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 36 | ~~ OF THE POSSIBILITY OF SUCH DAMAGE. 37 | ~~ 38 | 39 | Hamcrest Matchers 40 | 41 | This library contains a few convenient utils for Hamcrest matching, including: 42 | 43 | * {{{./xhtml-matchers.html}<<>>}} - 44 | matches XML and XHTML with XPath; 45 | 46 | * {{{./regex-matchers.html}<<>>}} - 47 | matches Strings with Regular Expressions; 48 | 49 | * {{{./jaxb-converter.html}<<>>}} - 50 | converts JAXB-annotated objects into XML. 51 | 52 | The only dependency you need is 53 | (you can also download 54 | {{{http://repo1.maven.org/maven2/com/jcabi/jcabi-matchers/${project.version}/jcabi-matchers-${project.version}.jar}<<>>}} 55 | and add it to the classpath): 56 | 57 | +-- 58 | 59 | com.jcabi 60 | jcabi-matchers 61 | ${project.version} 62 | 63 | +-- 64 | 65 | Don't forget to add these two dependencies to your classpath: 66 | 67 | +-- 68 | 69 | org.hamcrest 70 | hamcrest-core 71 | 1.3 72 | test 73 | 74 | 75 | org.hamcrest 76 | hamcrest-library 77 | 1.3 78 | test 79 | 80 | +-- 81 | 82 | * Cutting Edge Version 83 | 84 | If you want to use current version of the product, you can do it with 85 | this configuration in your <<>>: 86 | 87 | +-- 88 | 89 | 90 | oss.sonatype.org 91 | https://oss.sonatype.org/content/repositories/snapshots/ 92 | 93 | 94 | 95 | 96 | com.jcabi 97 | jcabi-matchers 98 | 2.0-SNAPSHOT 99 | 100 | 101 | +-- 102 | -------------------------------------------------------------------------------- /src/site/apt/jaxb-converter.apt.vm: -------------------------------------------------------------------------------- 1 | ------ 2 | JAXB Converter 3 | ------ 4 | Yegor Bugayenko 5 | ------ 6 | 2014-04-27 7 | ------ 8 | 9 | ~~ 10 | ~~ Copyright (c) 2014-2025 Yegor Bugayenko 11 | ~~ All rights reserved. 12 | ~~ 13 | ~~ Redistribution and use in source and binary forms, with or without 14 | ~~ modification, are permitted provided that the following conditions 15 | ~~ are met: 1) Redistributions of source code must retain the above 16 | ~~ copyright notice, this list of conditions and the following 17 | ~~ disclaimer. 2) Redistributions in binary form must reproduce the above 18 | ~~ copyright notice, this list of conditions and the following 19 | ~~ disclaimer in the documentation and/or other materials provided 20 | ~~ with the distribution. 3) Neither the name of the jcabi.com nor 21 | ~~ the names of its contributors may be used to endorse or promote 22 | ~~ products derived from this software without specific prior written 23 | ~~ permission. 24 | ~~ 25 | ~~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | ~~ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 27 | ~~ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | ~~ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 29 | ~~ THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 30 | ~~ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | ~~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | ~~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 33 | ~~ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 34 | ~~ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ~~ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 36 | ~~ OF THE POSSIBILITY OF SUCH DAMAGE. 37 | ~~ 38 | 39 | JAXB Converter 40 | 41 | The object has to be annotated with JAXB annotations 42 | in order to be convertible. 43 | Let's consider an example JAXB-annotated class: 44 | 45 | +-- 46 | import javax.xml.bind.annotation.XmlAccessType; 47 | import javax.xml.bind.annotation.XmlAccessorType; 48 | import javax.xml.bind.annotation.XmlElement; 49 | import javax.xml.bind.annotation.XmlRootElement; 50 | @XmlRootElement(name = "employee") 51 | @XmlAccessorType(XmlAccessType.NONE) 52 | public class Employee { 53 | @XmlElement(name = "name") 54 | public String getName() { 55 | return "John Doe"; 56 | } 57 | } 58 | +-- 59 | 60 | Now you want to test how it works with real data after conversion 61 | to XML (in a unit test): 62 | 63 | +-- 64 | import com.jcabi.matchers.JaxbConverter; 65 | import com.jcabi.matchers.XhtmlMatchers; 66 | import org.hamcrest.MatcherAssert; 67 | import org.junit.Test; 68 | public class EmployeeTest { 69 | @Test 70 | public void testObjectToXmlConversion() throws Exception { 71 | final Object object = new Employee(); 72 | MatcherAssert.assertThat( 73 | JaxbConverter.the(object), 74 | XhtmlMatchers.hasXPath("/employee/name[.='John Doe']") 75 | ); 76 | } 77 | } 78 | +-- 79 | 80 | These dependencies you will need in your <<>>: 81 | 82 | +-- 83 | 84 | org.hamcrest 85 | hamcrest-library 86 | 1.3 87 | test 88 | 89 | 90 | org.hamcrest 91 | hamcrest-core 92 | 1.3 93 | test 94 | 95 | 96 | com.jcabi 97 | jcabi-xml 98 | 0.7.8 99 | test 100 | 101 | 102 | javax.xml.bind 103 | jaxb-api 104 | 2.2.11 105 | provided 106 | 107 | +-- 108 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/RegexMatchers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | import org.hamcrest.CoreMatchers; 12 | import org.hamcrest.Matcher; 13 | 14 | /** 15 | * Convenient matchers for checking Strings against regular expressions. 16 | * 17 | * @since 1.3 18 | */ 19 | @ToString 20 | @EqualsAndHashCode 21 | @SuppressWarnings("PMD.ProhibitPublicStaticMethods") 22 | public final class RegexMatchers { 23 | 24 | /** 25 | * Private ctor, it's a utility class. 26 | */ 27 | private RegexMatchers() { 28 | // Utility class, shouldn't be instantiated. 29 | } 30 | 31 | /** 32 | * Checks whether a String matches at lease one of given regular 33 | * expressions. 34 | * @param patterns Regular expression patterns 35 | * @return Matcher suitable for JUnit/Hamcrest matching 36 | */ 37 | @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 38 | public static Matcher matchesAnyPattern(final String... patterns) { 39 | final Collection> matchers = 40 | new ArrayList<>(patterns.length); 41 | for (final String pattern : patterns) { 42 | matchers.add(new RegexMatchingPatternMatcher(pattern)); 43 | } 44 | return CoreMatchers.anyOf(matchers); 45 | } 46 | 47 | /** 48 | * Checks whether a String matches the given regular expression. Works in a 49 | * similar manner to {@link String#matches(String)}. For example: 50 | * 51 | *
 MatcherAssert.assert(
 52 |      *   "abc123",
 53 |      *   RegexMatchers.matchesPattern("[a-c]+\\d{3}")
 54 |      * );
55 | * 56 | * @param pattern The pattern to match against 57 | * @return Matcher suitable for JUnit/Hamcrest matching 58 | */ 59 | public static Matcher matchesPattern(final String pattern) { 60 | return new RegexMatchingPatternMatcher(pattern); 61 | } 62 | 63 | /** 64 | * Checks whether a String contains a subsequence matching the given regular 65 | * expression. Works in a similar manner to 66 | * {@link java.util.regex.Matcher#find()}. For example: 67 | * 68 | *
 MatcherAssert.assert(
 69 |      *   "fooBar123",
 70 |      *   RegexMatchers.containsPattern("Bar12")
 71 |      * );
72 | * 73 | * @param pattern The pattern to match against 74 | * @return Matcher suitable for JUnit/Hamcrest matching 75 | */ 76 | public static Matcher containsPattern(final String pattern) { 77 | return new RegexContainingPatternMatcher(pattern); 78 | } 79 | 80 | /** 81 | * Checks whether a {@link String} contains a subsequence matching any of 82 | * the given regular expressions. 83 | * @param patterns The patterns to match against 84 | * @return Matcher suitable for JUnit/Hamcrest matching 85 | * @see java.util.regex.Matcher#find() 86 | * @see #containsPattern(String) 87 | */ 88 | public static Matcher containsAnyPattern(final String... patterns) { 89 | return CoreMatchers 90 | .anyOf(createContainingMatchers(patterns)); 91 | } 92 | 93 | /** 94 | * Checks whether a {@link String} contains a subsequence matching any of 95 | * the given regular expressions. 96 | * @param patterns The patterns to match against 97 | * @return Matcher suitable for JUnit/Hamcrest matching 98 | * @see java.util.regex.Matcher#find() 99 | * @see #containsPattern(String) 100 | */ 101 | public static Matcher containsAllPatterns( 102 | final String... patterns) { 103 | return CoreMatchers 104 | .allOf(createContainingMatchers(patterns)); 105 | } 106 | 107 | /** 108 | * Creates a {@link Collection} of {@link Matcher}'s for the given patterns. 109 | * @param patterns The given patterns 110 | * @return A {@link Collection} of {@link Matcher}'s 111 | */ 112 | @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 113 | private static Collection> createContainingMatchers( 114 | final String... patterns) { 115 | final Collection> matchers = 116 | new ArrayList<>(patterns.length); 117 | for (final String pattern : patterns) { 118 | matchers.add(new RegexContainingPatternMatcher(pattern)); 119 | } 120 | return matchers; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/JaxbConverterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import javax.xml.bind.annotation.XmlAccessType; 8 | import javax.xml.bind.annotation.XmlAccessorType; 9 | import javax.xml.bind.annotation.XmlElement; 10 | import javax.xml.bind.annotation.XmlRootElement; 11 | import javax.xml.bind.annotation.XmlType; 12 | import org.hamcrest.MatcherAssert; 13 | import org.hamcrest.Matchers; 14 | import org.junit.jupiter.api.Test; 15 | 16 | /** 17 | * Test case for {@link JaxbConverter}. 18 | * @since 0.1 19 | */ 20 | final class JaxbConverterTest { 21 | 22 | @Test 23 | void convertsJaxbObjectToXml() throws Exception { 24 | final Object object = new JaxbConverterTest.Employee(); 25 | MatcherAssert.assertThat( 26 | "should has xpath", 27 | JaxbConverter.the(object), 28 | XhtmlMatchers.hasXPath("/employee/name[.='\u0443\u0440\u0430']") 29 | ); 30 | } 31 | 32 | @Test 33 | void convertsObjectToSourceRenderableAsText() throws Exception { 34 | final Object object = new JaxbConverterTest.Employee(); 35 | MatcherAssert.assertThat( 36 | "should contains a string", 37 | JaxbConverter.the(object).toString(), 38 | Matchers.containsString("ƻ") 39 | ); 40 | } 41 | 42 | @Test 43 | void convertsAnObjectThatHasOthersInjected() throws Exception { 44 | final JaxbConverterTest.Employee employee = 45 | new JaxbConverterTest.Employee(); 46 | employee.inject(new JaxbConverterTest.Foo()); 47 | MatcherAssert.assertThat( 48 | "should has xpath", 49 | JaxbConverter.the(employee, JaxbConverterTest.Foo.class), 50 | XhtmlMatchers.hasXPath( 51 | "/employee/injected/ns1:name", 52 | JaxbConverterTest.Foo.NAMESPACE 53 | ) 54 | ); 55 | } 56 | 57 | @Test 58 | void convertsNonRootObject() throws Exception { 59 | final Object object = new JaxbConverterTest.Bar(); 60 | MatcherAssert.assertThat( 61 | "should has xpath", 62 | JaxbConverter.the(object), 63 | XhtmlMatchers.hasXPath("/bar/name") 64 | ); 65 | } 66 | 67 | @Test 68 | void convertsNonRootObjectWithNamespace() throws Exception { 69 | final Object object = new JaxbConverterTest.Foo(); 70 | MatcherAssert.assertThat( 71 | "should has xpath", 72 | JaxbConverter.the(object), 73 | XhtmlMatchers.hasXPath( 74 | "/ns1:foo/ns1:name", JaxbConverterTest.Foo.NAMESPACE 75 | ) 76 | ); 77 | } 78 | 79 | /** 80 | * Dummy test object. 81 | * 82 | * @since 0.1 83 | */ 84 | @XmlRootElement(name = "employee") 85 | @XmlAccessorType(XmlAccessType.NONE) 86 | private static final class Employee { 87 | /** 88 | * Injected object. 89 | */ 90 | private transient Object injected = "some text"; 91 | 92 | /** 93 | * Inject an object. 94 | * @param obj The object to inject 95 | */ 96 | public void inject(final Object obj) { 97 | this.injected = obj; 98 | } 99 | 100 | /** 101 | * Injected object. This method is not used directly, but is used 102 | * during JAXB converting of this object into XML, at 103 | * {@link #convertsAnObjectThatHasOthersInjected()}. 104 | * @return The object 105 | */ 106 | @XmlElement 107 | public Object getInjected() { 108 | return this.injected; 109 | } 110 | 111 | /** 112 | * Returns a simple string. This method is not called directly, but 113 | * is used in {@code convertsJaxbObjectToXml()} for JAXB converting 114 | * of the object to XML. 115 | * @return The text 116 | */ 117 | @XmlElement(name = "name") 118 | public String getName() { 119 | return "\u0443\u0440\u0430"; 120 | } 121 | } 122 | 123 | /** 124 | * Dummy test object. 125 | * 126 | * @since 0.1 127 | */ 128 | @XmlType(name = "foo", namespace = JaxbConverterTest.Foo.NAMESPACE) 129 | @XmlAccessorType(XmlAccessType.NONE) 130 | public static final class Foo { 131 | /** 132 | * XML namespace. 133 | */ 134 | public static final String NAMESPACE = "foo-namespace"; 135 | 136 | /** 137 | * Simple name. 138 | * @return The name 139 | */ 140 | @XmlElement(namespace = JaxbConverterTest.Foo.NAMESPACE) 141 | public String getName() { 142 | return "Foo: \u0443\u0440\u0430"; 143 | } 144 | } 145 | 146 | /** 147 | * Dummy test object. 148 | * 149 | * @since 0.1 150 | */ 151 | @XmlType(name = "bar") 152 | @XmlAccessorType(XmlAccessType.NONE) 153 | public static final class Bar { 154 | /** 155 | * Simple name. 156 | * @return The name 157 | */ 158 | @XmlElement 159 | public String getName() { 160 | return "Bar: \u0443\u0440\u0430"; 161 | } 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/NoBrokenLinks.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.http.Response; 8 | import com.jcabi.http.response.XmlResponse; 9 | import com.jcabi.log.Logger; 10 | import java.io.IOException; 11 | import java.net.HttpURLConnection; 12 | import java.net.MalformedURLException; 13 | import java.net.URI; 14 | import java.net.URL; 15 | import java.util.Collection; 16 | import java.util.LinkedList; 17 | import lombok.EqualsAndHashCode; 18 | import lombok.ToString; 19 | import org.hamcrest.BaseMatcher; 20 | import org.hamcrest.Description; 21 | 22 | /** 23 | * Finds broken links in HTML. 24 | * 25 | * @since 0.3.4 26 | */ 27 | @ToString 28 | @EqualsAndHashCode(callSuper = false, of = "home") 29 | public final class NoBrokenLinks extends BaseMatcher { 30 | 31 | /** 32 | * Home page. 33 | */ 34 | private final transient URI home; 35 | 36 | /** 37 | * List of broken links. 38 | */ 39 | private final transient Collection broken; 40 | 41 | /** 42 | * Public ctor. 43 | * @param uri Home page URI, for relative links 44 | */ 45 | public NoBrokenLinks(final URI uri) { 46 | super(); 47 | this.home = uri; 48 | this.broken = new LinkedList<>(); 49 | } 50 | 51 | @Override 52 | public boolean matches(final Object item) { 53 | this.check(Response.class.cast(item)); 54 | return this.broken.isEmpty(); 55 | } 56 | 57 | @Override 58 | public void describeTo(final Description description) { 59 | description.appendText( 60 | Logger.format( 61 | "%d broken link(s) found: %[list]s", 62 | this.broken.size(), this.broken 63 | ) 64 | ); 65 | } 66 | 67 | /** 68 | * Check for validness. 69 | * @param response Response to check 70 | */ 71 | private void check(final Response response) { 72 | final Collection links = new XmlResponse(response).xml().xpath( 73 | new StringBuilder("//head/link/@href") 74 | .append(" | //body//a/@href") 75 | .append(" | //body//img/@src") 76 | .append(" | //xhtml:img/@src") 77 | .append(" | //xhtml:a/@href") 78 | .append(" | //xhtml:link/@href") 79 | .toString() 80 | ); 81 | Logger.debug( 82 | this, "#assertThat(): %d links found: %[list]s", 83 | links.size(), links 84 | ); 85 | this.broken.clear(); 86 | for (final String link : links) { 87 | final URI uri; 88 | if (link.isEmpty() || link.charAt(0) != '/') { 89 | uri = URI.create(link); 90 | } else { 91 | uri = this.home.resolve(link); 92 | } 93 | if (!uri.isAbsolute() || !NoBrokenLinks.isValid(uri)) { 94 | this.broken.add(uri); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Check whether the URI is valid and returns code 200. 101 | * @param uri The URI to check 102 | * @return TRUE if it's valid 103 | */ 104 | private static boolean isValid(final URI uri) { 105 | boolean valid = false; 106 | try { 107 | final int code = NoBrokenLinks.http(uri.toURL()); 108 | if (code < HttpURLConnection.HTTP_BAD_REQUEST) { 109 | valid = true; 110 | } else { 111 | Logger.warn( 112 | NoBrokenLinks.class, 113 | "#isValid('%s'): not valid since response code is %d", 114 | uri, code 115 | ); 116 | } 117 | } catch (final MalformedURLException ex) { 118 | Logger.warn( 119 | NoBrokenLinks.class, 120 | "#isValid('%s'): invalid URL: %s", 121 | uri, ex.getMessage() 122 | ); 123 | } 124 | return valid; 125 | } 126 | 127 | /** 128 | * Get HTTP response code from this URL. 129 | * @param url The URL to get 130 | * @return HTTP response code 131 | */ 132 | private static int http(final URL url) { 133 | int code = HttpURLConnection.HTTP_BAD_REQUEST; 134 | try { 135 | final HttpURLConnection conn = 136 | HttpURLConnection.class.cast(url.openConnection()); 137 | try { 138 | code = conn.getResponseCode(); 139 | Logger.debug( 140 | NoBrokenLinks.class, 141 | "#http('%s'): response code is %s", 142 | url, code 143 | ); 144 | } catch (final IOException ex) { 145 | Logger.warn( 146 | NoBrokenLinks.class, 147 | "#http('%s'): can't get response code: %s", 148 | url, ex.getMessage() 149 | ); 150 | } finally { 151 | conn.disconnect(); 152 | } 153 | } catch (final IOException ex) { 154 | Logger.warn( 155 | NoBrokenLinks.class, 156 | "#http('%s'): can't open connection: %s", 157 | url, ex.getMessage() 158 | ); 159 | } 160 | return code; 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/JaxbConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import java.io.StringWriter; 8 | import javax.xml.bind.JAXBContext; 9 | import javax.xml.bind.JAXBElement; 10 | import javax.xml.bind.JAXBException; 11 | import javax.xml.bind.JAXBIntrospector; 12 | import javax.xml.bind.Marshaller; 13 | import javax.xml.bind.annotation.XmlElement; 14 | import javax.xml.bind.annotation.XmlType; 15 | import javax.xml.namespace.QName; 16 | import javax.xml.transform.Source; 17 | import lombok.EqualsAndHashCode; 18 | import lombok.ToString; 19 | 20 | /** 21 | * JAXB-empowered object to XML converting utility. 22 | * 23 | *

The object has to be annotated with JAXB annotations 24 | * in order to be convertible. Let's consider an example JAXB-annotated class: 25 | * 26 | *

 import javax.xml.bind.annotation.XmlAccessType;
 27 |  * import javax.xml.bind.annotation.XmlAccessorType;
 28 |  * import javax.xml.bind.annotation.XmlElement;
 29 |  * import javax.xml.bind.annotation.XmlRootElement;
 30 |  * @XmlRootElement(name = "employee")
 31 |  * @XmlAccessorType(XmlAccessType.NONE)
 32 |  * public class Employee {
 33 |  *   @XmlElement(name = "name")
 34 |  *   public String getName() {
 35 |  *     return "John Doe";
 36 |  *   }
 37 |  * }
38 | * 39 | *

Now you want to test how it works with real data after conversion 40 | * to XML (in a unit test): 41 | * 42 | *

 import com.jcabi.matchers.JaxbConverter;
 43 |  * import com.jcabi.matchers.XhtmlMatchers;
 44 |  * import org.junit.Assert;
 45 |  * import org.junit.Test;
 46 |  * public final class EmployeeTest {
 47 |  *   @Test
 48 |  *   public void testObjectToXmlConversion() throws Exception {
 49 |  *     final Object object = new Employee();
 50 |  *     Assert.assertThat(
 51 |  *       JaxbConverter.the(object),
 52 |  *       XhtmlMatchers.hasXPath("/employee/name[.='John Doe']")
 53 |  *     );
 54 |  *   }
 55 |  * }
56 | * 57 | * @since 0.1 58 | */ 59 | @ToString 60 | @EqualsAndHashCode 61 | @SuppressWarnings("PMD.ProhibitPublicStaticMethods") 62 | public final class JaxbConverter { 63 | 64 | /** 65 | * Private ctor, to avoid direct instantiation of the class. 66 | */ 67 | private JaxbConverter() { 68 | // intentionally empty 69 | } 70 | 71 | /** 72 | * Convert an object to XML. 73 | * 74 | *

The method will throw {@link AssertionError} if marshalling of 75 | * provided object fails for some reason. 76 | * 77 | *

The name of the method is motivated by 78 | * xmlatchers project 79 | * and their {@code XmlMatchers.the(String)} method. Looks like this name 80 | * is short enough and convenient for unit tests. 81 | * 82 | * @param object The object to convert 83 | * @param deps Dependencies that we should take into account 84 | * @return DOM source/document 85 | * @throws JAXBException If an exception occurs inside 86 | */ 87 | public static Source the(final Object object, final Class... deps) 88 | throws JAXBException { 89 | final Class[] classes = new Class[deps.length + 1]; 90 | classes[0] = object.getClass(); 91 | System.arraycopy(deps, 0, classes, 1, deps.length); 92 | final JAXBContext ctx; 93 | try { 94 | ctx = JAXBContext.newInstance(classes); 95 | } catch (final JAXBException ex) { 96 | throw new IllegalArgumentException(ex); 97 | } 98 | final JAXBIntrospector intro = ctx.createJAXBIntrospector(); 99 | Object subject = object; 100 | if (intro.getElementName(object) == null) { 101 | @SuppressWarnings("unchecked") 102 | final Class type = (Class) object.getClass(); 103 | subject = new JAXBElement( 104 | JaxbConverter.qname(object), 105 | type, 106 | object 107 | ); 108 | } 109 | final Marshaller mrsh = JaxbConverter.marshaller(ctx); 110 | final StringWriter writer = new StringWriter(); 111 | try { 112 | mrsh.marshal(subject, writer); 113 | } catch (final JAXBException ex) { 114 | throw new AssertionError(ex); 115 | } 116 | final String xml = writer.toString(); 117 | return new StringSource(xml); 118 | } 119 | 120 | /** 121 | * Create marshaller. 122 | * @param ctx The context 123 | * @return Marshaller 124 | * @throws JAXBException If an exception occurs inside 125 | */ 126 | private static Marshaller marshaller(final JAXBContext ctx) 127 | throws JAXBException { 128 | final Marshaller mrsh = ctx.createMarshaller(); 129 | mrsh.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); 130 | return mrsh; 131 | } 132 | 133 | /** 134 | * Get type name, if XmlType annotation is present (exception otherwise). 135 | * @param obj The object 136 | * @return Qualified name 137 | * @see XmlElement#namespace() 138 | */ 139 | private static QName qname(final Object obj) { 140 | final XmlType type = XmlType.class.cast( 141 | obj.getClass().getAnnotation(XmlType.class) 142 | ); 143 | if (type == null) { 144 | throw new AssertionError( 145 | String.format( 146 | // @checkstyle LineLength (1 line) 147 | "@XmlType or @XmlRootElement annotation required at %s", 148 | obj.getClass().getName() 149 | ) 150 | ); 151 | } 152 | final QName qname; 153 | if ("##default".equals(type.namespace())) { 154 | qname = new QName(type.name()); 155 | } else { 156 | qname = new QName(type.namespace(), type.name()); 157 | } 158 | return qname; 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/com/jcabi/matchers/XhtmlMatchers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import com.jcabi.xml.XPathContext; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.io.Reader; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.Arrays; 14 | import java.util.Collection; 15 | import java.util.LinkedList; 16 | import java.util.Scanner; 17 | import javax.xml.namespace.NamespaceContext; 18 | import javax.xml.transform.Source; 19 | import lombok.EqualsAndHashCode; 20 | import lombok.ToString; 21 | import org.hamcrest.Matcher; 22 | import org.w3c.dom.Node; 23 | 24 | /** 25 | * Convenient set of matchers for XHTML/XML content. 26 | * 27 | *

For example: 28 | * 29 | *

 MatcherAssert.assertThat(
 30 |  *   "<root><a>hello</a></root>",
 31 |  *   XhtmlMatchers.hasXPath("/root/a[.='hello']")
 32 |  * );
33 | * 34 | * @since 0.2.6 35 | */ 36 | @ToString 37 | @EqualsAndHashCode 38 | @SuppressWarnings("PMD.ProhibitPublicStaticMethods") 39 | public final class XhtmlMatchers { 40 | 41 | /** 42 | * Private ctor, it's a utility class. 43 | */ 44 | private XhtmlMatchers() { 45 | // intentionally empty 46 | } 47 | 48 | /** 49 | * Makes XHTML source presentable for testing. 50 | * 51 | *

Useful method for assertions in unit tests. For example: 52 | * 53 | *

 MatcherAssert.assertThat(
 54 |      *   XhtmlMatchers.xhtml(dom_xml_element),
 55 |      *   XhtmlMatchers.hasXPath("/root/data")
 56 |      * );
57 | * 58 | *

The method understands different input types differently. For example, 59 | * an {@link InputStream} will be read as a UTF-8 document, {@link Reader} 60 | * will be read as a document, a {@link Source} will be used "as is", 61 | * {@link Node} will be printed as a text, etc. The goal is to make any 62 | * input type presentable as an XML document, as much as it is possible. 63 | * 64 | * @param xhtml The source of data 65 | * @param Type of source 66 | * @return Renderable source 67 | * @since 0.4.10 68 | */ 69 | public static Source xhtml(final T xhtml) { 70 | final Source source; 71 | if (xhtml instanceof Source) { 72 | source = (Source) xhtml; 73 | } else if (xhtml instanceof InputStream) { 74 | try (InputStream stream = (InputStream) xhtml) { 75 | source = new StringSource( 76 | readAsString(new InputStreamReader(stream, StandardCharsets.UTF_8)) 77 | ); 78 | } catch (final IOException ex) { 79 | throw new IllegalStateException(ex); 80 | } 81 | } else if (xhtml instanceof Reader) { 82 | try (Reader reader = (Reader) xhtml) { 83 | source = new StringSource(readAsString(reader)); 84 | } catch (final IOException ex) { 85 | throw new IllegalStateException(ex); 86 | } 87 | } else if (xhtml instanceof Node) { 88 | source = new StringSource((Node) xhtml); 89 | } else { 90 | source = new StringSource(xhtml.toString()); 91 | } 92 | return source; 93 | } 94 | 95 | /** 96 | * Matches content against XPath query. 97 | * @param query The query 98 | * @param Type of XML content provided 99 | * @return Matcher suitable for JUnit/Hamcrest matching 100 | */ 101 | public static Matcher hasXPath(final String query) { 102 | return XhtmlMatchers.hasXPath(query, new XPathContext()); 103 | } 104 | 105 | /** 106 | * Matches content against XPath query, with custom namespaces. 107 | * 108 | *

Every namespace from the {@code namespaces} list will be assigned to 109 | * its own prefix, in order of appearance. Start with {@code 1}. 110 | * For example: 111 | * 112 | *

 MatcherAssert.assert(
113 |      *   "<foo xmlns='my-namespace'></foo>",
114 |      *   XhtmlMatchers.hasXPath("/ns1:foo", "my-namespace")
115 |      * );
116 | * 117 | * @param query The query 118 | * @param namespaces List of namespaces 119 | * @param Type of XML content provided 120 | * @return Matcher suitable for JUnit/Hamcrest matching 121 | */ 122 | public static Matcher hasXPath(final String query, 123 | final Object... namespaces) { 124 | return XhtmlMatchers.hasXPath(query, new XPathContext(namespaces)); 125 | } 126 | 127 | /** 128 | * Matches content against XPath query, with custom context. 129 | * @param query The query 130 | * @param ctx The context 131 | * @param Type of XML content provided 132 | * @return Matcher suitable for JUnit/Hamcrest matching 133 | */ 134 | public static Matcher hasXPath(final String query, 135 | final NamespaceContext ctx) { 136 | return new XPathMatcher(query, ctx); 137 | } 138 | 139 | /** 140 | * Matches content against list of XPaths. 141 | * @param xpaths The query 142 | * @param Type of XML content provided 143 | * @return Matcher suitable for JUnit/Hamcrest matching 144 | */ 145 | public static Matcher hasXPaths(final String... xpaths) { 146 | return XhtmlMatchers.hasXPaths(Arrays.asList(xpaths)); 147 | } 148 | 149 | /** 150 | * Matches content against list of XPaths. 151 | * @param xpaths The query 152 | * @param Type of XML content provided 153 | * @return Matcher suitable for JUnit/Hamcrest matching 154 | * @since 1.8.0 155 | */ 156 | public static Matcher hasXPaths(final Iterable xpaths) { 157 | final Collection> list = new LinkedList<>(); 158 | for (final String xpath : xpaths) { 159 | list.add(XhtmlMatchers.hasXPath(xpath)); 160 | } 161 | return new AllOfThatPrintsOnlyWrongMatchers<>(list); 162 | } 163 | 164 | /** 165 | * Reads an entire reader's contents into a string. 166 | * @param reader The stream to read 167 | * @return The reader content, in String form 168 | */ 169 | private static String readAsString(final Reader reader) { 170 | final String result; 171 | try (Scanner scanner = new Scanner(reader).useDelimiter("\\A")) { 172 | if (scanner.hasNext()) { 173 | result = scanner.next(); 174 | } else { 175 | result = ""; 176 | } 177 | } 178 | return result; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 4.0.0 33 | 34 | com.jcabi 35 | jcabi 36 | 1.39.0 37 | 38 | jcabi-matchers 39 | 2.0-SNAPSHOT 40 | jar 41 | jcabi-matchers 42 | Hamcrest Matchers 43 | 44 | github 45 | https://github.com/jcabi/jcabi-matchers/issues 46 | 47 | 48 | GitHub 49 | https://github.com/jcabi/jcabi-matchers 50 | 51 | 52 | scm:git:github.com:jcabi/jcabi-matchers.git 53 | scm:git:github.com:jcabi/jcabi-matchers.git 54 | https://github.com/jcabi/jcabi-matchers 55 | 56 | 57 | 58 | github-pages 59 | https://matchers.jcabi.com/ 60 | 61 | 62 | 63 | 64 | org.projectlombok 65 | lombok 66 | 1.18.36 67 | provided 68 | 69 | 70 | org.hamcrest 71 | hamcrest 72 | 3.0 73 | compile 74 | 75 | 76 | com.jcabi 77 | jcabi-log 78 | 0.24.3 79 | 80 | 81 | com.jcabi 82 | jcabi-aspects 83 | 0.26.0 84 | 85 | 86 | com.jcabi 87 | jcabi-xml 88 | 0.33.3 89 | true 90 | 91 | 92 | com.jcabi 93 | jcabi-http 94 | 2.0.0 95 | true 96 | 97 | 98 | com.jcabi 99 | jcabi-w3c 100 | 1.4.0 101 | true 102 | 103 | 104 | org.apache.commons 105 | commons-lang3 106 | 3.17.0 107 | test 108 | 109 | 110 | commons-io 111 | commons-io 112 | 2.18.0 113 | test 114 | 115 | 116 | xml-apis 117 | xml-apis 118 | 2.0.2 119 | provided 120 | 121 | 122 | javax.xml.bind 123 | jaxb-api 124 | 2.4.0-b180830.0359 125 | provided 126 | 127 | 128 | org.glassfish.jaxb 129 | jaxb-runtime 130 | 2.3.9 131 | test 132 | 133 | 134 | javax.ws.rs 135 | jsr311-api 136 | 1.1.1 137 | provided 138 | 139 | 140 | net.sf.saxon 141 | Saxon-HE 142 | 12.5 143 | test 144 | 145 | 146 | org.slf4j 147 | slf4j-log4j12 148 | 2.0.16 149 | test 150 | 151 | 152 | log4j 153 | log4j 154 | 1.2.17 155 | test 156 | 157 | 158 | org.glassfish 159 | javax.json 160 | 1.1.4 161 | test 162 | 163 | 164 | com.sun.jersey 165 | jersey-client 166 | 1.19.4 167 | test 168 | 169 | 170 | 171 | 172 | 173 | maven-failsafe-plugin 174 | 175 | 176 | 177 | integration-test 178 | verify 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | qulice 188 | 189 | 190 | 191 | com.qulice 192 | qulice-maven-plugin 193 | 0.24.0 194 | 195 | 196 | duplicatefinder:.* 197 | 198 | file:${basedir}/LICENSE.txt 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /src/test/java/com/jcabi/matchers/XhtmlMatchersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package com.jcabi.matchers; 6 | 7 | import java.io.InputStreamReader; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.Arrays; 10 | import javax.xml.bind.annotation.XmlAccessType; 11 | import javax.xml.bind.annotation.XmlAccessorType; 12 | import javax.xml.bind.annotation.XmlType; 13 | import javax.xml.parsers.DocumentBuilderFactory; 14 | import org.apache.commons.io.IOUtils; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.hamcrest.MatcherAssert; 17 | import org.hamcrest.Matchers; 18 | import org.junit.jupiter.api.Test; 19 | import org.w3c.dom.Document; 20 | import org.w3c.dom.Element; 21 | 22 | /** 23 | * Test case for {@link XhtmlMatchers}. 24 | * @since 0.1 25 | */ 26 | @SuppressWarnings("PMD.TooManyMethods") 27 | final class XhtmlMatchersTest { 28 | 29 | @Test 30 | void matchesWithCustomNamespace() { 31 | MatcherAssert.assertThat( 32 | "should matches with custom namespace", 33 | "abc.txt", 34 | XhtmlMatchers.hasXPath("/ns1:a/ns1:file[.='abc.txt']", "foo") 35 | ); 36 | } 37 | 38 | @Test 39 | void doesntMatch() { 40 | MatcherAssert.assertThat( 41 | "should does not match", 42 | "", 43 | Matchers.not(XhtmlMatchers.hasXPath("/foo")) 44 | ); 45 | } 46 | 47 | @Test 48 | void matchesPlainString() { 49 | MatcherAssert.assertThat( 50 | "should has xpath", 51 | "abc.txt", 52 | XhtmlMatchers.hasXPath("/ns1:b/ns1:file[.='abc.txt']", "bar") 53 | ); 54 | MatcherAssert.assertThat("should has xpath", "", XhtmlMatchers.hasXPath("//b")); 55 | } 56 | 57 | @Test 58 | void matchesInputStreamAndReader() { 59 | MatcherAssert.assertThat( 60 | "should matches input stream and reader", 61 | IOUtils.toInputStream( 62 | "foo.txt", 63 | StandardCharsets.UTF_8 64 | ), 65 | XhtmlMatchers.hasXPath("/b/file[.='foo.txt']") 66 | ); 67 | MatcherAssert.assertThat( 68 | "should matches input stream and reader", 69 | new InputStreamReader( 70 | IOUtils.toInputStream( 71 | "", 72 | StandardCharsets.UTF_8 73 | ), 74 | StandardCharsets.UTF_8 75 | ), 76 | XhtmlMatchers.hasXPath("/xx/y") 77 | ); 78 | } 79 | 80 | @Test 81 | void matchesAfterJaxbConverter() throws Exception { 82 | MatcherAssert.assertThat( 83 | "should matches after jaxb converter", 84 | JaxbConverter.the(new Foo()), 85 | XhtmlMatchers.hasXPath("/ns1:foo", Foo.NAMESPACE) 86 | ); 87 | } 88 | 89 | @Test 90 | void matchesWithGenericType() { 91 | final Foo foo = new Foo(); 92 | MatcherAssert.assertThat( 93 | "should matches all of patterns", 94 | foo, 95 | Matchers.allOf( 96 | Matchers.hasProperty("abc", Matchers.containsString("some")), 97 | XhtmlMatchers.hasXPath("//c") 98 | ) 99 | ); 100 | } 101 | 102 | @Test 103 | void convertsTextToXml() { 104 | MatcherAssert.assertThat( 105 | "should converts text to xml", 106 | StringUtils.join( 107 | "", 108 | "

\u0443

" 109 | ), 110 | XhtmlMatchers.hasXPath("/xhtml:html/xhtml:body/xhtml:p[.='\u0443']") 111 | ); 112 | } 113 | 114 | @Test 115 | void convertsTextToXmlWithUnicode() { 116 | MatcherAssert.assertThat( 117 | "should converts text to xml with unicode", 118 | "\u8514 ›", 119 | XhtmlMatchers.hasXPath("/a") 120 | ); 121 | } 122 | 123 | @Test 124 | void preservesProcessingInstructions() { 125 | MatcherAssert.assertThat( 126 | "should preserves processing instructions", 127 | "", 128 | XhtmlMatchers.hasXPath( 129 | "/processing-instruction('pi')[contains(.,'foo')]" 130 | ) 131 | ); 132 | } 133 | 134 | @Test 135 | void processesDocumentsWithDoctype() { 136 | final String text = 137 | // @checkstyle StringLiteralsConcatenation (6 lines) 138 | "" 139 | + "" 141 | + "" 142 | + "

\u0443\u0440\u0430!

" 143 | + ""; 144 | MatcherAssert.assertThat( 145 | "should processes documents with doctype", 146 | text, 147 | Matchers.allOf( 148 | XhtmlMatchers.hasXPath("/*"), 149 | XhtmlMatchers.hasXPath("//*"), 150 | XhtmlMatchers.hasXPath( 151 | "/xhtml:html/xhtml:body/xhtml:p[.='\u0443\u0440\u0430!']" 152 | ), 153 | XhtmlMatchers.hasXPath("//xhtml:p[contains(., '\u0443')]") 154 | ) 155 | ); 156 | } 157 | 158 | @Test 159 | void convertsNodeToXml() throws Exception { 160 | final Document doc = DocumentBuilderFactory 161 | .newInstance() 162 | .newDocumentBuilder() 163 | .newDocument(); 164 | final Element root = doc.createElement("foo-1"); 165 | root.appendChild(doc.createTextNode("boom")); 166 | doc.appendChild(root); 167 | MatcherAssert.assertThat( 168 | "should converts node to xml", 169 | doc, 170 | XhtmlMatchers.hasXPath("/foo-1[.='boom']") 171 | ); 172 | } 173 | 174 | @Test 175 | void hasXPaths() { 176 | MatcherAssert.assertThat( 177 | "should has xpaths", 178 | "def.txtghi.txt", 179 | XhtmlMatchers.hasXPaths( 180 | "/b/file[.='def.txt']", 181 | "/b/file[.='ghi.txt']" 182 | ) 183 | ); 184 | } 185 | 186 | @Test 187 | void hasXPathsPrintsOnlyWrongXPaths() { 188 | try { 189 | MatcherAssert.assertThat( 190 | "should has xpaths prints only wrong xpaths", 191 | "some.txtgni.txt", 192 | XhtmlMatchers.hasXPaths( 193 | Arrays.asList( 194 | "/b/file[.='some.txt']", 195 | "/b/file[.='gnx.txt']", 196 | "/b/file[.='gni.txt']" 197 | ) 198 | ) 199 | ); 200 | } catch (final AssertionError error) { 201 | MatcherAssert.assertThat( 202 | "should contains a string", 203 | error.getMessage(), 204 | Matchers.containsString( 205 | "Expected: (an XML document with XPath /b/file[.='gnx.txt'])" 206 | ) 207 | ); 208 | } 209 | } 210 | 211 | /** 212 | * Foo. 213 | * 214 | * @since 0.1 215 | */ 216 | @XmlType(name = "foo", namespace = XhtmlMatchersTest.Foo.NAMESPACE) 217 | @XmlAccessorType(XmlAccessType.NONE) 218 | public static final class Foo { 219 | /** 220 | * XML namespace. 221 | */ 222 | public static final String NAMESPACE = "foo-namespace"; 223 | 224 | @Override 225 | public String toString() { 226 | return "
"; 227 | } 228 | 229 | /** 230 | * Property abc. 231 | * @return Value of abc 232 | */ 233 | public String getAbc() { 234 | return "some value"; 235 | } 236 | } 237 | } 238 | --------------------------------------------------------------------------------