├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── my-artifact-publisher.gradle.kts │ ├── my-checkstyle.gradle.kts │ ├── my-jacoco.gradle.kts │ ├── my-pitest.gradle.kts │ ├── my-spotbugs.gradle.kts │ └── my-test-percentage-printer.gradle.kts ├── checkstyle.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main └── java │ └── io │ └── github │ └── ricoapon │ └── readableregex │ ├── ExtendableReadableRegex.java │ ├── FinishBuilder.java │ ├── GroupBuilder.java │ ├── IncorrectConstructionException.java │ ├── PatternFlag.java │ ├── QuantifierBuilder.java │ ├── ReadableRegex.java │ ├── ReadableRegexPattern.java │ ├── RegexObjectInstantiation.java │ ├── RegexObjectInstantiationException.java │ ├── StandaloneBlockBuilder.java │ ├── SyntacticSugarBuilder.java │ ├── internal │ ├── MethodOrderChecker.java │ ├── ReadableRegexBuilder.java │ ├── ReadableRegexOrderChecker.java │ ├── ReadableRegexPatternImpl.java │ ├── instantiation │ │ ├── ParameterInfo.java │ │ ├── RegexObjectInstantiationImpl.java │ │ └── StringConverter.java │ └── package-info.java │ └── package-info.java └── test └── java └── io └── github └── ricoapon └── readableregex ├── Constants.java ├── FinishTests.java ├── GroupTests.java ├── PatternFlagTests.java ├── QuantifierTests.java ├── ReadableRegexPatternTest.java ├── ReadmeTests.java ├── StandaloneBlockTests.java ├── SyntacticSugarTests.java ├── extendable ├── ExtendableReadableRegexTest.java └── TestExtension.java ├── instantiation ├── ConstructorParamUnknownType.java ├── MultipleConstructorsWithMultipleInjectAnnotations.java ├── MultipleConstructorsWithSingleInjectAnnotation.java ├── PrimitiveAndBoxedPrimitiveTypesAndString.java ├── RegexObjectInstantiationTest.java ├── SingleConstructorWithInjectAnnotation.java ├── SingleConstructorWithoutInjectAnnotation.java └── StringConverterTest.java ├── internal ├── MethodOrderCheckerTest.java └── ReadableRegexOrderCheckerTest.java └── matchers └── PatternMatchMatcher.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk11 4 | after_success: 5 | - bash <(curl -s https://codecov.io/bash) 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | ### Added 8 | - Groups that have been added are now recorded in the builder. The following two methods are added to retrieve this information: 9 | `ReadableRegexPattern#groups()` and `ReadableRegexPattern#nrOfGroups()`. 10 | - It is now possible to instantiate objects using data and a pattern. The library automatically matches the group name 11 | to the variable used in the constructor. See `RegexObjectInstantiation` for more information. 12 | 13 | ## [0.4.0] 14 | ### Added 15 | - It is now possible to extend the builder. You can add new methods or overwrite existing methods. See the README for a code example.
16 | Note that all usages of `ReadableRegex` should be replaced with `ReadableRegex< ? >`, since the class now has a generic type. 17 | This is not a breaking change, since adding `< ? >` is not required to compile. It may give warning in some IDEs. 18 | 19 | ## [0.3.0] 20 | ### Added 21 | - Added methods for start/end of line/input: `startOfLine()`, `startOfInput()`, `endOfLine()` and `endOfInput()`. 22 | - Added methods for a tab and line break: `tab()` and `lineBreak()`. 23 | 24 | ### Changed 25 | - Moved syntactic sugar methods from the interface GroupBuilder into SyntacticSugarBuilder. 26 | - Improved the Javadoc as a whole: added missing comments and rewritten comments to make it more clear. 27 | 28 | ## [0.2.0] 29 | ### Added 30 | - Added method for or constructions (corresponds to `|`). Example: `regex().oneOf(regex().literal("a"), regex().digit()).build()`. 31 | - Added methods for ranges (corresponds to `[]` and `[^]`): `range(char... boundaries)`, `notInRange(char... boundaries)`, 32 | `anyCharacterOf(String characters)` and `anyCharacterExcept(String characters)`. 33 | - Added methods for word characters and boundaries: `wordCharacter()`, `nonWordCharacter()`, `wordBoundary()` and `nonWordBoundary()`. 34 | Also, added the method `word()` which searches for words. 35 | - All greedy quantifiers are now available. 36 | - Added methods using dot (`.`): `anyCharacter()` and `anything()`. 37 | - It is now possible to make existing quantifiers reluctant or possessive by appending the method `reluctant()` or `possessive()` 38 | after the quantifier. 39 | - Added shortcut method for matching text: `pattern.matchesTextExactly(String text)`. 40 | - Makes it possible to start unnamed groups: `startUnnamedGroup()`. 41 | 42 | ### Changed 43 | - Spotbugs annotations is removed as dependency. 44 | 45 | ## [0.1] 46 | ### Added 47 | - First setup with minimal features. 48 | 49 | [Unreleased]: https://github.com/ricoapon/readable-regex/compare/v0.4.0...HEAD 50 | [0.4.0]: https://github.com/ricoapon/readable-regex/releases/tag/v0.4.0 51 | [0.3.0]: https://github.com/ricoapon/readable-regex/releases/tag/v0.3.0 52 | [0.2.0]: https://github.com/ricoapon/readable-regex/releases/tag/v0.2.0 53 | [0.1]: https://github.com/ricoapon/readable-regex/releases/tag/v0.1 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rico Apon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readable Regex [![Maven Central](https://img.shields.io/maven-central/v/io.github.ricoapon/readable-regex)](https://search.maven.org/artifact/io.github.ricoapon/readable-regex) [![javadoc](https://javadoc.io/badge2/io.github.ricoapon/readable-regex/javadoc.svg?color=blue)](https://javadoc.io/doc/io.github.ricoapon/readable-regex) [![Build Status](https://app.travis-ci.com/ricoapon/readable-regex.svg?branch=master)](https://app.travis-ci.com/ricoapon/readable-regex) [![codecov](https://codecov.io/gh/ricoapon/readable-regex/branch/master/graph/badge.svg?token=O236UO0ZNZ)](https://codecov.io/gh/ricoapon/readable-regex) 2 | 3 | With this library, you can create regular expressions in a readable way! 4 | 5 | ## Table of contents 6 | 1. [About the library](#about-the-library) 7 | 1. [User guide](#user-guide) 8 | 1. [Examples](#examples) 9 | 1. [Quantifiers](#quantifiers) 10 | 1. [Working around the limits of the library](#working-around-the-limits-of-the-library) 11 | 1. [Extending the builder](#extending-the-builder) 12 | 1. [Instantiating objects](#instantiating-objects) 13 | 1. [Javadoc](#javadoc) 14 | 1. [Contributing](#contributing) 15 | 1. [Local development](#local-development) 16 | 17 | ## About the library 18 | This library uses the builder pattern to create regular expressions. Using methods with understandable names to create 19 | your expression, should be more readable and therefore easier to maintain! 20 | 21 | ### Regular expression engine 22 | This library uses the engine implemented in the JDK. All the details and specifics of the engine can be found in the 23 | JavaDoc of the class [Pattern](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html). 24 | 25 | ### Replacement of JavaVerbalExpressions 26 | [JavaVerbalExpressions](https://github.com/VerbalExpressions/JavaVerbalExpressions) is another library created for Java 27 | to construct regular expressions using a Builder pattern. I liked this library, but there were a few caveats: 28 | * It seems that it is not maintained anymore. 29 | * It misses some functionality (for example, lookahead). 30 | * It is not written with Java in mind (the idea is ported to all languages). 31 | 32 | This library is created to be a better version of JavaVerbalExpressions. 33 | 34 | ### Readability over performance 35 | This library is focussed fully on readability and correctness. Very often performance of regular expressions is not 36 | important. However, in some cases (especially with large input or with catastrophic backtracking) it can be very 37 | troublesome. There is a lot of information online on how to make your regular expressions as fast as possible. However, 38 | changing the builder to get good performing regular expressions may not be readable. If you are reliant on good 39 | performing expressions, this library may not be the best choice. 40 | 41 | ## User guide 42 | Note: [Hamcrest](http://hamcrest.org/) is used for all the examples to show the expected outcome. If you want the examples 43 | to compile in your own project, you should include this library. 44 | 45 | ### Examples 46 | Let's try a basic URL matching pattern: 47 | ``` 48 | ReadableRegexPattern pattern = 49 | regex() // Always start with the regex method to start the builder. 50 | .literal("http") // Literals are escaped automatically, no need to do this yourself. 51 | .literal("s").optional() // You can follow up with optional to make the "s" optional. 52 | .literal("://") 53 | .anyCharacterExcept(" ").zeroOrMore() // This comes down to [^ ]*. 54 | .build(); // Create the pattern with the final method. 55 | 56 | // The matchesText will return a boolean whether we have an *exact* match or not! 57 | assertThat(pattern.matchesTextExactly("https://www.github.com"), equalTo(true)); 58 | 59 | // toString() method will return the underlying pattern. Not really readable though, that is why we have this library! 60 | assertThat(pattern.toString(), equalTo("(?:\\Qhttp\\E)(?:\\Qs\\E)?(?:\\Q://\\E)[^ ]*")); 61 | ``` 62 | 63 | With the library, you can create the JDK Matcher object when matching a text. Using this object, you can do the usual 64 | things like getting the value of groups. 65 | ``` 66 | ReadableRegexPattern pattern = regex() 67 | .startGroup() // You can use this method to start capturing the expression inside a group. 68 | .word() 69 | .endGroup() // This ends the last group. 70 | .whitespace() 71 | .startGroup("secondWord") // You can also give names to your group. 72 | .word() 73 | .endGroup() 74 | .build(); 75 | 76 | Matcher matcher = pattern.matches("abc def"); 77 | assertThat(matcher.matches(), equalTo(true)); 78 | 79 | // Groups can always be found based on the order they are used. 80 | assertThat(matcher.group(1), equalTo("abc")); 81 | assertThat(matcher.group(2), equalTo("def")); 82 | 83 | // If you need details about the groups afterwards, this is not possible using the JDK Pattern. 84 | // However, using this library, this is now possible: 85 | assertThat(pattern.groups(), contains(null, "secondWord")); 86 | 87 | // If you have given the group a name, you can also find it based on the name. 88 | assertThat(matcher.group("secondWord"), equalTo("def")); 89 | ``` 90 | 91 | The useful thing about this library is that you can include pattern inside other patterns! 92 | ``` 93 | // It does not matter if you have already built the pattern, you can include it anyway. 94 | ReadableRegex digits = regex().startGroup().digit().oneOrMore().endGroup().whitespace(); 95 | ReadableRegexPattern word = regex().startGroup().word().endGroup().whitespace().build(); 96 | 97 | ReadableRegexPattern pattern = regex() 98 | .add(digits) 99 | .add(digits) 100 | .add(word) 101 | .add(digits) 102 | .literal("END") 103 | .build(); 104 | 105 | Matcher matcher = pattern.matches("12\t11\thello\t0000\tEND"); 106 | assertThat(matcher.matches(), equalTo(true)); 107 | // Note that captures are always a String! 108 | assertThat(matcher.group(1), equalTo("12")); 109 | assertThat(matcher.group(2), equalTo("11")); 110 | assertThat(matcher.group(3), equalTo("hello")); 111 | assertThat(matcher.group(4), equalTo("0000")); 112 | ``` 113 | 114 | Some random stuff you can do: 115 | ``` 116 | ReadableRegexPattern pattern = regex() 117 | .oneOf(regex().literal("abc"), regex().digit()) // The oneOf method represents "or". 118 | .whitespace() 119 | // If we want to add a quantifier over a larger expression, we can encapsulate it with the add method, 120 | // which encloses the expression in an unnamed group. 121 | .add(regex().literal("a").digit()).exactlyNTimes(3) 122 | .whitespace() 123 | // Alternatively, you can use the startUnnamedGroup() for this to avoid nested structures. 124 | .startUnnamedGroup().literal("b").digit().endGroup().atMostNTimes(2) 125 | .build(); 126 | 127 | assertThat(pattern.matchesTextExactly("abc a1a2a3 b2"), equalTo(true)); 128 | assertThat(pattern.matchesTextExactly("1 a3a6a9 "), equalTo(true)); 129 | ``` 130 | 131 | ### Quantifiers 132 | All the quantifiers are greedy by default. If you want to make them reluctant or possessive, you can use the methods 133 | `reluctant()` and `possessive()` after the quantifier. 134 | 135 | If you want to know the differences between these types of quantifiers, read about it in 136 | [this post](https://stackoverflow.com/questions/5319840/greedy-vs-reluctant-vs-possessive-quantifiers). 137 | 138 | ``` 139 | ReadableRegexPattern greedyPattern = regex().anything().literal("foo").build(); 140 | ReadableRegexPattern reluctantPattern = regex().anything().reluctant().literal("foo").build(); 141 | ReadableRegexPattern possessivePattern = regex().anything().possessive().literal("foo").build(); 142 | 143 | String text = "xfooxxxxxxfoo"; 144 | assertThat(greedyPattern.matchesText(text), equalTo(true)); 145 | 146 | Matcher matcher = reluctantPattern.matches(text); 147 | assertThat(matcher.find(), equalTo(true)); 148 | assertThat(matcher.group(), equalTo("xfoo")); 149 | assertThat(matcher.find(), equalTo(true)); 150 | assertThat(matcher.group(), equalTo("xxxxxxfoo")); 151 | 152 | matcher = possessivePattern.matches(text); 153 | assertThat(matcher.find(), equalTo(false)); 154 | ``` 155 | 156 | ### Working around the limits of the library 157 | Not everything will be supported by the library. Sometimes you may want something very specific. There are a few methods 158 | to help you with that. 159 | 160 | The following example creates the regular expression `[a-z&&[^p]]`, which matches all lower-case letters except `p`. 161 | ``` 162 | ReadableRegexPattern pattern1 = regex() 163 | .regexFromString("[a-z&&[^p]]") // With this method, you can add any kind of expression. 164 | .build(); 165 | 166 | // Or you could use the overloaded variant of the regex method, which is the same: 167 | ReadableRegexPattern pattern2 = regex("[a-z&&[^p]]").build(); 168 | 169 | assertThat(pattern1.matchesTextExactly("p"), equalTo(false)); 170 | assertThat(pattern1.matchesTextExactly("c"), equalTo(true)); 171 | assertThat(pattern2.matchesTextExactly("p"), equalTo(false)); 172 | assertThat(pattern2.matchesTextExactly("c"), equalTo(true)); 173 | ``` 174 | 175 | Note that the ReadableRegexPattern class is basically a wrapper of a JDK Pattern object. So if you need specific methods, 176 | you can use the underlying object: 177 | ``` 178 | ReadableRegexPattern pattern = regex().literal(".").build(); // Don't forget that literal escapes any meta character like dot! 179 | 180 | Pattern jdkPattern = pattern.getUnderlyingPattern(); 181 | assertThat(jdkPattern.split("a.b.c"), equalTo(new String[]{"a", "b", "c"})); 182 | ``` 183 | 184 | ### Extending the builder 185 | You can fully customize the builder to your own needs! It is possible to add new methods and override existing methods. 186 | The code below is an example on how to create your own extension: 187 | ``` 188 | // You have to extend from ExtendableReadableRegex, where you fill in your own class as generic type. 189 | public class TestExtension extends ExtendableReadableRegex { 190 | 191 | // It is highly advised to create your own static method "regex()". This way you can easily instantiate 192 | // your class and in your existing code you only have to change your import statement. 193 | public static TestExtension regex() { 194 | return new TestExtension(); 195 | } 196 | 197 | // In your own extension you can add any method you like. 198 | public TestExtension digitWhitespaceDigit() { 199 | // For the implementation of your extension, you can only use the publicly available methods. All variables 200 | // and other methods are made private in the instance. 201 | // If you want to add arbitrary expressions, you can always use the method "regexFromString(...)". 202 | return digit().whitespace().digit(); 203 | } 204 | 205 | // You can also override existing methods! To make sure that the code doesn't break, please always end 206 | // with calling the super method. 207 | @Override 208 | public ReadableRegexPattern buildWithFlags(PatternFlag... patternFlags) { 209 | return super.buildWithFlags(PatternFlag.DOT_ALL); 210 | } 211 | } 212 | ``` 213 | This can now be used: 214 | ``` 215 | ReadableRegexPattern pattern = TestExtension.regex().digitWhitespaceDigit().build(); 216 | 217 | assertThat(pattern.matchesTextExactly("1 3"), equalTo(true)); 218 | assertThat(pattern.enabledFlags(), contains(PatternFlag.DOT_ALL)); 219 | ``` 220 | 221 | ### Instantiating objects 222 | The library supports instantiating objects using patterns to retrieve the data from a string. Suppose we have a small class: 223 | ``` 224 | public static class MyPojo { 225 | public final String name; 226 | public final int id; 227 | 228 | @Inject // If you have only one constructor, you can leave this out. 229 | public MyPojo(String name, int id) { 230 | this.name = name; 231 | this.id = id; 232 | } 233 | } 234 | ``` 235 | Then we can create a new instance dynamically as follows: 236 | ``` 237 | String data = "name: example, id: 15"; 238 | ReadableRegexPattern pattern = regex() 239 | .literal("name: ").group("name", regex().word()) 240 | .literal(", id: ").group("id", regex().digit().oneOrMore()).build(); 241 | 242 | MyPojo myPojo = instantiateObject(pattern, data, MyPojo.class); 243 | 244 | assertThat(myPojo.name, equalTo("example")); 245 | // Types are automatically converted! 246 | assertThat(myPojo.id, equalTo(15)); 247 | ``` 248 | 249 | ### Javadoc 250 | If you are looking for in-depth information about all the available methods, take a look at the Javadoc. 251 | You can find the latest version [here](https://javadoc.io/doc/io.github.ricoapon/readable-regex/latest/index.html). 252 | 253 | ## Contributing 254 | If you have any suggestions, submit an issue right here in the GitHub project! Any bugs, features or random thoughts 255 | are appreciated :) 256 | 257 | ## Local development 258 | ### Checks 259 | All additional plugins to check the code base should run when calling the following gradle command: 260 | ``` 261 | gradle checks 262 | ``` 263 | This will trigger [SpotBugs](https://spotbugs.github.io/), [Checkstyle](https://checkstyle.sourceforge.io/), [JaCoCo](https://www.jacoco.org/jacoco/) and [Pitest](https://pitest.org/). 264 | If the checks succeed, the test coverage is printed. Of course, the test coverage should be 100%. 265 | 266 | If you want to run one of the components individually (because this could be faster), you can use: 267 | ```` 268 | gradle spotbugs 269 | gradle checkstyle 270 | gradle jacoco 271 | gradle pitest 272 | gradle printTestPercentages 273 | ```` 274 | The reports are available in HTML form and are located in `build/reports`. 275 | 276 | ### Publishing new releases 277 | Every release should correspond to a tag in git. This tag should be manually added. 278 | Uploading new releases to Maven Central can be done using the following command: 279 | ```` 280 | gradle publish -PcustomVersion=X 281 | ```` 282 | If no version is supplied, the default `head-SNAPSHOT` is used. 283 | 284 | Note that at this time, only the creator of this library (Rico Apon) can upload new releases. 285 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | `my-checkstyle` 4 | `my-jacoco` 5 | `my-spotbugs` 6 | `my-pitest` 7 | `my-test-percentage-printer` 8 | `my-artifact-publisher` apply false // We can only apply the plugin after the version has been determined. 9 | } 10 | 11 | group = "io.github.ricoapon" 12 | version = when { 13 | project.hasProperty("customVersion") -> project.property("customVersion").toString() 14 | else -> "head-SNAPSHOT" 15 | } 16 | println("Using version $version") 17 | plugins.apply("my-artifact-publisher") 18 | 19 | java { 20 | sourceCompatibility = JavaVersion.VERSION_1_8 21 | targetCompatibility = JavaVersion.VERSION_1_8 22 | withJavadocJar() 23 | withSourcesJar() 24 | } 25 | 26 | repositories { 27 | mavenCentral() 28 | } 29 | 30 | dependencies { 31 | api("javax.inject:javax.inject:1") 32 | implementation("com.thoughtworks.paranamer:paranamer:2.8"); 33 | 34 | testImplementation("org.hamcrest:hamcrest:2.2") 35 | 36 | val junitVersion = "5.6.2" 37 | testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") 38 | testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") 39 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") 40 | } 41 | 42 | val test by tasks.getting(Test::class) { 43 | useJUnitPlatform() 44 | } 45 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | } 8 | 9 | // External plugins cannot be included with a version inside the standalone scripts. Therefore we need to add them as 10 | // a dependency inside this build script in a specific way. 11 | dependencies { 12 | implementation(plugin("com.github.spotbugs", "4.5.0")) 13 | implementation(plugin("info.solidsoft.pitest", "1.5.1")) 14 | } 15 | 16 | fun plugin(id: String, version: String) = "$id:$id.gradle.plugin:$version" 17 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/my-artifact-publisher.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.* 2 | 3 | /** 4 | * This file contains all the build logic related to publishing artifacts to Maven Central. 5 | * This plugin should only be applied after the version of the project has been determined. 6 | */ 7 | plugins { 8 | java 9 | `maven-publish` 10 | signing 11 | } 12 | 13 | publishing { 14 | publications { 15 | create("mavenJava") { 16 | artifactId = "readable-regex" 17 | from(components["java"]) 18 | pom { 19 | name.set("Readable Regex") 20 | description.set("Regular expressions made readable in Java") 21 | url.set("https://github.com/ricoapon/readable-regex") 22 | inceptionYear.set("2020") 23 | 24 | licenses { 25 | license { 26 | name.set("MIT License") 27 | url.set("https://github.com/ricoapon/readable-regex/blob/master/LICENSE") 28 | } 29 | } 30 | 31 | developers { 32 | developer { 33 | id.set("ricoapon") 34 | name.set("Rico Apon") 35 | } 36 | } 37 | 38 | scm { 39 | url.set("https://github.com/ricoapon/readable-regex") 40 | connection.set("scm:https://github.com/ricoapon/readable-regex.git") 41 | developerConnection.set("scm:git@github.com:ricoapon/readable-regex.git") 42 | } 43 | } 44 | } 45 | } 46 | 47 | repositories { 48 | maven { 49 | val releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2" 50 | val snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots" 51 | url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) 52 | 53 | credentials { 54 | username = project.findProperty("ossrhUsername") as String? 55 | password = project.findProperty("ossrhPassword") as String? 56 | } 57 | } 58 | } 59 | } 60 | 61 | signing { 62 | sign(publishing.publications["mavenJava"]) 63 | } 64 | 65 | tasks.javadoc { 66 | if (JavaVersion.current().isJava9Compatible) { 67 | (options as StandardJavadocDocletOptions).addBooleanOption("html5", true) 68 | } 69 | } 70 | 71 | // Make sure that we can only publish if all checks have been completed successfully. 72 | // Don't add this to the task "publish", because this can go wrong with parallel execution. 73 | tasks.withType { 74 | dependsOn("check") 75 | } 76 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/my-checkstyle.gradle.kts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the logic to configure checkstyle. 3 | */ 4 | plugins { 5 | java 6 | checkstyle 7 | } 8 | 9 | tasks.withType().configureEach { 10 | configFile = File("checkstyle.xml") 11 | } 12 | tasks.register("checkstyle") { 13 | dependsOn(tasks.checkstyleMain) 14 | dependsOn(tasks.checkstyleTest) 15 | } 16 | 17 | // Checkstyle automatically adds the checkstyleMain and checkstyleTest to the check task. 18 | // We don't need to do this ourselves. 19 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/my-jacoco.gradle.kts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the logic to configure jacoco. 3 | */ 4 | plugins { 5 | java 6 | jacoco 7 | } 8 | 9 | jacoco { 10 | // Experimental support for Java 15 has only been added to 0.8.6. Release version 0.8.7 will officially support Java 15. 11 | // This needs to be set, otherwise this task won't run properly if you use Java 15+ locally. 12 | toolVersion = "0.8.6" 13 | } 14 | 15 | tasks.jacocoTestReport { 16 | // Tests are required before generating the report. For some reason, you need to do this yourself. 17 | dependsOn(tasks.test) 18 | 19 | reports { 20 | // Codecov.io depends on xml format report. 21 | xml.isEnabled = true 22 | // Add HTML report readable by humans. 23 | html.isEnabled = true 24 | } 25 | } 26 | 27 | tasks.register("jacoco") { 28 | dependsOn(tasks.jacocoTestCoverageVerification) 29 | dependsOn(tasks.jacocoTestReport) 30 | } 31 | 32 | tasks.check { 33 | // Reports are always generated after running the checks. 34 | finalizedBy(tasks.jacocoTestReport) 35 | } 36 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/my-pitest.gradle.kts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the logic to configure pitest. 3 | */ 4 | plugins { 5 | java 6 | id("info.solidsoft.pitest") 7 | } 8 | 9 | pitest { 10 | junit5PluginVersion.set("0.12") 11 | outputFormats.set(listOf("HTML")) 12 | timestampedReports.set(false) 13 | threads.set(4) 14 | } 15 | 16 | tasks.check { 17 | dependsOn(tasks.pitest) 18 | } 19 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/my-spotbugs.gradle.kts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the logic to configure spotbugs. 3 | */ 4 | plugins { 5 | java 6 | id("com.github.spotbugs") 7 | } 8 | 9 | dependencies { 10 | // The spotbugs annotation dependency may not occur as dependency. It is only needed at compile time. 11 | // Annotations are currently not used in production code. If this should be the case in the future, add the dependency 12 | // as compileOnly. 13 | testCompileOnly("com.github.spotbugs:spotbugs-annotations:4.1.1") 14 | } 15 | 16 | spotbugs { 17 | // Display final report as HTML. 18 | // Use different HTML template (stylesheet) that is prettier. 19 | tasks.spotbugsMain { 20 | reports.create("html") { 21 | isEnabled = true 22 | setStylesheet("fancy-hist.xsl") 23 | } 24 | } 25 | tasks.spotbugsTest { 26 | reports.create("html") { 27 | isEnabled = true 28 | setStylesheet("fancy-hist.xsl") 29 | } 30 | } 31 | } 32 | 33 | tasks.register("spotbugs") { 34 | dependsOn(tasks.spotbugsMain) 35 | dependsOn(tasks.spotbugsTest) 36 | } 37 | 38 | // Spotbugs automatically adds the spotbugsMain and spotbugsTest to the check task. 39 | // We don't need to do this ourselves. 40 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/my-test-percentage-printer.gradle.kts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file adds the logic for printing test percentages from reports to the console. It is assumed that the jacoco 3 | * and pitest plugins are applied. 4 | */ 5 | plugins { 6 | java 7 | } 8 | 9 | tasks.register("printTestPercentages") { 10 | // Don't add the input files to the task. The percentages should always show, also if the task has already been executed. 11 | doLast { 12 | printTestPercentages() 13 | } 14 | dependsOn("jacoco", "pitest") 15 | } 16 | 17 | tasks.check { 18 | finalizedBy("printTestPercentages") 19 | } 20 | 21 | fun printTestPercentages() { 22 | var areAllTests100Percent = true; 23 | 24 | var resultString = "Coverage Summary:\n" 25 | readTestPercentageFromJacocoReport().forEach { 26 | resultString += createLine(it) 27 | if (it.value.first != it.value.second) { 28 | areAllTests100Percent = false 29 | } 30 | } 31 | readTestPercentagesFromPitestReport().forEach { 32 | resultString += createLine(it) 33 | if (it.value.first != it.value.second) { 34 | areAllTests100Percent = false; 35 | } 36 | } 37 | 38 | print(resultString) 39 | 40 | if (!areAllTests100Percent) { 41 | throw GradleException("The test coverage is not 100%!") 42 | } 43 | } 44 | 45 | fun createLine(result: Map.Entry>): String = 46 | "${result.key.padEnd(18)}: ${Math.floorDiv(result.value.first * 100, result.value.second).toString().padStart(3)}% " + 47 | "(${result.value.first.toString().padStart(4)} / ${result.value.second.toString().padStart(4)})\n" 48 | 49 | fun readTestPercentageFromJacocoReport(): Map> { 50 | val reportFileContent = File(project.buildDir.resolve("reports/jacoco/test/jacocoTestReport.xml").toURI()).readText() 51 | 52 | val pattern = Regex("<\\/package>" + 53 | "" + 54 | "") 55 | val (instructionMissed, instructionTotal, branchMissed, branchTotal, lineMissed, lineTotal) = pattern.find(reportFileContent)!!.destructured 56 | 57 | return mapOf("JACOCO_INSTRUCTION" to Pair(Integer.parseInt(instructionTotal) - Integer.parseInt(instructionMissed), Integer.parseInt(instructionTotal)), 58 | "JACOCO_BRANCH" to Pair(Integer.parseInt(branchTotal) - Integer.parseInt(branchMissed), Integer.parseInt(branchTotal)), 59 | "JACOCO_LINE" to Pair(Integer.parseInt(lineTotal) - Integer.parseInt(lineMissed), Integer.parseInt(lineTotal))) 60 | } 61 | 62 | fun readTestPercentagesFromPitestReport(): Map> { 63 | val reportFileContent = File(project.buildDir.resolve("reports/pitest/index.html").toURI()).readText() 64 | 65 | val pattern = Regex("\\d+%
(\\d+)/(\\d+)
.*\\r?\\n?" + 66 | "\\s+\\d+%
(\\d+)/(\\d+)
") 67 | val (lineCoverageHit, lineCoverageTotal, mutationCoverageHit, mutationCoverageTotal) = pattern.find(reportFileContent)!!.destructured 68 | 69 | return mapOf("PITEST_LINE" to Pair(Integer.parseInt(lineCoverageHit), Integer.parseInt(lineCoverageTotal)), 70 | "PITEST_MUTATION" to Pair(Integer.parseInt(mutationCoverageHit), Integer.parseInt(mutationCoverageTotal))) 71 | } 72 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricoapon/readable-regex/1febde9d1af6e399e8a2e951502e49a078707039/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.5.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = "readable-regex" 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/ExtendableReadableRegex.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import io.github.ricoapon.readableregex.internal.MethodOrderChecker; 4 | import io.github.ricoapon.readableregex.internal.ReadableRegexOrderChecker; 5 | 6 | /** 7 | * This class can be extended to create your own builder. Example code: 8 | *
 9 |  * // You have to extend from ExtendableReadableRegex, where you fill in your own class as generic type.
10 |  * public class MyExtension extends ExtendableReadableRegex<TestExtension> {
11 |  *
12 |  *     // It is highly advised to create your own static method "regex()". This way you can easily instantiate
13 |  *     // your class and in your existing code you only have to change your import statement.
14 |  *     public static TestExtension regex() {
15 |  *         return new TestExtension();
16 |  *     }
17 |  *
18 |  *     // In your own extension you can add any method you like.
19 |  *     public TestExtension digitWhitespaceDigit() {
20 |  *         // For the implementation of your extension, you can only use the publicly available methods. All variables
21 |  *         // and other methods are made private in the instance.
22 |  *         // If you want to add arbitrary expressions, you can always use the method "regexFromString(...)".
23 |  *         return digit().whitespace().digit();
24 |  *     }
25 |  *
26 |  *     // You can also override existing methods! To make sure that the code doesn't break, please always end
27 |  *     // with calling the super method.
28 |  *     {@literal @}Override
29 |  *     public ReadableRegexPattern buildWithFlags(PatternFlag... patternFlags) {
30 |  *         return super.buildWithFlags(PatternFlag.DOT_ALL);
31 |  *     }
32 |  * }
33 | * 34 | * This can now be used as follows: 35 | *
36 |  * ReadableRegexPattern pattern = TestExtension.regex().digitWhitespaceDigit().build();
37 |  *
38 |  * assertThat(pattern.matchesTextExactly("1 3"), equalTo(true));
39 |  * assertThat(pattern.enabledFlags(), contains(PatternFlag.DOT_ALL));
40 |  * 
41 | * @param The new type that is used as builder. 42 | */ 43 | public class ExtendableReadableRegex> extends ReadableRegexOrderChecker { 44 | /** 45 | * Constructor. 46 | */ 47 | public ExtendableReadableRegex() { 48 | super(new MethodOrderChecker()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/FinishBuilder.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * Builder interface with all the methods that finish building the expression. 7 | */ 8 | public interface FinishBuilder { 9 | /** 10 | * @return Compiled regular expression into {@link ReadableRegexPattern} object. 11 | */ 12 | default ReadableRegexPattern build() { 13 | return buildWithFlags(); 14 | } 15 | 16 | /** 17 | * @param patternFlags The flags that are enabled for the regular expression. 18 | * @return Compiled regular expression into {@link ReadableRegexPattern} object. 19 | */ 20 | ReadableRegexPattern buildWithFlags(PatternFlag... patternFlags); 21 | 22 | /** 23 | * @return Compiled regular expression into {@link Pattern} object. 24 | */ 25 | default Pattern buildJdkPattern() { 26 | return build().getUnderlyingPattern(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/GroupBuilder.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | /** 4 | * Builder interface with all the methods related to groups. 5 | */ 6 | public interface GroupBuilder> { 7 | /** 8 | * Starts a new capturing group. This is the same as {@code (}. You must call {@link #endGroup()} somewhere after this method. 9 | * @return This builder. 10 | */ 11 | T startGroup(); 12 | 13 | /** 14 | * Starts a new capturing group with a given name. This is the same as {@code (}. You must call {@link #endGroup()} 15 | * somewhere after this method. 16 | * @param groupName The name of the group. 17 | * @return This builder. 18 | */ 19 | T startGroup(String groupName); 20 | 21 | /** 22 | * Starts a non-capturing group. This is the same as {@code (?:}. You must call {@link #endGroup()} somewhere after 23 | * this method. 24 | * @return This builder. 25 | */ 26 | T startUnnamedGroup(); 27 | 28 | /** 29 | * Starts a non-capturing group for positive lookbehind. This is the same as {@code (?<=}. You must call {@link #endGroup()} 30 | * somewhere after this method. 31 | * @return This builder. 32 | */ 33 | T startPositiveLookbehind(); 34 | 35 | /** 36 | * Starts a non-capturing group for negative lookbehind. This is the same as {@code (?> { 7 | /** 8 | * Makes the previous block repeat one or more times (greedy). This is the same as adding {@code +}. 9 | * @return This builder. 10 | */ 11 | T oneOrMore(); 12 | 13 | /** 14 | * Makes the previous block optional (greedy). This is the same as adding {@code ?}. 15 | * @return This builder. 16 | */ 17 | T optional(); 18 | 19 | /** 20 | * Makes the previous block repeat zero or more times (greedy). This is the same as adding {@code *}. 21 | * @return This builder. 22 | */ 23 | T zeroOrMore(); 24 | 25 | /** 26 | * Makes the previous block repeat exactly n times (greedy). This is the same as adding {@code {n}}. 27 | * @param n The number of times the block should repeat. 28 | * @return This builder. 29 | */ 30 | T exactlyNTimes(int n); 31 | 32 | /** 33 | * Makes the previous block repeat at least n times (greedy). This is the same as adding {@code {n,}}. 34 | * @param n The minimum number of times the block should repeat. 35 | * @return This builder. 36 | */ 37 | T atLeastNTimes(int n); 38 | 39 | /** 40 | * Makes the previous block repeat between n and m times (greedy). This is the same as adding {@code {n, m}}. 41 | * @param n The minimum number of times the block should repeat. 42 | * @param m The maximum number of times the block should repeat. 43 | * @return This builder. 44 | */ 45 | T betweenNAndMTimes(int n, int m); 46 | 47 | /** 48 | * Makes the previous block repeat at most n times (greedy). This is the same as adding {@code {0, n}}. 49 | * @param n The maximum number of times the block should repeat. 50 | * @return This builder. 51 | */ 52 | default T atMostNTimes(int n) { 53 | return betweenNAndMTimes(0, n); 54 | } 55 | 56 | /** 57 | * Makes the previous quantifier reluctant. This is the same as adding {@code ?}. 58 | * @return This builder. 59 | */ 60 | T reluctant(); 61 | 62 | /** 63 | * Makes the previous quantifier possessive. This is the same as adding {@code +}. 64 | * @return This builder. 65 | */ 66 | T possessive(); 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/ReadableRegex.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import io.github.ricoapon.readableregex.internal.MethodOrderChecker; 4 | import io.github.ricoapon.readableregex.internal.ReadableRegexOrderChecker; 5 | 6 | /** 7 | * Interface which extends all the other interfaces for constructing readable regular expressions using the builder pattern. 8 | *

9 | * Using this builder, you can create {@link ReadableRegexPattern} objects, which can be used to match the pattern against text. 10 | */ 11 | public interface ReadableRegex> extends SyntacticSugarBuilder, StandaloneBlockBuilder, QuantifierBuilder, FinishBuilder, GroupBuilder { 12 | /** 13 | * This method is the starting point for all the builder methods. 14 | * @return Instance of the builder. 15 | */ 16 | static ReadableRegex regex() { 17 | return new ReadableRegexOrderChecker<>(new MethodOrderChecker()); 18 | } 19 | 20 | /** 21 | * Starts the builder initialized with a regular expression. 22 | *

23 | * Syntactic sugar for "{@link #regex()}.{@link #regexFromString(String)}". 24 | * @param regex The regular expression. 25 | * @return Instance of the builder initialized with the given regular expression. 26 | */ 27 | static ReadableRegex regex(String regex) { 28 | return regex().regexFromString(regex); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/ReadableRegexPattern.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | /** 9 | * Wrapper of {@link Pattern} with some extra useful methods. 10 | *

11 | * If you want to use methods from {@link Pattern} that are missing in this interface, you can use {@link #getUnderlyingPattern()} 12 | * to get the {@link Pattern} object. 13 | */ 14 | public interface ReadableRegexPattern { 15 | /** 16 | * Matches the regular expression to the text. See {@link Pattern#matcher(CharSequence)} for more information. 17 | * @param text The text to be matched. 18 | * @return {@link Matcher} 19 | */ 20 | Matcher matches(String text); 21 | 22 | /** 23 | * @param text The text to be matched. 24 | * @return {@code true} if the pattern matches the full text, else {@code false}. 25 | */ 26 | default boolean matchesTextExactly(String text) { 27 | return matches(text).matches(); 28 | } 29 | 30 | /** 31 | * @return All the {@link PatternFlag}s that are enabled on this pattern. 32 | */ 33 | Set enabledFlags(); 34 | 35 | /** 36 | * Returns a list with all the recorded groups. Note that only the groups that have been added using the methods 37 | * {@link GroupBuilder#startGroup(String)} or {@link SyntacticSugarBuilder#group(String, ReadableRegex)} are recorded. 38 | * If you have added your own group in any other way (for example: {@code regexFromString("(.*)"}), then the result 39 | * of this method will not be correct. 40 | * 41 | * @return List of all group names. If the name is null, it is an unnamed group. 42 | */ 43 | List groups(); 44 | 45 | /** 46 | * @return The number of groups used in this pattern. 47 | */ 48 | default int nrOfGroups() { 49 | return groups().size(); 50 | } 51 | 52 | /** 53 | * @return The wrapped {@link Pattern} object. 54 | */ 55 | Pattern getUnderlyingPattern(); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/RegexObjectInstantiation.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import io.github.ricoapon.readableregex.internal.instantiation.RegexObjectInstantiationImpl; 4 | 5 | /** 6 | * Class with methods that are used to instantiate objects based on regular expressions. 7 | */ 8 | public interface RegexObjectInstantiation { 9 | /** 10 | * Creates a new instance of an object of the given class. The following criteria must hold to make this work: 11 | *

    12 | *
  • If there is more than one constructor, exactly one constructor must be annotated with {@link javax.inject.Inject}.
  • 13 | *
  • There must exist a named group inside the pattern for each name of the constructor parameter.
  • 14 | *
  • All constructor parameters must be a primitive type, a boxed primitive type or {@link String}.
  • 15 | *
16 | * @param pattern The regular expression. 17 | * @param data The {@link String} containing the information in the format as defined in the {@code pattern}. 18 | * @param clazz The class of the object to instantiate. 19 | * @param The type of the object to instantiate. 20 | * @return Instance of {@link T}. 21 | */ 22 | static T instantiateObject(ReadableRegexPattern pattern, String data, Class clazz) { 23 | return new RegexObjectInstantiationImpl().constructObject(pattern, data, clazz); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/RegexObjectInstantiationException.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | /** 4 | * Exception that will be thrown when a new object could not be instantiated using methods in {@link RegexObjectInstantiation}. 5 | */ 6 | public class RegexObjectInstantiationException extends RuntimeException { 7 | /** 8 | * Constructor. 9 | * @param message The message of the exception. 10 | */ 11 | public RegexObjectInstantiationException(String message) { 12 | super(message); 13 | } 14 | 15 | /** 16 | * Constructor. 17 | * @param message The message of the exception. 18 | * @param cause The cause of the exception. 19 | */ 20 | public RegexObjectInstantiationException(String message, Throwable cause) { 21 | super(message, cause); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/StandaloneBlockBuilder.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | /** 4 | * Builder interface with all the methods that create standalone blocks in regular expressions. A standalone block 5 | * is something can be followed by a quantifier and is matched in its entirety. So for example: {@code \s} or 6 | * {@code (?:\Qa.c\E)}. But {@code ab} is not a standalone block, since adding the optional quantifier {@code ?}, for example, would make 7 | * the expression {@code ab?}. This is different from {@code (ab)?}). 8 | */ 9 | public interface StandaloneBlockBuilder> { 10 | /** 11 | * Appends the regular expression. The value is not changed or sanitized in any way. 12 | *

13 | * This should only be used as a last resort when other methods cannot satisfy the expression you are looking for. 14 | * To avoid issues with other methods, make sure to encapsulate your regex with an unnamed group. 15 | * @param regex The regular expression. 16 | * @return This builder. 17 | */ 18 | T regexFromString(String regex); 19 | 20 | /** 21 | * See {@link #add(ReadableRegexPattern)}. 22 | * @param regexBuilder The regular expression. 23 | * @return This builder. 24 | */ 25 | default T add(ReadableRegex regexBuilder) { 26 | return add(regexBuilder.build()); 27 | } 28 | 29 | /** 30 | * Appends the regular expression created using another builder instance to this builder. The regular expression 31 | * is surrounded in a non-capturing group {@code (?:...)}. 32 | * @param pattern The pattern. 33 | * @return This builder. 34 | */ 35 | T add(ReadableRegexPattern pattern); 36 | 37 | /** 38 | * Appends a literal expression. All metacharacters are escaped. 39 | * @param literalValue The value to add. 40 | * @return This builder. 41 | */ 42 | T literal(String literalValue); 43 | 44 | /** 45 | * Adds a digit. This is the same as {@code [0-9]}. 46 | * @return This builder. 47 | */ 48 | T digit(); 49 | 50 | /** 51 | * Adds a whitespace. This is the same as {@code \s}. 52 | * @return This builder. 53 | */ 54 | T whitespace(); 55 | 56 | /** 57 | * Adds a tab. This is the same as {@code \t}. 58 | * @return This builder. 59 | */ 60 | T tab(); 61 | 62 | /** 63 | * Adds either or block. This is the same as {@code (?:X|Y)}, where {@code X} and {@code Y} are given regular expressions. 64 | * @param regexBuilders Regular expressions for which one needs to match. 65 | * @return This builder. 66 | */ 67 | T oneOf(ReadableRegex... regexBuilders); 68 | 69 | /** 70 | * Adds a specified range. This is the same as {@code [a-z]}. 71 | *

72 | * Example: {@code range('a', 'f', '0', '9')} comes down to {@code [a-f0-9]}. 73 | * @param boundaries All the boundaries. You must supply an even amount of arguments. 74 | * @return This builder. 75 | */ 76 | T range(char... boundaries); 77 | 78 | /** 79 | * Adds a negated specified range. This is the same as {@code [^a-z]}. 80 | *

81 | * Example: {@code notInRange('a', 'f', '0', '9')} comes down to {@code [^a-f0-9]}. 82 | * @param boundaries All the boundaries. You must supply an even amount of arguments. 83 | * @return This builder. 84 | */ 85 | T notInRange(char... boundaries); 86 | 87 | /** 88 | * Adds a range with the specified characters. This is the same as {@code [...]}. 89 | *

90 | * Example: {@code anyCharacterOf("abc")} comes down to {@code [abc]}. 91 | * @param characters The characters to match. 92 | * @return This builder. 93 | */ 94 | T anyCharacterOf(String characters); 95 | 96 | /** 97 | * Adds a negated range with the specified characters. This is the same as {@code [^...]}. 98 | *

99 | * Example: {@code anyCharacterExcept("abc")} comes down to {@code [^abc]}. 100 | * @param characters The characters to match. 101 | * @return This builder. 102 | */ 103 | T anyCharacterExcept(String characters); 104 | 105 | /** 106 | * Adds a word character. This is the same as {@code \w}. 107 | * @return This builder. 108 | */ 109 | T wordCharacter(); 110 | 111 | /** 112 | * Adds a non word character. This is the same as {@code \W}. 113 | * @return This builder. 114 | */ 115 | T nonWordCharacter(); 116 | 117 | /** 118 | * Adds a word boundary. This is the same as {@code \b}. 119 | * @return This builder. 120 | */ 121 | T wordBoundary(); 122 | 123 | /** 124 | * Adds a non word boundary. This is the same as {@code \B}. 125 | * @return This builder. 126 | */ 127 | T nonWordBoundary(); 128 | 129 | /** 130 | * Adds any character. This is the same as {@code .}. 131 | *

132 | * Note that you have to enable {@link PatternFlag#DOT_ALL} to match line terminators. 133 | * @return This builder. 134 | */ 135 | T anyCharacter(); 136 | 137 | /** 138 | * Adds a start of line anchor. This is the same as {@code ^}. 139 | *

140 | * Note that this only works if the flag {@link PatternFlag#MULTILINE} is enabled. This is done automatically when 141 | * using this method. If you want to match the start of the input instead, please use {@link #startOfInput()}. 142 | * @return This builder. 143 | */ 144 | T startOfLine(); 145 | 146 | /** 147 | * Adds a start of input anchor. This is the same as {@code \A}. 148 | * @return This builder. 149 | */ 150 | T startOfInput(); 151 | 152 | /** 153 | * Adds an end of line anchor. This is the same as {@code $}. 154 | *

155 | * Note that this only works if the flag {@link PatternFlag#MULTILINE} is enabled. This is done automatically when 156 | * using this method. If you want to match the end of the input instead, please use {@link #endOfInput()}. 157 | * @return This builder. 158 | */ 159 | T endOfLine(); 160 | 161 | /** 162 | * Adds end of input anchor. This is the same as {@code \z}. 163 | * @return This builder. 164 | */ 165 | T endOfInput(); 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/SyntacticSugarBuilder.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 4 | 5 | /** 6 | * Builder interface with methods to enhance user experience using shortcut methods. 7 | */ 8 | public interface SyntacticSugarBuilder> extends StandaloneBlockBuilder, QuantifierBuilder, GroupBuilder { 9 | /* 10 | START of methods related to standalone blocks. 11 | */ 12 | 13 | /** 14 | * Matches a word. This is the same as {@code \w+} 15 | *

16 | * Syntactic sugar for "{@link #wordCharacter()}.{@link #oneOrMore()}". 17 | * @return This builder. 18 | */ 19 | default T word() { 20 | return wordCharacter().oneOrMore(); 21 | } 22 | 23 | /** 24 | * Matches anything. This is the same as {@code .*}. Note that you need to enable the flag {@link PatternFlag#DOT_ALL} 25 | * to also match new lines with this method. 26 | *

27 | * Syntactic sugar for "{@link #anyCharacter()}.{@link #zeroOrMore()}". 28 | * @return This builder. 29 | */ 30 | default T anything() { 31 | return anyCharacter().zeroOrMore(); 32 | } 33 | 34 | /** 35 | * Adds a universal line break. This is the same as {@code \r\n|\n}. 36 | * @return This builder. 37 | */ 38 | default T lineBreak() { 39 | return oneOf(regex("\\r\\n?"), regex("\\n")); 40 | } 41 | 42 | /* 43 | START of methods related to groups. 44 | */ 45 | 46 | /** 47 | * Adds a regular expression inside a group. 48 | *

49 | * Syntactic sugar for "{@link #startGroup()}.{@link #add(ReadableRegex)}.{@link #endGroup()}". 50 | * {@code .startGroup().add(regexBuilder).endGroup()}. 51 | * @param regexBuilder The regular expression. 52 | * @return This builder. 53 | */ 54 | default T group(ReadableRegex regexBuilder) { 55 | return startGroup().add(regexBuilder).endGroup(); 56 | } 57 | 58 | /** 59 | * Adds a regular expression inside a group. 60 | *

61 | * Syntactic sugar for "{@link #startGroup(String)}.{@link #add(ReadableRegex)}.{@link #endGroup()}". 62 | * @param groupName The name of the group. 63 | * @param regexBuilder The regular expression. 64 | * @return This builder. 65 | */ 66 | default T group(String groupName, ReadableRegex regexBuilder) { 67 | return startGroup(groupName).add(regexBuilder).endGroup(); 68 | } 69 | 70 | /** 71 | * Adds a regular expression inside a positive lookbehind block. 72 | *

73 | * Syntactic sugar for "{@link #startPositiveLookbehind()}.{@link #add(ReadableRegex)}.{@link #endGroup()}". 74 | * @param regexBuilder The regular expression. 75 | * @return This builder. 76 | */ 77 | default T positiveLookbehind(ReadableRegex regexBuilder) { 78 | return startPositiveLookbehind().add(regexBuilder).endGroup(); 79 | } 80 | 81 | /** 82 | * Adds a regular expression inside a positive lookbehind block. 83 | *

84 | * Syntactic sugar for "{@link #startNegativeLookbehind()}.{@link #add(ReadableRegex)}.{@link #endGroup()}". 85 | * @param regexBuilder The regular expression. 86 | * @return This builder. 87 | */ 88 | default T negativeLookbehind(ReadableRegex regexBuilder) { 89 | return startNegativeLookbehind().add(regexBuilder).endGroup(); 90 | } 91 | 92 | /** 93 | * Adds a regular expression inside a positive lookbehind block. 94 | *

95 | * Syntactic sugar for "{@link #startPositiveLookahead()}.{@link #add(ReadableRegex)}.{@link #endGroup()}". 96 | * @param regexBuilder The regular expression. 97 | * @return This builder. 98 | */ 99 | default T positiveLookahead(ReadableRegex regexBuilder) { 100 | return startPositiveLookahead().add(regexBuilder).endGroup(); 101 | } 102 | 103 | /** 104 | * Adds a regular expression inside a positive lookbehind block. 105 | *

106 | * Syntactic sugar for "{@link #startNegativeLookahead()}.{@link #add(ReadableRegex)}.{@link #endGroup()}". 107 | * @param regexBuilder The regular expression. 108 | * @return This builder. 109 | */ 110 | default T negativeLookahead(ReadableRegex regexBuilder) { 111 | return startNegativeLookahead().add(regexBuilder).endGroup(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/internal/MethodOrderChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal; 2 | 3 | import io.github.ricoapon.readableregex.FinishBuilder; 4 | import io.github.ricoapon.readableregex.GroupBuilder; 5 | import io.github.ricoapon.readableregex.IncorrectConstructionException; 6 | import io.github.ricoapon.readableregex.QuantifierBuilder; 7 | import io.github.ricoapon.readableregex.ReadableRegex; 8 | import io.github.ricoapon.readableregex.StandaloneBlockBuilder; 9 | 10 | /** 11 | * Class for maintaining the status of a building a regular expression and checking whether the execution order is valid. 12 | */ 13 | public class MethodOrderChecker { 14 | /** The types of methods that can be executed by {@link ReadableRegex}. */ 15 | enum Method { 16 | /** Methods from the interface {@link StandaloneBlockBuilder}. */ 17 | STANDALONE_BLOCK, 18 | /** Methods from the interface {@link QuantifierBuilder}. */ 19 | QUANTIFIER, 20 | /** Methods {@link QuantifierBuilder#reluctant()} and {@link QuantifierBuilder#possessive()}. */ 21 | RELUCTANT_OR_POSSESSIVE, 22 | /** Methods from the interface {@link FinishBuilder}. */ 23 | FINISH, 24 | /** Starting a group with a method from the interface {@link GroupBuilder}. */ 25 | START_GROUP, 26 | /** Ending a group with the method {@link GroupBuilder#endGroup()}. */ 27 | END_GROUP 28 | } 29 | 30 | /** Indicates if a quantifier is allowed as the next method. Start with {@code false}, because you cannot start with a quantifier. */ 31 | private boolean isQuantifierPossibleAfterThisMethod = false; 32 | 33 | /** Indicates if the previous method was {@link Method#QUANTIFIER}. */ 34 | private boolean wasPreviousMethodAQuantifier = false; 35 | 36 | /** Counts how many groups are started and are still left open. These must be closed before finishing. */ 37 | private int nrOfGroupsStarted = 0; 38 | 39 | /** 40 | * Checks if a method can be called. If not, it will throw an {@link IncorrectConstructionException}. 41 | * @param method The method to execute. 42 | */ 43 | public void checkCallingMethod(Method method) { 44 | if (method == Method.STANDALONE_BLOCK) { 45 | standaloneBlock(); 46 | } else if (method == Method.QUANTIFIER) { 47 | quantifier(); 48 | } else if (method == Method.RELUCTANT_OR_POSSESSIVE) { 49 | reluctantOrPossessive(); 50 | } else if (method == Method.FINISH) { 51 | finish(); 52 | } else if (method == Method.START_GROUP) { 53 | startGroup(); 54 | } else if (method == Method.END_GROUP) { 55 | endGroup(); 56 | } 57 | } 58 | 59 | private void standaloneBlock() { 60 | isQuantifierPossibleAfterThisMethod = true; 61 | wasPreviousMethodAQuantifier = false; 62 | } 63 | 64 | private void quantifier() { 65 | if (!isQuantifierPossibleAfterThisMethod) { 66 | throw new IncorrectConstructionException("You cannot add a quantifier after a quantifier. Remove one of the incorrect quantifiers. " + 67 | "Or, if you haven't done anything yet, you started with a quantifier. That is not possible."); 68 | } 69 | 70 | isQuantifierPossibleAfterThisMethod = false; 71 | wasPreviousMethodAQuantifier = true; 72 | } 73 | 74 | private void reluctantOrPossessive() { 75 | if (!wasPreviousMethodAQuantifier) { 76 | throw new IncorrectConstructionException("You can only use the reluctant or possessive method after a quantifier. " + 77 | "Remove the method call reluctant() or possessive(), or place it after a quantifier."); 78 | } 79 | 80 | isQuantifierPossibleAfterThisMethod = false; 81 | wasPreviousMethodAQuantifier = false; 82 | } 83 | 84 | private void finish() { 85 | if (nrOfGroupsStarted != 0) { 86 | throw new IncorrectConstructionException("You forgot to close all the groups that have started. Please close them all using the endGroup() method."); 87 | } 88 | } 89 | 90 | private void startGroup() { 91 | nrOfGroupsStarted++; 92 | isQuantifierPossibleAfterThisMethod = false; 93 | wasPreviousMethodAQuantifier = false; 94 | } 95 | 96 | private void endGroup() { 97 | if (nrOfGroupsStarted == 0) { 98 | throw new IncorrectConstructionException("You cannot close a group, since none have started. " + 99 | "Remove this method call or start a group."); 100 | } 101 | nrOfGroupsStarted--; 102 | isQuantifierPossibleAfterThisMethod = true; 103 | wasPreviousMethodAQuantifier = false; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/internal/ReadableRegexBuilder.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal; 2 | 3 | import io.github.ricoapon.readableregex.PatternFlag; 4 | import io.github.ricoapon.readableregex.ReadableRegex; 5 | import io.github.ricoapon.readableregex.ReadableRegexPattern; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Objects; 11 | import java.util.regex.Pattern; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * Implementation that builds the regular expressions. 16 | */ 17 | public abstract class ReadableRegexBuilder> implements ReadableRegex { 18 | /** The internal regular expression. This field should only be modified using the {@link #_addRegex(String)} method. */ 19 | private final StringBuilder regexBuilder = new StringBuilder(); 20 | 21 | /** Indicates whether the flag {@link PatternFlag#MULTILINE} should be enabled when building the pattern object. */ 22 | private boolean enableMultilineFlag = false; 23 | 24 | /** List of group names in order. If the name is {@code null}, it means it is an unnamed group. */ 25 | private final List groups = new ArrayList<>(); 26 | 27 | @SuppressWarnings("MagicConstant") 28 | @Override 29 | public ReadableRegexPattern buildWithFlags(PatternFlag... patternFlags) { 30 | int flags = Arrays.stream(patternFlags).map(PatternFlag::getJdkPatternFlagCode) 31 | .reduce(0, (integer, integer2) -> integer | integer2); 32 | 33 | // If we should enable multiline, make sure it is part of the flags variable. 34 | if (enableMultilineFlag && (flags & PatternFlag.MULTILINE.getJdkPatternFlagCode()) == 0) { 35 | flags = flags | PatternFlag.MULTILINE.getJdkPatternFlagCode(); 36 | } 37 | 38 | Pattern pattern = Pattern.compile(regexBuilder.toString(), flags); 39 | return new ReadableRegexPatternImpl(pattern, groups); 40 | } 41 | 42 | /** 43 | * @return {@code this} casted to {@code T}. 44 | */ 45 | private T thisT() { 46 | //noinspection unchecked 47 | return (T) this; 48 | } 49 | 50 | /** 51 | * Adds the regular expression to {@link #regexBuilder}. 52 | * @param regex The regular expression. 53 | * @return This builder. 54 | */ 55 | private T _addRegex(String regex) { 56 | Objects.requireNonNull(regex); 57 | regexBuilder.append(regex); 58 | return thisT(); 59 | } 60 | 61 | @Override 62 | public T regexFromString(String regex) { 63 | return _addRegex(regex); 64 | } 65 | 66 | @Override 67 | public T add(ReadableRegexPattern pattern) { 68 | Objects.requireNonNull(pattern); 69 | String regexToInclude = pattern.toString(); 70 | 71 | // Wrap in an unnamed group, to make sure that quantifiers work on the entire block. 72 | return _addRegex("(?:" + regexToInclude + ")"); 73 | } 74 | 75 | @Override 76 | public T literal(String literalValue) { 77 | Objects.requireNonNull(literalValue); 78 | // Surround input with \Q\E to make sure that all the meta characters are escaped. 79 | // Wrap in an unnamed group, to make sure that quantifiers work on the entire block. 80 | return _addRegex("(?:\\Q" + literalValue + "\\E)"); 81 | } 82 | 83 | @Override 84 | public T digit() { 85 | return _addRegex("\\d"); 86 | } 87 | 88 | @Override 89 | public T whitespace() { 90 | return _addRegex("\\s"); 91 | } 92 | 93 | @Override 94 | public T tab() { 95 | return _addRegex("\\t"); 96 | } 97 | 98 | @Override 99 | public T oneOf(ReadableRegex... regexBuilders) { 100 | String middlePart = Arrays.stream(regexBuilders) 101 | .map(ReadableRegex::build) 102 | .map(ReadableRegexPattern::toString) 103 | .collect(Collectors.joining("|")); 104 | 105 | return _addRegex("(?:" + middlePart + ")"); 106 | } 107 | 108 | @Override 109 | public T range(char... boundaries) { 110 | if (boundaries.length % 2 != 0) { 111 | throw new IllegalArgumentException("You have to supply an even amount of boundaries."); 112 | } else if (boundaries.length == 0) { 113 | throw new IllegalArgumentException("An empty range is pointless. Please supply boundaries!"); 114 | } 115 | 116 | StringBuilder expression = new StringBuilder("["); 117 | for (int i = 0; i < boundaries.length; i += 2) { 118 | expression.append(boundaries[i]) 119 | .append('-') 120 | .append(boundaries[i + 1]); 121 | } 122 | expression.append("]"); 123 | 124 | return _addRegex(expression.toString()); 125 | } 126 | 127 | @Override 128 | public T notInRange(char... boundaries) { 129 | if (boundaries.length % 2 != 0) { 130 | throw new IllegalArgumentException("You have to supply an even amount of boundaries."); 131 | } else if (boundaries.length == 0) { 132 | throw new IllegalArgumentException("An empty range is pointless. Please supply boundaries!"); 133 | } 134 | 135 | StringBuilder expression = new StringBuilder("[^"); 136 | for (int i = 0; i < boundaries.length; i += 2) { 137 | expression.append(boundaries[i]) 138 | .append('-') 139 | .append(boundaries[i + 1]); 140 | } 141 | expression.append("]"); 142 | 143 | return _addRegex(expression.toString()); 144 | } 145 | 146 | @Override 147 | public T anyCharacterOf(String characters) { 148 | Objects.requireNonNull(characters); 149 | if (characters.length() == 0) { 150 | throw new IllegalArgumentException("An empty range is pointless. Please supply boundaries!"); 151 | } 152 | 153 | return _addRegex("[" + characters + "]"); 154 | } 155 | 156 | @Override 157 | public T anyCharacterExcept(String characters) { 158 | Objects.requireNonNull(characters); 159 | if (characters.length() == 0) { 160 | throw new IllegalArgumentException("An empty range is pointless. Please supply boundaries!"); 161 | } 162 | 163 | return _addRegex("[^" + characters + "]"); 164 | } 165 | 166 | @Override 167 | public T wordCharacter() { 168 | return _addRegex("\\w"); 169 | } 170 | 171 | @Override 172 | public T nonWordCharacter() { 173 | return _addRegex("\\W"); 174 | } 175 | 176 | @Override 177 | public T wordBoundary() { 178 | return _addRegex("\\b"); 179 | } 180 | 181 | @Override 182 | public T nonWordBoundary() { 183 | return _addRegex("\\B"); 184 | } 185 | 186 | @Override 187 | public T anyCharacter() { 188 | return _addRegex("."); 189 | } 190 | 191 | @Override 192 | public T startOfLine() { 193 | enableMultilineFlag = true; 194 | // Surround with an unnamed group, to make sure that it can be followed up with quantifiers. 195 | return _addRegex("(?:^)"); 196 | } 197 | 198 | @Override 199 | public T startOfInput() { 200 | return _addRegex("\\A"); 201 | } 202 | 203 | @Override 204 | public T endOfLine() { 205 | enableMultilineFlag = true; 206 | // Surround with an unnamed group, to make sure that it can be followed up with quantifiers. 207 | return _addRegex("(?:$)"); 208 | } 209 | 210 | @Override 211 | public T endOfInput() { 212 | return _addRegex("\\z"); 213 | } 214 | 215 | @Override 216 | public T oneOrMore() { 217 | return _addRegex("+"); 218 | } 219 | 220 | @Override 221 | public T optional() { 222 | return _addRegex("?"); 223 | } 224 | 225 | @Override 226 | public T zeroOrMore() { 227 | return _addRegex("*"); 228 | } 229 | 230 | private T _countRange(int n, Integer m) { 231 | if (n < 0 || m <= 0) { 232 | throw new IllegalArgumentException("The number of times the block should repeat must be larger than zero."); 233 | } else if (n > m) { 234 | throw new IllegalArgumentException("The ranges of the repeating block must be valid. Please make n smaller than m."); 235 | } 236 | 237 | if (Integer.MAX_VALUE == m) { 238 | return _addRegex("{" + n + ",}"); 239 | } 240 | 241 | return _addRegex("{" + n + "," + m + "}"); 242 | } 243 | 244 | @Override 245 | public T exactlyNTimes(int n) { 246 | return _countRange(n, n); 247 | } 248 | 249 | @Override 250 | public T atLeastNTimes(int n) { 251 | return _countRange(n, Integer.MAX_VALUE); 252 | } 253 | 254 | @Override 255 | public T betweenNAndMTimes(int n, int m) { 256 | return _countRange(n, m); 257 | } 258 | 259 | @Override 260 | public T reluctant() { 261 | return _addRegex("?"); 262 | } 263 | 264 | @Override 265 | public T possessive() { 266 | return _addRegex("+"); 267 | } 268 | 269 | @Override 270 | public T startGroup() { 271 | groups.add(null); 272 | return _addRegex("("); 273 | } 274 | 275 | @Override 276 | public T startGroup(String groupName) { 277 | Objects.requireNonNull(groupName); 278 | if (!Pattern.matches("[a-zA-Z][a-zA-Z0-9]*", groupName)) { 279 | throw new IllegalArgumentException("The group name '" + groupName + "' is not valid: it should start with a letter " + 280 | "and only contain letters and digits."); 281 | } 282 | 283 | groups.add(groupName); 284 | return _addRegex("(?<" + groupName + ">"); 285 | } 286 | 287 | @Override 288 | public T startUnnamedGroup() { 289 | return _addRegex("(?:"); 290 | } 291 | 292 | @Override 293 | public T startPositiveLookbehind() { 294 | return _addRegex("(?<="); 295 | } 296 | 297 | @Override 298 | public T startNegativeLookbehind() { 299 | return _addRegex("(?> extends ReadableRegexBuilder { 14 | /** Object for maintaining the status of calling methods. */ 15 | private final MethodOrderChecker methodOrderChecker; 16 | 17 | /** 18 | * Constructor. 19 | * @param methodOrderChecker Object for checking that methods are called in the right order. 20 | */ 21 | public ReadableRegexOrderChecker(MethodOrderChecker methodOrderChecker) { 22 | this.methodOrderChecker = methodOrderChecker; 23 | } 24 | 25 | @Override 26 | public ReadableRegexPattern buildWithFlags(PatternFlag... patternFlags) { 27 | methodOrderChecker.checkCallingMethod(FINISH); 28 | return super.buildWithFlags(patternFlags); 29 | } 30 | 31 | @Override 32 | public T regexFromString(String regex) { 33 | // We are not actually sure that the regex is a standalone block. If we don't do this however, it is never possible 34 | // to add a quantifier after this block. I leave the user responsible for the outcome. 35 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 36 | return super.regexFromString(regex); 37 | } 38 | 39 | @Override 40 | public T add(ReadableRegexPattern pattern) { 41 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 42 | return super.add(pattern); 43 | } 44 | 45 | @Override 46 | public T literal(String literalValue) { 47 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 48 | return super.literal(literalValue); 49 | } 50 | 51 | @Override 52 | public T digit() { 53 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 54 | return super.digit(); 55 | } 56 | 57 | @Override 58 | public T whitespace() { 59 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 60 | return super.whitespace(); 61 | } 62 | 63 | @Override 64 | public T tab() { 65 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 66 | return super.tab(); 67 | } 68 | 69 | @Override 70 | public T oneOf(ReadableRegex... regexBuilders) { 71 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 72 | return super.oneOf(regexBuilders); 73 | } 74 | 75 | @Override 76 | public T range(char... boundaries) { 77 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 78 | return super.range(boundaries); 79 | } 80 | 81 | @Override 82 | public T notInRange(char... boundaries) { 83 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 84 | return super.notInRange(boundaries); 85 | } 86 | 87 | @Override 88 | public T anyCharacterOf(String characters) { 89 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 90 | return super.anyCharacterOf(characters); 91 | } 92 | 93 | @Override 94 | public T anyCharacterExcept(String characters) { 95 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 96 | return super.anyCharacterExcept(characters); 97 | } 98 | 99 | @Override 100 | public T wordCharacter() { 101 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 102 | return super.wordCharacter(); 103 | } 104 | 105 | @Override 106 | public T nonWordCharacter() { 107 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 108 | return super.nonWordCharacter(); 109 | } 110 | 111 | @Override 112 | public T wordBoundary() { 113 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 114 | return super.wordBoundary(); 115 | } 116 | 117 | @Override 118 | public T nonWordBoundary() { 119 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 120 | return super.nonWordBoundary(); 121 | } 122 | 123 | @Override 124 | public T anyCharacter() { 125 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 126 | return super.anyCharacter(); 127 | } 128 | 129 | @Override 130 | public T startOfLine() { 131 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 132 | return super.startOfLine(); 133 | } 134 | 135 | @Override 136 | public T startOfInput() { 137 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 138 | return super.startOfInput(); 139 | } 140 | 141 | @Override 142 | public T endOfLine() { 143 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 144 | return super.endOfLine(); 145 | } 146 | 147 | @Override 148 | public T endOfInput() { 149 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 150 | return super.endOfInput(); 151 | } 152 | 153 | @Override 154 | public T oneOrMore() { 155 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 156 | return super.oneOrMore(); 157 | } 158 | 159 | @Override 160 | public T optional() { 161 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 162 | return super.optional(); 163 | } 164 | 165 | @Override 166 | public T zeroOrMore() { 167 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 168 | return super.zeroOrMore(); 169 | } 170 | 171 | @Override 172 | public T exactlyNTimes(int n) { 173 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 174 | return super.exactlyNTimes(n); 175 | } 176 | 177 | @Override 178 | public T atLeastNTimes(int n) { 179 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 180 | return super.atLeastNTimes(n); 181 | } 182 | 183 | @Override 184 | public T betweenNAndMTimes(int n, int m) { 185 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 186 | return super.betweenNAndMTimes(n, m); 187 | } 188 | 189 | @Override 190 | public T reluctant() { 191 | methodOrderChecker.checkCallingMethod(RELUCTANT_OR_POSSESSIVE); 192 | return super.reluctant(); 193 | } 194 | 195 | @Override 196 | public T possessive() { 197 | methodOrderChecker.checkCallingMethod(RELUCTANT_OR_POSSESSIVE); 198 | return super.possessive(); 199 | } 200 | 201 | @Override 202 | public T startGroup() { 203 | methodOrderChecker.checkCallingMethod(START_GROUP); 204 | return super.startGroup(); 205 | } 206 | 207 | @Override 208 | public T startGroup(String groupName) { 209 | methodOrderChecker.checkCallingMethod(START_GROUP); 210 | return super.startGroup(groupName); 211 | } 212 | 213 | @Override 214 | public T startUnnamedGroup() { 215 | methodOrderChecker.checkCallingMethod(START_GROUP); 216 | return super.startUnnamedGroup(); 217 | } 218 | 219 | @Override 220 | public T startPositiveLookbehind() { 221 | methodOrderChecker.checkCallingMethod(START_GROUP); 222 | return super.startPositiveLookbehind(); 223 | } 224 | 225 | @Override 226 | public T startNegativeLookbehind() { 227 | methodOrderChecker.checkCallingMethod(START_GROUP); 228 | return super.startNegativeLookbehind(); 229 | } 230 | 231 | @Override 232 | public T startPositiveLookahead() { 233 | methodOrderChecker.checkCallingMethod(START_GROUP); 234 | return super.startPositiveLookahead(); 235 | } 236 | 237 | @Override 238 | public T startNegativeLookahead() { 239 | methodOrderChecker.checkCallingMethod(START_GROUP); 240 | return super.startNegativeLookahead(); 241 | } 242 | 243 | @Override 244 | public T endGroup() { 245 | methodOrderChecker.checkCallingMethod(END_GROUP); 246 | return super.endGroup(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/internal/ReadableRegexPatternImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal; 2 | 3 | import io.github.ricoapon.readableregex.PatternFlag; 4 | import io.github.ricoapon.readableregex.ReadableRegexPattern; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * Implementation of {@link ReadableRegexPattern}. 16 | */ 17 | public class ReadableRegexPatternImpl implements ReadableRegexPattern { 18 | private final Pattern pattern; 19 | 20 | /** Maps group index to the name. If the name is null, it means it is an unnamed group. */ 21 | private final List groups; 22 | 23 | public ReadableRegexPatternImpl(Pattern pattern, List groups) { 24 | this.pattern = pattern; 25 | this.groups = Collections.unmodifiableList(groups); 26 | } 27 | 28 | @Override 29 | public Matcher matches(String text) { 30 | return pattern.matcher(text); 31 | } 32 | 33 | @Override 34 | public Set enabledFlags() { 35 | return Arrays.stream(PatternFlag.values()) 36 | .filter(flag -> (pattern.flags() & flag.getJdkPatternFlagCode()) != 0) 37 | .collect(Collectors.toSet()); 38 | } 39 | 40 | @Override 41 | public List groups() { 42 | return groups; 43 | } 44 | 45 | @Override 46 | public Pattern getUnderlyingPattern() { 47 | return pattern; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return pattern.toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/internal/instantiation/ParameterInfo.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal.instantiation; 2 | 3 | /** 4 | * Container for information needed about parameters of a constructor. 5 | */ 6 | public class ParameterInfo { 7 | private final String name; 8 | private final Class type; 9 | 10 | public ParameterInfo(String name, Class type) { 11 | this.name = name; 12 | this.type = type; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public Class getType() { 20 | return type; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/internal/instantiation/RegexObjectInstantiationImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal.instantiation; 2 | 3 | import com.thoughtworks.paranamer.BytecodeReadingParanamer; 4 | import com.thoughtworks.paranamer.Paranamer; 5 | import io.github.ricoapon.readableregex.ReadableRegexPattern; 6 | import io.github.ricoapon.readableregex.RegexObjectInstantiation; 7 | import io.github.ricoapon.readableregex.RegexObjectInstantiationException; 8 | 9 | import javax.inject.Inject; 10 | import java.lang.reflect.Constructor; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.regex.Matcher; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * Implementation of {@link RegexObjectInstantiation}. 20 | * @param The type of the object to instantiate. 21 | */ 22 | public class RegexObjectInstantiationImpl implements RegexObjectInstantiation { 23 | /** 24 | * See {@link RegexObjectInstantiation#instantiateObject(ReadableRegexPattern, String, Class)}. 25 | * @param pattern The pattern. 26 | * @param data The data. 27 | * @param clazz The class of the object to instantiate. 28 | * @return Instance of {@link T}. 29 | */ 30 | public T constructObject(ReadableRegexPattern pattern, String data, Class clazz) { 31 | Constructor constructor = determineConstructorForInjection(clazz); 32 | List parameterInfoList = determineParameterNamesAndTypes(constructor); 33 | checkThatPatternHaveANamedGroupForEachParameter(pattern, clazz, parameterInfoList); 34 | Matcher matcher = createExactMatcher(pattern, data); 35 | Object[] constructorArgs = createConstructorArgs(parameterInfoList, matcher); 36 | return createNewInstance(constructor, constructorArgs); 37 | } 38 | 39 | /** 40 | * Finds the constructor we should use for instantiation. We have the following criteria: 41 | *

    42 | *
  • If there is only one constructor, use that one.
  • 43 | *
  • If there is more than one constructor, use the constructor that is annotated with {@link Inject}
  • . 44 | *
  • If there are multiple constructors annotated with {@link Inject}, throw an exception.
  • 45 | *
46 | * @param clazz The class of the object to instantiate. 47 | * @return The constructor. 48 | */ 49 | private Constructor determineConstructorForInjection(Class clazz) { 50 | if (clazz.getConstructors().length == 1) { 51 | return clazz.getConstructors()[0]; 52 | } 53 | 54 | List> validConstructors = Arrays.stream(clazz.getConstructors()) 55 | .filter(constructor -> constructor.isAnnotationPresent(Inject.class)) 56 | .collect(Collectors.toList()); 57 | 58 | if (validConstructors.size() > 1) { 59 | throw new RegexObjectInstantiationException("The class " + clazz.getName() + " has more than one constructor annotated " + 60 | "with @Inject. This is not possible. Fix your code by making sure at most one constructor is annotated with @Inject."); 61 | } 62 | 63 | return validConstructors.get(0); 64 | } 65 | 66 | /** 67 | * Creates a list containing the needed information about the constructor parameters. This contains: 68 | *
    69 | *
  • The name of the parameter.
  • 70 | *
  • The type of the parameter.
  • 71 | *
72 | * @param constructor The constructor. 73 | * @return List with {@link ParameterInfo}. 74 | */ 75 | private List determineParameterNamesAndTypes(Constructor constructor) { 76 | Paranamer paranamer = new BytecodeReadingParanamer(); 77 | 78 | String[] parameterNames = paranamer.lookupParameterNames(constructor); 79 | Class[] parameterTypes = constructor.getParameterTypes(); 80 | 81 | List parameterInfos = new ArrayList<>(); 82 | for (int i = 0; i < constructor.getParameters().length; i++) { 83 | parameterInfos.add(new ParameterInfo(parameterNames[i], parameterTypes[i])); 84 | } 85 | return parameterInfos; 86 | } 87 | 88 | /** 89 | * Checks that there exists a parameter name for which there does not exist a group in the pattern. 90 | * @param pattern The pattern. 91 | * @param clazz The class of the object to instantiate. 92 | * @param parameterInfoList The information about the parameters of the constructor. 93 | * @throws RegexObjectInstantiationException If the check failed. 94 | */ 95 | private void checkThatPatternHaveANamedGroupForEachParameter(ReadableRegexPattern pattern, Class clazz, List parameterInfoList) 96 | throws RegexObjectInstantiationException { 97 | for (ParameterInfo parameterInfo : parameterInfoList) { 98 | if (!pattern.groups().contains(parameterInfo.getName())) { 99 | throw new RegexObjectInstantiationException("The constructor of the class " + clazz.getName() + " has a parameter with the name '" + 100 | parameterInfo.getName() + "'. But this name does not occur in the given pattern. You can fix this by " + 101 | "changing the pattern to add a group with this name."); 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Creates a {@link Matcher} based on the given pattern and data. 108 | * @param pattern The pattern. 109 | * @param data The data. 110 | * @return {@link Matcher} for which {@link Matcher#matches()} is already called and returns {@code true}. 111 | * @throws RegexObjectInstantiationException If there is no exact match. 112 | */ 113 | private Matcher createExactMatcher(ReadableRegexPattern pattern, String data) throws RegexObjectInstantiationException { 114 | Matcher matcher = pattern.matches(data); 115 | if (!matcher.matches()) { 116 | throw new RegexObjectInstantiationException("The given pattern does not match the given string. Make sure to write your pattern " + 117 | "in such a way that you match the COMPLETE string."); 118 | } 119 | return matcher; 120 | } 121 | 122 | /** 123 | * Creates an array of objects that can be used to call the constructor. 124 | * @param parameterInfoList The information about the parameters of the constructor. 125 | * @param matcher The matcher containing the parameter values. 126 | * @return Constructor arguments. 127 | */ 128 | private Object[] createConstructorArgs(List parameterInfoList, Matcher matcher) { 129 | Object[] constructorArgs = new Object[parameterInfoList.size()]; 130 | int index = 0; 131 | 132 | for (ParameterInfo parameterInfo : parameterInfoList) { 133 | String argAsString = matcher.group(parameterInfo.getName()); 134 | constructorArgs[index] = StringConverter.convertStringTo(parameterInfo.getType(), argAsString); 135 | index++; 136 | } 137 | 138 | return constructorArgs; 139 | } 140 | 141 | /** 142 | * Creates an instance of type {@link T}. Throws an exception if anything goes wrong. 143 | * @param constructor The constructor. 144 | * @param constructorArgs The arguments of the constructor. 145 | * @return Instance of {@link T}. 146 | * @throws RegexObjectInstantiationException If the object could not be instantiated. 147 | */ 148 | private T createNewInstance(Constructor constructor, Object[] constructorArgs) throws RegexObjectInstantiationException { 149 | try { 150 | //noinspection unchecked 151 | return (T) constructor.newInstance(constructorArgs); 152 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 153 | throw new RegexObjectInstantiationException("Could not instantiate class.", e); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/internal/instantiation/StringConverter.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal.instantiation; 2 | 3 | import io.github.ricoapon.readableregex.RegexObjectInstantiationException; 4 | 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * Class with methods to help convert {@link String}s to any other type. 12 | */ 13 | public class StringConverter { 14 | private static final Map, Function> CONVERTER_MAP; 15 | 16 | static { 17 | Map, Function> map = new HashMap<>(); 18 | CONVERTER_MAP = Collections.unmodifiableMap(map); 19 | 20 | map.put(Byte.class, Byte::valueOf); 21 | map.put(byte.class, Byte::parseByte); 22 | map.put(Short.class, Short::valueOf); 23 | map.put(short.class, Short::parseShort); 24 | map.put(Integer.class, Integer::valueOf); 25 | map.put(int.class, Integer::parseInt); 26 | map.put(Long.class, Long::valueOf); 27 | map.put(long.class, Long::parseLong); 28 | map.put(Float.class, Float::valueOf); 29 | map.put(float.class, Float::parseFloat); 30 | map.put(Double.class, Double::valueOf); 31 | map.put(double.class, Double::parseDouble); 32 | map.put(Boolean.class, Boolean::valueOf); 33 | map.put(boolean.class, Boolean::parseBoolean); 34 | map.put(Character.class, (s) -> s.charAt(0)); 35 | map.put(char.class, (s) -> s.charAt(0)); 36 | map.put(String.class, Function.identity()); 37 | } 38 | 39 | /** 40 | * Converts a {@link String} to an object of a specified class. 41 | * @param clazz The class of the object to convert to. 42 | * @param s The {@link String} to convert. 43 | * @param The type of the object to convert to. 44 | * @return The converted object. 45 | */ 46 | public static T convertStringTo(Class clazz, String s) { 47 | if (!CONVERTER_MAP.containsKey(clazz)) { 48 | throw new RegexObjectInstantiationException("Injecting an object of class " + clazz.getName() + " is not supported."); 49 | } 50 | 51 | //noinspection unchecked 52 | return (T) CONVERTER_MAP.get(clazz).apply(s); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/internal/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal implementation of the interfaces. 3 | */ 4 | package io.github.ricoapon.readableregex.internal; 5 | -------------------------------------------------------------------------------- /src/main/java/io/github/ricoapon/readableregex/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * All the public interfaces and classes. 3 | */ 4 | package io.github.ricoapon.readableregex; 5 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/Constants.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | /** 4 | * Class with useful constants that can be used in test cases. 5 | */ 6 | public class Constants { 7 | public final static String A_TO_Z_LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; 8 | public final static String A_TO_Z_UPPERCASE = A_TO_Z_LOWERCASE.toUpperCase(); 9 | public final static String WORD_CHARACTERS = A_TO_Z_LOWERCASE + A_TO_Z_UPPERCASE + "_"; 10 | public static final String DIGITS = "0123456789"; 11 | public static final String WHITESPACES = " \t\n\f\r"; 12 | public static final String NON_LETTERS = ";'[]{}|?/"; 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/FinishTests.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.Matchers.equalTo; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | /** 12 | * Tests related to methods that are inside {@link FinishBuilder}. 13 | */ 14 | @SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT", justification = "Lambda inside throwNpeWhenTextToMatchIsNull " + 15 | "triggers SpotBugs, but we know we don't use the return value. We expect to throw an NPE. " + 16 | "For some reason, this only has to be ignored after commit f1a8d20a9611d1f940823e16721d6efaeb4d16ed. Before the " + 17 | "(unrelated!) code was committed, this did not happen.") 18 | class FinishTests { 19 | private final static String REGEX = "a1?"; 20 | private final ReadableRegex readableRegex = regex(REGEX); 21 | 22 | @Test 23 | void underlyingPatternIsExposed() { 24 | assertThat(readableRegex.buildJdkPattern().toString(), equalTo(REGEX)); 25 | assertThat(readableRegex.build().getUnderlyingPattern().toString(), equalTo(REGEX)); 26 | } 27 | 28 | @Test 29 | void toStringReturnsPattern() { 30 | assertThat(readableRegex.build().toString(), equalTo(REGEX)); 31 | } 32 | 33 | @Test 34 | void throwNpeWhenTextToMatchIsNull() { 35 | assertThrows(NullPointerException.class, () -> readableRegex.build().matches(null)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/GroupTests.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.ValueSource; 6 | 7 | import java.util.regex.Matcher; 8 | 9 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 10 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.doesntMatchAnythingFrom; 11 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.matchesSomethingFrom; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.equalTo; 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | 16 | /** 17 | * Tests related to methods inside {@link GroupBuilder}. 18 | */ 19 | class GroupTests { 20 | @Test 21 | void nestedGroupsAreCaptured() { 22 | // Test using start/end syntax. 23 | ReadableRegexPattern pattern = regex().digit().startGroup().digit().startGroup().digit().endGroup().endGroup().build(); 24 | Matcher matcher = pattern.matches("123"); 25 | 26 | assertThat(matcher.matches(), equalTo(true)); 27 | assertThat(matcher.group(1), equalTo("23")); 28 | assertThat(matcher.group(2), equalTo("3")); 29 | } 30 | 31 | @Test 32 | void nestedNamedGroupsAreCaptured() { 33 | // Test using start/end syntax. 34 | String firstGroupName = "first"; 35 | String secondGroupName = "second"; 36 | ReadableRegexPattern pattern = regex().digit().startGroup(firstGroupName).digit().startGroup(secondGroupName).digit().endGroup().endGroup().build(); 37 | Matcher matcher = pattern.matches("123"); 38 | 39 | assertThat(matcher.matches(), equalTo(true)); 40 | assertThat(matcher.group(firstGroupName), equalTo("23")); 41 | assertThat(matcher.group(secondGroupName), equalTo("3")); 42 | } 43 | 44 | @ParameterizedTest 45 | @ValueSource(strings = {"0a", "a[", "a_", ""}) 46 | void invalidGroupNameThrowsIllegalArgumentException(String groupName) { 47 | assertThrows(IllegalArgumentException.class, () -> regex().startGroup(groupName)); 48 | } 49 | 50 | @Test 51 | void unnamedGroupsDontCapture() { 52 | ReadableRegexPattern pattern = regex().startUnnamedGroup().digit().endGroup() 53 | .group(regex().digit()).build(); 54 | 55 | Matcher matcher = pattern.matches("12"); 56 | assertThat(matcher.matches(), equalTo(true)); 57 | assertThat(matcher.group(1), equalTo("2")); 58 | } 59 | 60 | @Test 61 | void lookbehindsWork() { 62 | // Test using start/end syntax. 63 | ReadableRegexPattern pattern = regex().startPositiveLookbehind().digit().endGroup().whitespace().build(); 64 | assertThat(pattern, matchesSomethingFrom("1 ")); 65 | assertThat(pattern, doesntMatchAnythingFrom(" ")); 66 | 67 | // Test using start/end syntax. 68 | pattern = regex().startNegativeLookbehind().digit().endGroup().whitespace().build(); 69 | assertThat(pattern, doesntMatchAnythingFrom("1 ")); 70 | assertThat(pattern, matchesSomethingFrom(" ")); 71 | } 72 | 73 | @Test 74 | void lookaheadsWork() { 75 | // Test using start/end syntax. 76 | ReadableRegexPattern pattern = regex().whitespace().startPositiveLookahead().digit().endGroup().build(); 77 | assertThat(pattern, matchesSomethingFrom(" 1")); 78 | assertThat(pattern, doesntMatchAnythingFrom(" ")); 79 | 80 | // Test using start/end syntax. 81 | pattern = regex().whitespace().startNegativeLookahead().digit().endGroup().build(); 82 | assertThat(pattern, doesntMatchAnythingFrom(" 1")); 83 | assertThat(pattern, matchesSomethingFrom(" ")); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/PatternFlagTests.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 6 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.*; 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.Matchers.containsInAnyOrder; 9 | import static org.hamcrest.Matchers.empty; 10 | 11 | /** 12 | * Tests related to enabling specific pattern flags. 13 | */ 14 | public class PatternFlagTests { 15 | @Test 16 | void byDefaultNoFlagsAreEnabled() { 17 | ReadableRegexPattern pattern = regex().build(); 18 | 19 | assertThat(pattern.enabledFlags(), empty()); 20 | } 21 | 22 | @Test 23 | void enabledFlagsAreReturned() { 24 | ReadableRegexPattern pattern = regex().buildWithFlags(PatternFlag.CASE_INSENSITIVE, PatternFlag.DOT_ALL); 25 | 26 | assertThat(pattern.enabledFlags(), containsInAnyOrder(PatternFlag.CASE_INSENSITIVE, PatternFlag.DOT_ALL)); 27 | } 28 | 29 | @Test 30 | void caseInsensitiveWorks() { 31 | // No flag means case sensitive matches. 32 | ReadableRegexPattern pattern = regex().literal("a").build(); 33 | assertThat(pattern, matchesExactly("a")); 34 | assertThat(pattern, doesntMatchAnythingFrom("A")); 35 | 36 | pattern = regex().literal("a").buildWithFlags(PatternFlag.CASE_INSENSITIVE); 37 | assertThat(pattern, matchesExactly("a")); 38 | assertThat(pattern, matchesExactly("A")); 39 | } 40 | 41 | @Test 42 | void dotAllWorks() { 43 | // No flag means .* does not match new line symbols. 44 | ReadableRegexPattern pattern = regex().anything().build(); 45 | assertThat(pattern, doesntMatchExactly("a\na")); 46 | 47 | pattern = regex().anything().buildWithFlags(PatternFlag.DOT_ALL); 48 | assertThat(pattern, matchesExactly("a\na")); 49 | } 50 | 51 | @Test 52 | void multilineWorks() { 53 | // No flag means ^ matches the start of the entire input. 54 | ReadableRegexPattern pattern = regex().regexFromString("^a").build(); 55 | assertThat(pattern, doesntMatchAnythingFrom("\na")); 56 | 57 | pattern = regex().regexFromString("^a").buildWithFlags(PatternFlag.MULTILINE); 58 | assertThat(pattern, matchesSomethingFrom("\na")); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/QuantifierTests.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.regex.Matcher; 6 | 7 | import static io.github.ricoapon.readableregex.Constants.DIGITS; 8 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 9 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.doesntMatchExactly; 10 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.matchesExactly; 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.Matchers.equalTo; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | 15 | /** 16 | * Tests related to methods that are inside {@link QuantifierBuilder}. 17 | */ 18 | class QuantifierTests { 19 | @Test 20 | void oneOrMoreMatchesCorrectly() { 21 | ReadableRegexPattern pattern = regex().digit().oneOrMore().build(); 22 | 23 | assertThat(pattern, matchesExactly(DIGITS)); 24 | assertThat(pattern, doesntMatchExactly("")); 25 | } 26 | 27 | @Test 28 | void optionalMatchesCorrectly() { 29 | ReadableRegexPattern pattern = regex().literal("a").digit().optional().build(); 30 | 31 | assertThat(pattern, matchesExactly("a1")); 32 | assertThat(pattern, matchesExactly("a")); 33 | assertThat(pattern, doesntMatchExactly("")); 34 | } 35 | 36 | @Test 37 | void zeroOrMoreMatchesCorrectly() { 38 | ReadableRegexPattern pattern = regex().digit().zeroOrMore().build(); 39 | 40 | assertThat(pattern, matchesExactly("")); 41 | assertThat(pattern, matchesExactly("1")); 42 | assertThat(pattern, matchesExactly("11111")); 43 | assertThat(pattern, doesntMatchExactly("a")); 44 | } 45 | 46 | @Test 47 | void countQuantifiersMatchCorrectly() { 48 | ReadableRegexPattern pattern = regex() 49 | .literal("a").exactlyNTimes(2) 50 | .literal("b").atLeastNTimes(2) 51 | .literal("c").atMostNTimes(2) 52 | .literal("d").betweenNAndMTimes(1, 3) 53 | .build(); 54 | 55 | assertThat(pattern, matchesExactly("aabbccdd")); 56 | assertThat(pattern, matchesExactly("aabbbddd")); 57 | assertThat(pattern, doesntMatchExactly("abbbddd")); 58 | assertThat(pattern, doesntMatchExactly("aaabbbddd")); 59 | assertThat(pattern, doesntMatchExactly("aabddd")); 60 | assertThat(pattern, doesntMatchExactly("aabbcccddd")); 61 | assertThat(pattern, doesntMatchExactly("aabbccc")); 62 | assertThat(pattern, doesntMatchExactly("aabbcccdddd")); 63 | } 64 | 65 | @Test 66 | void countQuantifiersThrowIaeForInvalidArguments() { 67 | assertThrows(IllegalArgumentException.class, () -> regex().digit().exactlyNTimes(-1)); 68 | assertThrows(IllegalArgumentException.class, () -> regex().digit().exactlyNTimes(0)); 69 | regex().digit().exactlyNTimes(1); 70 | 71 | assertThrows(IllegalArgumentException.class, () -> regex().digit().atLeastNTimes(-1)); 72 | regex().digit().atLeastNTimes(0); 73 | regex().digit().atLeastNTimes(1); 74 | 75 | assertThrows(IllegalArgumentException.class, () -> regex().digit().betweenNAndMTimes(10, 1)); 76 | assertThrows(IllegalArgumentException.class, () -> regex().digit().betweenNAndMTimes(-4, -5)); 77 | assertThrows(IllegalArgumentException.class, () -> regex().digit().betweenNAndMTimes(-1, 4)); 78 | regex().digit().betweenNAndMTimes(0, 4); 79 | 80 | assertThrows(IllegalArgumentException.class, () -> regex().digit().atMostNTimes(-1)); 81 | assertThrows(IllegalArgumentException.class, () -> regex().digit().atMostNTimes(0)); 82 | regex().digit().atMostNTimes(1); 83 | } 84 | 85 | @Test 86 | void greedyReluctantPossessiveWorksCorrectly() { 87 | ReadableRegexPattern patternGreedy = regex().anything().literal("foo").build(); 88 | ReadableRegexPattern patternReluctant = regex().anything().reluctant().literal("foo").build(); 89 | ReadableRegexPattern patternPossessive = regex().anything().possessive().literal("foo").build(); 90 | 91 | String text = "xfooxxxxxxfoo"; 92 | assertThat(patternGreedy, matchesExactly(text)); 93 | 94 | Matcher matcher = patternReluctant.matches(text); 95 | assertThat(matcher.find(), equalTo(true)); 96 | assertThat(matcher.group(), equalTo("xfoo")); 97 | assertThat(matcher.find(), equalTo(true)); 98 | assertThat(matcher.group(), equalTo("xxxxxxfoo")); 99 | 100 | assertThat(patternPossessive, doesntMatchExactly(text)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/ReadableRegexPatternTest.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.contains; 8 | import static org.hamcrest.Matchers.equalTo; 9 | 10 | class ReadableRegexPatternTest { 11 | @Test 12 | void matchesExactlyWorks() { 13 | ReadableRegexPattern pattern = regex().digit().build(); 14 | 15 | assertThat(pattern.matchesTextExactly("1"), equalTo(true)); 16 | assertThat(pattern.matchesTextExactly("a"), equalTo(false)); 17 | } 18 | 19 | @Test 20 | void groupsAreRecordedInTheCorrectOrder_and_unnamedGroupsAreNull_and_numberOfGroupsCountsAllGroupsg() { 21 | ReadableRegexPattern pattern = regex() 22 | .group("first", regex().digit()) 23 | .group(regex().digit()) 24 | .group("third", regex().digit()) 25 | .build(); 26 | 27 | assertThat(pattern.groups(), contains("first", null, "third")); 28 | assertThat(pattern.nrOfGroups(), equalTo(3)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/ReadmeTests.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import io.github.ricoapon.readableregex.matchers.PatternMatchMatcher; 5 | import org.junit.jupiter.api.Nested; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.inject.Inject; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 13 | import static io.github.ricoapon.readableregex.RegexObjectInstantiation.instantiateObject; 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.contains; 16 | import static org.hamcrest.Matchers.equalTo; 17 | 18 | /** 19 | * All the code in the README should be identical to the code in this file. This way, we make sure that the code in the 20 | * README compiles and works as expected. 21 | *

22 | * Note that we cannot use the class {@link PatternMatchMatcher}. It should be possible to compile all the examples in 23 | * any project that is using Hamcrest as well. 24 | *

25 | * Also note that we cannot use these tests for coverage itself. These examples are ONLY meant for the README. The tests 26 | * will overlap with other tests, that is ok. 27 | */ 28 | @SuppressFBWarnings(value = "SIC_INNER_SHOULD_BE_STATIC", justification = "@Nested classes should be non-static, but SpotBugs wants them static." + 29 | "See https://github.com/spotbugs/spotbugs/issues/560 for the bug (open since 2018).") 30 | class ReadmeTests { 31 | @Nested 32 | class Examples { 33 | @Test 34 | void example1() { 35 | ReadableRegexPattern pattern = 36 | regex() // Always start with the regex method to start the builder. 37 | .literal("http") // Literals are escaped automatically, no need to do this yourself. 38 | .literal("s").optional() // You can follow up with optional to make the "s" optional. 39 | .literal("://") 40 | .anyCharacterExcept(" ").zeroOrMore() // This comes down to [^ ]*. 41 | .build(); // Create the pattern with the final method. 42 | 43 | // The matchesText will return a boolean whether we have an *exact* match or not! 44 | assertThat(pattern.matchesTextExactly("https://www.github.com"), equalTo(true)); 45 | 46 | // toString() method will return the underlying pattern. Not really readable though, that is why we have this library! 47 | assertThat(pattern.toString(), equalTo("(?:\\Qhttp\\E)(?:\\Qs\\E)?(?:\\Q://\\E)[^ ]*")); 48 | } 49 | 50 | @Test 51 | void example2() { 52 | ReadableRegexPattern pattern = regex() 53 | .startGroup() // You can use this method to start capturing the expression inside a group. 54 | .word() 55 | .endGroup() // This ends the last group. 56 | .whitespace() 57 | .startGroup("secondWord") // You can also give names to your group. 58 | .word() 59 | .endGroup() 60 | .build(); 61 | 62 | Matcher matcher = pattern.matches("abc def"); 63 | assertThat(matcher.matches(), equalTo(true)); 64 | 65 | // Groups can always be found based on the order they are used. 66 | assertThat(matcher.group(1), equalTo("abc")); 67 | assertThat(matcher.group(2), equalTo("def")); 68 | 69 | // If you need details about the groups afterwards, this is not possible using the JDK Pattern. 70 | // However, using this library, this is now possible: 71 | assertThat(pattern.groups(), contains(null, "secondWord")); 72 | 73 | // If you have given the group a name, you can also find it based on the name. 74 | assertThat(matcher.group("secondWord"), equalTo("def")); 75 | } 76 | 77 | @Test 78 | void example3() { 79 | // It does not matter if you have already built the pattern, you can include it anyway. 80 | ReadableRegex digits = regex().startGroup().digit().oneOrMore().endGroup().whitespace(); 81 | ReadableRegexPattern word = regex().startGroup().word().endGroup().whitespace().build(); 82 | 83 | ReadableRegexPattern pattern = regex() 84 | .add(digits) 85 | .add(digits) 86 | .add(word) 87 | .add(digits) 88 | .literal("END") 89 | .build(); 90 | 91 | Matcher matcher = pattern.matches("12\t11\thello\t0000\tEND"); 92 | assertThat(matcher.matches(), equalTo(true)); 93 | // Note that captures are always a String! 94 | assertThat(matcher.group(1), equalTo("12")); 95 | assertThat(matcher.group(2), equalTo("11")); 96 | assertThat(matcher.group(3), equalTo("hello")); 97 | assertThat(matcher.group(4), equalTo("0000")); 98 | } 99 | 100 | @Test 101 | void example4() { 102 | ReadableRegexPattern pattern = regex() 103 | .oneOf(regex().literal("abc"), regex().digit()) // The oneOf method represents "or". 104 | .whitespace() 105 | // If we want to add a quantifier over a larger expression, we can encapsulate it with the add method, 106 | // which encloses the expression in an unnamed group. 107 | .add(regex().literal("a").digit()).exactlyNTimes(3) 108 | .whitespace() 109 | // Alternatively, you can use the startUnnamedGroup() for this to avoid nested structures. 110 | .startUnnamedGroup().literal("b").digit().endGroup().atMostNTimes(2) 111 | .build(); 112 | 113 | System.out.println(pattern.toString()); 114 | assertThat(pattern.matchesTextExactly("abc a1a2a3 b2"), equalTo(true)); 115 | assertThat(pattern.matchesTextExactly("1 a3a6a9 "), equalTo(true)); 116 | } 117 | } 118 | 119 | @Nested 120 | class Quantifiers { 121 | @Test 122 | void example1() { 123 | ReadableRegexPattern greedyPattern = regex().anything().literal("foo").build(); 124 | ReadableRegexPattern reluctantPattern = regex().anything().reluctant().literal("foo").build(); 125 | ReadableRegexPattern possessivePattern = regex().anything().possessive().literal("foo").build(); 126 | 127 | String text = "xfooxxxxxxfoo"; 128 | assertThat(greedyPattern.matchesTextExactly(text), equalTo(true)); 129 | 130 | Matcher matcher = reluctantPattern.matches(text); 131 | assertThat(matcher.find(), equalTo(true)); 132 | assertThat(matcher.group(), equalTo("xfoo")); 133 | assertThat(matcher.find(), equalTo(true)); 134 | assertThat(matcher.group(), equalTo("xxxxxxfoo")); 135 | 136 | matcher = possessivePattern.matches(text); 137 | assertThat(matcher.find(), equalTo(false)); 138 | } 139 | } 140 | 141 | @Nested 142 | class WorkingAroundTheLimitsOfTheLibrary { 143 | @Test 144 | void example1() { 145 | ReadableRegexPattern pattern1 = regex() 146 | .regexFromString("[a-z&&[^p]]") // With this method, you can add any kind of expression. 147 | .build(); 148 | 149 | // Or you could use the overloaded variant of the regex method, which is the same: 150 | ReadableRegexPattern pattern2 = regex("[a-z&&[^p]]").build(); 151 | 152 | assertThat(pattern1.matchesTextExactly("p"), equalTo(false)); 153 | assertThat(pattern1.matchesTextExactly("c"), equalTo(true)); 154 | assertThat(pattern2.matchesTextExactly("p"), equalTo(false)); 155 | assertThat(pattern2.matchesTextExactly("c"), equalTo(true)); 156 | } 157 | 158 | @Test 159 | void example2() { 160 | ReadableRegexPattern pattern = regex().literal(".").build(); 161 | 162 | Pattern jdkPattern = pattern.getUnderlyingPattern(); 163 | assertThat(jdkPattern.split("a.b.c"), equalTo(new String[]{"a", "b", "c"})); 164 | } 165 | } 166 | 167 | // You have to extend from ExtendableReadableRegex, where you fill in your own class as generic type. 168 | public static class TestExtension extends ExtendableReadableRegex { 169 | 170 | // It is highly advised to create your own static method "regex()". This way you can easily instantiate 171 | // your class and in your existing code you only have to change your import statement. 172 | public static TestExtension regex() { 173 | return new TestExtension(); 174 | } 175 | 176 | // In your own extension you can add any method you like. 177 | public TestExtension digitWhitespaceDigit() { 178 | // For the implementation of your extension, you can only use the publicly available methods. All variables 179 | // and other methods are made private in the instance. 180 | // If you want to add arbitrary expressions, you can always use the method "regexFromString(...)". 181 | return digit().whitespace().digit(); 182 | } 183 | 184 | // You can also override existing methods! To make sure that the code doesn't break, please always end 185 | // with calling the super method. 186 | @Override 187 | public ReadableRegexPattern buildWithFlags(PatternFlag... patternFlags) { 188 | return super.buildWithFlags(PatternFlag.DOT_ALL); 189 | } 190 | } 191 | 192 | /** 193 | * Note that this code occurs in both the README and in the Javadoc of {@link ExtendableReadableRegex}. 194 | * Don't forget to change both if you make any changes to this code! 195 | */ 196 | @Nested 197 | class ExtendingTheBuilder { 198 | @Test 199 | void example1() { 200 | ReadableRegexPattern pattern = TestExtension.regex().digitWhitespaceDigit().build(); 201 | 202 | assertThat(pattern.matchesTextExactly("1 3"), equalTo(true)); 203 | assertThat(pattern.enabledFlags(), contains(PatternFlag.DOT_ALL)); 204 | } 205 | } 206 | 207 | public static class MyPojo { 208 | public final String name; 209 | public final int id; 210 | 211 | @Inject // If you have only one constructor, you can leave this out. 212 | public MyPojo(String name, int id) { 213 | this.name = name; 214 | this.id = id; 215 | } 216 | } 217 | 218 | @Nested 219 | class InstantiatingObjects { 220 | @Test 221 | void example() { 222 | String data = "name: example, id: 15"; 223 | ReadableRegexPattern pattern = regex() 224 | .literal("name: ").group("name", regex().word()) 225 | .literal(", id: ").group("id", regex().digit().oneOrMore()).build(); 226 | 227 | MyPojo myPojo = instantiateObject(pattern, data, MyPojo.class); 228 | 229 | assertThat(myPojo.name, equalTo("example")); 230 | // Types are automatically converted! 231 | assertThat(myPojo.id, equalTo(15)); 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/StandaloneBlockTests.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import org.junit.jupiter.api.Nested; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static io.github.ricoapon.readableregex.Constants.*; 8 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 9 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.*; 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.Matchers.contains; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | 14 | /** 15 | * Tests related to methods that are inside {@link StandaloneBlockBuilder}. 16 | */ 17 | @SuppressFBWarnings(value = "SIC_INNER_SHOULD_BE_STATIC", justification = "@Nested classes should be non-static, but SpotBugs wants them static." + 18 | "See https://github.com/spotbugs/spotbugs/issues/560 for the bug (open since 2018).") 19 | class StandaloneBlockTests { 20 | @Nested 21 | class RegexFromString { 22 | @Test 23 | void nullAsArgumentThrowsNpe() { 24 | assertThrows(NullPointerException.class, () -> regex().regexFromString(null)); 25 | } 26 | 27 | @Test 28 | void inputIsNotSurroundedWithUnnamedGroup() { 29 | ReadableRegexPattern pattern = regex().regexFromString("ab").oneOrMore().build(); 30 | 31 | assertThat(pattern, matchesExactly("abbbb")); 32 | assertThat(pattern, doesntMatchExactly("aaabbb")); 33 | assertThat(pattern, doesntMatchAnythingFrom("a")); 34 | } 35 | } 36 | 37 | @Nested 38 | class Add { 39 | @SuppressWarnings("ConstantConditions") 40 | @Test 41 | void nullAsArgumentThrowsNpe() { 42 | assertThrows(NullPointerException.class, () -> regex().add((ReadableRegex) null)); 43 | assertThrows(NullPointerException.class, () -> regex().add((ReadableRegexPattern) null)); 44 | } 45 | 46 | @Test 47 | void inputIsCapturedInUnnamedGroup() { 48 | ReadableRegexPattern pattern = regex().add(regex().literal("a").digit()).oneOrMore().build(); 49 | 50 | assertThat(pattern, matchesExactly("a1a2a3")); 51 | assertThat(pattern, doesntMatchExactly("a111")); 52 | 53 | // Same test, but now applying "build()". 54 | pattern = regex().add(regex().literal("a").digit().build()).oneOrMore().build(); 55 | 56 | assertThat(pattern, matchesExactly("a1a2a3")); 57 | assertThat(pattern, doesntMatchExactly("a111")); 58 | } 59 | } 60 | 61 | @Nested 62 | class Digits { 63 | @Test 64 | void digitOnlyMatchesDigits() { 65 | ReadableRegexPattern pattern = regex().digit().build(); 66 | 67 | // Matches exactly every character inside DIGITS. 68 | for (String digit : DIGITS.split("")) { 69 | assertThat(pattern, matchesExactly(digit)); 70 | } 71 | assertThat(pattern, doesntMatchAnythingFrom(WORD_CHARACTERS)); 72 | assertThat(pattern, doesntMatchAnythingFrom(NON_LETTERS)); 73 | assertThat(pattern, doesntMatchAnythingFrom(WHITESPACES)); 74 | } 75 | } 76 | 77 | @Nested 78 | class Literals { 79 | @Test 80 | void nullAsArgumentThrowsNpe() { 81 | assertThrows(NullPointerException.class, () -> regex().literal(null)); 82 | } 83 | 84 | @Test 85 | void literalCharactersAreEscaped() { 86 | ReadableRegexPattern pattern = regex() 87 | .literal("a.()[]\\/|?.+*") 88 | .build(); 89 | 90 | assertThat(pattern, matchesExactly("a.()[]\\/|?.+*")); 91 | } 92 | 93 | @Test 94 | void literalCanBeCombinedWithMetaCharacters() { 95 | ReadableRegexPattern pattern = regex() 96 | .literal(".").digit().whitespace().literal("*") 97 | .build(); 98 | 99 | assertThat(pattern, matchesExactly(".1 *")); 100 | assertThat(pattern, matchesExactly(".2\t*")); 101 | assertThat(pattern, doesntMatchExactly("a1 *")); 102 | } 103 | 104 | @Test 105 | void literalsAreStandaloneBlocks() { 106 | ReadableRegexPattern pattern = regex().digit().literal("a").oneOrMore().build(); 107 | 108 | assertThat(pattern, matchesExactly("1aaaa")); 109 | assertThat(pattern, doesntMatchExactly("1a1a")); 110 | } 111 | } 112 | 113 | @Nested 114 | class Whitespace { 115 | @Test 116 | void whitespaceOnlyMatchesWhitespaces() { 117 | ReadableRegexPattern pattern = regex().whitespace().build(); 118 | 119 | // Matches exactly every character inside WHITESPACES. 120 | for (String digit : WHITESPACES.split("")) { 121 | assertThat(pattern, matchesExactly(digit)); 122 | } 123 | assertThat(pattern, matchesExactly(WHITESPACES.substring(0, 1))); 124 | assertThat(pattern, doesntMatchAnythingFrom(WORD_CHARACTERS)); 125 | assertThat(pattern, doesntMatchAnythingFrom(NON_LETTERS)); 126 | assertThat(pattern, doesntMatchAnythingFrom(DIGITS)); 127 | } 128 | } 129 | 130 | @Nested 131 | class Tab { 132 | @Test 133 | void tabOnlyMatchesTab() { 134 | ReadableRegexPattern pattern = regex().tab().build(); 135 | 136 | assertThat(pattern, matchesExactly("\t")); 137 | assertThat(pattern, doesntMatchAnythingFrom(WHITESPACES.replaceAll("\t", ""))); 138 | } 139 | } 140 | 141 | @Nested 142 | class OneOf { 143 | @Test 144 | void oneOfCanBeUsedWithZeroOrOneArguments() { 145 | ReadableRegexPattern pattern = regex().oneOf().oneOf(regex().literal("b")).build(); 146 | 147 | assertThat(pattern, matchesExactly("b")); 148 | } 149 | 150 | @Test 151 | void oneOfCanBeUsedWithMultipleArguments() { 152 | ReadableRegexPattern pattern = regex().oneOf(regex().literal("b"), regex().literal("c")).build(); 153 | 154 | assertThat(pattern, matchesExactly("b")); 155 | assertThat(pattern, matchesExactly("c")); 156 | assertThat(pattern, doesntMatchAnythingFrom("")); 157 | } 158 | 159 | @Test 160 | void oneOfAreStandaloneBlocks() { 161 | ReadableRegexPattern pattern = regex().literal("a").oneOf(regex().literal("b")).optional().build(); 162 | 163 | assertThat(pattern, matchesExactly("a")); 164 | assertThat(pattern, matchesExactly("ab")); 165 | assertThat(pattern, doesntMatchAnythingFrom("")); 166 | } 167 | } 168 | 169 | @Nested 170 | class RangeAndNotInRange { 171 | @Test 172 | void throwIaeWhenZeroOrOddNumberOfArgumentsIsSupplied() { 173 | assertThrows(IllegalArgumentException.class, () -> regex().range('a')); 174 | assertThrows(IllegalArgumentException.class, () -> regex().range()); 175 | assertThrows(IllegalArgumentException.class, () -> regex().notInRange('a')); 176 | assertThrows(IllegalArgumentException.class, () -> regex().notInRange()); 177 | } 178 | 179 | @Test 180 | void canBeUsedWithMultipleArguments() { 181 | ReadableRegexPattern pattern = regex().range('a', 'e', 'x', 'z').notInRange('a', 'z', 'A', 'Z', '0', '9').build(); 182 | 183 | assertThat(pattern, matchesExactly("b|")); 184 | assertThat(pattern, matchesExactly("y~")); 185 | assertThat(pattern, doesntMatchAnythingFrom("f|")); 186 | assertThat(pattern, doesntMatchAnythingFrom("yZ")); 187 | } 188 | } 189 | 190 | @Nested 191 | class AnyCharacterOfAndExcept { 192 | @Test 193 | void throwNpeWhenArgumentIsNullOrZeroLengthString() { 194 | assertThrows(NullPointerException.class, () -> regex().anyCharacterOf(null)); 195 | assertThrows(IllegalArgumentException.class, () -> regex().anyCharacterOf("")); 196 | assertThrows(NullPointerException.class, () -> regex().anyCharacterExcept(null)); 197 | assertThrows(IllegalArgumentException.class, () -> regex().anyCharacterExcept("")); 198 | } 199 | 200 | @Test 201 | void characterRangesAreMatched() { 202 | ReadableRegexPattern pattern = regex().anyCharacterOf("aeiou") 203 | .anyCharacterExcept("abc") 204 | .build(); 205 | 206 | assertThat(pattern, matchesExactly("ix")); 207 | assertThat(pattern, doesntMatchAnythingFrom("ia")); 208 | assertThat(pattern, doesntMatchAnythingFrom("xx")); 209 | } 210 | } 211 | 212 | @Nested 213 | class WordCharacter { 214 | @Test 215 | void matchCorrectCharacter() { 216 | ReadableRegexPattern pattern = regex().wordCharacter().nonWordCharacter().build(); 217 | 218 | assertThat(pattern, matchesExactly("a.")); 219 | assertThat(pattern, doesntMatchAnythingFrom("ab")); 220 | assertThat(pattern, doesntMatchAnythingFrom(".a")); 221 | } 222 | } 223 | 224 | @Nested 225 | class WordBoundary { 226 | @Test 227 | void wordBoundaryMatchesCorrectly() { 228 | ReadableRegexPattern pattern = regex().literal("abc").wordBoundary().literal(" ").build(); 229 | 230 | assertThat(pattern, matchesExactly("abc ")); 231 | } 232 | 233 | @Test 234 | void nonWordBoundaryMatchesCorrectly() { 235 | ReadableRegexPattern pattern = regex().literal("a").nonWordBoundary().literal("b").build(); 236 | 237 | assertThat(pattern, matchesExactly("ab")); 238 | } 239 | } 240 | 241 | /** 242 | * No tests are needed for the method {@link StandaloneBlockBuilder#anyCharacter()}, because this is already covered by other tests: 243 | *

    244 | *
  • {@link PatternFlagTests#dotAllWorks()}
  • 245 | *
  • {@link SyntacticSugarTests.StandaloneBlock#anythingWorks()}
  • 246 | *
247 | */ 248 | @Nested 249 | class AnyCharacter { 250 | } 251 | 252 | @Nested 253 | class StartOfLine { 254 | @Test 255 | void quantifierAfterWorks() { 256 | ReadableRegexPattern pattern = regex().startOfLine().exactlyNTimes(1).literal("a").build(); 257 | 258 | assertThat(pattern, matchesExactly("a")); 259 | assertThat(pattern, matchesSomethingFrom("\na")); 260 | assertThat(pattern, doesntMatchAnythingFrom("ba")); 261 | } 262 | 263 | @Test 264 | void methodEnablesMultilineFlag() { 265 | ReadableRegexPattern pattern = regex().startOfLine().build(); 266 | ReadableRegexPattern pattern2 = regex().startOfLine().buildWithFlags(PatternFlag.MULTILINE); 267 | 268 | assertThat(pattern.enabledFlags(), contains(PatternFlag.MULTILINE)); 269 | assertThat(pattern2.enabledFlags(), contains(PatternFlag.MULTILINE)); 270 | } 271 | } 272 | 273 | @Nested 274 | class StartOfInput { 275 | @Test 276 | void worksCorrectlyWithStartOfLine() { 277 | ReadableRegexPattern pattern = regex().startOfInput().regexFromString("\n").startOfLine().literal("a").build(); 278 | 279 | assertThat(pattern, matchesExactly("\na")); 280 | assertThat(pattern, doesntMatchAnythingFrom("\n\na")); 281 | } 282 | } 283 | 284 | @Nested 285 | class EndOfLine { 286 | @Test 287 | void quantifierAfterWorks() { 288 | ReadableRegexPattern pattern = regex().literal("a").endOfLine().exactlyNTimes(1).build(); 289 | 290 | assertThat(pattern, matchesExactly("a")); 291 | assertThat(pattern, matchesSomethingFrom("a\n")); 292 | assertThat(pattern, doesntMatchAnythingFrom("ab")); 293 | } 294 | 295 | @Test 296 | void methodEnablesMultilineFlag() { 297 | ReadableRegexPattern pattern = regex().endOfLine().build(); 298 | ReadableRegexPattern pattern2 = regex().endOfLine().buildWithFlags(PatternFlag.MULTILINE); 299 | 300 | assertThat(pattern.enabledFlags(), contains(PatternFlag.MULTILINE)); 301 | assertThat(pattern2.enabledFlags(), contains(PatternFlag.MULTILINE)); 302 | } 303 | } 304 | 305 | @Nested 306 | class EndOfInput { 307 | @Test 308 | void worksCorrectlyWithEndOfLine() { 309 | ReadableRegexPattern pattern = regex().literal("a").endOfLine().regexFromString("\n").endOfInput().build(); 310 | 311 | assertThat(pattern, matchesExactly("a\n")); 312 | assertThat(pattern, doesntMatchAnythingFrom("a\n\n")); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/SyntacticSugarTests.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import org.junit.jupiter.api.Nested; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.regex.Matcher; 8 | 9 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 10 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.*; 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.Matchers.equalTo; 13 | 14 | /** 15 | * Tests related to methods that are inside {@link SyntacticSugarBuilder}. 16 | */ 17 | @SuppressFBWarnings(value = "SIC_INNER_SHOULD_BE_STATIC", justification = "@Nested classes should be non-static, but SpotBugs wants them static." + 18 | "See https://github.com/spotbugs/spotbugs/issues/560 for the bug (open since 2018).") 19 | class SyntacticSugarTests { 20 | @Nested 21 | class StandaloneBlock { 22 | @Test 23 | void wordWorks() { 24 | String groupName = "result"; 25 | ReadableRegexPattern pattern = regex().startGroup(groupName).word().endGroup().build(); 26 | 27 | Matcher matcher = pattern.matches("abc de_f1 ghi|jkl"); 28 | assertThat(matcher.find(), equalTo(true)); 29 | assertThat(matcher.group(groupName), equalTo("abc")); 30 | assertThat(matcher.find(), equalTo(true)); 31 | assertThat(matcher.group(groupName), equalTo("de_f1")); 32 | assertThat(matcher.find(), equalTo(true)); 33 | assertThat(matcher.group(groupName), equalTo("ghi")); 34 | assertThat(matcher.find(), equalTo(true)); 35 | assertThat(matcher.group(groupName), equalTo("jkl")); 36 | } 37 | 38 | @Test 39 | void anythingWorks() { 40 | ReadableRegexPattern pattern = regex().anything().build(); 41 | 42 | assertThat(pattern, matchesExactly("")); 43 | assertThat(pattern, matchesExactly(Constants.WORD_CHARACTERS)); 44 | assertThat(pattern, matchesExactly(Constants.DIGITS)); 45 | assertThat(pattern, matchesExactly(Constants.NON_LETTERS)); 46 | } 47 | 48 | @Test 49 | void lineBreakWorks() { 50 | ReadableRegexPattern pattern = regex().lineBreak().build(); 51 | 52 | assertThat(pattern, matchesExactly("\n")); 53 | assertThat(pattern, matchesExactly("\r\n")); 54 | assertThat(pattern, matchesExactly("\r")); 55 | assertThat(pattern, doesntMatchAnythingFrom(" ")); 56 | } 57 | } 58 | 59 | @Nested 60 | class Group { 61 | @Test 62 | void groupWorks() { 63 | ReadableRegexPattern pattern = regex().digit().group("firstGroupName", regex().digit().group(regex().digit())).build(); 64 | Matcher matcher = pattern.matches("123"); 65 | 66 | assertThat(matcher.matches(), equalTo(true)); 67 | assertThat(matcher.group("firstGroupName"), equalTo("23")); 68 | assertThat(matcher.group(2), equalTo("3")); 69 | } 70 | 71 | @Test 72 | void lookbehindWorks() { 73 | ReadableRegexPattern pattern = regex().positiveLookbehind(regex().digit()).whitespace().build(); 74 | assertThat(pattern, matchesSomethingFrom("1 ")); 75 | assertThat(pattern, doesntMatchAnythingFrom(" ")); 76 | 77 | pattern = regex().negativeLookbehind(regex().digit()).whitespace().build(); 78 | assertThat(pattern, doesntMatchAnythingFrom("1 ")); 79 | assertThat(pattern, matchesSomethingFrom(" ")); 80 | } 81 | 82 | @Test 83 | void lookaheadWorks() { 84 | ReadableRegexPattern pattern = regex().whitespace().positiveLookahead(regex().digit()).build(); 85 | assertThat(pattern, matchesSomethingFrom(" 1")); 86 | assertThat(pattern, doesntMatchAnythingFrom(" ")); 87 | 88 | pattern = regex().whitespace().negativeLookahead(regex().digit()).build(); 89 | assertThat(pattern, doesntMatchAnythingFrom(" 1")); 90 | assertThat(pattern, matchesSomethingFrom(" ")); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/extendable/ExtendableReadableRegexTest.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.extendable; 2 | 3 | import io.github.ricoapon.readableregex.PatternFlag; 4 | import io.github.ricoapon.readableregex.ReadableRegexPattern; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.doesntMatchAnythingFrom; 8 | import static io.github.ricoapon.readableregex.matchers.PatternMatchMatcher.matchesExactly; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.contains; 11 | 12 | /** 13 | * Tests related to extending the builder using {@link io.github.ricoapon.readableregex.ExtendableReadableRegex}. 14 | */ 15 | class ExtendableReadableRegexTest { 16 | @Test 17 | void methodCanBeAdded() { 18 | ReadableRegexPattern pattern = TestExtension.regex().literal("a").digitWhitespaceDigit().literal("a").build(); 19 | 20 | assertThat(pattern, matchesExactly("a1 4a")); 21 | assertThat(pattern, doesntMatchAnythingFrom("1 1")); 22 | assertThat(pattern, doesntMatchAnythingFrom("a11a")); 23 | } 24 | 25 | @Test 26 | void methodCanBeOverwritten() { 27 | ReadableRegexPattern pattern = TestExtension.regex().build(); 28 | 29 | assertThat(pattern.enabledFlags(), contains(PatternFlag.DOT_ALL)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/extendable/TestExtension.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.extendable; 2 | 3 | import io.github.ricoapon.readableregex.ExtendableReadableRegex; 4 | import io.github.ricoapon.readableregex.PatternFlag; 5 | import io.github.ricoapon.readableregex.ReadableRegexPattern; 6 | 7 | /** 8 | * Dummy extension that adds a new method and overrides an existing method. 9 | */ 10 | public class TestExtension extends ExtendableReadableRegex { 11 | public static TestExtension regex() { 12 | return new TestExtension(); 13 | } 14 | 15 | public TestExtension digitWhitespaceDigit() { 16 | return digit().whitespace().digit(); 17 | } 18 | 19 | @Override 20 | public ReadableRegexPattern buildWithFlags(PatternFlag... patternFlags) { 21 | // Always build with exactly one flag: DOT_ALL. 22 | return super.buildWithFlags(PatternFlag.DOT_ALL); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/ConstructorParamUnknownType.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | @SuppressWarnings({"FieldCanBeLocal", "unused"}) 4 | public class ConstructorParamUnknownType { 5 | private final ConstructorParamUnknownType unknownType; 6 | 7 | public ConstructorParamUnknownType(ConstructorParamUnknownType unknownType) { 8 | this.unknownType = unknownType; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/MultipleConstructorsWithMultipleInjectAnnotations.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | import javax.inject.Inject; 4 | 5 | @SuppressWarnings({"MultipleInjectedConstructorsForClass", "unused"}) 6 | public class MultipleConstructorsWithMultipleInjectAnnotations { 7 | 8 | @Inject 9 | public MultipleConstructorsWithMultipleInjectAnnotations(int n) { 10 | 11 | } 12 | 13 | @Inject 14 | public MultipleConstructorsWithMultipleInjectAnnotations(int n, boolean uselessParam) { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/MultipleConstructorsWithSingleInjectAnnotation.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | import javax.inject.Inject; 4 | 5 | @SuppressWarnings("unused") 6 | public class MultipleConstructorsWithSingleInjectAnnotation { 7 | private final int n; 8 | 9 | @Inject 10 | public MultipleConstructorsWithSingleInjectAnnotation(int n) { 11 | this.n = n; 12 | } 13 | 14 | public MultipleConstructorsWithSingleInjectAnnotation(int n, boolean uselessParam) { 15 | this.n = n; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/PrimitiveAndBoxedPrimitiveTypesAndString.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | public class PrimitiveAndBoxedPrimitiveTypesAndString { 4 | public final Byte byteBoxed; 5 | public final byte bytePrimitive; 6 | public final Short shortBoxed; 7 | public final short shortPrimitive; 8 | public final Integer integerBoxed; 9 | public final int integerPrimitive; 10 | public final Long longBoxed; 11 | public final long longPrimitive; 12 | public final Float floatBoxed; 13 | public final float floatPrimitive; 14 | public final Double doubleBoxed; 15 | public final double doublePrimitive; 16 | public final Boolean booleanBoxed; 17 | public final boolean booleanPrimitive; 18 | public final Character characterBoxed; 19 | public final char characterPrimitive; 20 | public final String string; 21 | 22 | public PrimitiveAndBoxedPrimitiveTypesAndString(Byte byteBoxed, byte bytePrimitive, Short shortBoxed, short shortPrimitive, Integer integerBoxed, int integerPrimitive, Long longBoxed, long longPrimitive, Float floatBoxed, float floatPrimitive, Double doubleBoxed, double doublePrimitive, Boolean booleanBoxed, boolean booleanPrimitive, Character characterBoxed, char characterPrimitive, String string) { 23 | this.byteBoxed = byteBoxed; 24 | this.bytePrimitive = bytePrimitive; 25 | this.shortBoxed = shortBoxed; 26 | this.shortPrimitive = shortPrimitive; 27 | this.integerBoxed = integerBoxed; 28 | this.integerPrimitive = integerPrimitive; 29 | this.longBoxed = longBoxed; 30 | this.longPrimitive = longPrimitive; 31 | this.floatBoxed = floatBoxed; 32 | this.floatPrimitive = floatPrimitive; 33 | this.doubleBoxed = doubleBoxed; 34 | this.doublePrimitive = doublePrimitive; 35 | this.booleanBoxed = booleanBoxed; 36 | this.booleanPrimitive = booleanPrimitive; 37 | this.characterBoxed = characterBoxed; 38 | this.characterPrimitive = characterPrimitive; 39 | this.string = string; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/RegexObjectInstantiationTest.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | import io.github.ricoapon.readableregex.ReadableRegexPattern; 4 | import io.github.ricoapon.readableregex.RegexObjectInstantiation; 5 | import io.github.ricoapon.readableregex.RegexObjectInstantiationException; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.lang.reflect.Constructor; 9 | import java.util.regex.Pattern; 10 | 11 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 12 | import static io.github.ricoapon.readableregex.RegexObjectInstantiation.instantiateObject; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.equalTo; 15 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 16 | import static org.junit.jupiter.api.Assertions.assertThrows; 17 | 18 | class RegexObjectInstantiationTest { 19 | @Test 20 | void primitivesCanBeInjectedAsBoxedOrPrimitive_AndStringCanBeInjected() { 21 | // Given 22 | String value = "Byte:1byte:1Short:500short:500Integer:35000integer:35000Long:3000000000long:3000000000" + 23 | "Float:3.6float:3.6Double:3.6double:3.6Boolean:trueboolean:trueCharacter:ccharacter:cString:abc"; 24 | ReadableRegexPattern pattern = regex() 25 | .literal("Byte:").group("byteBoxed", regex().digit()) 26 | .literal("byte:").group("bytePrimitive", regex().digit()) 27 | .literal("Short:").group("shortBoxed", regex().digit().oneOrMore()) 28 | .literal("short:").group("shortPrimitive", regex().digit().oneOrMore()) 29 | .literal("Integer:").group("integerBoxed", regex().digit().oneOrMore()) 30 | .literal("integer:").group("integerPrimitive", regex().digit().oneOrMore()) 31 | .literal("Long:").group("longBoxed", regex().digit().oneOrMore()) 32 | .literal("long:").group("longPrimitive", regex().digit().oneOrMore()) 33 | .literal("Float:").group("floatBoxed", regex().anyCharacterOf("0-9\\.").oneOrMore()) 34 | .literal("float:").group("floatPrimitive", regex().anyCharacterOf("0-9\\.").oneOrMore()) 35 | .literal("Double:").group("doubleBoxed", regex().anyCharacterOf("0-9\\.").oneOrMore()) 36 | .literal("double:").group("doublePrimitive", regex().anyCharacterOf("0-9\\.").oneOrMore()) 37 | .literal("Boolean:").group("booleanBoxed", regex().word()) 38 | .literal("boolean:").group("booleanPrimitive", regex().word()) 39 | .literal("Character:").group("characterBoxed", regex().anyCharacter()) 40 | .literal("character:").group("characterPrimitive", regex().anyCharacter()) 41 | .literal("String:").group("string", regex().word()) 42 | .build(); 43 | 44 | // When 45 | PrimitiveAndBoxedPrimitiveTypesAndString primitiveAndBoxedPrimitiveTypesAndString = 46 | instantiateObject(pattern, value, PrimitiveAndBoxedPrimitiveTypesAndString.class); 47 | 48 | // Then 49 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.byteBoxed, equalTo((byte) 1)); 50 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.bytePrimitive, equalTo((byte) 1)); 51 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.shortBoxed, equalTo((short) 500)); 52 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.shortPrimitive, equalTo((short) 500)); 53 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.integerBoxed, equalTo(35000)); 54 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.integerPrimitive, equalTo(35000)); 55 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.longBoxed, equalTo(3000000000L)); 56 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.longPrimitive, equalTo(3000000000L)); 57 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.floatBoxed, equalTo(3.6f)); 58 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.floatPrimitive, equalTo(3.6f)); 59 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.doubleBoxed, equalTo(3.6d)); 60 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.doublePrimitive, equalTo(3.6d)); 61 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.booleanBoxed, equalTo(Boolean.TRUE)); 62 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.booleanPrimitive, equalTo(true)); 63 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.characterBoxed, equalTo('c')); 64 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.characterPrimitive, equalTo('c')); 65 | assertThat(primitiveAndBoxedPrimitiveTypesAndString.string, equalTo("abc")); 66 | } 67 | 68 | @Test 69 | void throwIfConstructorIsNotValid() { 70 | ReadableRegexPattern pattern = regex().group("n", regex().digit()).build(); 71 | String data = "1"; 72 | 73 | assertThrows(RegexObjectInstantiationException.class, () -> instantiateObject(pattern, data, MultipleConstructorsWithMultipleInjectAnnotations.class)); 74 | assertDoesNotThrow(() -> instantiateObject(pattern, data, MultipleConstructorsWithSingleInjectAnnotation.class)); 75 | assertDoesNotThrow(() -> instantiateObject(pattern, data, SingleConstructorWithInjectAnnotation.class)); 76 | assertDoesNotThrow(() -> instantiateObject(pattern, data, SingleConstructorWithoutInjectAnnotation.class)); 77 | } 78 | 79 | @Test 80 | void throwIfConstructorParamIsUnknownType() { 81 | ReadableRegexPattern pattern = regex().group("unknownType", regex()).build(); 82 | String data = ""; 83 | 84 | assertThrows(RegexObjectInstantiationException.class, () -> instantiateObject(pattern, data, ConstructorParamUnknownType.class)); 85 | } 86 | 87 | @Test 88 | void throwIfParameterNameDoesNotOccurAsGroupName() { 89 | ReadableRegexPattern pattern = regex().build(); 90 | String data = ""; 91 | 92 | assertThrows(RegexObjectInstantiationException.class, () -> instantiateObject(pattern, data, SingleConstructorWithoutInjectAnnotation.class)); 93 | } 94 | 95 | @Test 96 | void throwIfMatchIsNotExact() { 97 | ReadableRegexPattern pattern = regex().group("n", regex().digit()).build(); 98 | String data = "1a"; 99 | 100 | assertThrows(RegexObjectInstantiationException.class, () -> instantiateObject(pattern, data, SingleConstructorWithoutInjectAnnotation.class)); 101 | } 102 | 103 | private static class NotInstantiatable { 104 | public NotInstantiatable() { 105 | } 106 | } 107 | 108 | @Test 109 | void throwIfClassCannotBeInstantiated() { 110 | ReadableRegexPattern pattern = regex().build(); 111 | String data = ""; 112 | 113 | assertThrows(RegexObjectInstantiationException.class, () -> instantiateObject(pattern, data, NotInstantiatable.class)); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/SingleConstructorWithInjectAnnotation.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | import javax.inject.Inject; 4 | 5 | @SuppressWarnings({"FieldCanBeLocal", "unused"}) 6 | public class SingleConstructorWithInjectAnnotation { 7 | private final int n; 8 | 9 | @Inject 10 | public SingleConstructorWithInjectAnnotation(int n) { 11 | this.n = n; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/SingleConstructorWithoutInjectAnnotation.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | @SuppressWarnings({"FieldCanBeLocal", "unused"}) 4 | public class SingleConstructorWithoutInjectAnnotation { 5 | private final int n; 6 | 7 | public SingleConstructorWithoutInjectAnnotation(int n) { 8 | this.n = n; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/instantiation/StringConverterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.instantiation; 2 | 3 | import io.github.ricoapon.readableregex.internal.instantiation.StringConverter; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class StringConverterTest { 7 | @Test 8 | void callConstructorForCodeCoverage() { 9 | //noinspection InstantiationOfUtilityClass 10 | new StringConverter(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/internal/MethodOrderCheckerTest.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal; 2 | 3 | import io.github.ricoapon.readableregex.IncorrectConstructionException; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static io.github.ricoapon.readableregex.internal.MethodOrderChecker.Method.*; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | 10 | class MethodOrderCheckerTest { 11 | private MethodOrderChecker methodOrderChecker; 12 | 13 | @BeforeEach 14 | void setUp() { 15 | methodOrderChecker = new MethodOrderChecker(); 16 | } 17 | 18 | @Test 19 | void testFor100PercentCodeCoverage() { 20 | // We know there are a fixed number of enums. This means that the if-statements will never reach the branch 21 | // false-false...-false. However, if we input null, we do reach this case. Ugly way to achieve this, but it works. 22 | // Also note that we cannot use a switch statement anymore, since that doesn't allow null. 23 | methodOrderChecker.checkCallingMethod(null); 24 | } 25 | 26 | @Test 27 | void quantifierCanNotBeDoneAtTheStart() { 28 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(QUANTIFIER)); 29 | } 30 | 31 | @Test 32 | void quantifierIsPossibleAfterStandaloneBlockButNotAfterQualifier() { 33 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 34 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 35 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(QUANTIFIER)); 36 | } 37 | 38 | @Test 39 | void cannotCloseGroupIfNoneStarted() { 40 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(END_GROUP)); 41 | } 42 | 43 | @Test 44 | void cannotFinishWhenGroupsAreOpen() { 45 | methodOrderChecker.checkCallingMethod(START_GROUP); 46 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(FINISH)); 47 | } 48 | 49 | @Test 50 | void quantifierIsNotPossibleAfterStartingGroup() { 51 | methodOrderChecker.checkCallingMethod(START_GROUP); 52 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(QUANTIFIER)); 53 | } 54 | 55 | @Test 56 | void quantifierIsPossibleAfterEndingGroup() { 57 | methodOrderChecker.checkCallingMethod(START_GROUP); 58 | methodOrderChecker.checkCallingMethod(END_GROUP); 59 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 60 | } 61 | 62 | @Test 63 | void reluctantOrPossessiveIsOnlyPossibleAfterQuantifier() { 64 | methodOrderChecker.checkCallingMethod(STANDALONE_BLOCK); 65 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(RELUCTANT_OR_POSSESSIVE)); 66 | 67 | methodOrderChecker.checkCallingMethod(START_GROUP); 68 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(RELUCTANT_OR_POSSESSIVE)); 69 | 70 | methodOrderChecker.checkCallingMethod(END_GROUP); 71 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(RELUCTANT_OR_POSSESSIVE)); 72 | 73 | methodOrderChecker.checkCallingMethod(QUANTIFIER); 74 | methodOrderChecker.checkCallingMethod(RELUCTANT_OR_POSSESSIVE); 75 | assertThrows(IncorrectConstructionException.class, () -> methodOrderChecker.checkCallingMethod(RELUCTANT_OR_POSSESSIVE)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/internal/ReadableRegexOrderCheckerTest.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.internal; 2 | 3 | import io.github.ricoapon.readableregex.PatternFlag; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static io.github.ricoapon.readableregex.ReadableRegex.regex; 8 | import static io.github.ricoapon.readableregex.internal.MethodOrderChecker.Method.*; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.equalTo; 11 | 12 | class ReadableRegexOrderCheckerTest { 13 | /** Dummy implementation of {@link MethodOrderChecker} to check {@link MethodOrderChecker#checkCallingMethod(Method)}. */ 14 | private static class DummyOrderChecker extends MethodOrderChecker { 15 | public Method calledMethod; 16 | 17 | @Override 18 | public void checkCallingMethod(Method method) { 19 | calledMethod = method; 20 | } 21 | } 22 | 23 | private ReadableRegexOrderChecker readableRegexOrderChecker; 24 | private DummyOrderChecker dummyOrderChecker; 25 | 26 | @BeforeEach 27 | void setUp() { 28 | dummyOrderChecker = new DummyOrderChecker(); 29 | 30 | // Call digit, so that quantifiers can be called directly after. Does not matter for others. 31 | readableRegexOrderChecker = new ReadableRegexOrderChecker<>(dummyOrderChecker); 32 | } 33 | 34 | @Test 35 | void build_Finish() { 36 | readableRegexOrderChecker.build(); 37 | assertThat(dummyOrderChecker.calledMethod, equalTo(FINISH)); 38 | } 39 | 40 | @Test 41 | void buildWithFlags_Finish() { 42 | readableRegexOrderChecker.buildWithFlags(PatternFlag.CASE_INSENSITIVE, PatternFlag.MULTILINE); 43 | assertThat(dummyOrderChecker.calledMethod, equalTo(FINISH)); 44 | } 45 | 46 | @Test 47 | void regexFromString_StandaloneBlock() { 48 | readableRegexOrderChecker.regexFromString(""); 49 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 50 | } 51 | 52 | @Test 53 | void add_StandaloneBlock() { 54 | readableRegexOrderChecker.add(regex()); 55 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 56 | 57 | readableRegexOrderChecker.add(regex().build()); 58 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 59 | } 60 | 61 | @Test 62 | void literal_StandaloneBlock() { 63 | readableRegexOrderChecker.literal(""); 64 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 65 | } 66 | 67 | @Test 68 | void digit_StandaloneBlock() { 69 | readableRegexOrderChecker.digit(); 70 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 71 | } 72 | 73 | @Test 74 | void whitespace_StandaloneBlock() { 75 | readableRegexOrderChecker.whitespace(); 76 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 77 | } 78 | 79 | @Test 80 | void tab_StandaloneBlock() { 81 | readableRegexOrderChecker.tab(); 82 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 83 | } 84 | 85 | @Test 86 | void oneOf_StandaloneBlock() { 87 | readableRegexOrderChecker.oneOf(); 88 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 89 | } 90 | 91 | @Test 92 | void range_StandaloneBlock() { 93 | readableRegexOrderChecker.range('a', 'b'); 94 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 95 | } 96 | 97 | @Test 98 | void notInRange_StandaloneBlock() { 99 | readableRegexOrderChecker.notInRange('a', 'b'); 100 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 101 | } 102 | 103 | @Test 104 | void anyCharacterOf_StandaloneBlock() { 105 | readableRegexOrderChecker.anyCharacterOf("abc"); 106 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 107 | } 108 | 109 | @Test 110 | void anyCharacterExcept_StandaloneBlock() { 111 | readableRegexOrderChecker.anyCharacterExcept("abc"); 112 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 113 | } 114 | 115 | @Test 116 | void wordCharacter_StandaloneBlock() { 117 | readableRegexOrderChecker.wordCharacter(); 118 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 119 | } 120 | 121 | @Test 122 | void nonWordCharacter_StandaloneBlock() { 123 | readableRegexOrderChecker.nonWordCharacter(); 124 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 125 | } 126 | 127 | @Test 128 | void wordBoundary_StandaloneBlock() { 129 | readableRegexOrderChecker.wordBoundary(); 130 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 131 | } 132 | 133 | @Test 134 | void nonWordBoundary_StandaloneBlock() { 135 | readableRegexOrderChecker.nonWordBoundary(); 136 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 137 | } 138 | 139 | @Test 140 | void anyCharacter_StandaloneBlock() { 141 | readableRegexOrderChecker.anyCharacter(); 142 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 143 | } 144 | 145 | @Test 146 | void startOfLine_StandaloneBlock() { 147 | readableRegexOrderChecker.startOfLine(); 148 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 149 | } 150 | 151 | @Test 152 | void startOfInput_StandaloneBlock() { 153 | readableRegexOrderChecker.startOfInput(); 154 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 155 | } 156 | 157 | @Test 158 | void endOfLine_StandaloneBlock() { 159 | readableRegexOrderChecker.endOfLine(); 160 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 161 | } 162 | 163 | @Test 164 | void endOfInput_StandaloneBlock() { 165 | readableRegexOrderChecker.endOfInput(); 166 | assertThat(dummyOrderChecker.calledMethod, equalTo(STANDALONE_BLOCK)); 167 | } 168 | 169 | @Test 170 | void oneOrMore_Quantifier() { 171 | readableRegexOrderChecker.digit().oneOrMore(); 172 | assertThat(dummyOrderChecker.calledMethod, equalTo(QUANTIFIER)); 173 | } 174 | 175 | @Test 176 | void optional_Quantifier() { 177 | readableRegexOrderChecker.digit().optional(); 178 | assertThat(dummyOrderChecker.calledMethod, equalTo(QUANTIFIER)); 179 | } 180 | 181 | @Test 182 | void zeroOrMore_Quantifier() { 183 | readableRegexOrderChecker.digit().zeroOrMore(); 184 | assertThat(dummyOrderChecker.calledMethod, equalTo(QUANTIFIER)); 185 | } 186 | 187 | @Test 188 | void exactlyNTimes_Quantifier() { 189 | readableRegexOrderChecker.digit().exactlyNTimes(2); 190 | assertThat(dummyOrderChecker.calledMethod, equalTo(QUANTIFIER)); 191 | } 192 | 193 | @Test 194 | void etLeastNTimes_Quantifier() { 195 | readableRegexOrderChecker.digit().atLeastNTimes(2); 196 | assertThat(dummyOrderChecker.calledMethod, equalTo(QUANTIFIER)); 197 | } 198 | 199 | @Test 200 | void betweenNAndMTimes_Quantifier() { 201 | readableRegexOrderChecker.digit().betweenNAndMTimes(2, 4); 202 | assertThat(dummyOrderChecker.calledMethod, equalTo(QUANTIFIER)); 203 | } 204 | 205 | @Test 206 | void atMostNTimes_Quantifier() { 207 | readableRegexOrderChecker.digit().atMostNTimes(2); 208 | assertThat(dummyOrderChecker.calledMethod, equalTo(QUANTIFIER)); 209 | } 210 | 211 | @Test 212 | void reluctant_ReluctantOrPossessive() { 213 | readableRegexOrderChecker.digit().atMostNTimes(2).reluctant(); 214 | assertThat(dummyOrderChecker.calledMethod, equalTo(RELUCTANT_OR_POSSESSIVE)); 215 | } 216 | 217 | @Test 218 | void possessive_ReluctantOrPossessive() { 219 | readableRegexOrderChecker.digit().atMostNTimes(2).possessive(); 220 | assertThat(dummyOrderChecker.calledMethod, equalTo(RELUCTANT_OR_POSSESSIVE)); 221 | } 222 | 223 | @Test 224 | void startGroup_StartGroup() { 225 | readableRegexOrderChecker.startGroup(); 226 | assertThat(dummyOrderChecker.calledMethod, equalTo(START_GROUP)); 227 | 228 | readableRegexOrderChecker.startGroup("a"); 229 | assertThat(dummyOrderChecker.calledMethod, equalTo(START_GROUP)); 230 | } 231 | 232 | @Test 233 | void startUnnamedGroup_StartGroup() { 234 | readableRegexOrderChecker.startUnnamedGroup(); 235 | assertThat(dummyOrderChecker.calledMethod, equalTo(START_GROUP)); 236 | } 237 | 238 | @Test 239 | void startLook_StartGroup() { 240 | readableRegexOrderChecker.startPositiveLookbehind(); 241 | assertThat(dummyOrderChecker.calledMethod, equalTo(START_GROUP)); 242 | 243 | readableRegexOrderChecker.startNegativeLookbehind(); 244 | assertThat(dummyOrderChecker.calledMethod, equalTo(START_GROUP)); 245 | 246 | readableRegexOrderChecker.startPositiveLookahead(); 247 | assertThat(dummyOrderChecker.calledMethod, equalTo(START_GROUP)); 248 | 249 | readableRegexOrderChecker.startNegativeLookahead(); 250 | assertThat(dummyOrderChecker.calledMethod, equalTo(START_GROUP)); 251 | } 252 | 253 | @Test 254 | void endGroup_EndGroup() { 255 | readableRegexOrderChecker.startGroup().endGroup(); 256 | assertThat(dummyOrderChecker.calledMethod, equalTo(END_GROUP)); 257 | } 258 | 259 | @Test 260 | void group_StandaloneBlock() { 261 | readableRegexOrderChecker.group(regex()); 262 | assertThat(dummyOrderChecker.calledMethod, equalTo(END_GROUP)); 263 | 264 | readableRegexOrderChecker.group("a", regex()); 265 | assertThat(dummyOrderChecker.calledMethod, equalTo(END_GROUP)); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/test/java/io/github/ricoapon/readableregex/matchers/PatternMatchMatcher.java: -------------------------------------------------------------------------------- 1 | package io.github.ricoapon.readableregex.matchers; 2 | 3 | import io.github.ricoapon.readableregex.ReadableRegexPattern; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.TypeSafeMatcher; 6 | 7 | import java.util.regex.Matcher; 8 | 9 | /** 10 | * Hamcrest matcher for checking whether the {@link ReadableRegexPattern} matches a given {@link String}. 11 | */ 12 | public class PatternMatchMatcher extends TypeSafeMatcher { 13 | enum MatchStrategy { 14 | MATCH_EXACTLY, NOT_MATCH_EXACTLY, MATCH_SOMETHING, NOT_MATCH_ANYTHING 15 | } 16 | private final String textToMatch; 17 | private final MatchStrategy matchStrategy; 18 | 19 | public PatternMatchMatcher(String textToMatch, MatchStrategy matchStrategy) { 20 | this.textToMatch = textToMatch; 21 | this.matchStrategy = matchStrategy; 22 | } 23 | 24 | public static PatternMatchMatcher matchesExactly(String textToMatch) { 25 | return new PatternMatchMatcher(textToMatch, MatchStrategy.MATCH_EXACTLY); 26 | } 27 | 28 | public static PatternMatchMatcher doesntMatchExactly(String textToMatch) { 29 | return new PatternMatchMatcher(textToMatch, MatchStrategy.NOT_MATCH_EXACTLY); 30 | } 31 | 32 | public static PatternMatchMatcher doesntMatchAnythingFrom(String textToMatch) { 33 | return new PatternMatchMatcher(textToMatch, MatchStrategy.NOT_MATCH_ANYTHING); 34 | } 35 | 36 | public static PatternMatchMatcher matchesSomethingFrom(String textToMatch) { 37 | return new PatternMatchMatcher(textToMatch, MatchStrategy.MATCH_SOMETHING); 38 | } 39 | 40 | @Override 41 | protected boolean matchesSafely(ReadableRegexPattern item) { 42 | Matcher matcher = item.matches(textToMatch); 43 | if (MatchStrategy.MATCH_EXACTLY.equals(matchStrategy)) { 44 | return matcher.matches(); 45 | } else if (MatchStrategy.NOT_MATCH_EXACTLY.equals(matchStrategy)) { 46 | return !matcher.matches(); 47 | } else if (MatchStrategy.MATCH_SOMETHING.equals(matchStrategy)) { 48 | return matcher.find(); 49 | } else if (MatchStrategy.NOT_MATCH_ANYTHING.equals(matchStrategy)) { 50 | return !matcher.find(); 51 | } 52 | 53 | throw new RuntimeException("Match strategy was not set correctly. Fix the static construction methods."); 54 | } 55 | 56 | @Override 57 | public void describeTo(Description description) { 58 | description.appendText("Regex should match '").appendText(textToMatch).appendText("'"); 59 | } 60 | 61 | @Override 62 | protected void describeMismatchSafely(ReadableRegexPattern item, Description mismatchDescription) { 63 | mismatchDescription.appendText("'").appendText(item.toString()).appendText("'") 64 | .appendText(" doesn't match"); 65 | } 66 | } 67 | --------------------------------------------------------------------------------