From 87aa1c7ca154aa280dfbda1665b704de817cba81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Mon, 24 Jul 2017 16:02:50 +0200 Subject: [PATCH] initial commit --- .gitignore | 16 + LICENSE.txt | 202 + README.adoc | 11 + build.gradle | 114 + config/checkstyle/checkstyle.xml | 323 + gradle.properties | 8 + gradle/ext.gradle | 8 + gradle/publish.gradle | 66 + gradle/sonarqube.gradle | 39 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54712 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 + gradlew.bat | 84 + src/docs/asciidoc/css/foundation.css | 684 ++ src/docs/asciidoc/index.adoc | 11 + src/docs/asciidoclet/overview.adoc | 4 + .../java/org/xbib/net/IRISyntaxException.java | 18 + .../java/org/xbib/net/PercentDecoder.java | 182 + .../java/org/xbib/net/PercentEncoder.java | 176 + .../java/org/xbib/net/PercentEncoders.java | 172 + .../java/org/xbib/net/ProtocolVersion.java | 9 + .../java/org/xbib/net/QueryParameters.java | 67 + .../org/xbib/net/SimpleNamespaceContext.java | 118 + src/main/java/org/xbib/net/URL.java | 1259 ++++ .../java/org/xbib/net/internal/LRUCache.java | 26 + .../org/xbib/net/internal/package-info.java | 4 + .../org/xbib/net/matcher/CharMatcher.java | 752 ++ .../org/xbib/net/matcher/package-info.java | 4 + src/main/java/org/xbib/net/package-info.java | 4 + .../java/org/xbib/net/path/PathDecoder.java | 121 + .../java/org/xbib/net/path/PathMatcher.java | 313 + .../org/xbib/net/path/PathNormalizer.java | 194 + .../xbib/net/path/PathPatternComparator.java | 61 + .../org/xbib/net/path/PathPatternInfo.java | 89 + .../net/path/PathSeparatorPatternCache.java | 23 + .../org/xbib/net/path/PathStringMatcher.java | 80 + .../java/org/xbib/net/path/package-info.java | 4 + .../org/xbib/net/scheme/AbstractScheme.java | 34 + .../org/xbib/net/scheme/DefaultScheme.java | 12 + .../java/org/xbib/net/scheme/DnsScheme.java | 17 + .../java/org/xbib/net/scheme/FileScheme.java | 12 + .../java/org/xbib/net/scheme/FtpScheme.java | 16 + .../java/org/xbib/net/scheme/GitScheme.java | 16 + .../xbib/net/scheme/GitSecureHttpScheme.java | 16 + .../org/xbib/net/scheme/GopherScheme.java | 12 + .../java/org/xbib/net/scheme/HttpScheme.java | 35 + .../java/org/xbib/net/scheme/ImapScheme.java | 17 + .../java/org/xbib/net/scheme/IrcScheme.java | 18 + .../java/org/xbib/net/scheme/LdapScheme.java | 17 + .../org/xbib/net/scheme/MailtoScheme.java | 14 + .../java/org/xbib/net/scheme/NewsScheme.java | 18 + .../java/org/xbib/net/scheme/NntpScheme.java | 14 + .../java/org/xbib/net/scheme/Pop3Scheme.java | 18 + .../java/org/xbib/net/scheme/RedisScheme.java | 12 + .../java/org/xbib/net/scheme/RsyncScheme.java | 12 + .../java/org/xbib/net/scheme/RtmpScheme.java | 12 + .../java/org/xbib/net/scheme/RtspScheme.java | 14 + src/main/java/org/xbib/net/scheme/Scheme.java | 48 + .../org/xbib/net/scheme/SchemeRegistry.java | 79 + .../org/xbib/net/scheme/SecureHttpScheme.java | 12 + .../org/xbib/net/scheme/SecureImapScheme.java | 13 + .../org/xbib/net/scheme/SecureLdapScheme.java | 14 + .../org/xbib/net/scheme/SecureNewsScheme.java | 14 + .../org/xbib/net/scheme/SecurePop3Scheme.java | 13 + .../org/xbib/net/scheme/SecureSmtpScheme.java | 14 + .../net/scheme/SecureWebSocketScheme.java | 12 + .../java/org/xbib/net/scheme/SftpScheme.java | 12 + .../java/org/xbib/net/scheme/SmtpScheme.java | 18 + .../java/org/xbib/net/scheme/SshScheme.java | 15 + .../org/xbib/net/scheme/TelnetScheme.java | 14 + .../java/org/xbib/net/scheme/TftpScheme.java | 13 + .../java/org/xbib/net/scheme/UrnScheme.java | 14 + .../org/xbib/net/scheme/WebSocketScheme.java | 16 + .../org/xbib/net/scheme/package-info.java | 4 + .../org/xbib/net/template/URITemplate.java | 48 + .../template/expression/ExpressionType.java | 103 + .../expression/TemplateExpression.java | 71 + .../template/expression/TemplateLiteral.java | 20 + .../expression/URITemplateExpression.java | 10 + .../net/template/expression/package-info.java | 4 + .../org/xbib/net/template/package-info.java | 4 + .../net/template/parse/ExpressionParser.java | 63 + .../net/template/parse/LiteralParser.java | 45 + .../net/template/parse/TemplateParser.java | 12 + .../net/template/parse/URITemplateParser.java | 47 + .../template/parse/VariableSpecParser.java | 128 + .../xbib/net/template/parse/package-info.java | 4 + .../net/template/render/ListRenderer.java | 50 + .../xbib/net/template/render/MapRenderer.java | 62 + .../template/render/MultiValueRenderer.java | 49 + .../net/template/render/NullRenderer.java | 22 + .../net/template/render/StringRenderer.java | 53 + .../net/template/render/ValueRenderer.java | 81 + .../net/template/render/package-info.java | 4 + .../org/xbib/net/template/vars/Variables.java | 131 + .../xbib/net/template/vars/package-info.java | 4 + .../template/vars/specs/ExplodedVariable.java | 46 + .../template/vars/specs/PrefixVariable.java | 49 + .../template/vars/specs/SimpleVariable.java | 46 + .../net/template/vars/specs/VariableSpec.java | 66 + .../template/vars/specs/VariableSpecType.java | 21 + .../net/template/vars/specs/package-info.java | 4 + .../net/template/vars/values/ListValue.java | 104 + .../net/template/vars/values/MapValue.java | 104 + .../net/template/vars/values/NullValue.java | 15 + .../net/template/vars/values/ScalarValue.java | 29 + .../net/template/vars/values/ValueType.java | 68 + .../template/vars/values/VariableValue.java | 66 + .../template/vars/values/package-info.java | 4 + src/test/java/org/xbib/net/IRITest.java | 193 + .../java/org/xbib/net/PercentDecoderTest.java | 138 + .../java/org/xbib/net/PercentEncoderTest.java | 80 + .../java/org/xbib/net/URIComponentTest.java | 69 + .../java/org/xbib/net/URLBuilderTest.java | 268 + src/test/java/org/xbib/net/URLParserTest.java | 383 + .../java/org/xbib/net/URLResolverTest.java | 75 + src/test/java/org/xbib/net/URLTest.java | 120 + src/test/java/org/xbib/net/package-info.java | 4 + .../org/xbib/net/path/PathDecoderTest.java | 40 + .../org/xbib/net/path/PathMatcherTest.java | 578 ++ .../org/xbib/net/path/PathNormalizerTest.java | 73 + .../java/org/xbib/net/path/package-info.java | 4 + .../xbib/net/template/URITemplateTest.java | 412 ++ .../org/xbib/net/template/package-info.java | 4 + src/test/resources/extended-tests.json | 119 + src/test/resources/lists.json | 74 + src/test/resources/multipleLists.json | 41 + src/test/resources/multipleStrings.json | 65 + src/test/resources/negative-tests.json | 49 + src/test/resources/rfcExamples.json | 80 + .../resources/spec-examples-by-section.json | 437 ++ src/test/resources/spec-examples.json | 218 + src/test/resources/strings.json | 43 + src/test/resources/urltestdata.json | 6491 +++++++++++++++++ 134 files changed, 17780 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.adoc create mode 100644 build.gradle create mode 100644 config/checkstyle/checkstyle.xml create mode 100644 gradle.properties create mode 100644 gradle/ext.gradle create mode 100644 gradle/publish.gradle create mode 100644 gradle/sonarqube.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 src/docs/asciidoc/css/foundation.css create mode 100644 src/docs/asciidoc/index.adoc create mode 100644 src/docs/asciidoclet/overview.adoc create mode 100644 src/main/java/org/xbib/net/IRISyntaxException.java create mode 100644 src/main/java/org/xbib/net/PercentDecoder.java create mode 100644 src/main/java/org/xbib/net/PercentEncoder.java create mode 100644 src/main/java/org/xbib/net/PercentEncoders.java create mode 100644 src/main/java/org/xbib/net/ProtocolVersion.java create mode 100644 src/main/java/org/xbib/net/QueryParameters.java create mode 100644 src/main/java/org/xbib/net/SimpleNamespaceContext.java create mode 100755 src/main/java/org/xbib/net/URL.java create mode 100644 src/main/java/org/xbib/net/internal/LRUCache.java create mode 100644 src/main/java/org/xbib/net/internal/package-info.java create mode 100644 src/main/java/org/xbib/net/matcher/CharMatcher.java create mode 100644 src/main/java/org/xbib/net/matcher/package-info.java create mode 100644 src/main/java/org/xbib/net/package-info.java create mode 100644 src/main/java/org/xbib/net/path/PathDecoder.java create mode 100644 src/main/java/org/xbib/net/path/PathMatcher.java create mode 100644 src/main/java/org/xbib/net/path/PathNormalizer.java create mode 100644 src/main/java/org/xbib/net/path/PathPatternComparator.java create mode 100644 src/main/java/org/xbib/net/path/PathPatternInfo.java create mode 100644 src/main/java/org/xbib/net/path/PathSeparatorPatternCache.java create mode 100644 src/main/java/org/xbib/net/path/PathStringMatcher.java create mode 100644 src/main/java/org/xbib/net/path/package-info.java create mode 100644 src/main/java/org/xbib/net/scheme/AbstractScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/DefaultScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/DnsScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/FileScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/FtpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/GitScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/GitSecureHttpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/GopherScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/HttpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/ImapScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/IrcScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/LdapScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/MailtoScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/NewsScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/NntpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/Pop3Scheme.java create mode 100644 src/main/java/org/xbib/net/scheme/RedisScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/RsyncScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/RtmpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/RtspScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/Scheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SchemeRegistry.java create mode 100644 src/main/java/org/xbib/net/scheme/SecureHttpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SecureImapScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SecureLdapScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SecureNewsScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SecurePop3Scheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SecureSmtpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SecureWebSocketScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SftpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SmtpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/SshScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/TelnetScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/TftpScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/UrnScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/WebSocketScheme.java create mode 100644 src/main/java/org/xbib/net/scheme/package-info.java create mode 100644 src/main/java/org/xbib/net/template/URITemplate.java create mode 100644 src/main/java/org/xbib/net/template/expression/ExpressionType.java create mode 100644 src/main/java/org/xbib/net/template/expression/TemplateExpression.java create mode 100644 src/main/java/org/xbib/net/template/expression/TemplateLiteral.java create mode 100644 src/main/java/org/xbib/net/template/expression/URITemplateExpression.java create mode 100644 src/main/java/org/xbib/net/template/expression/package-info.java create mode 100644 src/main/java/org/xbib/net/template/package-info.java create mode 100644 src/main/java/org/xbib/net/template/parse/ExpressionParser.java create mode 100644 src/main/java/org/xbib/net/template/parse/LiteralParser.java create mode 100644 src/main/java/org/xbib/net/template/parse/TemplateParser.java create mode 100644 src/main/java/org/xbib/net/template/parse/URITemplateParser.java create mode 100644 src/main/java/org/xbib/net/template/parse/VariableSpecParser.java create mode 100644 src/main/java/org/xbib/net/template/parse/package-info.java create mode 100644 src/main/java/org/xbib/net/template/render/ListRenderer.java create mode 100644 src/main/java/org/xbib/net/template/render/MapRenderer.java create mode 100644 src/main/java/org/xbib/net/template/render/MultiValueRenderer.java create mode 100644 src/main/java/org/xbib/net/template/render/NullRenderer.java create mode 100644 src/main/java/org/xbib/net/template/render/StringRenderer.java create mode 100644 src/main/java/org/xbib/net/template/render/ValueRenderer.java create mode 100644 src/main/java/org/xbib/net/template/render/package-info.java create mode 100644 src/main/java/org/xbib/net/template/vars/Variables.java create mode 100644 src/main/java/org/xbib/net/template/vars/package-info.java create mode 100644 src/main/java/org/xbib/net/template/vars/specs/ExplodedVariable.java create mode 100644 src/main/java/org/xbib/net/template/vars/specs/PrefixVariable.java create mode 100644 src/main/java/org/xbib/net/template/vars/specs/SimpleVariable.java create mode 100644 src/main/java/org/xbib/net/template/vars/specs/VariableSpec.java create mode 100644 src/main/java/org/xbib/net/template/vars/specs/VariableSpecType.java create mode 100644 src/main/java/org/xbib/net/template/vars/specs/package-info.java create mode 100644 src/main/java/org/xbib/net/template/vars/values/ListValue.java create mode 100644 src/main/java/org/xbib/net/template/vars/values/MapValue.java create mode 100644 src/main/java/org/xbib/net/template/vars/values/NullValue.java create mode 100644 src/main/java/org/xbib/net/template/vars/values/ScalarValue.java create mode 100644 src/main/java/org/xbib/net/template/vars/values/ValueType.java create mode 100644 src/main/java/org/xbib/net/template/vars/values/VariableValue.java create mode 100644 src/main/java/org/xbib/net/template/vars/values/package-info.java create mode 100644 src/test/java/org/xbib/net/IRITest.java create mode 100644 src/test/java/org/xbib/net/PercentDecoderTest.java create mode 100644 src/test/java/org/xbib/net/PercentEncoderTest.java create mode 100644 src/test/java/org/xbib/net/URIComponentTest.java create mode 100644 src/test/java/org/xbib/net/URLBuilderTest.java create mode 100644 src/test/java/org/xbib/net/URLParserTest.java create mode 100644 src/test/java/org/xbib/net/URLResolverTest.java create mode 100644 src/test/java/org/xbib/net/URLTest.java create mode 100644 src/test/java/org/xbib/net/package-info.java create mode 100644 src/test/java/org/xbib/net/path/PathDecoderTest.java create mode 100644 src/test/java/org/xbib/net/path/PathMatcherTest.java create mode 100644 src/test/java/org/xbib/net/path/PathNormalizerTest.java create mode 100644 src/test/java/org/xbib/net/path/package-info.java create mode 100644 src/test/java/org/xbib/net/template/URITemplateTest.java create mode 100644 src/test/java/org/xbib/net/template/package-info.java create mode 100644 src/test/resources/extended-tests.json create mode 100644 src/test/resources/lists.json create mode 100644 src/test/resources/multipleLists.json create mode 100644 src/test/resources/multipleStrings.json create mode 100644 src/test/resources/negative-tests.json create mode 100644 src/test/resources/rfcExamples.json create mode 100644 src/test/resources/spec-examples-by-section.json create mode 100644 src/test/resources/spec-examples.json create mode 100644 src/test/resources/strings.json create mode 100644 src/test/resources/urltestdata.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9543d32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/data +/work +/logs +/.idea +/target +.DS_Store +*.iml +/.settings +/.classpath +/.project +/.gradle +/build +/plugins +/sessions +*~ +*.MARC diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..860b70b --- /dev/null +++ b/README.adoc @@ -0,0 +1,11 @@ +# Network classes for Java + +image:https://api.travis-ci.org/xbib/net.svg[title="Build status", link="https://travis-ci.org/xbib/net/"] +image:https://img.shields.io/sonar/http/nemo.sonarqube.com/org.xbib%3Anet/coverage.svg?style=flat-square[title="Coverage", link="https://sonarqube.com/dashboard/index?id=org.xbib%3Anet"] +image:https://maven-badges.herokuapp.com/maven-central/org.xbib/net/badge.svg[title="Maven Central", link="http://search.maven.org/#search%7Cga%7C1%7Cxbib%20net"] +image:https://img.shields.io/badge/License-Apache%202.0-blue.svg[title="Apache License 2.0", link="https://opensource.org/licenses/Apache-2.0"] +image:https://img.shields.io/twitter/url/https/twitter.com/xbib.svg?style=social&label=Follow%20%40xbib[title="Twitter", link="https://twitter.com/xbib"] + + +image:https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif[title="PayPal", link="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=GVHFQYZ9WZ8HG"] + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..a42a402 --- /dev/null +++ b/build.gradle @@ -0,0 +1,114 @@ + +plugins { + id "org.sonarqube" version "2.5" + id "org.xbib.gradle.plugin.asciidoctor" version "1.5.4.1.0" + id "io.codearte.nexus-staging" version "0.7.0" +} + +printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + + "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", + InetAddress.getLocalHost(), + System.getProperty("os.name"), + System.getProperty("os.arch"), + System.getProperty("os.version"), + System.getProperty("java.version"), + System.getProperty("java.vm.version"), + System.getProperty("java.vm.vendor"), + System.getProperty("java.vm.name"), + GroovySystem.getVersion(), + gradle.gradleVersion + + +apply plugin: 'java' +apply plugin: 'maven' +apply plugin: 'signing' +apply plugin: 'findbugs' +apply plugin: 'pmd' +apply plugin: 'checkstyle' +apply plugin: "jacoco" +apply plugin: "io.codearte.nexus-staging" +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +repositories { + mavenCentral() +} + +configurations { + asciidoclet + wagon +} + +dependencies { + testCompile "junit:junit:${project.property('junit.version')}" + wagon "org.apache.maven.wagon:wagon-ssh-external:2.12" + testCompile "com.fasterxml.jackson.core:jackson-databind:${project.property('jackson.version')}" + asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}" + wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}" + +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-profile" << "compact1" +} + +jar { + manifest { + attributes('Implementation-Version': project.version) + } +} + +test { + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + } + systemProperty 'java.net.preferIPv4Stack','false' + systemProperty 'java.net.preferIPv6Addresses', 'true' +} + +asciidoctor { + backends 'html5' + separateOutputDirs = false + attributes 'source-highlighter': 'coderay', + toc : '', + idprefix : '', + idseparator : '-', + stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css" +} + +javadoc { + options.docletpath = configurations.asciidoclet.files.asType(List) + options.doclet = 'org.asciidoctor.Asciidoclet' + options.overview = "src/docs/asciidoclet/overview.adoc" + options.addStringOption "-base-dir", "${projectDir}" + options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" + configure(options) { + noTimestamp = true + } +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier 'sources' + from sourceSets.main.allSource +} +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' +} +artifacts { + archives sourcesJar, javadocJar +} +if (project.hasProperty('signing.keyId')) { + signing { + sign configurations.archives + } +} + +apply from: "${rootProject.projectDir}/gradle/ext.gradle" +apply from: "${rootProject.projectDir}/gradle/publish.gradle" +apply from: "${rootProject.projectDir}/gradle/sonarqube.gradle" + diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..3dc3e16 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..19d0f91 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,8 @@ +group = org.xbib +name = net +version = 1.0.0 + +junit.version = 4.12 +asciidoclet.version = 1.5.4 +wagon.version = 2.12 +jackson.version = 2.8.4 diff --git a/gradle/ext.gradle b/gradle/ext.gradle new file mode 100644 index 0000000..6d3ed1f --- /dev/null +++ b/gradle/ext.gradle @@ -0,0 +1,8 @@ +ext { + user = 'xbib' + projectName = 'content' + projectDescription = 'Content processing library for Java' + scmUrl = 'https://github.com/xbib/content' + scmConnection = 'scm:git:git://github.com/xbib/content.git' + scmDeveloperConnection = 'scm:git:git://github.com/xbib/content.git' +} diff --git a/gradle/publish.gradle b/gradle/publish.gradle new file mode 100644 index 0000000..caf0531 --- /dev/null +++ b/gradle/publish.gradle @@ -0,0 +1,66 @@ + +task xbibUpload(type: Upload, dependsOn: build) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('xbibUsername')) { + mavenDeployer { + configuration = configurations.wagon + repository(url: uri('scpexe://xbib.org/repository')) { + authentication(userName: xbibUsername, privateKey: xbibPrivateKey) + } + } + } + } +} + +task sonatypeUpload(type: Upload, dependsOn: build) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('ossrhUsername')) { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + repository(url: uri(ossrhReleaseUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + snapshotRepository(url: uri(ossrhSnapshotUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + pom.project { + groupId project.group + artifactId project.name + version project.version + name project.name + description projectDescription + packaging 'jar' + inceptionYear '2016' + url scmUrl + organization { + name 'xbib' + url 'http://xbib.org' + } + developers { + developer { + id user + name 'Jörg Prante' + email 'joergprante@gmail.com' + url 'https://github.com/jprante' + } + } + scm { + url scmUrl + connection scmConnection + developerConnection scmDeveloperConnection + } + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } + } +} diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle new file mode 100644 index 0000000..3985a4f --- /dev/null +++ b/gradle/sonarqube.gradle @@ -0,0 +1,39 @@ +tasks.withType(FindBugs) { + ignoreFailures = true + reports { + xml.enabled = false + html.enabled = true + } +} +tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} +tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} + +jacocoTestReport { + reports { + xml.enabled = true + csv.enabled = false + } +} + +sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.java.coveragePlugin", "jacoco" + property "sonar.junit.reportsPath", "build/test-results/test/" + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c8f75464242443e480454f1eb7a4b14dec7a9a59 GIT binary patch literal 54712 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

49F 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d1ec295 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jul 13 21:48:43 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/docs/asciidoc/css/foundation.css b/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..1f11ab8 --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,11 @@ += Net classes for Java +Jörg Prante +Version 1.0 +:sectnums: +:toc: preamble +:toclevels: 4 +:!toc-title: Content +:experimental: +:description: Net classes for URL +:keywords: Java, Net, URL, URI, IRI +:icons: font diff --git a/src/docs/asciidoclet/overview.adoc b/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..8398dd7 --- /dev/null +++ b/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Net classes for Java +Jörg Prante +Version 1.0 + diff --git a/src/main/java/org/xbib/net/IRISyntaxException.java b/src/main/java/org/xbib/net/IRISyntaxException.java new file mode 100644 index 0000000..e9233a6 --- /dev/null +++ b/src/main/java/org/xbib/net/IRISyntaxException.java @@ -0,0 +1,18 @@ +package org.xbib.net; + +/** + * + */ +public class IRISyntaxException extends RuntimeException { + + private static final long serialVersionUID = 1813084470937980392L; + + IRISyntaxException(String message) { + super(message); + } + + IRISyntaxException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/org/xbib/net/PercentDecoder.java b/src/main/java/org/xbib/net/PercentDecoder.java new file mode 100644 index 0000000..cfc37b9 --- /dev/null +++ b/src/main/java/org/xbib/net/PercentDecoder.java @@ -0,0 +1,182 @@ +package org.xbib.net; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; + +/** + * Decodes percent-encoded strings. + */ +public class PercentDecoder { + + /** + * Written to with decoded chars by decoder. + */ + private final CharBuffer decodedCharBuf; + + private final CharsetDecoder decoder; + + /** + * The decoded string for the current input. + */ + private final StringBuilder outputBuf; + + /** + * The bytes represented by the current sequence of %-triples. Resized as needed. + */ + private ByteBuffer encodedBuf; + + public PercentDecoder() { + this(StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT)); + } + + /** + * Construct a new PercentDecoder with default buffer sizes. + * + * @param charsetDecoder Charset to decode bytes into chars with + * @see PercentDecoder#PercentDecoder(CharsetDecoder, int, int) + */ + public PercentDecoder(CharsetDecoder charsetDecoder) { + this(charsetDecoder, 16, 16); + } + + /** + * @param charsetDecoder Charset to decode bytes into chars with + * @param initialEncodedByteBufSize Initial size of buffer that holds encoded bytes + * @param decodedCharBufSize Size of buffer that encoded bytes are decoded into + */ + public PercentDecoder(CharsetDecoder charsetDecoder, int initialEncodedByteBufSize, + int decodedCharBufSize) { + this.outputBuf = new StringBuilder(); + this.encodedBuf = ByteBuffer.allocate(initialEncodedByteBufSize); + this.decodedCharBuf = CharBuffer.allocate(decodedCharBufSize); + this.decoder = charsetDecoder; + } + + /** + * Decode a percent-encoded character sequuence to a string. + * + * @param input Input with %-encoded representation of characters in this instance's configured character set, e.g. + * "%20" for a space character + * @return Corresponding string with %-encoded data decoded and converted to their corresponding characters + * @throws MalformedInputException if decoder is configured to report errors and malformed input is detected + * @throws UnmappableCharacterException if decoder is configured to report errors and an unmappable character is + * detected + */ + public String decode(CharSequence input) throws MalformedInputException, UnmappableCharacterException { + if (input == null) { + return null; + } + outputBuf.setLength(0); + outputBuf.ensureCapacity((input.length() / 8)); + encodedBuf.clear(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c != '%') { + handleEncodedBytes(); + outputBuf.append(c); + continue; + } + if (i + 2 >= input.length()) { + throw new IllegalArgumentException("could not percent decode <" + + input + + ">: incomplete %-pair at position " + i); + } + if (encodedBuf.remaining() == 0) { + ByteBuffer largerBuf = ByteBuffer.allocate(encodedBuf.capacity() * 2); + encodedBuf.flip(); + largerBuf.put(encodedBuf); + encodedBuf = largerBuf; + } + int c1 = input.charAt(++i); + int c2 = input.charAt(++i); + encodedBuf.put(decode((char) c1, (char) c2)); + } + handleEncodedBytes(); + return outputBuf.toString(); + } + + private static byte decode(char c1, char c2) { + byte b1 = (byte) decode(c1); + byte b2 = (byte) decode(c2); + if (b1 == -1 || b2 == -1) { + throw new IllegalArgumentException("invalid %-tuple <%" + c1 + c2 + ">"); + } + return (byte) ((b1 & 0xf) << 4 | (b2 & 0xf)); + } + + private static int decode(char c) { + return (c >= '0' && c <= '9') ? c - '0' : + (c >= 'A' && c <= 'F') ? c - 'A' + 10 : + (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; + } + + /** + * Decode any buffered encoded bytes and write them to the output buf. + */ + private void handleEncodedBytes() throws MalformedInputException, UnmappableCharacterException { + if (encodedBuf.position() == 0) { + return; + } + decoder.reset(); + CoderResult coderResult = CoderResult.OVERFLOW; + encodedBuf.flip(); + while (coderResult == CoderResult.OVERFLOW && encodedBuf.hasRemaining()) { + decodedCharBuf.clear(); + coderResult = decoder.decode(encodedBuf, decodedCharBuf, false); + throwIfError(coderResult); + decodedCharBuf.flip(); + outputBuf.append(decodedCharBuf); + } + decodedCharBuf.clear(); + coderResult = decoder.decode(encodedBuf, decodedCharBuf, true); + throwIfError(coderResult); + if (encodedBuf.hasRemaining()) { + throw new IllegalStateException("final decode didn't error, but didn't consume remaining input bytes"); + } + if (coderResult != CoderResult.UNDERFLOW) { + throw new IllegalStateException("expected underflow, but instead final decode returned " + coderResult); + } + decodedCharBuf.flip(); + outputBuf.append(decodedCharBuf); + encodedBuf.clear(); + flush(); + } + + /** + * Must only be called when the input encoded bytes buffer is empty. + */ + private void flush() throws MalformedInputException, UnmappableCharacterException { + CoderResult coderResult; + decodedCharBuf.clear(); + coderResult = decoder.flush(decodedCharBuf); + decodedCharBuf.flip(); + outputBuf.append(decodedCharBuf); + throwIfError(coderResult); + if (coderResult != CoderResult.UNDERFLOW) { + throw new IllegalStateException("decoder flush resulted in " + coderResult); + } + } + + /** + * If the coder result is considered an error (i.e. not overflow or underflow), throw the corresponding + * CharacterCodingException. + * + * @param coderResult result to check + * @throws MalformedInputException if result represents malformed input + * @throws UnmappableCharacterException if result represents an unmappable character + */ + private static void throwIfError(CoderResult coderResult) throws MalformedInputException, UnmappableCharacterException { + if (coderResult.isMalformed()) { + throw new MalformedInputException(coderResult.length()); + } + if (coderResult.isUnmappable()) { + throw new UnmappableCharacterException(coderResult.length()); + } + } +} diff --git a/src/main/java/org/xbib/net/PercentEncoder.java b/src/main/java/org/xbib/net/PercentEncoder.java new file mode 100644 index 0000000..68677de --- /dev/null +++ b/src/main/java/org/xbib/net/PercentEncoder.java @@ -0,0 +1,176 @@ +package org.xbib.net; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.util.BitSet; + +/** + * Encodes unsafe characters as a sequence of %XX hex-encoded bytes. + * + * This is typically done when encoding components of URLs. See {@link PercentEncoders} for pre-configured + * PercentEncoder instances. + */ +public class PercentEncoder { + + private static final char[] HEX_CODE = "0123456789ABCDEF".toCharArray(); + + private final BitSet safeChars; + + private final CharsetEncoder encoder; + + private final StringBuilderPercentEncoderOutputHandler stringHandler; + + private final ByteBuffer encodedBytes; + + private final CharBuffer unsafeCharsToEncode; + + /** + * @param safeChars the set of chars to NOT encode, stored as a bitset with the int positions corresponding to + * those chars set to true. Treated as read only. + * @param charsetEncoder charset encoder to encode characters with. Make sure to not re-use CharsetEncoder instances + * across threads. + */ + PercentEncoder(BitSet safeChars, CharsetEncoder charsetEncoder) { + this.safeChars = safeChars; + this.encoder = charsetEncoder; + this.stringHandler = new StringBuilderPercentEncoderOutputHandler(); + int maxBytesPerChar = 1 + (int) encoder.maxBytesPerChar(); + encodedBytes = ByteBuffer.allocate(maxBytesPerChar * 2); + unsafeCharsToEncode = CharBuffer.allocate(2); + } + + /** + * Encode the input and pass output chars to a handler. + * + * @param input input string + * @param handler handler to call on each output character + * @throws MalformedInputException if encoder is configured to report errors and malformed input is detected + * @throws UnmappableCharacterException if encoder is configured to report errors and an unmappable character is + * detected + */ + private void encode(CharSequence input, StringBuilderPercentEncoderOutputHandler handler) + throws MalformedInputException, UnmappableCharacterException { + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + int cp = Character.codePointAt(String.valueOf(c), 0); + if (safeChars.get(cp)) { + handler.onOutputChar(c); + continue; + } + unsafeCharsToEncode.clear(); + unsafeCharsToEncode.append(c); + if (Character.isHighSurrogate(c)) { + if (input.length() > i + 1) { + char lowSurrogate = input.charAt(i + 1); + if (Character.isLowSurrogate(lowSurrogate)) { + unsafeCharsToEncode.append(lowSurrogate); + i++; + } else { + throw new IllegalArgumentException("invalid UTF-16: character " + + i + " is a high surrogate (\\u" + + Integer.toHexString(cp) + "), but char " + (i + 1) + + " is not a low surrogate (\\u" + + Integer.toHexString(Character.codePointAt(String.valueOf(lowSurrogate), 0)) + ")"); + } + } else { + throw new IllegalArgumentException("invalid UTF-16: the last character in the input string " + + "was a high surrogate (\\u" + Integer.toHexString(cp) + ")"); + } + } + flushUnsafeCharBuffer(handler); + } + } + + /** + * Encode the input and return the resulting text as a String. + * + * @param input input string + * @return the input string with every character that's not in safeChars turned into its byte representation via the + * instance's encoder and then percent-encoded + * @throws MalformedInputException if encoder is configured to report errors and malformed input is detected + * @throws UnmappableCharacterException if encoder is configured to report errors and an unmappable character is + * detected + */ + public String encode(CharSequence input) throws MalformedInputException, UnmappableCharacterException { + if (input == null) { + return null; + } + stringHandler.reset(); + stringHandler.ensureCapacity(input.length()); + encode(input, stringHandler); + return stringHandler.getContents(); + } + + /** + * Encode unsafeCharsToEncode to bytes as per charsetEncoder, then percent-encode those bytes into output. + * + * Side effects: unsafeCharsToEncode will be read from and cleared. encodedBytes will be cleared and written to. + * + * @param handler where the encoded versions of the contents of unsafeCharsToEncode will be written + */ + private void flushUnsafeCharBuffer(StringBuilderPercentEncoderOutputHandler handler) + throws MalformedInputException, UnmappableCharacterException { + // need to read from the char buffer, which was most recently written to + unsafeCharsToEncode.flip(); + encodedBytes.clear(); + encoder.reset(); + CoderResult result = encoder.encode(unsafeCharsToEncode, encodedBytes, true); + throwIfError(result); + result = encoder.flush(encodedBytes); + throwIfError(result); + encodedBytes.flip(); + while (encodedBytes.hasRemaining()) { + byte b = encodedBytes.get(); + handler.onOutputChar('%'); + handler.onOutputChar(HEX_CODE[b >> 4 & 0xF]); + handler.onOutputChar(HEX_CODE[b & 0xF]); + } + } + + /** + * @param result result to check + * @throws IllegalStateException if result is overflow + * @throws MalformedInputException if result represents malformed input + * @throws UnmappableCharacterException if result represents an unmappable character + */ + private static void throwIfError(CoderResult result) throws MalformedInputException, UnmappableCharacterException { + if (result.isOverflow()) { + throw new IllegalStateException("Byte buffer overflow, this should not happen"); + } + if (result.isMalformed()) { + throw new MalformedInputException(result.length()); + } + if (result.isUnmappable()) { + throw new UnmappableCharacterException(result.length()); + } + } + + static class StringBuilderPercentEncoderOutputHandler { + + private final StringBuilder stringBuilder; + + StringBuilderPercentEncoderOutputHandler() { + stringBuilder = new StringBuilder(); + } + + String getContents() { + return stringBuilder.toString(); + } + + void reset() { + stringBuilder.setLength(0); + } + + void ensureCapacity(int length) { + stringBuilder.ensureCapacity(length); + } + + void onOutputChar(char c) { + stringBuilder.append(c); + } + } +} diff --git a/src/main/java/org/xbib/net/PercentEncoders.java b/src/main/java/org/xbib/net/PercentEncoders.java new file mode 100644 index 0000000..2e53b6f --- /dev/null +++ b/src/main/java/org/xbib/net/PercentEncoders.java @@ -0,0 +1,172 @@ +package org.xbib.net; + +import java.nio.charset.Charset; +import java.util.BitSet; + +import static java.nio.charset.CodingErrorAction.REPORT; + +/** + * See RFC 3986, RFC 1738 and http://www.lunatech-research.com/archives/2009/02/03/what-every-web-developer-must-know-about-url-encoding. + */ +public class PercentEncoders { + + private static final BitSet UNRESERVED_BIT_SET = new BitSet(); + /** + * an encoder for RFC 3986 reg-names. + */ + private static final BitSet REG_NAME_BIT_SET = new BitSet(); + private static final BitSet PATH_BIT_SET = new BitSet(); + private static final BitSet MATRIX_BIT_SET = new BitSet(); + private static final BitSet QUERY_BIT_SET = new BitSet(); + private static final BitSet QUERY_PARAM_BIT_SET = new BitSet(); + private static final BitSet FRAGMENT_BIT_SET = new BitSet(); + + static { + // minimal encoding, for URI templates RFC 6570 + addUnreserved(UNRESERVED_BIT_SET); + // RFC 3986 'reg-name'. This is not very aggressive. + // It's quite possible to have DNS-illegal names out of this. + // Regardless, it will at least be URI-compliant even if it's not HTTP URL-compliant. + addUnreserved(REG_NAME_BIT_SET); + addSubdelims(REG_NAME_BIT_SET); + // Represents RFC 3986 'pchar'. Remove delimiter that starts matrix section. + addPChar(PATH_BIT_SET); + PATH_BIT_SET.clear((int) ';'); + // Remove delims for HTTP matrix params as per RFC 1738 S3.3. + // The other reserved chars ('/' and '?') are already excluded. + addPChar(MATRIX_BIT_SET); + MATRIX_BIT_SET.clear((int) ';'); + MATRIX_BIT_SET.clear((int) '='); + /* + * At this point it represents RFC 3986 'query'. http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 also + * specifies that "+" can mean space in a query, so we will make sure to say that '+' is not safe to leave as-is + */ + addQuery(QUERY_BIT_SET); + QUERY_BIT_SET.clear((int) '+'); + /* + * Create more stringent requirements for HTML4 queries: remove delimiters for HTML query params so that key=value + * pairs can be used. + */ + QUERY_PARAM_BIT_SET.or(QUERY_BIT_SET); + QUERY_PARAM_BIT_SET.clear((int) '='); + QUERY_PARAM_BIT_SET.clear((int) '&'); + addFragment(FRAGMENT_BIT_SET); + } + + public static PercentEncoder getUnreservedEncoder(Charset charset) { + return new PercentEncoder(UNRESERVED_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getCookieEncoder(Charset charset) { + return new PercentEncoder(UNRESERVED_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getRegNameEncoder(Charset charset) { + return new PercentEncoder(REG_NAME_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getPathEncoder(Charset charset) { + return new PercentEncoder(PATH_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getMatrixEncoder(Charset charset) { + return new PercentEncoder(MATRIX_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getQueryEncoder(Charset charset) { + return new PercentEncoder(QUERY_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getQueryParamEncoder(Charset charset) { + return new PercentEncoder(QUERY_PARAM_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getFragmentEncoder(Charset charset) { + return new PercentEncoder(FRAGMENT_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + private PercentEncoders() { + } + + /** + * Add code points for 'fragment' chars. + * + * @param fragmentBitSet bit set + */ + private static void addFragment(BitSet fragmentBitSet) { + addPChar(fragmentBitSet); + fragmentBitSet.set((int) '/'); + fragmentBitSet.set((int) '?'); + } + + /** + * Add code points for 'query' chars. + * + * @param queryBitSet bit set + */ + private static void addQuery(BitSet queryBitSet) { + addPChar(queryBitSet); + queryBitSet.set((int) '/'); + queryBitSet.set((int) '?'); + } + + /** + * Add code points for 'pchar' chars. + * + * @param bs bitset + */ + private static void addPChar(BitSet bs) { + addUnreserved(bs); + addSubdelims(bs); + bs.set((int) ':'); + bs.set((int) '@'); + } + + /** + * Add codepoints for 'unreserved' chars. + * + * @param bs bitset to add codepoints to + */ + private static void addUnreserved(BitSet bs) { + for (int i = 'a'; i <= 'z'; i++) { + bs.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + bs.set(i); + } + for (int i = '0'; i <= '9'; i++) { + bs.set(i); + } + bs.set((int) '-'); + bs.set((int) '.'); + bs.set((int) '_'); + bs.set((int) '~'); + } + + /** + * Add codepoints for 'sub-delims' chars. + * + * @param bs bitset to add codepoints to + */ + private static void addSubdelims(BitSet bs) { + bs.set((int) '!'); + bs.set((int) '$'); + bs.set((int) '&'); + bs.set((int) '\''); + bs.set((int) '('); + bs.set((int) ')'); + bs.set((int) '*'); + bs.set((int) '+'); + bs.set((int) ','); + bs.set((int) ';'); + bs.set((int) '='); + } +} diff --git a/src/main/java/org/xbib/net/ProtocolVersion.java b/src/main/java/org/xbib/net/ProtocolVersion.java new file mode 100644 index 0000000..2f4f96e --- /dev/null +++ b/src/main/java/org/xbib/net/ProtocolVersion.java @@ -0,0 +1,9 @@ +package org.xbib.net; + +/** + * The TCP/IP network protocol versions. + */ +public enum ProtocolVersion { + + IPV4, IPV6, IPV46, NONE +} diff --git a/src/main/java/org/xbib/net/QueryParameters.java b/src/main/java/org/xbib/net/QueryParameters.java new file mode 100644 index 0000000..183ca3a --- /dev/null +++ b/src/main/java/org/xbib/net/QueryParameters.java @@ -0,0 +1,67 @@ +package org.xbib.net; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + */ +public class QueryParameters extends ArrayList> { + + private static final long serialVersionUID = 1195469379836789386L; + + private final int max; + + public QueryParameters() { + this(1024); + } + + public QueryParameters(int max) { + this.max = max; + } + + public List get(String key) { + return stream() + .filter(p -> key.equals(p.getFirst())) + .map(Pair::getSecond) + .collect(Collectors.toList()); + } + + public QueryParameters add(String name, String value) { + add(new Pair<>(name, value)); + return this; + } + + @Override + public boolean add(QueryParameters.Pair element) { + return size() < max && super.add(element); + } + + /** + * A pair of query parameters. + * @param the key type parameter + * @param the value type parameter + */ + public static class Pair { + private final K first; + private final V second; + + public Pair(K first, V second) { + this.first = first; + this.second = second; + } + + public K getFirst() { + return first; + } + + public V getSecond() { + return second; + } + + @Override + public String toString() { + return first + ":" + second; + } + } +} diff --git a/src/main/java/org/xbib/net/SimpleNamespaceContext.java b/src/main/java/org/xbib/net/SimpleNamespaceContext.java new file mode 100644 index 0000000..8b0c9ee --- /dev/null +++ b/src/main/java/org/xbib/net/SimpleNamespaceContext.java @@ -0,0 +1,118 @@ +package org.xbib.net; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Contains a simple context for namespaces. + */ +public class SimpleNamespaceContext { + + private static final Logger logger = Logger.getLogger(SimpleNamespaceContext.class.getName()); + + private static final String DEFAULT_RESOURCE = + SimpleNamespaceContext.class.getPackage().getName().replace('.', '/') + '/' + "namespace"; + + private static final SimpleNamespaceContext DEFAULT_CONTEXT = newDefaultInstance(); + + // sort namespace by length in descending order, useful for compacting prefix + protected final SortedMap namespaces = new TreeMap<>(); + + private final SortedMap> prefixes = new TreeMap<>(); + + protected SimpleNamespaceContext() { + } + + protected SimpleNamespaceContext(ResourceBundle bundle) { + Enumeration en = bundle.getKeys(); + while (en.hasMoreElements()) { + String prefix = en.nextElement(); + String namespace = bundle.getString(prefix); + addNamespace(prefix, namespace); + } + } + + public static SimpleNamespaceContext getInstance() { + return DEFAULT_CONTEXT; + } + + /** + * Empty namespace context. + * + * @return an XML namespace context + */ + public static SimpleNamespaceContext newInstance() { + return new SimpleNamespaceContext(); + } + + public static SimpleNamespaceContext newDefaultInstance() { + return newInstance(DEFAULT_RESOURCE); + } + + /** + * Use thread context class laoder to instantiate a namespace context. + * @param bundleName the resource bundle name + * @return XML namespace context + */ + public static SimpleNamespaceContext newInstance(String bundleName) { + return newInstance(bundleName, Locale.getDefault(), Thread.currentThread().getContextClassLoader()); + } + + public static SimpleNamespaceContext newInstance(String bundleName, Locale locale, ClassLoader classLoader) { + try { + return new SimpleNamespaceContext(ResourceBundle.getBundle(bundleName, locale, classLoader)); + } catch (MissingResourceException e) { + logger.log(Level.WARNING, e.getMessage(), e); + return new SimpleNamespaceContext(); + } + } + + public void addNamespace(String prefix, String namespace) { + namespaces.put(prefix, namespace); + if (prefixes.containsKey(namespace)) { + prefixes.get(namespace).add(prefix); + } else { + Set set = new HashSet<>(); + set.add(prefix); + prefixes.put(namespace, set); + } + } + + public SortedMap getNamespaces() { + return namespaces; + } + + public String getNamespaceURI(String prefix) { + if (prefix == null) { + return null; + } + return namespaces.getOrDefault(prefix, null); + } + + public String getPrefix(String namespaceURI) { + Iterator it = getPrefixes(namespaceURI); + return it != null && it.hasNext() ? it.next() : null; + } + + public Iterator getPrefixes(String namespace) { + if (namespace == null) { + throw new IllegalArgumentException("namespace URI cannot be null"); + } + return prefixes.containsKey(namespace) ? + prefixes.get(namespace).iterator() : null; + } + + @Override + public String toString() { + return namespaces.toString(); + } +} diff --git a/src/main/java/org/xbib/net/URL.java b/src/main/java/org/xbib/net/URL.java new file mode 100755 index 0000000..5324afa --- /dev/null +++ b/src/main/java/org/xbib/net/URL.java @@ -0,0 +1,1259 @@ +package org.xbib.net; + +import org.xbib.net.scheme.Scheme; +import org.xbib.net.scheme.SchemeRegistry; + +import java.io.Serializable; +import java.net.IDN; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link URL} is a Java implementation of the Uniform Resource Identifier ({@code RFC 3986}) + * in accordance with the link:https://url.spec.whatwg.org/[{@code WHATWG} URL standard]. + * + * [source,java] + * -- + * URL url = URL.http().resolveFromHost("google.com").build(); + * -- + * + */ +public class URL implements Serializable { + + private static final Logger logger = Logger.getLogger(URL.class.getName()); + + private static final long serialVersionUID = 7936984038051707342L; + + private static final char SEPARATOR_CHAR = '/'; + + private static final char QUESTION_CHAR = '?'; + + private static final char COLON_CHAR = ':'; + + private static final char SEMICOLON_CHAR = ';'; + + private static final char EQUAL_CHAR = '='; + + private static final char AMPERSAND_CHAR = '&'; + + private static final char NUMBER_SIGN_CHAR = '#'; + + private static final char AT_CHAR = '@'; + + private static final String DOUBLE_SLASH = "//"; + + private static final String EMPTY = ""; + + private static final PathSegment EMPTY_SEGMENT = new PathSegment(EMPTY); + + private final transient Builder builder; + + private final transient Charset charset; + + private final transient Scheme scheme; + + private final String hostinfo; + + private final String path; + + private final String query; + + private final String fragment; + + private String internalStringRepresentation; + + private String externalStringRepresentation; + + private URL(Builder builder) { + this.builder = builder; + this.charset = builder.charset; + this.scheme = SchemeRegistry.getInstance().getScheme(builder.scheme); + this.hostinfo = encodeHostInfo(); + this.path = encodePath(); + this.query = encodeQuery(); + this.fragment = encodeFragment(); + } + + public static final URL INVALID = URL.builder().build(); + + public static Builder file() { + return new Builder().scheme(Scheme.FILE); + } + + public static Builder ftp() { + return new Builder().scheme(Scheme.FTP); + } + + public static Builder git() { + return new Builder().scheme(Scheme.GIT); + } + + public static Builder gopher() { + return new Builder().scheme(Scheme.GOPHER); + } + + public static Builder http() { + return new Builder().scheme(Scheme.HTTP); + } + + public static Builder https() { + return new Builder().scheme(Scheme.HTTPS); + } + + public static Builder imap() { + return new Builder().scheme(Scheme.IMAP); + } + + public static Builder imaps() { + return new Builder().scheme(Scheme.IMAPS); + } + + public static Builder irc() { + return new Builder().scheme(Scheme.IRC); + } + + public static Builder ldap() { + return new Builder().scheme(Scheme.LDAP); + } + + public static Builder ldaps() { + return new Builder().scheme(Scheme.LDAPS); + } + + public static Builder mailto() { + return new Builder().scheme(Scheme.MAILTO); + } + + public static Builder news() { + return new Builder().scheme(Scheme.NEWS); + } + + public static Builder nntp() { + return new Builder().scheme(Scheme.NNTP); + } + + public static Builder pop3() { + return new Builder().scheme(Scheme.POP3); + } + + public static Builder pop3s() { + return new Builder().scheme(Scheme.POP3S); + } + + public static Builder rtmp() { + return new Builder().scheme(Scheme.RTMP); + } + + public static Builder rtsp() { + return new Builder().scheme(Scheme.RTSP); + } + + public static Builder redis() { + return new Builder().scheme(Scheme.REDIS); + } + + public static Builder rsync() { + return new Builder().scheme(Scheme.RSYNC); + } + + public static Builder sftp() { + return new Builder().scheme(Scheme.SFTP); + } + + public static Builder smtp() { + return new Builder().scheme(Scheme.SMTP); + } + + public static Builder smtps() { + return new Builder().scheme(Scheme.SMTPS); + } + + public static Builder snews() { + return new Builder().scheme(Scheme.SNEWS); + } + + public static Builder ssh() { + return new Builder().scheme(Scheme.SSH); + } + + public static Builder telnet() { + return new Builder().scheme(Scheme.TELNET); + } + + public static Builder tftp() { + return new Builder().scheme(Scheme.TFTP); + } + + public static Builder ws() { + return new Builder().scheme(Scheme.WS); + } + + public static Builder wss() { + return new Builder().scheme(Scheme.WSS); + } + + public static Builder builder() { + return new Builder(); + } + + public static Parser parser() { + return new Parser(); + } + + public static Resolver base(URL base) { + return new Resolver(base); + } + + public static Resolver base(String base) { + return new Resolver(URL.create(base)); + } + + public static URL from(String input) { + try { + return parser().parse(input, true); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } + + public static URL create(String input) { + try { + return parser().parse(input, false); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } + + public URL resolve(String spec) { + return new Resolver(this).resolve(spec); + } + + public String decode(String input) { + try { + return builder.percentDecoder.decode(input); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new IllegalArgumentException(e); + } + } + + public Builder newBuilder() { + return builder; + } + + @Override + public boolean equals(Object other) { + return other != null && other instanceof URL && toString().equals(other.toString()); + } + + @Override + public String toString() { + return toString(true); + } + + public String toString(boolean withFragment) { + if (internalStringRepresentation != null) { + return internalStringRepresentation; + } + internalStringRepresentation = toInternalForm(withFragment); + return internalStringRepresentation; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * Gets the scheme of this {@code URL}. + * @return the scheme ('http' or 'file' or 'ftp' etc...) of the URL if it exists, or null. + */ + public String getScheme() { + return builder.scheme; + } + + /** + * Get the user info of this {@code URL}. + * @return the user info part if it exists. + */ + public String getUserInfo() { + return builder.userInfo; + } + + /** + * Get the host name ('www.example.com' or '192.168.0.1:8080' or '[fde2:d7de:302::]') of the {@code URL}. + * @return the host name + */ + public String getHost() { + return builder.host; + } + + public String getDecodedHost() { + return decode(builder.host); + } + + public String getHostInfo() { + return hostinfo; + } + + public ProtocolVersion getProtocolVersion() { + return builder.protocolVersion; + } + + public Integer getPort() { + return builder.port; + } + + /** + * Get the encoded path ('/path/to/my/file.html') of the {@code URL} if it exists. + * @return the encoded path + */ + public String getPath() { + return path; + } + + public String getDecodedPath() { + return decode(path); + } + + /** + * Get the query ('?q=foo{@literal &}bar') of the {@code URL} if it exists. + * @return the query + */ + public String getQuery() { + return query; + } + + public String getDecodedQuery() { + return decode(query); + } + + public QueryParameters getQueryParams() { + return builder.queryParams; + } + + /** + * @return the fragment ('#foo{@literal &}bar') of the URL if it exists. + */ + public String getFragment() { + return fragment; + } + + public String getDecodedFragment() { + return decode(fragment); + } + + /** + * @return the opaque part of the URL if it exists. + */ + public String getSchemeSpecificPart() { + return builder.schemeSpecificPart; + } + + /** + * @return true if URL is opaque. + */ + public boolean isOpaque() { + return !isNullOrEmpty(builder.scheme) && !isNullOrEmpty(builder.schemeSpecificPart) && builder.host == null; + } + + /** + * @return true if URL is absolute. + */ + public boolean isAbsolute() { + return !isNullOrEmpty(builder.scheme); + } + + public Comparator withFragmentComparator() { + return new URLWithFragmentComparator(); + } + + public Comparator withoutFragmentComparator() { + return new URLWithoutFragmentComparator(); + } + + public URL normalize() { + return scheme.normalize(this); + } + + public String toExternalForm() { + if (externalStringRepresentation != null) { + return externalStringRepresentation; + } + externalStringRepresentation = writeExternalForm(); + return externalStringRepresentation; + } + + private String toInternalForm(boolean withFragment) { + StringBuilder sb = new StringBuilder(); + if (!isNullOrEmpty(builder.scheme)) { + sb.append(builder.scheme).append(':'); + } + if (isOpaque()) { + sb.append(builder.schemeSpecificPart); + } else { + appendHostInfo(sb, false, true); + appendPath(sb, false); + if (!isNullOrEmpty(query)) { + sb.append(QUESTION_CHAR).append(query); + } + if (!isNullOrEmpty(fragment) && withFragment) { + sb.append(NUMBER_SIGN_CHAR).append(fragment); + } + } + return sb.toString(); + } + + private String writeExternalForm() { + StringBuilder sb = new StringBuilder(); + if (!isNullOrEmpty(builder.scheme)) { + sb.append(builder.scheme).append(':'); + } + if (isOpaque()) { + sb.append(builder.schemeSpecificPart); + } else { + appendHostInfo(sb, true, true); + appendPath(sb, true); + appendQuery(sb, true, true); + appendFragment(sb, true, true); + } + return sb.toString(); + } + + private String encodeHostInfo() { + StringBuilder sb = new StringBuilder(); + appendHostInfo(sb, true, false); + return sb.toString(); + } + + private void appendHostInfo(StringBuilder sb, boolean encoded, boolean withSlash) { + if (builder.host == null) { + return; + } + if (withSlash) { + if (scheme != null) { + sb.append(DOUBLE_SLASH); + } else { + sb.append(SEPARATOR_CHAR); + } + } + if (!builder.host.isEmpty()) { + if (!isNullOrEmpty(builder.userInfo)) { + sb.append(builder.userInfo).append(AT_CHAR); + } + if (builder.protocolVersion != null) { + switch (builder.protocolVersion) { + case IPV6: + String s = "localhost".equals(builder.host) ? + InetAddress.getLoopbackAddress().getHostAddress() : builder.host; + // prefer host name over numeric address + if (s != null && !s.equals(builder.hostAddress)) { + sb.append(s); + } else if (builder.hostAddress != null) { + sb.append("[").append(builder.hostAddress).append("]"); + } + break; + case IPV4: + case IPV46: + sb.append(builder.host); + break; + default: + if (encoded) { + try { + String encodedHostName = PercentEncoders.getRegNameEncoder(charset).encode(builder.host); + validateHostnameCharacters(encodedHostName); + sb.append(encodedHostName); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } else { + sb.append(builder.host); + } + break; + } + } else { + if (encoded) { + try { + String encodedHostName = PercentEncoders.getRegNameEncoder(charset).encode(builder.host); + validateHostnameCharacters(encodedHostName); + sb.append(encodedHostName); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } else { + sb.append(builder.host); + } + } + if (scheme != null && builder.port != null && builder.port != scheme.getDefaultPort()) { + sb.append(':'); + if (builder.port != -1) { + sb.append(builder.port); + } + } + } + } + + private void validateHostnameCharacters(String hostname) { + boolean valid; + for (int i = 0; i < hostname.length(); i++) { + char c = hostname.charAt(i); + valid = ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + ('0' <= c && c <= '9') || + c == '-' || c == '.' || c == '_' || c == '~' || + c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || + c == '*' || c == '+' || c == ',' || c == ';' || c == '=' || c == '%'; + if (!valid) { + throw new IllegalArgumentException("invalid host name character in: " + hostname); + } + } + } + + private String encodePath() { + StringBuilder sb = new StringBuilder(); + appendPath(sb, true); + return sb.toString(); + } + + private void appendPath(StringBuilder sb, boolean encoded) { + PercentEncoder pathEncoder = PercentEncoders.getPathEncoder(charset); + PercentEncoder matrixEncoder = PercentEncoders.getMatrixEncoder(charset); + Iterator it = builder.pathSegments.iterator(); + while (it.hasNext()) { + PathSegment pathSegment = it.next(); + try { + sb.append(encoded ? pathEncoder.encode(pathSegment.segment) : pathSegment.segment); + for (Pair matrixParam : pathSegment.getMatrixParams()) { + sb.append(SEMICOLON_CHAR).append(encoded ? + matrixEncoder.encode(matrixParam.getFirst()) : matrixParam.getFirst()); + if (matrixParam.getSecond() != null) { + sb.append(EQUAL_CHAR).append(encoded ? + matrixEncoder.encode(matrixParam.getSecond()) : matrixParam.getSecond()); + } + } + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + if (it.hasNext()) { + sb.append(SEPARATOR_CHAR); + } + } + } + + private String encodeQuery() { + StringBuilder sb = new StringBuilder(); + appendQuery(sb, true, false); + return sb.length() == 0 ? null : sb.toString(); + } + + private void appendQuery(StringBuilder sb, boolean encoded, boolean withQuestionMark) { + if (!builder.queryParams.isEmpty()) { + if (withQuestionMark) { + sb.append(QUESTION_CHAR); + } + Iterator> it = builder.queryParams.iterator(); + PercentEncoder queryParamEncoder = PercentEncoders.getQueryParamEncoder(charset); + while (it.hasNext()) { + QueryParameters.Pair queryParam = it.next(); + try { + sb.append(encoded ? queryParamEncoder.encode(queryParam.getFirst()) : queryParam.getFirst()); + if (queryParam.getSecond() != null) { + sb.append(EQUAL_CHAR).append(encoded ? + queryParamEncoder.encode(queryParam.getSecond()) : queryParam.getSecond()); + } + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + if (it.hasNext()) { + sb.append(AMPERSAND_CHAR); + } + } + } else if (!isNullOrEmpty(builder.query)) { + if (withQuestionMark) { + sb.append(QUESTION_CHAR); + } + if (encoded) { + try { + sb.append(PercentEncoders.getQueryEncoder(charset).encode(builder.query)); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } else { + sb.append(builder.query); + } + } + } + + private String encodeFragment() { + StringBuilder sb = new StringBuilder(); + appendFragment(sb, true, false); + return sb.length() == 0 ? null : sb.toString(); + } + + private void appendFragment(StringBuilder sb, boolean encoded, boolean withHashSymbol) { + if (!isNullOrEmpty(builder.fragment)) { + if (withHashSymbol) { + sb.append(NUMBER_SIGN_CHAR); + } + if (encoded) { + try { + sb.append(PercentEncoders.getFragmentEncoder(charset).encode(builder.fragment)); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } else { + sb.append(builder.fragment); + } + } + } + + /** + * Returns true if the parameter string is neither null nor empty. + */ + private static boolean isNullOrEmpty(String str) { + return str == null || str.isEmpty(); + } + + /** + * + */ + public static class Builder { + + private final PercentDecoder percentDecoder; + + private final QueryParameters queryParams; + + private final List pathSegments; + + private Charset charset; + + private String scheme; + + private String schemeSpecificPart; + + private String userInfo; + + private String host; + + private String hostAddress; + + private ProtocolVersion protocolVersion; + + private Integer port; + + private String query; + + private String fragment; + + private boolean fatalResolveErrorsEnabled; + + private Builder() { + this.percentDecoder = new PercentDecoder(); + this.queryParams = new QueryParameters(); + this.pathSegments = new ArrayList<>(); + this.charset = StandardCharsets.UTF_8; + } + + public Builder charset(Charset charset) { + this.charset = charset; + return this; + } + + public Builder scheme(String scheme) { + if (!isNullOrEmpty(scheme)) { + validateSchemeCharacters(scheme.toLowerCase()); + this.scheme = scheme; + } + return this; + } + + public Builder schemeSpecificPart(String schemeSpecificPart) { + this.schemeSpecificPart = schemeSpecificPart; + return this; + } + + public Builder userInfo(String userInfo) { + this.userInfo = userInfo; + return this; + } + + public Builder host(String host) { + this.host = host; + this.protocolVersion = ProtocolVersion.NONE; + return this; + } + + public Builder host(String host, ProtocolVersion protocolVersion) { + this.host = host; + this.protocolVersion = protocolVersion; + return this; + } + + public Builder fatalResolveErrors(boolean fatalResolveErrorsEnabled) { + this.fatalResolveErrorsEnabled = fatalResolveErrorsEnabled; + return this; + } + + public Builder resolveFromHost(String hostname) throws CharacterCodingException { + if (hostname == null) { + return this; + } + if (hostname.isEmpty()) { + host(EMPTY); + return this; + } + try { + InetAddress inetAddress = InetAddress.getByName(hostname); + hostAddress = inetAddress.getHostAddress(); + host(inetAddress.getHostName(), inetAddress instanceof Inet6Address ? + ProtocolVersion.IPV6 : inetAddress instanceof Inet4Address ? + ProtocolVersion.IPV4 : ProtocolVersion.NONE); + return this; + } catch (UnknownHostException e) { + if (fatalResolveErrorsEnabled) { + throw new IllegalStateException(e); + } + logger.log(Level.WARNING, e.getMessage(), e); + if (e.getMessage() != null && !e.getMessage().endsWith("invalid IPv6 address") && + hostname.charAt(0) != '[' && + hostname.charAt(hostname.length() - 1) != ']') { + String idna = IDN.toASCII(percentDecoder.decode(hostname)); + host(idna, ProtocolVersion.NONE); + } + } + return this; + } + + public Builder port(Integer port) { + this.port = port; + return this; + } + + public Builder path(String path) { + try { + parser().parsePathWithQueryAndFragment(this, path); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + return this; + } + + public Builder pathSegments(String... segments) { + for (String segment : segments) { + pathSegment(segment); + } + return this; + } + + public Builder pathSegment(String segment) { + if (pathSegments.isEmpty() && !isNullOrEmpty(host) && !isNullOrEmpty(segment)) { + pathSegments.add(EMPTY_SEGMENT); + } + pathSegments.add(new PathSegment(segment)); + return this; + } + + /** + * Add a query parameter. Query parameters will be encoded in the order added. + * + * Using query strings to encode key=value pairs is not part of the URI/URL specification. + * It is specified by http://www.w3.org/TR/html401/interact/forms.html#form-content-type. + * + * If you use this method to build a query string, or created this builder from an URL with a query string that can + * successfully be parsed into query param pairs, you cannot subsequently use + * {@link Builder#query(String)}. + * + * @param name param name + * @param value param value + * @return this + */ + public Builder queryParam(String name, String value) { + queryParams.add(name, value); + return this; + } + + /** + * Set the complete query string of arbitrary structure. This is useful when you want to specify a query string that + * is not of key=value format. If the query has previously been set via this method, subsequent calls will overwrite + * that query. + * If you use this method, or create a builder from a URL whose query is not parseable into query param pairs, you + * cannot subsequently use {@link Builder#queryParam(String, String)}. + * + * @param query Complete URI query, as specified by https://tools.ietf.org/html/rfc3986#section-3.4 + * @return this + */ + public Builder query(String query) { + this.query = query; + return this; + } + + /** + * Add a matrix param to the last added path segment. If no segments have been added, the param will be added to the + * root. Matrix params will be encoded in the order added. + * + * @param name param name + * @param value param value + * @return this + */ + public Builder matrixParam(String name, String value) { + if (pathSegments.isEmpty()) { + pathSegment(EMPTY); + } + pathSegments.get(pathSegments.size() - 1).getMatrixParams().add(new Pair<>(name, value)); + return this; + } + + /** + * Set the fragment. + * + * @param fragment fragment string + * @return this + */ + public Builder fragment(String fragment) { + if (!isNullOrEmpty(fragment)) { + this.fragment = fragment; + } + return this; + } + + public URL build() { + return new URL(this); + } + + /** + * Encode the current builder state into a string. + * + * @return a string + */ + String toUrlString() { + return build().toExternalForm(); + } + + void validateSchemeCharacters(String scheme) { + boolean valid; + for (int i = 0; i < scheme.length(); i++) { + char c = scheme.charAt(i); + if (i == 0) { + valid = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'); + } else { + valid = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || + ('0' <= c && c <= '9') || c == '+' || c == '-' || c == '.'; + } + if (!valid) { + throw new IllegalArgumentException("invalid scheme character in: " + scheme); + } + } + } + } + + /** + * + */ + public static class Parser { + + private final PercentDecoder percentDecoder; + + private Parser() { + percentDecoder = new PercentDecoder(); + } + + public URL parse(String input) throws CharacterCodingException { + return parse(input, true); + } + + public URL parse(String input, boolean resolve) throws CharacterCodingException { + if (isNullOrEmpty(input)) { + return null; + } + if (input.indexOf('\n') >= 0) { + return null; + } + if (input.indexOf('\t') >= 0) { + return null; + } + Builder builder = new Builder(); + String remaining = parseScheme(builder, input); + if (remaining != null) { + remaining = remaining.replace('\\', SEPARATOR_CHAR); + builder.schemeSpecificPart(remaining); + if (remaining.startsWith(DOUBLE_SLASH)) { + Scheme scheme = SchemeRegistry.getInstance().getScheme(builder.scheme); + if (builder.scheme == null || scheme.getDefaultPort() == -1) { + builder.host(EMPTY); + } else { + remaining = remaining.substring(2); + int i = remaining.indexOf(SEPARATOR_CHAR); + int j = remaining.indexOf(QUESTION_CHAR); + int pos = i >= 0 && j >= 0 ? Math.min(i, j) : i >= 0 ? i : j >= 0 ? j : -1; + String host = (pos >= 0 ? remaining.substring(0, pos) : remaining).toLowerCase(); + parseHostAndPort(builder, parseUserInfo(builder, host), resolve); + if (builder.host == null) { + return INVALID; + } + remaining = pos >= 0 ? remaining.substring(pos) : EMPTY; + } + } + if (!isNullOrEmpty(remaining)) { + parsePathWithQueryAndFragment(builder, remaining); + } + } + return builder.build(); + } + + private String parseScheme(Builder builder, String input) { + Pair p = indexOf(COLON_CHAR, input); + if (p.getSecond() == null) { + return input; + } + if (!isNullOrEmpty(p.getFirst())) { + builder.scheme(p.getFirst()); + } + return p.getSecond(); + } + + private String parseUserInfo(Builder builder, String input) { + String remaining = input; + int i = input.lastIndexOf(AT_CHAR); + if (i > 0) { + remaining = input.substring(i + 1); + builder.userInfo(input.substring(0, i)); + } + return remaining; + } + + private void parseHostAndPort(Builder builder, String host, boolean resolve) throws CharacterCodingException { + if (host.indexOf('[') == 0) { + int i = host.lastIndexOf(']'); + if (i >= 0) { + builder.port(parsePort(host.substring(i + 1))); + host = host.substring(1, i); + } + } else { + int i = host.indexOf(':'); + if (i >= 0) { + builder.port(parsePort(host.substring(i))); + host = host.substring(0, i); + } + } + if (resolve) { + builder.resolveFromHost(host); + } else { + builder.host(host); + } + } + + private Integer parsePort(String portStr) { + if (portStr == null || portStr.isEmpty()) { + return null; + } + int i = portStr.indexOf(":"); + if (i >= 0) { + portStr = portStr.substring(i + 1); + if (portStr.isEmpty()) { + return -1; + } + } + try { + int port = Integer.parseInt(portStr); + if (port > 0 && port < 65536) { + return port; + } else { + throw new IllegalArgumentException("invalid port"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("no numeric port: " + portStr); + } + } + + void parsePathWithQueryAndFragment(Builder builder, String input) throws CharacterCodingException { + if (input == null) { + return; + } + int i = input.lastIndexOf(NUMBER_SIGN_CHAR); + if (i >= 0) { + builder.fragment(percentDecoder.decode(input.substring(i + 1))); + input = input.substring(0, i); + } + i = input.indexOf(QUESTION_CHAR); + if (i >= 0) { + parseQuery(builder, input.substring(i + 1)); + input = input.substring(0, i); + } + if (input.length() > 0 && input.charAt(0) == SEPARATOR_CHAR) { + builder.pathSegment(EMPTY); + } + String s = input; + while (s != null) { + Pair pair = indexOf(SEPARATOR_CHAR, s); + String elem = pair.getFirst(); + if (!elem.isEmpty()) { + if (elem.charAt(0) == SEMICOLON_CHAR) { + builder.pathSegment(EMPTY); + String t = elem.substring(1); + while (t != null) { + Pair pathWithMatrixElem = indexOf(SEMICOLON_CHAR, t); + String matrixElem = pathWithMatrixElem.getFirst(); + Pair p = indexOf(EQUAL_CHAR, matrixElem); + builder.matrixParam(percentDecoder.decode(p.getFirst()), percentDecoder.decode(p.getSecond())); + t = pathWithMatrixElem.getSecond(); + } + } else { + String t = elem; + i = 0; + while (t != null) { + Pair pathWithMatrixElem = indexOf(SEMICOLON_CHAR, t); + String segment = pathWithMatrixElem.getFirst(); + if (i == 0) { + builder.pathSegment(percentDecoder.decode(segment)); + } else { + Pair p = indexOf(EQUAL_CHAR, segment); + builder.matrixParam(percentDecoder.decode(p.getFirst()), percentDecoder.decode(p.getSecond())); + } + t = pathWithMatrixElem.getSecond(); + i++; + } + } + } + s = pair.getSecond(); + } + if (input.endsWith("/")) { + builder.pathSegment(EMPTY); + } + } + + private void parseQuery(Builder builder, String query) throws CharacterCodingException { + if (query == null) { + return; + } + String s = query; + while (s != null) { + Pair p = indexOf(AMPERSAND_CHAR, s); + Pair param = indexOf(EQUAL_CHAR, p.getFirst()); + if (!isNullOrEmpty(param.getFirst())) { + builder.queryParam(percentDecoder.decode(param.getFirst()), percentDecoder.decode(param.getSecond())); + } + s = p.getSecond(); + } + if (builder.queryParams.isEmpty()) { + builder.query(percentDecoder.decode(query)); + } else { + builder.query(query); + } + } + + Pair indexOf(char ch, String input) { + int i = input.indexOf(ch); + String k = i >= 0 ? input.substring(0, i) : input; + String v = i >= 0 ? input.substring(i + 1) : null; + return new Pair<>(k, v); + } + } + + /** + * + */ + public static class Resolver { + + private final URL base; + + public Resolver(URL base) { + this.base = base; + } + + public URL resolve(String relative) { + if (relative == null) { + return null; + } + if (relative.isEmpty()) { + return base; + } + try { + URL url = parser().parse(relative); + return url != null ? resolve(url) : null; + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } + + public URL resolve(URL relative) throws CharacterCodingException { + if (relative == null) { + return null; + } + if (!base.isAbsolute()) { + throw new IllegalArgumentException("base is not absolute"); + } + Builder builder = new Builder(); + if (relative.isOpaque()) { + builder.scheme(relative.getScheme()); + builder.schemeSpecificPart(relative.getSchemeSpecificPart()); + return builder.build(); + } + if (relative.isAbsolute()) { + builder.scheme(relative.getScheme()); + } else { + builder.scheme(base.getScheme()); + } + if (!isNullOrEmpty(relative.getScheme()) || !isNullOrEmpty(relative.getHost())) { + builder.host(relative.getDecodedHost(), relative.getProtocolVersion()).port(relative.getPort()); + builder.path(relative.getPath()); + return builder.build(); + } + if (base.isOpaque()) { + builder.schemeSpecificPart(base.getSchemeSpecificPart()); + return builder.build(); + } + if (relative.getHost() != null) { + builder.host(relative.getDecodedHost(), relative.getProtocolVersion()).port(relative.getPort()); + } else { + builder.host(base.getDecodedHost(), base.getProtocolVersion()).port(base.getPort()); + } + builder.path(resolvePath(base, relative)); + return builder.build(); + } + + private String resolvePath(URL base, URL relative) { + String basePath = base.getPath(); + String baseQuery = base.getQuery(); + String baseFragment = base.getFragment(); + String relPath = relative.getPath(); + String relQuery = relative.getQuery(); + String relFragment = relative.getFragment(); + boolean isBase = false; + String merged; + List result = new ArrayList<>(); + if (isNullOrEmpty(relPath)) { + merged = basePath; + isBase = true; + } else if (relPath.charAt(0) != SEPARATOR_CHAR && !isNullOrEmpty(basePath)) { + merged = basePath.substring(0, basePath.lastIndexOf(SEPARATOR_CHAR) + 1) + relPath; + } else { + merged = relPath; + } + if (isNullOrEmpty(merged)) { + return EMPTY; + } + String[] parts = merged.split("/", -1); + for (String part : parts) { + switch (part) { + case EMPTY: + case ".": + break; + case "..": + if (result.size() > 0) { + result.remove(result.size() - 1); + } + break; + default: + result.add(part); + break; + } + } + if (parts.length > 0) { + switch (parts[parts.length - 1]) { + case EMPTY: + case ".": + case "..": + result.add(EMPTY); + break; + default: + break; + } + } + StringBuilder sb = new StringBuilder(); + sb.append(String.join(Character.toString(SEPARATOR_CHAR), result)); + if (sb.length() == 0 && result.size() == 1) { + sb.append(SEPARATOR_CHAR); + } + if (!isNullOrEmpty(relQuery)) { + sb.append(QUESTION_CHAR).append(relQuery); + } else if (isBase && !isNullOrEmpty(baseQuery)) { + sb.append(QUESTION_CHAR).append(baseQuery); + } + if (!isNullOrEmpty(relFragment)) { + sb.append(NUMBER_SIGN_CHAR).append(relFragment); + } else if (isBase && !isNullOrEmpty(baseFragment)) { + sb.append(NUMBER_SIGN_CHAR).append(baseFragment); + } + return sb.toString(); + } + } + + private static class URLWithFragmentComparator implements Comparator, Serializable { + + private static final long serialVersionUID = -5048272347975931901L; + + @Override + public int compare(URL o1, URL o2) { + return o1.toString(true).compareTo(o2.toString(true)); + } + } + + private static class URLWithoutFragmentComparator implements Comparator, Serializable { + + private static final long serialVersionUID = 818948352939772199L; + + @Override + public int compare(URL o1, URL o2) { + return o1.toString(false).compareTo(o2.toString(false)); + } + } + + private static class Pair { + private final K first; + private final V second; + + Pair(K first, V second) { + this.first = first; + this.second = second; + } + + K getFirst() { + return first; + } + + V getSecond() { + return second; + } + + @Override + public String toString() { + return first + "=" + second; + } + } + + /** + * A path segment with any associated matrix params. + */ + private static class PathSegment { + + private final String segment; + + private final List> params; + + PathSegment(String segment) { + this.segment = segment; + this.params = new ArrayList<>(); + } + + List> getMatrixParams() { + return params; + } + + @Override + public String toString() { + return segment + ";" + params; + } + } +} diff --git a/src/main/java/org/xbib/net/internal/LRUCache.java b/src/main/java/org/xbib/net/internal/LRUCache.java new file mode 100644 index 0000000..17078ce --- /dev/null +++ b/src/main/java/org/xbib/net/internal/LRUCache.java @@ -0,0 +1,26 @@ +package org.xbib.net.internal; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A simple LRU cache, based on a {@link LinkedHashMap}. + * + * @param the key type parameter + * @param the vale type parameter + */ +public class LRUCache extends LinkedHashMap { + + private static final long serialVersionUID = -2795566703268944901L; + + private final int cacheSize; + + public LRUCache(int cacheSize) { + super(16, 0.75f, true); + this.cacheSize = cacheSize; + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= cacheSize; + } +} diff --git a/src/main/java/org/xbib/net/internal/package-info.java b/src/main/java/org/xbib/net/internal/package-info.java new file mode 100644 index 0000000..dbd1281 --- /dev/null +++ b/src/main/java/org/xbib/net/internal/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for internal use in the {@code org.xbib.net} package. + */ +package org.xbib.net.internal; diff --git a/src/main/java/org/xbib/net/matcher/CharMatcher.java b/src/main/java/org/xbib/net/matcher/CharMatcher.java new file mode 100644 index 0000000..8a6f7ee --- /dev/null +++ b/src/main/java/org/xbib/net/matcher/CharMatcher.java @@ -0,0 +1,752 @@ +package org.xbib.net.matcher; + +import java.util.Arrays; +import java.util.BitSet; + +/** + * + */ +public abstract class CharMatcher { + + private static final String WHITESPACE_TABLE; + + private static final int WHITESPACE_MULTIPLIER; + + private static final int WHITESPACE_SHIFT; + + private static final CharMatcher WHITESPACE; + + private static final CharMatcher JAVA_ISO_CONTROL; + + public static final CharMatcher LITERALS; + + public static final CharMatcher PERCENT; + + public static final CharMatcher HEXDIGIT; + + static { + WHITESPACE_TABLE = "\u2002\u3000\r\u0085\u200A\u2005\u2000\u3000\u2029\u000B\u3000\u2008\u2003\u205F\u3000" + + "\u1680\u0009\u0020\u2006\u2001\u202F\u00A0\u000C\u2009\u3000\u2004\u3000\u3000\u2028\n\u2007\u3000"; + WHITESPACE_MULTIPLIER = 1682554634; + WHITESPACE_SHIFT = Integer.numberOfLeadingZeros(WHITESPACE_TABLE.length() - 1); + WHITESPACE = new FastMatcher() { + @Override + public boolean matches(char c) { + return WHITESPACE_TABLE.charAt((WHITESPACE_MULTIPLIER * c) >>> WHITESPACE_SHIFT) == c; + } + + @Override + void setBits(BitSet table) { + for (int i = 0; i < WHITESPACE_TABLE.length(); i++) { + table.set(WHITESPACE_TABLE.charAt(i)); + } + } + }; + JAVA_ISO_CONTROL = inRange('\u0000', '\u001f') + .or(inRange('\u007f', '\u009f')); + LITERALS = CharMatcher.JAVA_ISO_CONTROL + .or(CharMatcher.WHITESPACE) + .or(CharMatcher.anyOf("\"'<>\\^`{|}")) + .precomputed().negate(); + PERCENT = CharMatcher.is('%'); + HEXDIGIT = CharMatcher.inRange('0', '9') + .or(CharMatcher.inRange('a', 'f')) + .or(CharMatcher.inRange('A', 'F')) + .precomputed(); + } + + private static final int DISTINCT_CHARS = (Character.MAX_VALUE) - (Character.MIN_VALUE) + 1; + + private static T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + private static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + private static int checkPositionIndex(int index, int size) { + if (index < 0 || index > size) { + throw new IndexOutOfBoundsException("index=" + index + " size=" + size); + } + return index; + } + + private static final CharMatcher ANY = new FastMatcher() { + @Override + public boolean matches(char c) { + return true; + } + + @Override + int indexIn(CharSequence sequence) { + return sequence.length() == 0 ? -1 : 0; + } + + @Override + int indexIn(CharSequence sequence, int start) { + int length = sequence.length(); + checkPositionIndex(start, length); + return start == length ? -1 : start; + } + + @Override + int lastIndexIn(CharSequence sequence) { + return sequence.length() - 1; + } + + @Override + public boolean matchesAllOf(CharSequence sequence) { + checkNotNull(sequence); + return true; + } + + @Override + public boolean matchesNoneOf(CharSequence sequence) { + return sequence.length() == 0; + } + + @Override + String removeFrom(CharSequence sequence) { + checkNotNull(sequence); + return ""; + } + + @Override + String replaceFrom(CharSequence sequence, char replacement) { + char[] array = new char[sequence.length()]; + Arrays.fill(array, replacement); + return new String(array); + } + + @Override + int countIn(CharSequence sequence) { + return sequence.length(); + } + + @Override + public CharMatcher and(CharMatcher other) { + return checkNotNull(other); + } + + @Override + public CharMatcher or(CharMatcher other) { + checkNotNull(other); + return this; + } + + @Override + public CharMatcher negate() { + return NONE; + } + }; + + private static final CharMatcher NONE = new FastMatcher() { + @Override + public boolean matches(char c) { + return false; + } + + @Override + int indexIn(CharSequence sequence) { + checkNotNull(sequence); + return -1; + } + + @Override + int indexIn(CharSequence sequence, int start) { + int length = sequence.length(); + checkPositionIndex(start, length); + return -1; + } + + @Override + int lastIndexIn(CharSequence sequence) { + checkNotNull(sequence); + return -1; + } + + @Override + public boolean matchesAllOf(CharSequence sequence) { + return sequence.length() == 0; + } + + @Override + public boolean matchesNoneOf(CharSequence sequence) { + checkNotNull(sequence); + return true; + } + + @Override + String removeFrom(CharSequence sequence) { + return sequence.toString(); + } + + @Override + String replaceFrom(CharSequence sequence, char replacement) { + return sequence.toString(); + } + + @Override + int countIn(CharSequence sequence) { + checkNotNull(sequence); + return 0; + } + + @Override + public CharMatcher and(CharMatcher other) { + checkNotNull(other); + return this; + } + + @Override + public CharMatcher or(CharMatcher other) { + return checkNotNull(other); + } + + @Override + public CharMatcher negate() { + return ANY; + } + }; + + public static CharMatcher is(char match) { + return new FastMatcher() { + @Override + public boolean matches(char c) { + return c == match; + } + + @Override + String replaceFrom(CharSequence sequence, char replacement) { + return sequence.toString().replace(match, replacement); + } + + @Override + public CharMatcher and(CharMatcher other) { + return other.matches(match) ? this : NONE; + } + + @Override + public CharMatcher or(CharMatcher other) { + return other.matches(match) ? other : super.or(other); + } + + @Override + public CharMatcher negate() { + return isNot(match); + } + + @Override + void setBits(BitSet table) { + table.set((int) match); + } + }; + } + + public static CharMatcher isNot(char match) { + return new FastMatcher() { + @Override + public boolean matches(char c) { + return c != match; + } + + @Override + public CharMatcher and(CharMatcher other) { + return other.matches(match) ? super.and(other) : other; + } + + @Override + public CharMatcher or(CharMatcher other) { + return other.matches(match) ? ANY : this; + } + + @Override + void setBits(BitSet table) { + table.set(0, match); + table.set((match) + 1, (Character.MAX_VALUE) + 1); + } + + @Override + public CharMatcher negate() { + return is(match); + } + }; + } + + public static CharMatcher anyOf(CharSequence sequence) { + switch (sequence.length()) { + case 0: + return NONE; + case 1: + return is(sequence.charAt(0)); + case 2: + return isEither(sequence.charAt(0), sequence.charAt(1)); + default: + break; + } + char[] chars = sequence.toString().toCharArray(); + Arrays.sort(chars); + return new CharMatcher() { + @Override + public boolean matches(char c) { + return Arrays.binarySearch(chars, c) >= 0; + } + + @Override + void setBits(BitSet table) { + for (char c : chars) { + table.set(c); + } + } + }; + } + + public static CharMatcher isEither(char match1, char match2) { + return new FastMatcher() { + @Override + public boolean matches(char c) { + return c == match1 || c == match2; + } + + @Override + void setBits(BitSet table) { + table.set(match1); + table.set(match2); + } + }; + } + + public static CharMatcher noneOf(CharSequence sequence) { + return anyOf(sequence).negate(); + } + + public static CharMatcher inRange(char startInclusive, char endInclusive) { + checkArgument(endInclusive >= startInclusive); + return new FastMatcher() { + @Override + public boolean matches(char c) { + return startInclusive <= c && c <= endInclusive; + } + + @Override + void setBits(BitSet table) { + table.set(startInclusive, (endInclusive) + 1); + } + }; + } + + protected CharMatcher() { + } + + public abstract boolean matches(char c); + + public CharMatcher negate() { + return new NegatedMatcher(this); + } + + public CharMatcher and(CharMatcher other) { + return new And(this, checkNotNull(other)); + } + + public CharMatcher or(CharMatcher other) { + return new Or(this, other); + } + + public CharMatcher precomputed() { + return precomputedInternal(); + } + + private CharMatcher precomputedInternal() { + BitSet table = new BitSet(); + setBits(table); + int totalCharacters = table.cardinality(); + if (totalCharacters * 2 <= DISTINCT_CHARS) { + return precomputedPositive(totalCharacters, table); + } else { + table.flip(Character.MIN_VALUE, (Character.MAX_VALUE) + 1); + int negatedCharacters = DISTINCT_CHARS - totalCharacters; + return new NegatedFastMatcher(precomputedPositive(negatedCharacters, table)); + } + } + + private static CharMatcher precomputedPositive(int totalCharacters, BitSet table) { + switch (totalCharacters) { + case 0: + return NONE; + case 1: + return is((char) table.nextSetBit(0)); + case 2: + char c1 = (char) table.nextSetBit(0); + char c2 = (char) table.nextSetBit((c1) + 1); + return isEither(c1, c2); + default: + return isSmall(totalCharacters, table.length()) ? + SmallCharMatcher.from(table) : new BitSetMatcher(table); + } + } + + private static boolean isSmall(int totalCharacters, int tableLength) { + return totalCharacters <= SmallCharMatcher.MAX_SIZE && + tableLength > (totalCharacters * 4 * Character.SIZE); + } + + void setBits(BitSet table) { + for (int c = Character.MAX_VALUE; c >= Character.MIN_VALUE; c--) { + if (matches((char) c)) { + table.set(c); + } + } + } + + public boolean matchesAnyOf(CharSequence sequence) { + return !matchesNoneOf(sequence); + } + + public boolean matchesAllOf(CharSequence sequence) { + for (int i = sequence.length() - 1; i >= 0; i--) { + if (!matches(sequence.charAt(i))) { + return false; + } + } + return true; + } + + public boolean matchesNoneOf(CharSequence sequence) { + return indexIn(sequence) == -1; + } + + int indexIn(CharSequence sequence) { + int length = sequence.length(); + for (int i = 0; i < length; i++) { + if (matches(sequence.charAt(i))) { + return i; + } + } + return -1; + } + + int indexIn(CharSequence sequence, int start) { + int length = sequence.length(); + checkPositionIndex(start, length); + for (int i = start; i < length; i++) { + if (matches(sequence.charAt(i))) { + return i; + } + } + return -1; + } + + int lastIndexIn(CharSequence sequence) { + for (int i = sequence.length() - 1; i >= 0; i--) { + if (matches(sequence.charAt(i))) { + return i; + } + } + return -1; + } + + int countIn(CharSequence sequence) { + int count = 0; + for (int i = 0; i < sequence.length(); i++) { + if (matches(sequence.charAt(i))) { + count++; + } + } + return count; + } + + String removeFrom(CharSequence sequence) { + String string = sequence.toString(); + int pos = indexIn(string); + if (pos == -1) { + return string; + } + char[] chars = string.toCharArray(); + int spread = 1; + OUT: + while (true) { + pos++; + while (true) { + if (pos == chars.length) { + break OUT; + } + if (matches(chars[pos])) { + break; + } + chars[pos - spread] = chars[pos]; + pos++; + } + spread++; + } + return new String(chars, 0, pos - spread); + } + + String retainFrom(CharSequence sequence) { + return negate().removeFrom(sequence); + } + + String replaceFrom(CharSequence sequence, char replacement) { + String string = sequence.toString(); + int pos = indexIn(string); + if (pos == -1) { + return string; + } + char[] chars = string.toCharArray(); + chars[pos] = replacement; + for (int i = pos + 1; i < chars.length; i++) { + if (matches(chars[i])) { + chars[i] = replacement; + } + } + return new String(chars); + } + + boolean apply(Character character) { + return matches(character); + } + + private abstract static class FastMatcher extends CharMatcher { + FastMatcher() { + super(); + } + + @Override + public CharMatcher precomputed() { + return this; + } + + @Override + public CharMatcher negate() { + return new NegatedFastMatcher(this); + } + } + + private static class NegatedMatcher extends CharMatcher { + CharMatcher original; + + NegatedMatcher(CharMatcher original) { + super(); + this.original = original; + } + + @Override + public boolean matches(char c) { + return !original.matches(c); + } + + @Override + public boolean matchesAllOf(CharSequence sequence) { + return original.matchesNoneOf(sequence); + } + + @Override + public boolean matchesNoneOf(CharSequence sequence) { + return original.matchesAllOf(sequence); + } + + @Override + int countIn(CharSequence sequence) { + return sequence.length() - original.countIn(sequence); + } + + @Override + void setBits(BitSet table) { + BitSet tmp = new BitSet(); + original.setBits(tmp); + tmp.flip((Character.MIN_VALUE), (Character.MAX_VALUE) + 1); + table.or(tmp); + } + + @Override + public CharMatcher negate() { + return original; + }; + } + + private static class NegatedFastMatcher extends NegatedMatcher { + NegatedFastMatcher(CharMatcher original) { + super(original); + } + + @Override + public CharMatcher precomputed() { + return this; + } + } + + private static class And extends CharMatcher { + private final CharMatcher first; + private final CharMatcher second; + + And(CharMatcher a, CharMatcher b) { + super(); + first = checkNotNull(a); + second = checkNotNull(b); + } + + @Override + public boolean matches(char c) { + return first.matches(c) && second.matches(c); + } + + @Override + void setBits(BitSet table) { + BitSet tmp1 = new BitSet(); + first.setBits(tmp1); + BitSet tmp2 = new BitSet(); + second.setBits(tmp2); + tmp1.and(tmp2); + table.or(tmp1); + } + } + + private static class Or extends CharMatcher { + private final CharMatcher first; + private final CharMatcher second; + + Or(CharMatcher a, CharMatcher b) { + super(); + first = checkNotNull(a); + second = checkNotNull(b); + } + + @Override + void setBits(BitSet table) { + first.setBits(table); + second.setBits(table); + } + + @Override + public boolean matches(char c) { + return first.matches(c) || second.matches(c); + } + } + + private static class BitSetMatcher extends FastMatcher { + private final BitSet table; + + private BitSetMatcher(BitSet table) { + if (table.length() + Long.SIZE < table.size()) { + table = (BitSet) table.clone(); + } + this.table = table; + } + + @Override + public boolean matches(char c) { + return table.get(c); + } + + @Override + void setBits(BitSet bitSet) { + bitSet.or(table); + } + } + + private static class SmallCharMatcher extends FastMatcher { + + static final int MAX_SIZE = 1023; + + private static final int C1 = 0xcc9e2d51; + + private static final int C2 = 0x1b873593; + + private static final double DESIRED_LOAD_FACTOR = 0.5d; + + private final char[] table; + + private final boolean containsZero; + + private final long filter; + + private SmallCharMatcher(char[] table, long filter, boolean containsZero) { + super(); + this.table = table; + this.filter = filter; + this.containsZero = containsZero; + } + + static int smear(int hashCode) { + return C2 * Integer.rotateLeft(hashCode * C1, 15); + } + + private boolean checkFilter(int c) { + return 1 == (1 & (filter >> c)); + } + + static int chooseTableSize(int setSize) { + if (setSize == 1) { + return 2; + } + int tableSize = Integer.highestOneBit(setSize - 1) << 1; + while (tableSize * DESIRED_LOAD_FACTOR < setSize) { + tableSize <<= 1; + } + return tableSize; + } + + static CharMatcher from(BitSet chars) { + long filter = 0; + int size = chars.cardinality(); + boolean containsZero = chars.get(0); + char[] table = new char[chooseTableSize(size)]; + int mask = table.length - 1; + for (int c = chars.nextSetBit(0); c != -1; c = chars.nextSetBit(c + 1)) { + filter |= 1L << c; + int index = smear(c) & mask; + while (true) { + if (table[index] == 0) { + table[index] = (char) c; + break; + } + index = (index + 1) & mask; + } + } + return new SmallCharMatcher(table, filter, containsZero); + } + + @Override + public boolean matches(char c) { + if (c == 0) { + return containsZero; + } + if (!checkFilter(c)) { + return false; + } + int mask = table.length - 1; + int startingIndex = smear(c) & mask; + int index = startingIndex; + while (true) { + if (table[index] == 0) { + return false; + } else if (table[index] == c) { + return true; + } else { + index = (index + 1) & mask; + } + if (index == startingIndex) { + break; + } + } + return false; + } + + @Override + void setBits(BitSet table) { + if (containsZero) { + table.set(0); + } + for (char c : this.table) { + if (c != 0) { + table.set(c); + } + } + } + } +} diff --git a/src/main/java/org/xbib/net/matcher/package-info.java b/src/main/java/org/xbib/net/matcher/package-info.java new file mode 100644 index 0000000..5b88a4d --- /dev/null +++ b/src/main/java/org/xbib/net/matcher/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL matching. + */ +package org.xbib.net.matcher; diff --git a/src/main/java/org/xbib/net/package-info.java b/src/main/java/org/xbib/net/package-info.java new file mode 100644 index 0000000..740f554 --- /dev/null +++ b/src/main/java/org/xbib/net/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL building and parsing. + */ +package org.xbib.net; diff --git a/src/main/java/org/xbib/net/path/PathDecoder.java b/src/main/java/org/xbib/net/path/PathDecoder.java new file mode 100644 index 0000000..29cadc4 --- /dev/null +++ b/src/main/java/org/xbib/net/path/PathDecoder.java @@ -0,0 +1,121 @@ +package org.xbib.net.path; + +import org.xbib.net.PercentDecoder; +import org.xbib.net.QueryParameters; + +import java.nio.charset.Charset; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; + +/** + * + */ +public class PathDecoder { + + private static final Integer MAX_PARAM_COUNT = 1000; + + private PercentDecoder decoder; + + private String path; + + private String query; + + private QueryParameters params; + + public PathDecoder(String pathAndQuery) throws MalformedInputException, UnmappableCharacterException { + this(pathAndQuery, StandardCharsets.UTF_8); + } + + public PathDecoder(String pathAndQuery, Charset charset) + throws MalformedInputException, UnmappableCharacterException { + this(pathAndQuery, null, charset); + } + + public PathDecoder(String pathAndQuery, String queryString, Charset charset) + throws MalformedInputException, UnmappableCharacterException { + this.decoder = new PercentDecoder(charset.newDecoder()); + int pos = pathAndQuery.indexOf('?'); + String path = pos > 0 ? pathAndQuery.substring(0, pos) : pathAndQuery; + this.query = pos > 0 ? pathAndQuery.substring(pos + 1) : null; + this.path = PathNormalizer.normalize(path); + this.params = new QueryParameters(); + if (query != null) { + parse(query); + } + if (queryString != null) { + parse(queryString); + } + } + + public void parse(String queryString) + throws MalformedInputException, UnmappableCharacterException { + this.params.addAll(decodeQueryString(decoder, queryString)); + } + + public String path() { + return path; + } + + public String query() { + return query; + } + + public String decodedQuery() throws MalformedInputException, UnmappableCharacterException { + return decoder.decode(query); + } + + public QueryParameters params() { + return params; + } + + private static QueryParameters decodeQueryString(PercentDecoder decoder, String query) + throws MalformedInputException, UnmappableCharacterException { + QueryParameters params = new QueryParameters(); + if (query == null || query.isEmpty()) { + return params; + } + String name = null; + int count = 0; + int pos = 0; + int i; + char c; + for (i = 0; i < query.length(); i++) { + c = query.charAt(i); + if (c == '=' && name == null) { + if (pos != i) { + name = query.substring(pos, i).replaceAll("\\+", "%20"); + name = decoder.decode(name); + } + pos = i + 1; + } else if (c == '&' || c == ';') { + if (name == null && pos != i) { + if (++count > MAX_PARAM_COUNT) { + return params; + } + String s = query.substring(pos, i).replaceAll("\\+", "%20"); + params.add(decoder.decode(s), ""); + } else if (name != null) { + if (++count > MAX_PARAM_COUNT) { + return params; + } + String value = query.substring(pos, i).replaceAll("\\+", "%20"); + params.add(name, decoder.decode(value)); + name = null; + } + pos = i + 1; + } + } + if (pos != i) { + if (name == null) { + params.add(decoder.decode(query.substring(pos, i)), ""); + } else { + String value = query.substring(pos, i).replaceAll("\\+", "%20"); + params.add(name, decoder.decode(value)); + } + } else if (name != null) { + params.add(name, ""); + } + return params; + } +} diff --git a/src/main/java/org/xbib/net/path/PathMatcher.java b/src/main/java/org/xbib/net/path/PathMatcher.java new file mode 100644 index 0000000..9e72f5b --- /dev/null +++ b/src/main/java/org/xbib/net/path/PathMatcher.java @@ -0,0 +1,313 @@ +package org.xbib.net.path; + +import org.xbib.net.QueryParameters; +import org.xbib.net.internal.LRUCache; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +/** + */ +public class PathMatcher { + + private static final String DEFAULT_PATH_SEPARATOR = "/"; + + private final Map> tokenizedPatternCache = Collections.synchronizedMap(new LRUCache<>(1024)); + + private final Map stringMatcherCache = Collections.synchronizedMap(new LRUCache<>(1024)); + + private String pathSeparator; + + private PathSeparatorPatternCache pathSeparatorPatternCache; + + private boolean caseSensitive = true; + + private boolean trimTokens = true; + + private volatile boolean cachePatterns = true; + + public PathMatcher() { + this(DEFAULT_PATH_SEPARATOR); + } + + public PathMatcher(String pathSeparator) { + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); + } + + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); + } + + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + public QueryParameters extractUriTemplateVariables(String pattern, String path) { + QueryParameters queryParameters = new QueryParameters(); + if (!doMatch(pattern, path, true, queryParameters)) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return queryParameters; + } + + void setCachePatterns(boolean cachePatterns) { + this.cachePatterns = cachePatterns; + } + + Map stringMatcherCache() { + return stringMatcherCache; + } + + boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + String extractPathWithinPattern(String pattern, String path) { + List patternParts = tokenize(pattern, this.pathSeparator, this.trimTokens, true); + List pathParts = tokenize(path, this.pathSeparator, this.trimTokens, true); + StringBuilder sb = new StringBuilder(); + boolean pathStarted = false; + for (int segment = 0; segment < patternParts.size(); segment++) { + String patternPart = patternParts.get(segment); + if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { + while (segment < pathParts.size()) { + if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { + sb.append(pathSeparator); + } + sb.append(pathParts.get(segment)); + pathStarted = true; + segment++; + } + } + } + return sb.toString(); + } + + String combine(String pattern1, String pattern2) { + if (!hasText(pattern1) && !hasText(pattern2)) { + return ""; + } + if (!hasText(pattern1)) { + return pattern2; + } + if (!hasText(pattern2)) { + return pattern1; + } + boolean pattern1ContainsUriVar = pattern1.indexOf('{') != -1; + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + return pattern2; + } + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); + } + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { + return concat(pattern1, pattern2); + } + int starDotPos1 = pattern1.indexOf("*."); + if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { + return concat(pattern1, pattern2); + } + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + boolean ext1All = (ext1.equals(".*") || ext1.equals("")); + boolean ext2All = (ext2.equals(".*") || ext2.equals("")); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = ext1All ? ext2 : ext1; + return file2 + ext; + } + + public Comparator getPatternComparator(String path) { + return new PathPatternComparator(path); + } + + private static boolean hasText(CharSequence str) { + if (str == null || str.length() == 0) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator); + boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator); + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } else { + return path1 + this.pathSeparator + path2; + } + } + + private boolean doMatch(String pattern, String path, boolean fullMatch, QueryParameters queryParameters) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + List patternElements = tokenizePattern(pattern); + List pathElements = tokenizePath(path); + int pattIdxStart = 0; + int pattIdxEnd = patternElements.size() - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathElements.size() - 1; + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = patternElements.get(pattIdxStart); + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattDir, pathElements.get(pathIdxStart), queryParameters)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + if (pathIdxStart > pathIdxEnd) { + if (pattIdxStart > pattIdxEnd) { + return pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd + && patternElements.get(pattIdxStart).equals("*") + && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternElements.get(i).equals("**")) { + return false; + } + } + return true; + } else if (pattIdxStart > pattIdxEnd) { + return false; + } else if (!fullMatch && "**".equals(patternElements.get(pattIdxStart))) { + return true; + } + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = patternElements.get(pattIdxEnd); + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathElements.get(pathIdxEnd), queryParameters)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternElements.get(i).equals("**")) { + return false; + } + } + return true; + } + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (patternElements.get(i).equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + pattIdxStart++; + continue; + } + int patLength = patIdxTmp - pattIdxStart - 1; + int strLength = pathIdxEnd - pathIdxStart + 1; + int foundIdx = -1; + boolean strLoop = true; + while (strLoop) { + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = patternElements.get(pattIdxStart + j + 1); + String subStr = pathElements.get(pathIdxStart + i + j); + if (matchStrings(subPat, subStr, queryParameters)) { + strLoop = false; + break; + } + } + if (strLoop) { + foundIdx = pathIdxStart + i; + } else { + break; + } + } + } + if (foundIdx == -1) { + return false; + } + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternElements.get(i).equals("**")) { + return false; + } + } + return true; + } + + private List tokenizePattern(String pattern) { + return cachePatterns ? + tokenizedPatternCache.computeIfAbsent(pattern, this::tokenizePath) : + tokenizePath(pattern); + } + + private List tokenizePath(String path) { + return tokenize(path, this.pathSeparator, this.trimTokens, true); + } + + private static List tokenize(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + if (str == null) { + return null; + } + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return tokens; + } + + private boolean matchStrings(String pattern, String str, QueryParameters queryParameters) { + return getStringMatcher(pattern).matchStrings(str, queryParameters); + } + + private PathStringMatcher getStringMatcher(String pattern) { + return cachePatterns ? + stringMatcherCache.computeIfAbsent(pattern, p -> new PathStringMatcher(p, this.caseSensitive)) : + new PathStringMatcher(pattern, this.caseSensitive); + } +} diff --git a/src/main/java/org/xbib/net/path/PathNormalizer.java b/src/main/java/org/xbib/net/path/PathNormalizer.java new file mode 100644 index 0000000..6a49283 --- /dev/null +++ b/src/main/java/org/xbib/net/path/PathNormalizer.java @@ -0,0 +1,194 @@ +package org.xbib.net.path; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.StringTokenizer; + +/** + */ +public class PathNormalizer { + + private static final char separator = '/'; + + private PathNormalizer() { + } + + /*public static String normalizePath(String path) { + return normalizePath(path, false); + } + + public static String normalizePath(String path, boolean keepSeparator) { + if (path == null || path.equals("") || path.equals("/")) { + return "/"; + } + path = path.replaceAll("/+", "/"); + int size = path.length(); + if (size == 0) { + return path; + } + int prefix = getPrefixLength(path); + if (prefix < 0) { + return ""; + } + char[] ch = new char[size + 2]; + path.getChars(0, path.length(), ch, 0); + boolean firstIsDirectory = true; + if (ch[0] != separator) { + firstIsDirectory = false; + } + boolean lastIsDirectory = true; + if (ch[size - 1] != separator) { + lastIsDirectory = false; + } + for (int i = prefix + 1; i < size; i++) { + if (ch[i] == separator && ch[i - 1] == separator) { + System.arraycopy(ch, i, ch, i - 1, size - i); + size--; + i--; + } + } + for (int i = prefix + 1; i < size; i++) { + if (ch[i] == separator && ch[i - 1] == '.' + && (i == prefix + 1 || ch[i - 2] == separator)) { + if (i == size - 1) { + lastIsDirectory = true; + } + System.arraycopy(ch, i + 1, ch, i - 1, size - i); + size -=2; + i--; + } + } + int i = prefix + 2; + while (i < size) { + if (ch[i] == separator && ch[i - 1] == '.' && ch[i - 2] == '.' + && (i == prefix + 2 || ch[i - 3] == separator)) { + if (i == prefix + 2) { + return ""; + } + if (i == size - 1) { + lastIsDirectory = true; + } + int j; + boolean b = false; + for (j = i - 4 ; j >= prefix; j--) { + if (ch[j] == separator) { + System.arraycopy(ch, i + 1, ch, j + 1, size - i); + size -= (i - j); + i = j + 1; + b = true; + break; + } + } + if (b) { + continue; + } + System.arraycopy(ch, i + 1, ch, prefix, size - i); + size -= (i + 1 - prefix); + i = prefix + 1; + } + i++; + } + if (size <= 0) { + return ""; + } + String s = new String(ch, 0, size); + if (size <= prefix) { + return s; + } + if (!keepSeparator) { + if (firstIsDirectory && lastIsDirectory) { + return s.substring(1, s.length() - 1); + } else if (firstIsDirectory) { + return s.substring(1); + } else if (lastIsDirectory) { + return s.substring(0, s.length() - 1); + } + } + return s; + }*/ + + public static String normalize(String path) { + if (path == null || "".equals(path) || "/".equals(path)) { + return "/"; + } + path = path.replaceAll("/+", "/"); + int leadingSlashes = 0; + while (leadingSlashes < path.length() && path.charAt(leadingSlashes) == '/') { + ++leadingSlashes; + } + boolean isDir = (path.charAt(path.length() - 1) == '/'); + StringTokenizer st = new StringTokenizer(path, "/"); + LinkedList list = new LinkedList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if ("..".equals(token)) { + if (!list.isEmpty() && !"..".equals(list.getLast())) { + list.removeLast(); + if (!st.hasMoreTokens()) { + isDir = true; + } + } + } else if (!".".equals(token) && !"".equals(token)) { + list.add(token); + } + } + StringBuilder sb = new StringBuilder(); + while (leadingSlashes-- > 0) { + sb.append('/'); + } + for (Iterator it = list.iterator(); it.hasNext();) { + sb.append(it.next()); + if (it.hasNext()) { + sb.append('/'); + } + } + if (isDir && sb.length() > 0 && sb.charAt(sb.length() - 1) != '/') { + sb.append('/'); + } + return sb.toString(); + } + + private static int getPrefixLength(String filename) { + if (filename == null) { + return -1; + } + int len = filename.length(); + if (len == 0) { + return 0; + } + char ch0 = filename.charAt(0); + if (ch0 == ':') { + return -1; + } + if (len == 1) { + if (ch0 == '~') { + return 2; + } + return ch0 == separator ? 1 : 0; + } else { + if (ch0 == '~') { + int pos = filename.indexOf(separator, 1); + return pos == -1 ? len + 1 : pos + 1; + } + char ch1 = filename.charAt(1); + if (ch1 == ':') { + ch0 = Character.toUpperCase(ch0); + if (ch0 >= ('A') && ch0 <= ('Z')) { + if (len == 2 || filename.charAt(2) != separator) { + return 2; + } + return 3; + } + return -1; + } else if (ch0 == separator && ch1 == separator) { + int pos = filename.indexOf(separator, 2); + if (pos == -1 || pos == 2) { + return -1; + } + return pos + 1; + } else { + return ch0 == separator ? 1 : 0; + } + } + } +} diff --git a/src/main/java/org/xbib/net/path/PathPatternComparator.java b/src/main/java/org/xbib/net/path/PathPatternComparator.java new file mode 100644 index 0000000..4011570 --- /dev/null +++ b/src/main/java/org/xbib/net/path/PathPatternComparator.java @@ -0,0 +1,61 @@ +package org.xbib.net.path; + +import java.io.Serializable; +import java.util.Comparator; + +/** + */ +public class PathPatternComparator implements Comparator, Serializable { + + private static final long serialVersionUID = -5286803094119345841L; + + private final String path; + + PathPatternComparator(String path) { + this.path = path; + } + + @Override + public int compare(String pattern1, String pattern2) { + PathPatternInfo info1 = new PathPatternInfo(pattern1); + PathPatternInfo info2 = new PathPatternInfo(pattern2); + if (info1.isLeastSpecific() && info2.isLeastSpecific()) { + return 0; + } else if (info1.isLeastSpecific()) { + return 1; + } else if (info2.isLeastSpecific()) { + return -1; + } + boolean pattern1EqualsPath = pattern1.equals(path); + boolean pattern2EqualsPath = pattern2.equals(path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } else if (pattern1EqualsPath) { + return -1; + } else if (pattern2EqualsPath) { + return 1; + } + if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { + return 1; + } else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { + return -1; + } + if (info1.getTotalCount() != info2.getTotalCount()) { + return info1.getTotalCount() - info2.getTotalCount(); + } + if (info1.getLength() != info2.getLength()) { + return info2.getLength() - info1.getLength(); + } + if (info1.getSingleWildcards() < info2.getSingleWildcards()) { + return -1; + } else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { + return 1; + } + if (info1.getUriVars() < info2.getUriVars()) { + return -1; + } else if (info2.getUriVars() < info1.getUriVars()) { + return 1; + } + return 0; + } +} diff --git a/src/main/java/org/xbib/net/path/PathPatternInfo.java b/src/main/java/org/xbib/net/path/PathPatternInfo.java new file mode 100644 index 0000000..a113938 --- /dev/null +++ b/src/main/java/org/xbib/net/path/PathPatternInfo.java @@ -0,0 +1,89 @@ +package org.xbib.net.path; + +import java.util.regex.Pattern; + +/** + */ +class PathPatternInfo { + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}"); + + private final String pattern; + + private int uriVars; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + private Integer length; + + PathPatternInfo(String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals("/**"); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); + } + if (this.uriVars == 0) { + this.length = this.pattern != null ? this.pattern.length() : 0; + } + } + + private void initCounters() { + int pos = 0; + while (pos < this.pattern.length()) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { + this.singleWildcards++; + pos++; + } else { + pos++; + } + } else { + pos++; + } + } + } + + int getUriVars() { + return uriVars; + } + + int getSingleWildcards() { + return singleWildcards; + } + + int getDoubleWildcards() { + return doubleWildcards; + } + + boolean isLeastSpecific() { + return this.pattern == null || this.catchAllPattern; + } + + boolean isPrefixPattern() { + return this.prefixPattern; + } + + int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + int getLength() { + if (this.length == null) { + this.length = VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length(); + } + return length; + } +} diff --git a/src/main/java/org/xbib/net/path/PathSeparatorPatternCache.java b/src/main/java/org/xbib/net/path/PathSeparatorPatternCache.java new file mode 100644 index 0000000..c027587 --- /dev/null +++ b/src/main/java/org/xbib/net/path/PathSeparatorPatternCache.java @@ -0,0 +1,23 @@ +package org.xbib.net.path; + +/** + */ +class PathSeparatorPatternCache { + + private final String endsOnWildCard; + + private final String endsOnDoubleWildCard; + + PathSeparatorPatternCache(String pathSeparator) { + this.endsOnWildCard = pathSeparator + "*"; + this.endsOnDoubleWildCard = pathSeparator + "**"; + } + + String getEndsOnWildCard() { + return endsOnWildCard; + } + + String getEndsOnDoubleWildCard() { + return endsOnDoubleWildCard; + } +} diff --git a/src/main/java/org/xbib/net/path/PathStringMatcher.java b/src/main/java/org/xbib/net/path/PathStringMatcher.java new file mode 100644 index 0000000..acb44ff --- /dev/null +++ b/src/main/java/org/xbib/net/path/PathStringMatcher.java @@ -0,0 +1,80 @@ +package org.xbib.net.path; + +import org.xbib.net.QueryParameters; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + */ +class PathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final List variableNames = new ArrayList<>(); + + private final Pattern pattern; + + PathStringMatcher(String pattern, boolean caseSensitive) { + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } else if ("*".equals(match)) { + patternBuilder.append(".*"); + } else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('(').append(variablePattern).append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE); + } + + private static String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + boolean matchStrings(String str, QueryParameters queryParameters) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (queryParameters != null) { + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + queryParameters.add(name, value); + } + } + return true; + } else { + return false; + } + } +} diff --git a/src/main/java/org/xbib/net/path/package-info.java b/src/main/java/org/xbib/net/path/package-info.java new file mode 100644 index 0000000..b4e0319 --- /dev/null +++ b/src/main/java/org/xbib/net/path/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL path nomalizing, decoding, and matching. + */ +package org.xbib.net.path; diff --git a/src/main/java/org/xbib/net/scheme/AbstractScheme.java b/src/main/java/org/xbib/net/scheme/AbstractScheme.java new file mode 100644 index 0000000..44dad94 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/AbstractScheme.java @@ -0,0 +1,34 @@ +package org.xbib.net.scheme; + +import org.xbib.net.URL; + +/** + * Base implementation for scheme. + */ +public abstract class AbstractScheme implements Scheme { + + protected final String name; + + protected final int defaultPort; + + protected AbstractScheme(String name, int defaultPort) { + this.name = name; + this.defaultPort = defaultPort; + } + + @Override + public int getDefaultPort() { + return defaultPort; + } + + @Override + public String getName() { + return name; + } + + @Override + public URL normalize(URL url) { + return url; + } + +} diff --git a/src/main/java/org/xbib/net/scheme/DefaultScheme.java b/src/main/java/org/xbib/net/scheme/DefaultScheme.java new file mode 100644 index 0000000..cfb75dc --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/DefaultScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +public class DefaultScheme extends AbstractScheme { + + public DefaultScheme(String name) { + super(name, -1); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/DnsScheme.java b/src/main/java/org/xbib/net/scheme/DnsScheme.java new file mode 100644 index 0000000..2bce186 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/DnsScheme.java @@ -0,0 +1,17 @@ +package org.xbib.net.scheme; + +/** + * The DNS URI scheme. + * @see DNS RFC + */ +class DnsScheme extends HttpScheme { + + DnsScheme() { + super("dns", 53); + } + + DnsScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/FileScheme.java b/src/main/java/org/xbib/net/scheme/FileScheme.java new file mode 100644 index 0000000..ae5ffef --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/FileScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class FileScheme extends HttpScheme { + + FileScheme() { + super("file", -1); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/FtpScheme.java b/src/main/java/org/xbib/net/scheme/FtpScheme.java new file mode 100644 index 0000000..2fb46db --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/FtpScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * + */ +class FtpScheme extends HttpScheme { + + FtpScheme() { + super("ftp", 21); + } + + FtpScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/GitScheme.java b/src/main/java/org/xbib/net/scheme/GitScheme.java new file mode 100644 index 0000000..eb8bb85 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/GitScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * + */ +class GitScheme extends HttpScheme { + + GitScheme() { + super("git", 443); + } + + GitScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/GitSecureHttpScheme.java b/src/main/java/org/xbib/net/scheme/GitSecureHttpScheme.java new file mode 100644 index 0000000..829512e --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/GitSecureHttpScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * + */ +class GitSecureHttpScheme extends HttpScheme { + + GitSecureHttpScheme() { + super("git+https", 443); + } + + GitSecureHttpScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/GopherScheme.java b/src/main/java/org/xbib/net/scheme/GopherScheme.java new file mode 100644 index 0000000..797b372 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/GopherScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class GopherScheme extends AbstractScheme { + + GopherScheme() { + super("gopher", 70); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/HttpScheme.java b/src/main/java/org/xbib/net/scheme/HttpScheme.java new file mode 100644 index 0000000..60266c5 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/HttpScheme.java @@ -0,0 +1,35 @@ +package org.xbib.net.scheme; + +import org.xbib.net.URL; +import org.xbib.net.path.PathNormalizer; + +/** + * + */ +class HttpScheme extends AbstractScheme { + + HttpScheme() { + super("http", 80); + } + + HttpScheme(String name, int port) { + super(name, port); + } + + @Override + public URL normalize(URL url) { + String host = url.getHost(); + if (host != null) { + host = host.toLowerCase(); + } + return URL.builder() + .scheme(url.getScheme()) + .userInfo(url.getUserInfo()) + .host(host, url.getProtocolVersion()) + .port(url.getPort()) + .path(PathNormalizer.normalize(url.getPath())) + .query(url.getQuery()/*PercentEncoders.getQueryEncoder().encode(url.getDecodedQuery())*/) + .fragment(url.getFragment()/*PercentEncoders.getFragmentEncoder().encode(url.getDecodedFragment())*/) + .build(); + } +} diff --git a/src/main/java/org/xbib/net/scheme/ImapScheme.java b/src/main/java/org/xbib/net/scheme/ImapScheme.java new file mode 100644 index 0000000..e23d8d0 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/ImapScheme.java @@ -0,0 +1,17 @@ +package org.xbib.net.scheme; + +/** + * The IMAP scheme. + * + * @see IMAP RFC + */ +class ImapScheme extends AbstractScheme { + + ImapScheme() { + super("imap", 143); + } + + ImapScheme(String name, int port) { + super(name, port); + } +} diff --git a/src/main/java/org/xbib/net/scheme/IrcScheme.java b/src/main/java/org/xbib/net/scheme/IrcScheme.java new file mode 100644 index 0000000..6381b5f --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/IrcScheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The IRC scheme. + * + * @see IRC draft + */ +class IrcScheme extends HttpScheme { + + IrcScheme() { + super("irc", 194); + } + + IrcScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/LdapScheme.java b/src/main/java/org/xbib/net/scheme/LdapScheme.java new file mode 100644 index 0000000..588dbd6 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/LdapScheme.java @@ -0,0 +1,17 @@ +package org.xbib.net.scheme; + +/** + * The LDAP scheme. + * @see LDAP RFC + */ +class LdapScheme extends AbstractScheme { + + LdapScheme() { + super("ldap", 143); + } + + LdapScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/MailtoScheme.java b/src/main/java/org/xbib/net/scheme/MailtoScheme.java new file mode 100644 index 0000000..36a3a1b --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/MailtoScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The mailto scheme. + * + * @see mailto RFC + */ +public class MailtoScheme extends AbstractScheme { + + public MailtoScheme() { + super("mailto", -1); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/NewsScheme.java b/src/main/java/org/xbib/net/scheme/NewsScheme.java new file mode 100644 index 0000000..7cfed9f --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/NewsScheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The news scheme. + * + * @see news RFC + */ +class NewsScheme extends AbstractScheme { + + NewsScheme() { + super("nntp", 119); + } + + NewsScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/NntpScheme.java b/src/main/java/org/xbib/net/scheme/NntpScheme.java new file mode 100644 index 0000000..a5ed746 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/NntpScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The nttp scheme. + * + * @see NNTP RFC + */ +class NntpScheme extends AbstractScheme { + + NntpScheme() { + super("nntp", 119); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/Pop3Scheme.java b/src/main/java/org/xbib/net/scheme/Pop3Scheme.java new file mode 100644 index 0000000..dd7bda6 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/Pop3Scheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The POP3 scheme. + * + * @see POP3 RFC + */ +class Pop3Scheme extends AbstractScheme { + + Pop3Scheme() { + super("pop3", 110); + } + + Pop3Scheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/RedisScheme.java b/src/main/java/org/xbib/net/scheme/RedisScheme.java new file mode 100644 index 0000000..cd53057 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/RedisScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class RedisScheme extends AbstractScheme { + + RedisScheme() { + super("redis", 6379); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/RsyncScheme.java b/src/main/java/org/xbib/net/scheme/RsyncScheme.java new file mode 100644 index 0000000..ea20944 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/RsyncScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class RsyncScheme extends SshScheme { + + RsyncScheme() { + super("rsync", 873); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/RtmpScheme.java b/src/main/java/org/xbib/net/scheme/RtmpScheme.java new file mode 100644 index 0000000..3fce1d8 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/RtmpScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class RtmpScheme extends AbstractScheme { + + RtmpScheme() { + super("rtmp", 1935); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/RtspScheme.java b/src/main/java/org/xbib/net/scheme/RtspScheme.java new file mode 100644 index 0000000..eaca520 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/RtspScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The RTSP scheme. + * + * @see RTSP RFC + */ +class RtspScheme extends AbstractScheme { + + RtspScheme() { + super("rtsp", 554); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/Scheme.java b/src/main/java/org/xbib/net/scheme/Scheme.java new file mode 100644 index 0000000..cda1012 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/Scheme.java @@ -0,0 +1,48 @@ +package org.xbib.net.scheme; + +import org.xbib.net.URL; + +/** + * Interface implemented by custom scheme parsers. + */ +public interface Scheme { + + String DNS = "dns"; + String FILE = "file"; + String FTP = "ftp"; + String GIT = "git"; + String GIT_HTTPS = "git+https"; + String GOPHER = "gopher"; + String HTTP = "http"; + String HTTPS = "https"; + String IMAP = "imap"; + String IMAPS = "imaps"; + String IRC = "irc"; + String LDAP = "ldap"; + String LDAPS = "ldaps"; + String MAILTO = "mailto"; + String NEWS = "news"; + String NNTP = "nntp"; + String POP3 = "pop3"; + String POP3S = "pop3s"; + String REDIS = "redis"; + String RSYNC = "rsync"; + String RTMP = "rtmp"; + String RTSP = "rtsp"; + String SFTP = "sftp"; + String SMTP = "smtp"; + String SMTPS = "smtps"; + String SNEWS = "snews"; + String SSH = "ssh"; + String TELNET = "telnet"; + String TFTP = "tftp"; + String URN = "urn"; + String WS = "ws"; + String WSS = "wss"; + + String getName(); + + int getDefaultPort(); + + URL normalize(URL url); +} diff --git a/src/main/java/org/xbib/net/scheme/SchemeRegistry.java b/src/main/java/org/xbib/net/scheme/SchemeRegistry.java new file mode 100644 index 0000000..903e395 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SchemeRegistry.java @@ -0,0 +1,79 @@ +package org.xbib.net.scheme; + +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * Registry of URL schemes. + */ +public final class SchemeRegistry { + + private static final SchemeRegistry registry = new SchemeRegistry(); + + private final Map schemes; + + private SchemeRegistry() { + schemes = new HashMap<>(); + schemes.put(Scheme.DNS , new DnsScheme()); + schemes.put(Scheme.FILE , new FileScheme()); + schemes.put(Scheme.FTP, new FtpScheme()); + schemes.put(Scheme.GIT, new GitScheme()); + schemes.put(Scheme.GIT_HTTPS, new GitSecureHttpScheme()); + schemes.put(Scheme.GOPHER, new GopherScheme()); + schemes.put(Scheme.HTTP, new HttpScheme()); + schemes.put(Scheme.HTTPS, new SecureHttpScheme()); + schemes.put(Scheme.IMAP, new ImapScheme()); + schemes.put(Scheme.IMAPS, new SecureImapScheme()); + schemes.put(Scheme.IRC, new IrcScheme()); + schemes.put(Scheme.LDAP, new LdapScheme()); + schemes.put(Scheme.LDAPS, new SecureLdapScheme()); + schemes.put(Scheme.MAILTO, new MailtoScheme()); + schemes.put(Scheme.NEWS, new NewsScheme()); + schemes.put(Scheme.NNTP, new NntpScheme()); + schemes.put(Scheme.POP3, new Pop3Scheme()); + schemes.put(Scheme.POP3S, new SecurePop3Scheme()); + schemes.put(Scheme.REDIS, new RedisScheme()); + schemes.put(Scheme.RSYNC, new RsyncScheme()); + schemes.put(Scheme.RTMP, new RtmpScheme()); + schemes.put(Scheme.RTSP, new RtspScheme()); + schemes.put(Scheme.SFTP, new SftpScheme()); + schemes.put(Scheme.SMTP, new SmtpScheme()); + schemes.put(Scheme.SMTPS, new SecureSmtpScheme()); + schemes.put(Scheme.SNEWS, new SecureNewsScheme()); + schemes.put(Scheme.SSH, new SshScheme()); + schemes.put(Scheme.TELNET, new TelnetScheme()); + schemes.put(Scheme.TFTP, new TftpScheme()); + schemes.put(Scheme.URN, new UrnScheme()); + schemes.put(Scheme.WS, new WebSocketScheme()); + schemes.put(Scheme.WSS, new SecureWebSocketScheme()); + for (Scheme scheme : ServiceLoader.load(Scheme.class)) { + register(scheme); + } + } + + public static SchemeRegistry getInstance() { + return registry; + } + + public boolean register(Scheme scheme) { + String name = scheme.getName(); + if (name == null) { + return false; + } + if (!schemes.containsKey(name)) { + schemes.put(name.toLowerCase(), scheme); + return true; + } else { + return false; + } + } + + public Scheme getScheme(String scheme) { + if (scheme == null) { + return null; + } + Scheme s = schemes.get(scheme.toLowerCase()); + return s != null ? s : new DefaultScheme(scheme); + } +} diff --git a/src/main/java/org/xbib/net/scheme/SecureHttpScheme.java b/src/main/java/org/xbib/net/scheme/SecureHttpScheme.java new file mode 100644 index 0000000..d3f323b --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SecureHttpScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class SecureHttpScheme extends HttpScheme { + + SecureHttpScheme() { + super("https", 443); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SecureImapScheme.java b/src/main/java/org/xbib/net/scheme/SecureImapScheme.java new file mode 100644 index 0000000..8b2a0c2 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SecureImapScheme.java @@ -0,0 +1,13 @@ +package org.xbib.net.scheme; + +/** + * The IMAP scheme. + * @see IMAP scheme RFC + */ +class SecureImapScheme extends ImapScheme { + + SecureImapScheme() { + super("imaps", 993); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SecureLdapScheme.java b/src/main/java/org/xbib/net/scheme/SecureLdapScheme.java new file mode 100644 index 0000000..39398da --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SecureLdapScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The LDAPS scheme. + * + * @see LDAP RFC + */ +class SecureLdapScheme extends LdapScheme { + + SecureLdapScheme() { + super("ldaps", 636); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SecureNewsScheme.java b/src/main/java/org/xbib/net/scheme/SecureNewsScheme.java new file mode 100644 index 0000000..a842a94 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SecureNewsScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The snews scheme. + * + * @see news RFC + */ +class SecureNewsScheme extends NewsScheme { + + SecureNewsScheme() { + super("snews", 563); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SecurePop3Scheme.java b/src/main/java/org/xbib/net/scheme/SecurePop3Scheme.java new file mode 100644 index 0000000..d8f732a --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SecurePop3Scheme.java @@ -0,0 +1,13 @@ +package org.xbib.net.scheme; + +/** + * The POP3S scheme. + * + * @see POP3 RFC + */ +class SecurePop3Scheme extends Pop3Scheme { + + SecurePop3Scheme() { + super("pop3s", 995); + } +} diff --git a/src/main/java/org/xbib/net/scheme/SecureSmtpScheme.java b/src/main/java/org/xbib/net/scheme/SecureSmtpScheme.java new file mode 100644 index 0000000..f4959cb --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SecureSmtpScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The SMTPS scheme. + * + * @see SMTP RFC + */ +class SecureSmtpScheme extends SmtpScheme { + + SecureSmtpScheme() { + super("smtps", 587); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SecureWebSocketScheme.java b/src/main/java/org/xbib/net/scheme/SecureWebSocketScheme.java new file mode 100644 index 0000000..28fae29 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SecureWebSocketScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class SecureWebSocketScheme extends WebSocketScheme { + + SecureWebSocketScheme() { + super("wss", 443); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SftpScheme.java b/src/main/java/org/xbib/net/scheme/SftpScheme.java new file mode 100644 index 0000000..5777767 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SftpScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * + */ +class SftpScheme extends SshScheme { + + SftpScheme() { + super("sftp", 22); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SmtpScheme.java b/src/main/java/org/xbib/net/scheme/SmtpScheme.java new file mode 100644 index 0000000..f19fc0f --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SmtpScheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The SMTP scheme. + * + * @see SMTP RFC + */ +class SmtpScheme extends AbstractScheme { + + SmtpScheme() { + super("smtp", 25); + } + + SmtpScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/SshScheme.java b/src/main/java/org/xbib/net/scheme/SshScheme.java new file mode 100644 index 0000000..e3adc8d --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/SshScheme.java @@ -0,0 +1,15 @@ +package org.xbib.net.scheme; + +/** + * + */ +class SshScheme extends HttpScheme { + + SshScheme() { + super("ssh", 22); + } + + SshScheme(String name, int port) { + super(name, port); + } +} diff --git a/src/main/java/org/xbib/net/scheme/TelnetScheme.java b/src/main/java/org/xbib/net/scheme/TelnetScheme.java new file mode 100644 index 0000000..a885a30 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/TelnetScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The TELNET scheme. + * + * @see TELNET RFC + */ +class TelnetScheme extends AbstractScheme { + + TelnetScheme() { + super("telnet", 23); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/TftpScheme.java b/src/main/java/org/xbib/net/scheme/TftpScheme.java new file mode 100644 index 0000000..42f4797 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/TftpScheme.java @@ -0,0 +1,13 @@ +package org.xbib.net.scheme; + +/** + * The TFTP scheme. + * + * @see TFTP RFC + */ +class TftpScheme extends FtpScheme { + + TftpScheme() { + super("tftp", 69); + } +} diff --git a/src/main/java/org/xbib/net/scheme/UrnScheme.java b/src/main/java/org/xbib/net/scheme/UrnScheme.java new file mode 100644 index 0000000..246dc84 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/UrnScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The URN scheme. + * + * @see URN RFC + */ +class UrnScheme extends AbstractScheme { + + UrnScheme() { + super("urn", -1); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/WebSocketScheme.java b/src/main/java/org/xbib/net/scheme/WebSocketScheme.java new file mode 100644 index 0000000..87b0b31 --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/WebSocketScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * + */ +class WebSocketScheme extends HttpScheme { + + WebSocketScheme() { + super("ws", 80); + } + + WebSocketScheme(String name, int port) { + super(name, port); + } + +} diff --git a/src/main/java/org/xbib/net/scheme/package-info.java b/src/main/java/org/xbib/net/scheme/package-info.java new file mode 100644 index 0000000..df1a75c --- /dev/null +++ b/src/main/java/org/xbib/net/scheme/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for schemes. + */ +package org.xbib.net.scheme; diff --git a/src/main/java/org/xbib/net/template/URITemplate.java b/src/main/java/org/xbib/net/template/URITemplate.java new file mode 100644 index 0000000..34166d0 --- /dev/null +++ b/src/main/java/org/xbib/net/template/URITemplate.java @@ -0,0 +1,48 @@ +package org.xbib.net.template; + +import org.xbib.net.URL; +import org.xbib.net.template.expression.URITemplateExpression; +import org.xbib.net.template.parse.URITemplateParser; +import org.xbib.net.template.vars.Variables; + +import java.util.List; + +/** + * + */ +public class URITemplate { + + private final List expressions; + + public URITemplate(String input) { + this.expressions = URITemplateParser.parse(input); + } + + public List expressions() { + return expressions; + } + + /** + * Expand this template to a string given a list of variables. + * + * @param vars the variable map (names as keys, contents as values) + * @return expanded string + */ + public String toString(Variables vars) { + StringBuilder sb = new StringBuilder(); + for (URITemplateExpression expression : expressions) { + sb.append(expression.expand(vars)); + } + return sb.toString(); + } + + /** + * Expand this template to a URL given a set of variables. + * + * @param vars the variables + * @return a URL + */ + public URL toURL(Variables vars) { + return URL.from(toString(vars)); + } +} diff --git a/src/main/java/org/xbib/net/template/expression/ExpressionType.java b/src/main/java/org/xbib/net/template/expression/ExpressionType.java new file mode 100644 index 0000000..31aa1ac --- /dev/null +++ b/src/main/java/org/xbib/net/template/expression/ExpressionType.java @@ -0,0 +1,103 @@ +package org.xbib.net.template.expression; + +/** + */ +public enum ExpressionType { + /* + * Simple character expansion. + */ + SIMPLE("", ',', false, ""), + /* + * Reserved character expansion. + */ + RESERVED("", ',', false, ""), + /* + * Name labels expansion. + */ + NAME_LABELS(".", '.', false, ""), + /* + * Path segments expansion. + */ + PATH_SEGMENTS("/", '/', false, ""), + /* + * Path parameters expansion. + */ + PATH_PARAMETERS(";", ';', true, ""), + /* + * Query string expansion. + */ + QUERY_STRING("?", '&', true, "="), + /* + * Query string continuation expansion. + */ + QUERY_CONT("&", '&', true, "="), + /* + * Fragment expansion. + */ + FRAGMENT("#", ',', false, ""); + + /** + * Prefix string of expansion (requires at least one expanded token). + */ + private final String prefix; + + /** + * Separator if several tokens are present. + */ + private final char separator; + + /** + * Whether the variable (string, list) or key (map) name should be included + * if no explode modifier is found. + */ + private final boolean named; + + /** + * String to append to a name if the matching value is empty (empty string, + * empty list element, empty map value). + */ + private final String ifEmpty; + + ExpressionType(String prefix, char separator, boolean named, String ifEmpty) { + this.prefix = prefix; + this.separator = separator; + this.named = named; + this.ifEmpty = ifEmpty; + } + + /** + * Get the prefix string for this expansion type. + * + * @return the prefix string + */ + public String getPrefix() { + return prefix; + } + + /** + * Get the separator between token expansion elements. + * + * @return the separator + */ + public char getSeparator() { + return separator; + } + + /** + * Tell whether the variable name should be used in expansion. + * + * @return true if this is the case + */ + public boolean isNamed() { + return named; + } + + /** + * Get the substitution string for empty values. + * + * @return the substitution string + */ + public String getIfEmpty() { + return ifEmpty; + } +} diff --git a/src/main/java/org/xbib/net/template/expression/TemplateExpression.java b/src/main/java/org/xbib/net/template/expression/TemplateExpression.java new file mode 100644 index 0000000..06ee71e --- /dev/null +++ b/src/main/java/org/xbib/net/template/expression/TemplateExpression.java @@ -0,0 +1,71 @@ +package org.xbib.net.template.expression; + +import org.xbib.net.template.render.ValueRenderer; +import org.xbib.net.template.vars.Variables; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.ArrayList; +import java.util.List; + +/** + */ +public class TemplateExpression implements URITemplateExpression { + + private final ExpressionType expressionType; + + private final List variableSpecs; + + public TemplateExpression(ExpressionType expressionType, List variableSpecs) { + this.expressionType = expressionType; + this.variableSpecs = variableSpecs; + if (expressionType == null) { + throw new IllegalArgumentException("expression type must not be null"); + } + if (variableSpecs == null) { + throw new IllegalArgumentException("variables must not be null"); + } + } + + @Override + public String expand(Variables vars) { + List expansions = new ArrayList<>(); + VariableValue value; + ValueRenderer renderer; + for (VariableSpec varspec : variableSpecs) { + value = vars.get(varspec.getName()); + if (value == null) { + continue; + } + renderer = value.getType().selectRenderer(expressionType); + List list = renderer.render(varspec, value); + if (list != null) { + expansions.addAll(list); + } + } + if (expansions.isEmpty()) { + return ""; + } + return expressionType.getPrefix() + String.join(Character.toString(expressionType.getSeparator()), expansions); + } + + @Override + public int hashCode() { + return 31 * expressionType.hashCode() + variableSpecs.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + TemplateExpression other = (TemplateExpression) obj; + return expressionType == other.expressionType && variableSpecs.equals(other.variableSpecs); + } +} diff --git a/src/main/java/org/xbib/net/template/expression/TemplateLiteral.java b/src/main/java/org/xbib/net/template/expression/TemplateLiteral.java new file mode 100644 index 0000000..4012759 --- /dev/null +++ b/src/main/java/org/xbib/net/template/expression/TemplateLiteral.java @@ -0,0 +1,20 @@ +package org.xbib.net.template.expression; + +import org.xbib.net.template.vars.Variables; + +/** + */ +public +class TemplateLiteral implements URITemplateExpression { + + private final String literal; + + public TemplateLiteral(String literal) { + this.literal = literal; + } + + @Override + public String expand(Variables vars) { + return literal; + } +} diff --git a/src/main/java/org/xbib/net/template/expression/URITemplateExpression.java b/src/main/java/org/xbib/net/template/expression/URITemplateExpression.java new file mode 100644 index 0000000..c829b8d --- /dev/null +++ b/src/main/java/org/xbib/net/template/expression/URITemplateExpression.java @@ -0,0 +1,10 @@ +package org.xbib.net.template.expression; + +import org.xbib.net.template.vars.Variables; + +/** + */ +public interface URITemplateExpression { + + String expand(Variables vars); +} diff --git a/src/main/java/org/xbib/net/template/expression/package-info.java b/src/main/java/org/xbib/net/template/expression/package-info.java new file mode 100644 index 0000000..b11b99b --- /dev/null +++ b/src/main/java/org/xbib/net/template/expression/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template expressions. + */ +package org.xbib.net.template.expression; diff --git a/src/main/java/org/xbib/net/template/package-info.java b/src/main/java/org/xbib/net/template/package-info.java new file mode 100644 index 0000000..d9e3c11 --- /dev/null +++ b/src/main/java/org/xbib/net/template/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL templates. + */ +package org.xbib.net.template; diff --git a/src/main/java/org/xbib/net/template/parse/ExpressionParser.java b/src/main/java/org/xbib/net/template/parse/ExpressionParser.java new file mode 100644 index 0000000..4fa2b93 --- /dev/null +++ b/src/main/java/org/xbib/net/template/parse/ExpressionParser.java @@ -0,0 +1,63 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.matcher.CharMatcher; +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.expression.TemplateExpression; +import org.xbib.net.template.expression.URITemplateExpression; +import org.xbib.net.template.vars.specs.VariableSpec; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + */ +public class ExpressionParser implements TemplateParser { + + private static final Map EXPRESSION_TYPE_MAP; + static { + EXPRESSION_TYPE_MAP = new HashMap<>(); + EXPRESSION_TYPE_MAP.put('+', ExpressionType.RESERVED); + EXPRESSION_TYPE_MAP.put('#', ExpressionType.FRAGMENT); + EXPRESSION_TYPE_MAP.put('.', ExpressionType.NAME_LABELS); + EXPRESSION_TYPE_MAP.put('/', ExpressionType.PATH_SEGMENTS); + EXPRESSION_TYPE_MAP.put(';', ExpressionType.PATH_PARAMETERS); + EXPRESSION_TYPE_MAP.put('?', ExpressionType.QUERY_STRING); + EXPRESSION_TYPE_MAP.put('&', ExpressionType.QUERY_CONT); + } + private static final CharMatcher COMMA = CharMatcher.is(','); + private static final CharMatcher END_EXPRESSION = CharMatcher.is('}'); + + + @Override + public URITemplateExpression parse(CharBuffer buffer) { + buffer.get(); + if (!buffer.hasRemaining()) { + throw new IllegalArgumentException("early end of expression"); + } + ExpressionType type = ExpressionType.SIMPLE; + char c = buffer.charAt(0); + if (EXPRESSION_TYPE_MAP.containsKey(c)) { + char s = buffer.get(); + type = EXPRESSION_TYPE_MAP.get(s); + } + List varspecs = new ArrayList<>(); + while (true) { + varspecs.add(VariableSpecParser.parse(buffer)); + if (!buffer.hasRemaining()) { + throw new IllegalArgumentException("early end of expression"); + } + c = buffer.get(); + if (COMMA.matches(c)) { + continue; + } + if (END_EXPRESSION.matches(c)) { + break; + } + throw new IllegalArgumentException("unexpected token"); + } + return new TemplateExpression(type, varspecs); + } +} diff --git a/src/main/java/org/xbib/net/template/parse/LiteralParser.java b/src/main/java/org/xbib/net/template/parse/LiteralParser.java new file mode 100644 index 0000000..a583c17 --- /dev/null +++ b/src/main/java/org/xbib/net/template/parse/LiteralParser.java @@ -0,0 +1,45 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.matcher.CharMatcher; +import org.xbib.net.template.expression.TemplateLiteral; +import org.xbib.net.template.expression.URITemplateExpression; + +import java.nio.CharBuffer; + +/** + */ +public +class LiteralParser implements TemplateParser { + + @Override + public URITemplateExpression parse(CharBuffer buffer) { + StringBuilder sb = new StringBuilder(); + char c; + while (buffer.hasRemaining()) { + c = buffer.charAt(0); + if (!CharMatcher.LITERALS.matches(c)) { + break; + } + sb.append(buffer.get()); + if (CharMatcher.PERCENT.matches(c)) { + parsePercentEncoded(buffer, sb); + } + } + return new TemplateLiteral(sb.toString()); + } + + private static void parsePercentEncoded(CharBuffer buffer, StringBuilder sb) { + if (buffer.remaining() < 2) { + throw new IllegalArgumentException("short read"); + } + char first = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(first)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + char second = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(second)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + sb.append(first).append(second); + } +} diff --git a/src/main/java/org/xbib/net/template/parse/TemplateParser.java b/src/main/java/org/xbib/net/template/parse/TemplateParser.java new file mode 100644 index 0000000..ebd8cc7 --- /dev/null +++ b/src/main/java/org/xbib/net/template/parse/TemplateParser.java @@ -0,0 +1,12 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.template.expression.URITemplateExpression; + +import java.nio.CharBuffer; + +/** + * + */ +public interface TemplateParser { + URITemplateExpression parse(CharBuffer buffer); +} diff --git a/src/main/java/org/xbib/net/template/parse/URITemplateParser.java b/src/main/java/org/xbib/net/template/parse/URITemplateParser.java new file mode 100644 index 0000000..6401e3c --- /dev/null +++ b/src/main/java/org/xbib/net/template/parse/URITemplateParser.java @@ -0,0 +1,47 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.matcher.CharMatcher; +import org.xbib.net.template.expression.URITemplateExpression; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + */ +public class URITemplateParser { + + private static final CharMatcher BEGIN_EXPRESSION = CharMatcher.is('{'); + + private URITemplateParser() { + } + + public static List parse(String input) { + return parse(CharBuffer.wrap(input).asReadOnlyBuffer()); + } + + public static List parse(CharBuffer buffer) { + List ret = new ArrayList<>(); + TemplateParser templateParser; + URITemplateExpression expression; + while (buffer.hasRemaining()) { + templateParser = selectParser(buffer); + expression = templateParser.parse(buffer); + ret.add(expression); + } + return ret; + } + + private static TemplateParser selectParser(CharBuffer buffer) { + char c = buffer.charAt(0); + TemplateParser parser; + if (CharMatcher.LITERALS.matches(c)) { + parser = new LiteralParser(); + } else if (BEGIN_EXPRESSION.matches(c)) { + parser = new ExpressionParser(); + } else { + throw new IllegalArgumentException("no parser"); + } + return parser; + } +} diff --git a/src/main/java/org/xbib/net/template/parse/VariableSpecParser.java b/src/main/java/org/xbib/net/template/parse/VariableSpecParser.java new file mode 100644 index 0000000..5777cf8 --- /dev/null +++ b/src/main/java/org/xbib/net/template/parse/VariableSpecParser.java @@ -0,0 +1,128 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.matcher.CharMatcher; +import org.xbib.net.template.vars.specs.ExplodedVariable; +import org.xbib.net.template.vars.specs.PrefixVariable; +import org.xbib.net.template.vars.specs.SimpleVariable; +import org.xbib.net.template.vars.specs.VariableSpec; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + */ +public class VariableSpecParser { + + private static final CharMatcher DIGIT = CharMatcher.inRange('0', '9') + .precomputed(); + + private static final CharMatcher VARCHAR = DIGIT + .or(CharMatcher.inRange('a', 'z')) + .or(CharMatcher.inRange('A', 'Z')) + .or(CharMatcher.is('_')) + .or(CharMatcher.PERCENT) + .precomputed(); + + private static final CharMatcher DOT = CharMatcher.is('.'); + + private static final CharMatcher COLON = CharMatcher.is(':'); + + private static final CharMatcher STAR = CharMatcher.is('*'); + + private VariableSpecParser() { + } + + public static VariableSpec parse(CharBuffer buffer) { + String name = parseFullName(buffer); + if (!buffer.hasRemaining()) { + return new SimpleVariable(name); + } + char c = buffer.charAt(0); + if (STAR.matches(c)) { + buffer.get(); + return new ExplodedVariable(name); + } + if (COLON.matches(c)) { + buffer.get(); + return new PrefixVariable(name, getPrefixLength(buffer)); + } + return new SimpleVariable(name); + } + + private static String parseFullName(CharBuffer buffer) { + List components = new ArrayList<>(); + while (true) { + components.add(readName(buffer)); + if (!buffer.hasRemaining()) { + break; + } + if (!DOT.matches(buffer.charAt(0))) { + break; + } + buffer.get(); + } + return String.join(".", components); + } + + private static String readName(CharBuffer buffer) { + StringBuilder sb = new StringBuilder(); + char c; + while (buffer.hasRemaining()) { + c = buffer.charAt(0); + if (!VARCHAR.matches(c)) { + break; + } + sb.append(buffer.get()); + if (CharMatcher.PERCENT.matches(c)) { + parsePercentEncoded(buffer, sb); + } + } + String ret = sb.toString(); + if (ret.isEmpty()) { + throw new IllegalArgumentException("empty var name"); + } + return ret; + } + + private static void parsePercentEncoded(CharBuffer buffer, StringBuilder sb) { + if (buffer.remaining() < 2) { + throw new IllegalArgumentException("short read"); + } + char first = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(first)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + char second = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(second)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + sb.append(first).append(second); + } + + private static int getPrefixLength(CharBuffer buffer) { + StringBuilder sb = new StringBuilder(); + char c; + while (buffer.hasRemaining()) { + c = buffer.charAt(0); + if (!DIGIT.matches(c)) { + break; + } + sb.append(buffer.get()); + } + String s = sb.toString(); + if (s.isEmpty()) { + throw new IllegalArgumentException("empty prefix"); + } + int ret; + try { + ret = Integer.parseInt(s); + if (ret > 10000) { + throw new NumberFormatException(); + } + return ret; + } catch (NumberFormatException ignored) { + throw new IllegalArgumentException("prefix invalid / too large"); + } + } +} diff --git a/src/main/java/org/xbib/net/template/parse/package-info.java b/src/main/java/org/xbib/net/template/parse/package-info.java new file mode 100644 index 0000000..b711206 --- /dev/null +++ b/src/main/java/org/xbib/net/template/parse/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template parsers. + */ +package org.xbib.net.template.parse; diff --git a/src/main/java/org/xbib/net/template/render/ListRenderer.java b/src/main/java/org/xbib/net/template/render/ListRenderer.java new file mode 100644 index 0000000..1c54f4a --- /dev/null +++ b/src/main/java/org/xbib/net/template/render/ListRenderer.java @@ -0,0 +1,50 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * + */ +public class ListRenderer extends MultiValueRenderer { + + public ListRenderer(ExpressionType type) { + super(type); + } + + @Override + protected List renderNamedExploded(String varname, VariableValue value) { + return value.getListValue().stream().map(element -> + element.isEmpty() ? varname + ifEmpty : varname + '=' + pctEncode(element) + ).collect(Collectors.toList()); + } + + @Override + protected List renderUnnamedExploded(VariableValue value) { + return value.getListValue().stream().map(this::pctEncode).collect(Collectors.toList()); + } + + @Override + protected List renderNamedNormal(String varname, VariableValue value) { + StringBuilder sb = new StringBuilder(varname); + if (value.isEmpty()) { + return Collections.singletonList(sb.append(ifEmpty).toString()); + } + sb.append('='); + List elements = value.getListValue().stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(sb.toString() + String.join(",", elements)); + } + + @Override + protected List renderUnnamedNormal(VariableValue value) { + if (value.isEmpty()) { + return Collections.emptyList(); + } + List elements = value.getListValue().stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(String.join(",", elements)); + } +} diff --git a/src/main/java/org/xbib/net/template/render/MapRenderer.java b/src/main/java/org/xbib/net/template/render/MapRenderer.java new file mode 100644 index 0000000..cb4ccb8 --- /dev/null +++ b/src/main/java/org/xbib/net/template/render/MapRenderer.java @@ -0,0 +1,62 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * + */ +public class MapRenderer extends MultiValueRenderer { + + public MapRenderer(ExpressionType type) { + super(type); + } + + @Override + protected List renderNamedExploded(String varname, VariableValue value) { + List ret = new ArrayList<>(); + value.getMapValue().forEach((k, v) -> ret.add(pctEncode(k) + (v.isEmpty() ? ifEmpty : '=' + pctEncode(v)))); + return ret; + } + + @Override + protected List renderUnnamedExploded(VariableValue value) { + List ret = new ArrayList<>(); + value.getMapValue().forEach((k, v) -> ret.add(pctEncode(k) + '=' + pctEncode(v))); + return ret; + } + + @Override + protected List renderNamedNormal(String varname, VariableValue value) { + StringBuilder sb = new StringBuilder(varname); + if (value.isEmpty()) { + return Collections.singletonList(sb.append(ifEmpty).toString()); + } + sb.append('='); + List elements = mapAsList(value).stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(sb.toString() + String.join(",", elements)); + } + + @Override + protected List renderUnnamedNormal(VariableValue value) { + if (value.isEmpty()) { + return Collections.emptyList(); + } + List elements = mapAsList(value).stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(String.join(",", elements)); + } + + private static List mapAsList(VariableValue value) { + List ret = new ArrayList<>(); + value.getMapValue().forEach((k, v) -> { + ret.add(k); + ret.add(v); + }); + return ret; + } +} diff --git a/src/main/java/org/xbib/net/template/render/MultiValueRenderer.java b/src/main/java/org/xbib/net/template/render/MultiValueRenderer.java new file mode 100644 index 0000000..4b12280 --- /dev/null +++ b/src/main/java/org/xbib/net/template/render/MultiValueRenderer.java @@ -0,0 +1,49 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.List; + +/** + * + */ +abstract class MultiValueRenderer extends ValueRenderer { + + MultiValueRenderer(ExpressionType type) { + super(type); + } + + @Override + public List render(VariableSpec varspec, VariableValue value) { + if (varspec.getPrefixLength() != -1) { + throw new IllegalArgumentException("incompatible var spec value"); + } + String varname = varspec.getName(); + return named ? + (varspec.isExploded() ? renderNamedExploded(varname, value) : renderNamedNormal(varname, value)) : + (varspec.isExploded() ? renderUnnamedExploded(value) : renderUnnamedNormal(value)); + } + + protected abstract List renderNamedExploded(String varname, VariableValue value); + + protected abstract List renderUnnamedExploded(VariableValue value); + + /** + * Rendering method for named expressions and non exploded varspecs. + * + * @param varname name of the variable (used in lists) + * @param value value of the variable + * @return list of rendered elements + */ + protected abstract List renderNamedNormal(String varname, VariableValue value); + + /** + * Rendering method for non named expressions and non exploded varspecs. + * + * @param value value of the variable + * @return list of rendered elements + */ + protected abstract List renderUnnamedNormal(VariableValue value); +} diff --git a/src/main/java/org/xbib/net/template/render/NullRenderer.java b/src/main/java/org/xbib/net/template/render/NullRenderer.java new file mode 100644 index 0000000..591e15b --- /dev/null +++ b/src/main/java/org/xbib/net/template/render/NullRenderer.java @@ -0,0 +1,22 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.List; + +/** + * + */ +public class NullRenderer extends ValueRenderer { + + public NullRenderer(ExpressionType type) { + super(type); + } + + @Override + public List render(VariableSpec varspec, VariableValue value) { + return null; + } +} diff --git a/src/main/java/org/xbib/net/template/render/StringRenderer.java b/src/main/java/org/xbib/net/template/render/StringRenderer.java new file mode 100644 index 0000000..9df4dca --- /dev/null +++ b/src/main/java/org/xbib/net/template/render/StringRenderer.java @@ -0,0 +1,53 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.Collections; +import java.util.List; + +/** + * + */ +public class StringRenderer extends ValueRenderer { + + public StringRenderer(ExpressionType type) { + super(type); + } + + @Override + public List render(VariableSpec varspec, VariableValue value) { + return Collections.singletonList(doRender(varspec, value.getScalarValue())); + } + + private String doRender(VariableSpec varspec, String value) { + if (value == null) { + return ""; + } + StringBuilder sb = new StringBuilder(value.length()); + if (named) { + sb.append(varspec.getName()); + if (value.isEmpty()) { + return sb.append(ifEmpty).toString(); + } + sb.append('='); + } + int prefixLen = varspec.getPrefixLength(); + if (prefixLen == -1) { + return sb.append(pctEncode(value)).toString(); + } + int len = value.codePointCount(0, value.length()); + return len <= prefixLen ? + sb.append(pctEncode(value)).toString() : + sb.append(pctEncode(nFirstChars(value, prefixLen))).toString(); + } + + private static String nFirstChars(String s, int n) { + int realIndex = n; + while (s.codePointCount(0, realIndex) != n) { + realIndex++; + } + return s.substring(0, realIndex); + } +} diff --git a/src/main/java/org/xbib/net/template/render/ValueRenderer.java b/src/main/java/org/xbib/net/template/render/ValueRenderer.java new file mode 100644 index 0000000..820e70e --- /dev/null +++ b/src/main/java/org/xbib/net/template/render/ValueRenderer.java @@ -0,0 +1,81 @@ +package org.xbib.net.template.render; + +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * The algorithm used for rendering is centered around this class,and is + * adapted from the algorithm suggested in the RFC's appendix. + * + * Eventually, rendering can be viewed as joining a list of rendered strings + * with the expression type separator; if the resulting list is empty, the end + * result is the empty string; otherwise, it is the expression's prefix string + * (if any) followed by the joined list of rendered strings. + * + * This class renders one variable value according to the expression type and + * value type. The rendering method returns a list, which can be empty. + */ +public abstract class ValueRenderer { + /** + * Whether variable values are named during expansion. + */ + protected final boolean named; + + /** + * Substitution string for an empty value/list member/map value. + */ + protected final String ifEmpty; + + /** + * The percent encoder. + */ + private final PercentEncoder percentEncoder; + + protected ValueRenderer(ExpressionType type) { + named = type.isNamed(); + ifEmpty = type.getIfEmpty(); + switch (type) { + case RESERVED: + case FRAGMENT: + percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); + break; + default: + percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + break; + } + } + + /** + * Render a value given a varspec and value. + * + * @param varspec the varspec + * @param value the matching variable value + * @return a list of rendered strings + */ + public abstract List render(VariableSpec varspec, VariableValue value); + + /** + * Render a string value, doing character percent-encoding where needed. + * + * The character set on which to perform percent encoding is dependent + * on the expression type. + * + * @param s the string to encode + * @return an encoded string + */ + protected String pctEncode(String s) { + try { + return percentEncoder.encode(s); + } catch (CharacterCodingException e) { + throw new IllegalStateException(e); + } + } +} + diff --git a/src/main/java/org/xbib/net/template/render/package-info.java b/src/main/java/org/xbib/net/template/render/package-info.java new file mode 100644 index 0000000..411e81b --- /dev/null +++ b/src/main/java/org/xbib/net/template/render/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template renderers. + */ +package org.xbib.net.template.render; diff --git a/src/main/java/org/xbib/net/template/vars/Variables.java b/src/main/java/org/xbib/net/template/vars/Variables.java new file mode 100644 index 0000000..c3bb5e2 --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/Variables.java @@ -0,0 +1,131 @@ +package org.xbib.net.template.vars; + +import org.xbib.net.template.vars.values.ListValue; +import org.xbib.net.template.vars.values.MapValue; +import org.xbib.net.template.vars.values.ScalarValue; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + */ +public class Variables { + + private final Map vars; + + Variables(Builder builder) { + this.vars = builder.vars; + } + + /** + * Create a new builder for this class. + * + * @return a {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Get the value associated with a variable name. + * + * @param varname the variable name + * @return the value, or {@code null} if there is no matching value + */ + public VariableValue get(String varname) { + return vars.get(varname); + } + + @Override + public String toString() { + return vars.toString(); + } + + /** + * + */ + public static class Builder { + + private final Map vars = new LinkedHashMap<>(); + + Builder() { + } + + /** + * Associate a map, list, or object to a variable name. + * + * @param varname the variable name + * @param value the value, as a {@link VariableValue} + * @return this + */ + @SuppressWarnings("unchecked") + public Builder add(String varname, Object value) { + if (value instanceof VariableValue) { + addValue(varname, (VariableValue) value); + } else if (value instanceof Map) { + addValue(varname, (Map) value); + } else if (value instanceof List) { + addValue(varname, (List) value); + } else { + addValue(varname, new ScalarValue(value)); + } + return this; + } + + /** + * Associate a value to a variable name. + * + * @param varname the variable name + * @param value the value, as a {@link VariableValue} + * @return this + */ + private Builder addValue(String varname, VariableValue value) { + vars.put(varname, value); + return this; + } + + /** + * Shortcut method to associate a name with a list value. + * Any {@link Iterable} can be used (thereby including all collections: + * sets, lists, etc). Note that it is your responsibility that objects in + * this iterable implement {@link Object#toString()} correctly. + * + * @param varname the variable name + * @param iterable the iterable + * @return this + */ + private Builder addValue(String varname, Iterable iterable) { + return add(varname, ListValue.copyOf(iterable)); + } + + /** + * Method to associate a variable name to a map value. + * Values of the map can be of any type. You should ensure that they + * implement {@link Object#toString()} correctly. + * + * @param varname the variable name + * @param map the map + * @return this + */ + private Builder addValue(String varname, Map map) { + return add(varname, MapValue.copyOf(map)); + } + + /** + * Add all variable definitions from another variable map. + * @param other the other variable map to copy definitions from + * @return this + * @throws NullPointerException other variable map is null + */ + public Builder add(Variables other) { + vars.putAll(other.vars); + return this; + } + + public Variables build() { + return new Variables(this); + } + } +} diff --git a/src/main/java/org/xbib/net/template/vars/package-info.java b/src/main/java/org/xbib/net/template/vars/package-info.java new file mode 100644 index 0000000..70bf35a --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template variables. + */ +package org.xbib.net.template.vars; diff --git a/src/main/java/org/xbib/net/template/vars/specs/ExplodedVariable.java b/src/main/java/org/xbib/net/template/vars/specs/ExplodedVariable.java new file mode 100644 index 0000000..776d4aa --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/specs/ExplodedVariable.java @@ -0,0 +1,46 @@ +package org.xbib.net.template.vars.specs; + +/** + * + */ +public class ExplodedVariable extends VariableSpec { + + public ExplodedVariable(String name) { + super(VariableSpecType.EXPLODED, name); + } + + @Override + public boolean isExploded() { + return true; + } + + @Override + public int getPrefixLength() { + return -1; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + ExplodedVariable other = (ExplodedVariable) obj; + return name.equals(other.name); + } + + @Override + public String toString() { + return name + " (exploded)"; + } +} diff --git a/src/main/java/org/xbib/net/template/vars/specs/PrefixVariable.java b/src/main/java/org/xbib/net/template/vars/specs/PrefixVariable.java new file mode 100644 index 0000000..d9042ae --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/specs/PrefixVariable.java @@ -0,0 +1,49 @@ +package org.xbib.net.template.vars.specs; + +/** + * A varspec with a prefix modifier (for instance, {@code foo:3} in {@code {foo:3}}. + */ +public class PrefixVariable extends VariableSpec { + + private final int length; + + public PrefixVariable(String name, int length) { + super(VariableSpecType.PREFIX, name); + this.length = length; + } + + @Override + public boolean isExploded() { + return false; + } + + @Override + public int getPrefixLength() { + return length; + } + + @Override + public int hashCode() { + return 31 * name.hashCode() + length; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + PrefixVariable other = (PrefixVariable) obj; + return name.equals(other.name) && length == other.length; + } + + @Override + public String toString() { + return name + " (prefix length: " + length + ')'; + } +} diff --git a/src/main/java/org/xbib/net/template/vars/specs/SimpleVariable.java b/src/main/java/org/xbib/net/template/vars/specs/SimpleVariable.java new file mode 100644 index 0000000..f06bb4c --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/specs/SimpleVariable.java @@ -0,0 +1,46 @@ +package org.xbib.net.template.vars.specs; + +/** + * A varspec without modifier (for instance, {@code foo} in {@code {foo}}. + */ +public class SimpleVariable extends VariableSpec { + + public SimpleVariable(String name) { + super(VariableSpecType.SIMPLE, name); + } + + @Override + public boolean isExploded() { + return false; + } + + @Override + public int getPrefixLength() { + return -1; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + SimpleVariable other = (SimpleVariable) obj; + return name.equals(other.name); + } + + @Override + public String toString() { + return name + " (simple)"; + } +} diff --git a/src/main/java/org/xbib/net/template/vars/specs/VariableSpec.java b/src/main/java/org/xbib/net/template/vars/specs/VariableSpec.java new file mode 100644 index 0000000..cc1ef96 --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/specs/VariableSpec.java @@ -0,0 +1,66 @@ +package org.xbib.net.template.vars.specs; + +/** + * A variable specifier. + * + * A template expression can have one or more variable specifiers. For + * instance, in {@code {+path:3,var}}, variable specifiers are {@code path:3} + * and {@code var}. + * + * This class records the name of this specifier and its modifier, if any. + */ +public abstract class VariableSpec { + + protected final String name; + + private final VariableSpecType type; + + protected VariableSpec(VariableSpecType type, String name) { + this.type = type; + this.name = name; + } + + /** + * Get the modifier type for this var spec. + * + * @return the modifier type + */ + public final VariableSpecType getType() { + return type; + } + + /** + * Get the name for this var spec. + * + * @return the name + */ + public final String getName() { + return name; + } + + /** + * Tell whether this varspec has an explode modifier. + * + * @return true if an explode modifier is present + */ + public abstract boolean isExploded(); + + /** + * Return the prefix length for this varspec. + * + * Returns -1 if no prefix length is specified. Recall: valid values are + * integers between 0 and 10000. + * + * @return the prefix length, or -1 if no prefix modidifer + */ + public abstract int getPrefixLength(); + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract String toString(); +} diff --git a/src/main/java/org/xbib/net/template/vars/specs/VariableSpecType.java b/src/main/java/org/xbib/net/template/vars/specs/VariableSpecType.java new file mode 100644 index 0000000..a8230a6 --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/specs/VariableSpecType.java @@ -0,0 +1,21 @@ +package org.xbib.net.template.vars.specs; + +/** + * Enumeration of a variable modifier type. + */ +public enum VariableSpecType { + /** + * No modifier. + */ + SIMPLE, + /** + * Prefix modifier ({@code :xxx} where {@code xxx} is an integer). + * Only makes sense for string values. + */ + PREFIX, + /** + * Explode modifier ({@code *}). + * Only makes sense for list and map values. + */ + EXPLODED +} diff --git a/src/main/java/org/xbib/net/template/vars/specs/package-info.java b/src/main/java/org/xbib/net/template/vars/specs/package-info.java new file mode 100644 index 0000000..c1ddd4c --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/specs/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template var specs. + */ +package org.xbib.net.template.vars.specs; diff --git a/src/main/java/org/xbib/net/template/vars/values/ListValue.java b/src/main/java/org/xbib/net/template/vars/values/ListValue.java new file mode 100644 index 0000000..efa597f --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/values/ListValue.java @@ -0,0 +1,104 @@ +package org.xbib.net.template.vars.values; + +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +public class ListValue extends VariableValue { + + private final List list; + + private ListValue(Builder builder) { + super(ValueType.ARRAY); + list = builder.list; + } + + /** + * Create a new list value builder. + * + * @return a builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Build a list value out of an existing iterable (list, set, other). + * + * This calls {@link Builder#addAll(Iterable)} internally. + * + * @param iterable the iterable + * @param the type of iterable elements + * @return a new list value + */ + public static VariableValue copyOf(Iterable iterable) { + return new Builder().addAll(iterable).build(); + } + + @Override + public List getListValue() { + return list; + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public String toString() { + return list.toString(); + } + + /** + * Builder class for a {@link ListValue}. + */ + public static class Builder { + + private final List list = new ArrayList<>(); + + Builder() { + } + + /** + * Add a series of elements to this list. + * + * @param first first element + * @param other other elements, if any + * @return this + * @throws NullPointerException one argument at least is null + */ + public Builder add(Object first, Object... other) { + list.add(first.toString()); + for (Object o : other) { + list.add(o.toString()); + } + return this; + } + + /** + * Add elements from an iterable. + * + * @param iterable the iterable + * @param type of elements in the iterable + * @return this + */ + public Builder addAll(Iterable iterable) { + for (T t : iterable) { + list.add(t.toString()); + } + return this; + } + + /** + * Build the value. + * + * @return the list value as a {@link VariableValue} + */ + public VariableValue build() { + return new ListValue(this); + } + } +} diff --git a/src/main/java/org/xbib/net/template/vars/values/MapValue.java b/src/main/java/org/xbib/net/template/vars/values/MapValue.java new file mode 100644 index 0000000..85c44f6 --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/values/MapValue.java @@ -0,0 +1,104 @@ +package org.xbib.net.template.vars.values; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * + */ +public class MapValue extends VariableValue { + + private final Map map; + + private MapValue(Builder builder) { + super(ValueType.MAP); + map = builder.map; + } + + /** + * Create a new builder for this class. + * + * @return a {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Convenience method to build a variable value from an existing {@link Map}. + * + * @param map the map + * @param the type of values in this map + * @return a new map value as a {@link VariableValue} + * @throws NullPointerException map is null, or one of its keys or values + * is null + */ + public static VariableValue copyOf(Map map) { + return builder().putAll(map).build(); + } + + @Override + public Map getMapValue() { + return map; + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public String toString() { + return map.toString(); + } + + /** + * Builder class for a {@link MapValue}. + */ + public static class Builder { + + private final Map map = new LinkedHashMap<>(); + + Builder() { + } + + /** + * Add one key/value pair to the map. + * + * @param key the key + * @param value the value + * @param the type of the value + * @return this + * @throws NullPointerException the key or value is null + */ + public Builder put(String key, T value) { + map.put(key, value.toString()); + return this; + } + + /** + * Inject a map of key/value pairs. + * + * @param map the map + * @param the type of this map's values + * @return this + * @throws NullPointerException map is null, or one of its keys or + * values is null + */ + public Builder putAll(Map map) { + for (Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Build the value. + * + * @return the map value as a {@link VariableValue} + */ + public VariableValue build() { + return new MapValue(this); + } + } +} diff --git a/src/main/java/org/xbib/net/template/vars/values/NullValue.java b/src/main/java/org/xbib/net/template/vars/values/NullValue.java new file mode 100644 index 0000000..0f8632d --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/values/NullValue.java @@ -0,0 +1,15 @@ +package org.xbib.net.template.vars.values; + +/** + */ +public class NullValue extends VariableValue { + + public NullValue() { + super(ValueType.NULL); + } + + @Override + public boolean isEmpty() { + return true; + } +} diff --git a/src/main/java/org/xbib/net/template/vars/values/ScalarValue.java b/src/main/java/org/xbib/net/template/vars/values/ScalarValue.java new file mode 100644 index 0000000..41291fa --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/values/ScalarValue.java @@ -0,0 +1,29 @@ +package org.xbib.net.template.vars.values; + +/** + * + */ +public class ScalarValue extends VariableValue { + + private final String value; + + public ScalarValue(Object value) { + super(ValueType.SCALAR); + this.value = (String) value; + } + + @Override + public String getScalarValue() { + return value; + } + + @Override + public boolean isEmpty() { + return value.isEmpty(); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/org/xbib/net/template/vars/values/ValueType.java b/src/main/java/org/xbib/net/template/vars/values/ValueType.java new file mode 100644 index 0000000..5265eef --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/values/ValueType.java @@ -0,0 +1,68 @@ +package org.xbib.net.template.vars.values; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.render.ListRenderer; +import org.xbib.net.template.render.MapRenderer; +import org.xbib.net.template.render.NullRenderer; +import org.xbib.net.template.render.StringRenderer; +import org.xbib.net.template.render.ValueRenderer; + +/** + */ +public enum ValueType { + + NULL("null") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new NullRenderer(type); + } + }, + /** + * Render scalar values (simple string values). + */ + SCALAR("scalar") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new StringRenderer(type); + } + }, + /** + * Render array/list values. + */ + ARRAY("list") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new ListRenderer(type); + } + }, + /** + * Map values. + * + * Note: the RFC calls these "associative arrays". + */ + MAP("map") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new MapRenderer(type); + } + }; + + private final String name; + + ValueType(String name) { + this.name = name; + } + + /** + * Get the renderer for this value type and expression type. + * + * @param type the expression type + * @return the appropriate renderer + */ + public abstract ValueRenderer selectRenderer(ExpressionType type); + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/org/xbib/net/template/vars/values/VariableValue.java b/src/main/java/org/xbib/net/template/vars/values/VariableValue.java new file mode 100644 index 0000000..1e21db5 --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/values/VariableValue.java @@ -0,0 +1,66 @@ +package org.xbib.net.template.vars.values; + +import java.util.List; +import java.util.Map; + +/** + */ +public abstract class VariableValue { + + private final ValueType type; + + VariableValue(ValueType type) { + this.type = type; + } + + /** + * Get the type for this value. + * + * @return the value type + */ + public ValueType getType() { + return type; + } + + /** + * Get a simple string for this value. + * Only valid for string values. + * + * @return the string + * @throws IllegalArgumentException value is not a string value + */ + public String getScalarValue() { + throw new IllegalArgumentException("not a scalar"); + } + + /** + * Get a list for this value. + * Only valid for list values. + * + * @return the list + * @throws IllegalArgumentException value is not a list value + */ + public List getListValue() { + throw new IllegalArgumentException("not a list"); + } + + /** + * Get a map for this value. + * Only valid for map values. + * + * @return the map + * @throws IllegalArgumentException value is not a map value + */ + public Map getMapValue() { + throw new IllegalArgumentException("not a map"); + } + + /** + * Tell whether this value is empty. + * For strings, this tells whether the string itself is empty. For lists + * and maps, this tells whether the list or map have no elements/entries. + * + * @return true if the value is empty + */ + public abstract boolean isEmpty(); +} diff --git a/src/main/java/org/xbib/net/template/vars/values/package-info.java b/src/main/java/org/xbib/net/template/vars/values/package-info.java new file mode 100644 index 0000000..7ef9cfe --- /dev/null +++ b/src/main/java/org/xbib/net/template/vars/values/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template variables values. + */ +package org.xbib.net.template.vars.values; diff --git a/src/test/java/org/xbib/net/IRITest.java b/src/test/java/org/xbib/net/IRITest.java new file mode 100644 index 0000000..00cd9af --- /dev/null +++ b/src/test/java/org/xbib/net/IRITest.java @@ -0,0 +1,193 @@ +package org.xbib.net; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.text.Normalizer; + +/** + * + */ +public class IRITest { + + @Test + public void testIpv4() { + URL iri = URL.create("http://127.0.0.1"); + assertEquals("http://127.0.0.1", iri.toExternalForm()); + } + + @Test + public void testIpv6() { + URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]"); + assertTrue(iri.getProtocolVersion().equals(ProtocolVersion.IPV6)); + assertEquals("http://[2001:db8:85a3:8d3:1319:8a2e:370:7344]", iri.toString()); + } + + @Test + public void testIpv6Invalid() { + URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:734o]"); + assertEquals(URL.INVALID, iri); + } + + @Test + public void testSimple() { + URL iri = URL.create("http://validator.w3.org/check?uri=http%3A%2F%2Fr\u00E9sum\u00E9.example.org"); + //assertEquals("http://validator.w3.org/check?uri=http%3A%2F%2Fr\u00E9sum\u00E9.example.org", iri.toString()); + assertEquals("http://validator.w3.org/check?uri=http://r%C3%A9sum%C3%A9.example.org", + iri.toExternalForm()); + } + + @Test + public void testFile() throws Exception { + URL iri = URL.create("file:///tmp/test/foo"); + assertEquals("", iri.getHost()); + assertEquals("/tmp/test/foo", iri.getPath()); + assertEquals("file:///tmp/test/foo", iri.toExternalForm()); + assertEquals("file:///tmp/test/foo", iri.toString()); + } + + @Test + public void testSimple2() throws Exception { + URL iri = URL.create("http://www.example.org/red%09ros\u00E9#red"); + assertEquals("http://www.example.org/red%09ros%C3%A9#red", iri.toExternalForm()); + } + + @Test + public void testNotSoSimple() throws Exception { + URL iri = URL.create("http://example.com/\uD800\uDF00\uD800\uDF01\uD800\uDF02"); + assertEquals("http://example.com/%F0%90%8C%80%F0%90%8C%81%F0%90%8C%82", iri.toExternalForm()); + } + + @Test + public void testIRItoURI() throws Exception { + URL iri = URL.from("http://\u7D0D\u8C46.example.org/%E2%80%AE"); + assertEquals("http://xn--99zt52a.example.org/%E2%80%AE", iri.toExternalForm()); + } + + @Test + public void testComparison() throws Exception { + + URL url1 = URL.create("http://www.example.org/"); + URL url2 = URL.create("http://www.example.org/.."); + URL url3 = URL.create("http://www.Example.org:80"); + + assertNotEquals(url1, url2); + assertNotEquals(url1, url3); + assertNotEquals(url2, url1); + assertNotEquals(url2, url3); + assertNotEquals(url3, url1); + assertNotEquals(url3, url2); + + assertEquals(url1.normalize(), url2.normalize()); + assertEquals(url1.normalize(), url3.normalize()); + assertEquals(url2.normalize(), url1.normalize()); + assertEquals(url2.normalize(), url3.normalize()); + assertEquals(url3.normalize(), url1.normalize()); + assertEquals(url3.normalize(), url2.normalize()); + } + + @Test + public void testUCN() throws Exception { + URL iri1 = URL.create("http://www.example.org/r\u00E9sum\u00E9.html"); + String s = Normalizer.normalize("http://www.example.org/re\u0301sume\u0301.html", Normalizer.Form.NFC); + URL iri2 = URL.create(s); + assertEquals(iri2, iri1); + } + + @Test + public void testPercent() { + URL iri1 = URL.create("http://example.org/%7e%2Fuser?%2f"); + URL iri2 = URL.create("http://example.org/%7E%2fuser?/"); + assertEquals(iri1.normalize(), iri2.normalize()); + } + + @Test + public void testIDN() { + URL iri1 = URL.from("http://r\u00E9sum\u00E9.example.org"); + assertEquals("xn--rsum-bpad.example.org", iri1.getHost()); + } + + @Test + public void testResolveRelative() { + URL base = URL.create("http://example.org/foo/"); + assertEquals("http://example.org/", base.resolve("/").toString()); + assertEquals("http://example.org/test", base.resolve("/test").toString()); + assertEquals("http://example.org/foo/test", base.resolve("test").toString()); + assertEquals("http://example.org/test", base.resolve("../test").toString()); + assertEquals("http://example.org/foo/test", base.resolve("./test").toString()); + assertEquals("http://example.org/foo/", base.resolve("test/test/../../").toString()); + assertEquals("http://example.org/foo/?test", base.resolve("?test").toString()); + assertEquals("http://example.org/foo/#test", base.resolve("#test").toString()); + assertEquals("http://example.org/foo/", base.resolve(".").toString()); + } + + @Test + public void testSchemes() { + + URL iri = URL.create("http://a:b@c.org:80/d/e?f#g"); + assertEquals("http", iri.getScheme()); + assertEquals("a:b", iri.getUserInfo()); + assertEquals("c.org", iri.getHost()); + assertEquals(Integer.valueOf(80), iri.getPort()); + assertEquals("/d/e", iri.getPath()); + assertEquals("f", iri.getQuery()); + assertEquals("g", iri.getFragment()); + + iri = URL.create("https://a:b@c.org:80/d/e?f#g"); + assertEquals("https", iri.getScheme()); + assertEquals("a:b", iri.getUserInfo()); + assertEquals("c.org", iri.getHost()); + assertEquals(Integer.valueOf(80), iri.getPort()); + assertEquals("/d/e", iri.getPath()); + assertEquals("f", iri.getQuery()); + assertEquals("g", iri.getFragment()); + + iri = URL.create("ftp://a:b@c.org:80/d/e?f#g"); + assertEquals("ftp", iri.getScheme()); + assertEquals("a:b", iri.getUserInfo()); + assertEquals("c.org", iri.getHost()); + assertEquals(Integer.valueOf(80), iri.getPort()); + assertEquals("/d/e", iri.getPath()); + assertEquals("f", iri.getQuery()); + assertEquals("g", iri.getFragment()); + + iri = URL.create("mailto:joe@example.org?subject=foo"); + assertEquals("mailto", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("joe@example.org?subject=foo", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getFragment()); + + iri = URL.create("tag:example.org,2006:foo"); + assertEquals("tag", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("example.org,2006:foo", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getQuery()); + assertEquals(null, iri.getFragment()); + + iri = URL.create("urn:lsid:ibm.com:example:82437234964354895798234d"); + assertEquals("urn", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("lsid:ibm.com:example:82437234964354895798234d", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getQuery()); + assertEquals(null, iri.getFragment()); + + iri = URL.create("data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP"); + assertEquals("data", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("image/gif;base64,R0lGODdhMAAwAPAAAAAAAP", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getQuery()); + assertEquals(null, iri.getFragment()); + + } +} diff --git a/src/test/java/org/xbib/net/PercentDecoderTest.java b/src/test/java/org/xbib/net/PercentDecoderTest.java new file mode 100644 index 0000000..8d97070 --- /dev/null +++ b/src/test/java/org/xbib/net/PercentDecoderTest.java @@ -0,0 +1,138 @@ +package org.xbib.net; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import static java.lang.Character.isHighSurrogate; +import static java.lang.Character.isLowSurrogate; +import static java.lang.Integer.toHexString; + +/** + */ +public class PercentDecoderTest { + + private static final int CODE_POINT_IN_SUPPLEMENTARY = 2; + private static final int CODE_POINT_IN_BMP = 1; + + private PercentDecoder decoder; + + @Before + public void setUp() { + decoder = new PercentDecoder(StandardCharsets.UTF_8.newDecoder()); + } + + @Test + public void testDecodesWithoutPercents() throws Exception { + assertEquals("asdf", decoder.decode("asdf")); + } + + @Test + public void testDecodeSingleByte() throws Exception { + assertEquals("#", decoder.decode("%23")); + } + + @Test + public void testIncompletePercentPairNoNumbers() throws Exception { + try { + decoder.decode("%"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("could not percent decode <%>: incomplete %-pair at position 0", e.getMessage()); + } + } + + @Test + public void testIncompletePercentPairOneNumber() throws Exception { + try { + decoder.decode("%2"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("could not percent decode <%2>: incomplete %-pair at position 0", e.getMessage()); + } + } + + @Test + public void testInvalidHex() throws Exception { + try { + decoder.decode("%xz"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("invalid %-tuple <%xz>", e.getMessage()); + } + } + + @Test + public void testRandomStrings() throws MalformedInputException, UnmappableCharacterException { + PercentEncoder encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); + Random rand = new Random(); + long seed = rand.nextLong(); + rand.setSeed(seed); + char[] charBuf = new char[2]; + List codePoints = new ArrayList<>(); + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + buf.setLength(0); + codePoints.clear(); + randString(buf, codePoints, charBuf, rand, 1 + rand.nextInt(1000)); + byte[] origBytes = buf.toString().getBytes(StandardCharsets.UTF_8); + byte[] decodedBytes = null; + String codePointsHex = String.join("", codePoints.stream().map(Integer::toHexString).collect(Collectors.toList())); + try { + decodedBytes = decoder.decode(encoder.encode(buf.toString())).getBytes(StandardCharsets.UTF_8); + assertEquals("Seed: $seed Code points: $codePointsHex", toHex(origBytes), toHex(decodedBytes)); + } catch (IllegalArgumentException e) { + List charHex = new ArrayList<>(); + for (int j = 0; j < buf.toString().length(); j++) { + charHex.add(toHexString((int) buf.toString().charAt(j))); + } + fail("seed: " + seed + " code points: " + codePointsHex + " chars " + charHex + " " + e.getMessage()); + } + assertEquals(toHex(origBytes), toHex(decodedBytes)); + } + } + + /** + * Generate a random string. + * @param buf buffer to write into + * @param codePoints list of code points to write into + * @param charBuf char buf for temporary char wrangling (size 2) + * @param rand random source + * @param length max string length + */ + private static void randString(StringBuilder buf, List codePoints, char[] charBuf, Random rand, + int length) { + while (buf.length() < length) { + int codePoint = rand.nextInt(17 * 65536); + if (Character.isDefined(codePoint)) { + int res = Character.toChars(codePoint, charBuf, 0); + if (res == CODE_POINT_IN_BMP && (isHighSurrogate(charBuf[0]) || isLowSurrogate(charBuf[0]))) { + continue; + } + buf.append(charBuf[0]); + codePoints.add(codePoint); + if (res == CODE_POINT_IN_SUPPLEMENTARY) { + buf.append(charBuf[1]); + } + } + } + } + + private static List toHex(byte[] bytes) { + List list = new ArrayList<>(); + for (byte b: bytes) { + list.add(Integer.toHexString((int) b & 0xFF)); + } + return list; + } +} diff --git a/src/test/java/org/xbib/net/PercentEncoderTest.java b/src/test/java/org/xbib/net/PercentEncoderTest.java new file mode 100644 index 0000000..445ee69 --- /dev/null +++ b/src/test/java/org/xbib/net/PercentEncoderTest.java @@ -0,0 +1,80 @@ +package org.xbib.net; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.BitSet; + +import static java.nio.charset.CodingErrorAction.REPLACE; + +/** + * + */ +public class PercentEncoderTest { + + private PercentEncoder alnum; + private PercentEncoder alnum16; + + @Before + public void setUp() { + BitSet bs = new BitSet(); + for (int i = 'a'; i <= 'z'; i++) { + bs.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + bs.set(i); + } + for (int i = '0'; i <= '9'; i++) { + bs.set(i); + } + + this.alnum = new PercentEncoder(bs, StandardCharsets.UTF_8.newEncoder().onMalformedInput(REPLACE) + .onUnmappableCharacter(REPLACE)); + this.alnum16 = new PercentEncoder(bs, StandardCharsets.UTF_16BE.newEncoder().onMalformedInput(REPLACE) + .onUnmappableCharacter(REPLACE)); + } + + @Test + public void testDoesntEncodeSafe() throws Exception { + BitSet set = new BitSet(); + for (int i = 'a'; i <= 'z'; i++) { + set.set(i); + } + PercentEncoder pe = new PercentEncoder(set, StandardCharsets.UTF_8.newEncoder().onMalformedInput(REPLACE) + .onUnmappableCharacter(REPLACE)); + assertEquals("abcd%41%42%43%44", pe.encode("abcdABCD")); + } + + @Test + public void testEncodeInBetweenSafe() throws Exception { + assertEquals("abc%20123", alnum.encode("abc 123")); + } + + @Test + public void testSafeInBetweenEncoded() throws Exception { + assertEquals("%20abc%20", alnum.encode(" abc ")); + } + + @Test + public void testEncodeUtf8() throws Exception { + assertEquals("snowman%E2%98%83", alnum.encode("snowman\u2603")); + } + + @Test + public void testEncodeUtf8SurrogatePair() throws Exception { + assertEquals("clef%F0%9D%84%9E", alnum.encode("clef\ud834\udd1e")); + } + + @Test + public void testEncodeUtf16() throws Exception { + assertEquals("snowman%26%03", alnum16.encode("snowman\u2603")); + } + + @Test + public void testUrlEncodedUtf16SurrogatePair() throws Exception { + assertEquals("clef%D8%34%DD%1E", alnum16.encode("clef\ud834\udd1e")); + } +} diff --git a/src/test/java/org/xbib/net/URIComponentTest.java b/src/test/java/org/xbib/net/URIComponentTest.java new file mode 100644 index 0000000..eee5e0e --- /dev/null +++ b/src/test/java/org/xbib/net/URIComponentTest.java @@ -0,0 +1,69 @@ +package org.xbib.net; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; + + +/** + */ +public class URIComponentTest { + + @Test + public void testURI() { + URI uri = URI.create("ftp://user:pass@host:1234/path/to/filename.txt"); + assertEquals("ftp", scheme(uri)); + assertEquals("user", user(uri)); + assertEquals("pass", pass(uri)); + assertEquals("host", host(uri)); + assertEquals(1234, port(uri)); + assertEquals("/path/to/", parent(uri)); + assertEquals("filename.txt", filename(uri)); + } + + @Test + public void testURI2() { + URI uri = URI.create("sftp://user:pass@host:1234/filename.txt"); + assertEquals("sftp", scheme(uri)); + assertEquals("user", user(uri)); + assertEquals("pass", pass(uri)); + assertEquals("host", host(uri)); + assertEquals(1234, port(uri)); + assertEquals("/", parent(uri)); + assertEquals("filename.txt", filename(uri)); + } + + private static String scheme(URI uri) { + return uri.getScheme(); + } + + private static String user(URI uri) { + String auth = uri.getAuthority(); + return auth != null ? auth.split(":")[0] : null; + } + + private static String pass(URI uri) { + String auth = uri.getAuthority(); + return auth != null ? auth.split("@")[0].split(":")[1] : null; + } + + private static String host(URI uri) { + return uri.getHost(); + } + + private static int port(URI uri) { + return uri.getPort(); + } + + private static String parent(URI uri) { + return uri.resolve(".").getPath(); + } + + private static String filename(URI uri) { + String path = uri.getPath(); + int pos = path.lastIndexOf('/'); + return pos >= 0 ? path.substring(pos + 1) : path; + } +} diff --git a/src/test/java/org/xbib/net/URLBuilderTest.java b/src/test/java/org/xbib/net/URLBuilderTest.java new file mode 100644 index 0000000..27dfb35 --- /dev/null +++ b/src/test/java/org/xbib/net/URLBuilderTest.java @@ -0,0 +1,268 @@ +package org.xbib.net; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * + */ +public class URLBuilderTest { + + @Test + public void testNoUrlParts() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com").toUrlString(), "http://foo.com"); + } + + @Test + public void testWithPort() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com").port(33).toUrlString(), "http://foo.com:33"); + } + + @Test + public void testSimplePath() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("seg1") + .pathSegment("seg2") + .toUrlString(), + "http://foo.com/seg1/seg2"); + } + + @Test + public void testPathWithReserved() throws Exception { + // RFC 1738 S3.3 + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("seg/;?ment") + .pathSegment("seg=&2") + .toUrlString(), "http://foo.com/seg%2F%3B%3Fment/seg=&2"); + } + + @Test + public void testPathSegments() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegments("seg1", "seg2", "seg3") + .toUrlString(), "http://foo.com/seg1/seg2/seg3"); + } + + @Test + public void testMatrixWithReserved() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("foo") + .matrixParam("foo", "bar") + .matrixParam("res;=?#/erved", "value") + .pathSegment("baz") + .toUrlString(), "http://foo.com/foo;foo=bar;res%3B%3D%3F%23%2Ferved=value/baz"); + } + + @Test + public void testUrlEncodedPathSegmentUtf8() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("snowman").pathSegment("\u2603") + .toUrlString(), "http://foo.com/snowman/%E2%98%83"); + } + + @Test + public void testUrlEncodedPathSegmentUtf8SurrogatePair() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("clef").pathSegment("\ud834\udd1e") + .toUrlString(), "http://foo.com/clef/%F0%9D%84%9E"); + } + + @Test + public void testQueryParamNoPath() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .toUrlString(), "http://foo.com?foo=bar"); + } + + @Test + public void testQueryParamsDuplicated() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .queryParam("foo", "bar2") + .queryParam("baz", "quux") + .queryParam("baz", "quux2") + .toUrlString(), "http://foo.com?foo=bar&foo=bar2&baz=quux&baz=quux2"); + } + + @Test + public void testEncodeQueryParams() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar&=#baz") + .queryParam("foo", "bar?/2") + .toUrlString(), "http://foo.com?foo=bar%26%3D%23baz&foo=bar?/2"); + } + + @Test + public void testEncodeQueryParamWithSpaceAndPlus() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "spa ce") + .queryParam("fo+o", "plus+") + .toUrlString(), "http://foo.com?foo=spa%20ce&fo%2Bo=plus%2B"); + } + + @Test + public void testPlusInVariousParts() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("has+plus") + .matrixParam("plusMtx", "pl+us") + .queryParam("plusQp", "pl+us") + .fragment("plus+frag") + .toUrlString(), "http://foo.com/has+plus;plusMtx=pl+us?plusQp=pl%2Bus#plus+frag"); + } + + @Test + public void testFragment() throws Exception { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .fragment("#frag/?") + .toUrlString(), "http://foo.com?foo=bar#%23frag/?"); + } + + @Test + public void testAllParts() throws Exception { + assertUrl(URL.https().resolveFromHost("foo.bar.com").port(3333) + .pathSegment("foo") + .pathSegment("bar") + .matrixParam("mtx1", "val1") + .matrixParam("mtx2", "val2") + .queryParam("q1", "v1") + .queryParam("q2", "v2") + .fragment("zomg it's a fragment") + .toUrlString(), + "https://foo.bar.com:3333/foo/bar;mtx1=val1;mtx2=val2?q1=v1&q2=v2#zomg%20it's%20a%20fragment"); + } + + @Test + public void testSlashInHost() throws Exception { + URL.http().resolveFromHost("/").toUrlString(); + } + + @Test + public void testGoogle() throws Exception { + URL url = URL.https().resolveFromHost("google.com").build(); + assertEquals("https://google.com", url.toString()); + } + + @Test + public void testBadIPv4LiteralDoesntChoke() throws Exception { + assertUrl(URL.http().resolveFromHost("300.100.50.1") + .toUrlString(), "http://300.100.50.1"); + } + + @Test + public void testIPv4Literal() throws Exception { + if ("false".equals(System.getProperty("java.net.preferIPv6Addresses"))) { + assertUrl(URL.http().resolveFromHost("127.0.0.1") + .toUrlString(), "http://localhost"); + } else { + assertEquals("http://localhost", URL.http().resolveFromHost("127.0.0.1").toUrlString()); + } + } + + @Test + public void testIPv6LiteralLocalhost() throws Exception { + String s = URL.http().resolveFromHost("[::1]").toUrlString(); + if ("true".equals(System.getProperty("java.net.preferIPv6Addresses"))) { + assertEquals("http://[0:0:0:0:0:0:0:1]", s); + } else { + assertEquals("http://127.0.0.1", s); + } + } + + @Test + public void testIPv6Literal() throws Exception { + if ("true".equals(System.getProperty("java.net.preferIPv6Addresses"))) { + String s = URL.http().resolveFromHost("[2001:db8:85a3::8a2e:370:7334]") + .toUrlString(); + assertEquals("http://[2001:db8:85a3:0:0:8a2e:370:7334]", s); + } + } + + @Test + public void testEncodedRegNameSingleByte() throws Exception { + String s = URL.http().resolveFromHost("host?name;") + .toUrlString(); + assertEquals("http://host%3Fname;", s); + } + + @Test + public void testEncodedRegNameMultiByte() throws Exception { + String s = URL.http().host("snow\u2603man") + .toUrlString(); + assertEquals("http://snow%E2%98%83man", s); + } + + @Test + public void testThreePathSegments() throws Exception { + String s = URL.https().resolveFromHost("foo.com") + .pathSegments("a", "b", "c") + .toUrlString(); + assertEquals("https://foo.com/a/b/c", s); + } + + @Test + public void testThreePathSegmentsWithQueryParams() throws Exception { + String s = URL.https().resolveFromHost("foo.com") + .pathSegments("a", "b", "c") + .queryParam("foo", "bar") + .toUrlString(); + assertEquals("https://foo.com/a/b/c?foo=bar", s); + } + + @Test + public void testIntermingledMatrixParamsAndPathSegments() throws Exception { + String s = URL.http().resolveFromHost("foo.com") + .pathSegments("seg1", "seg2") + .matrixParam("m1", "v1") + .pathSegment("seg3") + .matrixParam("m2", "v2") + .toUrlString(); + assertEquals("http://foo.com/seg1/seg2;m1=v1/seg3;m2=v2", s); + } + + @Test + public void testUseQueryParamAfterQuery() throws Exception { + String s = URL.http().resolveFromHost("foo.com") + .query("q") + .queryParam("foo", "bar") + .toUrlString(); + assertEquals("http://foo.com?foo=bar", s); + } + + @Test + public void testUseQueryAfterQueryParam() throws Exception { + String s = URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .query("q") + .toUrlString(); + assertEquals("http://foo.com?foo=bar", s); + } + + @Test + public void testQueryWithNoSpecialChars() throws Exception { + String s = URL.http().resolveFromHost("foo.com") + .query("q") + .toUrlString(); + assertEquals("http://foo.com?q", s); + } + + @Test + public void testQueryWithOkSpecialChars() throws Exception { + String s = URL.http().resolveFromHost("foo.com") + .query("q?/&=").toUrlString(); + assertEquals("http://foo.com?q?/&=", s); + } + + @Test + public void testQueryWithEscapedSpecialChars() throws Exception { + String s = URL.http().resolveFromHost("foo.com") + .query("q#+").toUrlString(); + assertEquals("http://foo.com?q%23%2B", s); + } + + private void assertUrl(String urlString, String expected) throws Exception { + assertEquals(expected, urlString); + assertEquals(expected, URL.from(urlString).toExternalForm()); + } +} diff --git a/src/test/java/org/xbib/net/URLParserTest.java b/src/test/java/org/xbib/net/URLParserTest.java new file mode 100644 index 0000000..cf42f8d --- /dev/null +++ b/src/test/java/org/xbib/net/URLParserTest.java @@ -0,0 +1,383 @@ +package org.xbib.net; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * + */ +public class URLParserTest { + + @Test + public void testNull() { + assertNull(URL.from(null)); + } + + @Test + public void testEmpty() { + assertNull(URL.from("")); + } + + @Test + public void testNewline() { + assertNull(URL.from("\n")); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidScheme() throws Exception { + URL url = URL.from("/:23"); + } + + @Test + public void testScheme() throws Exception { + URL url = URL.from("http://"); + assertEquals("http://", url.toExternalForm()); + assertEquals("http://", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testPath(){ + URL url = URL.from("http"); + assertFalse(url.isAbsolute()); + assertNull(url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("http", url.getPath()); + assertEquals("http", url.toExternalForm()); + assertEquals("http", url.toString()); + } + + @Test + public void testOpaque() throws Exception { + URL url = URL.from("a:b"); + assertEquals("a", url.getScheme()); + assertEquals("b", url.getSchemeSpecificPart()); + assertEquals("a:b", url.toExternalForm()); + assertEquals("a:b", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testGopher() throws Exception { + URL url = URL.from("gopher:/example.com/"); + assertEquals("gopher:/example.com/", url.toExternalForm()); + } + + @Test + public void testWithoutDoubleSlash() throws Exception { + URL url = URL.from("http:foo.com"); + assertEquals("http:foo.com", url.toExternalForm()); + assertEquals("http:foo.com", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testSlashAfterScheme() throws Exception { + URL url = URL.from("http:/example.com/"); + assertEquals("http:/example.com/", url.toExternalForm()); + } + + @Test + public void testSchemeHost() throws Exception { + URL url = URL.from("http://foo.bar"); + assertEquals("http://foo.bar", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testSchemeHostPort() throws Exception { + URL url = URL.from("http://f:/c"); + assertEquals("http://f:/c", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testNetworkLocation() throws Exception { + URL url = URL.from("//foo.bar"); + assertEquals("//foo.bar", url.toExternalForm()); + assertEquals("//foo.bar", url.toString()); + } + + @Test + public void testSchemeHostAuthInfo() throws Exception { + URL url = URL.from("http://auth@foo.bar"); + assertEquals("http://auth@foo.bar", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testSchemeHostAuthInfoPort() throws Exception { + URL url = URL.from("http://auth@foo.bar:1"); + assertEquals("http://auth@foo.bar:1", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testSchemeHostAuthInfoPortPath() throws Exception { + URL url = URL.from("http://auth@foo.bar:1/path"); + assertEquals("http://auth@foo.bar:1/path", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testTrailingSlash() throws Exception { + URL url = URL.from("http://foo.bar/path/"); + assertEquals("http://foo.bar/path/", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testBackslash() throws Exception { + URL url = URL.from("http://foo.com/\\@"); + assertEquals("http://foo.com/@", url.toExternalForm()); + } + + @Test + public void testQuery() throws Exception { + URL url = URL.from("http://auth@foo.bar:1/path?query"); + assertEquals("http://auth@foo.bar:1/path?query", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testFragment() throws Exception { + URL url = URL.from("http://auth@foo.bar:1/path#fragment"); + assertEquals("http://auth@foo.bar:1/path#fragment", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testReservedChar() throws Exception { + URL url = URL.from("http://www.google.com/ig/calculator?q=1USD=?EUR"); + //assertEquals("http://www.google.com/ig/calculator?q=1USD=?EUR", url.toString()); + if ("false".equals(System.getProperty("java.net.preferIPv6Addresses"))) { + assertEquals("http://www.google.com/ig/calculator?q=1USD%3D?EUR", url.toString()); + } + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testPlus() throws Exception { + URL url = URL.from("http://foobar:8080/test/print?value=%EA%B0%80+%EB%82%98"); + assertEquals("http://foobar:8080/test/print?value=%EA%B0%80%2B%EB%82%98", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testIPv6() throws Exception { + URL url = URL.from("http://[2001:db8:85a3::8a2e:370:7334]"); + assertEquals("http://[2001:db8:85a3:0:0:8a2e:370:7334]", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testIPv6WithScope() throws Exception { + // test scope ID. Must be a valid IPv6 + URL url = URL.from("http://[3002:0:0:0:20c:29ff:fe64:614a%2]:8080/resource"); + assertEquals("http://[3002:0:0:0:20c:29ff:fe64:614a%2]:8080/resource", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testIPv6WithIPv4() throws Exception { + URL url = URL.from("http://[::192.168.1.1]:8080/resource"); + assertEquals("http://[0:0:0:0:0:0:c0a8:101]:8080/resource", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + public void testFromUrlWithEverything() throws Exception { + assertUrlCompatibility("https://foo.bar.com:3333/foo/ba%20r;mtx1=val1;mtx2=val%202/" + + "seg%203;m2=v2?q1=v1&q2=v%202#zomg%20it's%20a%20fragment"); + } + + @Test + public void testFromUrlWithEmptyPath() throws Exception { + assertUrlCompatibility("http://foo.com"); + } + + @Test + public void testFromUrlWithPort() throws Exception { + assertUrlCompatibility("http://foo.com:1234"); + } + + @Test + public void testFromUrlWithEncodedHost() throws Exception { + assertUrlCompatibility("http://f%20oo.com/bar"); + } + + @Test + public void testFromUrlWithEncodedPathSegment() throws Exception { + assertUrlCompatibility("http://foo.com/foo/b%20ar"); + } + + @Test + public void testFromUrlWithEncodedMatrixParam() throws Exception { + assertUrlCompatibility("http://foo.com/foo;m1=v1;m%202=v%202"); + } + + @Test + public void testFromUrlWithEncodedQueryParam() throws Exception { + assertUrlCompatibility("http://foo.com/foo?q%201=v%202&q2=v2"); + } + + @Test + public void testFromUrlWithEncodedQueryParamDelimiter() throws Exception { + assertUrlCompatibility("http://foo.com/foo?q1=%3Dv1&%26q2=v2"); + } + + @Test + public void testFromUrlWithEncodedFragment() throws Exception { + assertUrlCompatibility("http://foo.com/foo#b%20ar"); + } + + @Test + public void testFromUrlWithEmptyPathSegmentWithMatrixParams() throws Exception { + assertUrlCompatibility("http://foo.com/foo/;m1=v1"); + } + + @Test + public void testFromUrlWithEmptyPathWithMatrixParams() throws Exception { + assertUrlCompatibility("http://foo.com/;m1=v1"); + } + + @Test + public void testFromUrlWithEmptyPathWithMultipleMatrixParams() throws Exception { + assertUrlCompatibility("http://foo.com/;m1=v1;m2=v2"); + } + + @Test + public void testFromUrlMalformedQueryParamNoValue() throws Exception { + assertUrlCompatibility("http://foo.com/foo?q1=v1&q2"); + } + + @Test + public void testFromUrlMalformedQueryParamMultiValues() throws Exception { + assertRoundTrip("http://foo.com/foo?q1=v1=v2"); + } + + @Test + public void testFromUrlQueryWithEscapedChars() throws Exception { + assertRoundTrip("http://foo.com/foo?query==&%23"); + } + + @Test + public void testSimple() throws Exception { + URL url = URL.parser().parse("http://foo.com/seg1/seg2"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("/seg1/seg2", url.getPath()); + } + + @Test + public void testReserved() throws Exception { + URL url = URL.parser().parse("http://foo.com/seg%2F%3B%3Fment/seg=&2"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("/seg%2F%3B%3Fment/seg=&2", url.getPath()); + } + + @Test + public void testMatrix() throws Exception { + URL url = URL.parser().parse("http://foo.com/;foo=bar"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("/;foo=bar", url.getPath()); + } + + @Test + public void testAnotherQuery() throws Exception { + URL url = URL.parser().parse("http://foo.com?foo=bar"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("foo=bar", url.getQuery()); + } + + @Test + public void testQueryAndFragment() throws Exception { + URL url = URL.parser().parse("http://foo.com?foo=bar#fragment"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + } + + @Test + public void testRelative() throws Exception { + URL url = URL.parser().parse("/foo/bar?foo=bar#fragment"); + assertNull(url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/foo/bar", url.getPath()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + } + + @Test + public void testRelativeDecoded() throws Exception { + URL url = URL.parser().parse("/foo/bar%2F?foo=b%2Far#frag%2Fment"); + assertNull(url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/foo/bar/", url.getDecodedPath()); + assertEquals("foo=b/ar", url.getDecodedQuery()); + assertEquals("frag/ment", url.getDecodedFragment()); + } + + @Test + public void testFileSchemeSpecificPart() throws Exception { + URL url = URL.parser().parse("file:foo/bar?foo=bar#fragment"); + assertEquals("", url.getHostInfo()); + assertNotNull(url.getSchemeSpecificPart()); + assertEquals("foo/bar?foo=bar#fragment", url.getSchemeSpecificPart()); + } + + @Test + public void testRelativeFilePath() throws Exception { + URL url = URL.parser().parse("file:/foo/bar?foo=bar#fragment"); + assertEquals("file", url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/foo/bar", url.getPath()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + } + + @Test + public void testAbsoluteFilePath() throws Exception { + URL url = URL.parser().parse("file:///foo/bar?foo=bar#fragment"); + assertEquals("file", url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/foo/bar", url.getPath()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + } + + @Test + public void testMoreQuery() throws Exception { + URL url = URL.parser().parse("http://foo.com?foo=bar%26%3D%23baz&foo=bar?/2"); + assertEquals("foo=bar%26%3D%23baz&foo=bar?/2", url.getQuery()); + assertEquals("foo=bar&=#baz&foo=bar?/2", url.getDecodedQuery()); + } + + @Test + public void testAnotherPlus() throws Exception { + URL url = URL.parser().parse("http://foo.com/has+plus;plusMtx=pl+us?plusQp=pl%2Bus#plus+frag"); + assertEquals("/has+plus;plusMtx=pl+us", url.getPath()); + assertEquals("plusQp=pl%2Bus", url.getQuery()); + assertEquals("plus+frag", url.getFragment()); + } + + private void assertUrlCompatibility(String url) throws Exception { + String s = URL.from(url).toExternalForm(); + assertEquals(s, URL.from(s).toExternalForm()); + assertEquals(s, new java.net.URL(url).toExternalForm()); + } + + private void assertRoundTrip(String url) throws Exception { + String s = URL.from(url).toExternalForm(); + assertEquals(s, URL.from(s).toExternalForm()); + } +} diff --git a/src/test/java/org/xbib/net/URLResolverTest.java b/src/test/java/org/xbib/net/URLResolverTest.java new file mode 100644 index 0000000..d9ac245 --- /dev/null +++ b/src/test/java/org/xbib/net/URLResolverTest.java @@ -0,0 +1,75 @@ +package org.xbib.net; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + */ +public class URLResolverTest { + + @Test + public void testResolve() throws Exception { + URL base = URL.create("http://example.org/foo/"); + assertEquals("http://example.org/", base.resolve("/").toString()); + resolve("http://foo.bar", "foobar", "http://foo.bar/foobar"); + resolve("http://foo.bar/", "foobar", "http://foo.bar/foobar"); + resolve("http://foo.bar/foobar", "foobar", "http://foo.bar/foobar"); + } + + @Test + public void testFielding() throws Exception { + // http://www.ics.uci.edu/~fielding/url/test1.html + resolve("http://a/b/c/d;p?q", "g:h", "g:h"); + resolve("http://a/b/c/d;p?q", "g", "http://a/b/c/g"); + resolve("http://a/b/c/d;p?q", "./g", "http://a/b/c/g"); + resolve("http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"); + resolve("http://a/b/c/d;p?q", "/g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "//g", "http://g"); + resolve("http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"); + resolve("http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"); + resolve("http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"); + resolve("http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"); + resolve("http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"); + resolve("http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"); + resolve("http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"); + resolve("http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"); + resolve("http://a/b/c/d;p?q", ".", "http://a/b/c/"); + resolve("http://a/b/c/d;p?q", "./", "http://a/b/c/"); + resolve("http://a/b/c/d;p?q", "..", "http://a/b/"); + resolve("http://a/b/c/d;p?q", "../", "http://a/b/"); + resolve("http://a/b/c/d;p?q", "../g", "http://a/b/g"); + resolve("http://a/b/c/d;p?q", "../..", "http://a/"); + resolve("http://a/b/c/d;p?q", "../../", "http://a/"); + resolve("http://a/b/c/d;p?q", "../../g", "http://a/g"); + // abnormal cases + resolve("http://a/b/c/d;p?q", "../../../g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "../../../../g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "/./g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "/../g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "g.", "http://a/b/c/g."); + resolve("http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"); + resolve("http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."); + resolve("http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"); + // less likely + resolve("http://a/b/c/d;p?q", "./../g", "http://a/b/g"); + resolve("http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"); + resolve("http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"); + resolve("http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"); + resolve("http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"); + resolve("http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"); + // query component + resolve("http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"); + resolve("http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"); + // fragment component + resolve("http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"); + resolve("http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"); + // scheme + resolve("http://a/b/c/d;p?q", "http:g", "http:g"); + resolve("http://a/b/c/d;p?q", "http:", "http:"); + } + + private void resolve(String inputBase, String relative, String expected) { + assertEquals(expected, URL.base(inputBase).resolve(relative).toExternalForm()); + } +} diff --git a/src/test/java/org/xbib/net/URLTest.java b/src/test/java/org/xbib/net/URLTest.java new file mode 100644 index 0000000..87a3ccb --- /dev/null +++ b/src/test/java/org/xbib/net/URLTest.java @@ -0,0 +1,120 @@ +package org.xbib.net; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +/** + */ +public class URLTest { + + @Test + public void test() throws Exception { + List tests = readTests(fromResource("/urltestdata.json")); + for (JsonTest test : tests) { + String base = test.base; + String input = test.input; + System.err.println("testing: " + base + " " + input + " " + test.failure); + if (test.skip) { + continue; + } + if (test.failure) { + try { + URL url = URL.base(base).resolve(input); + System.err.println("resolved: " + url.toString()); + fail(); + } catch (Exception e) { + // pass + } + } else { + if (base != null && input != null) { + URL url = URL.base(base).resolve(input); + if (url != null) { + System.err.println("resolved: " + url.toString()); + if (test.protocol != null) { + assertEquals(test.protocol, url.getScheme() + ":"); + } + if (test.hostname != null) { + assertEquals(test.hostname, url.getHost()); + } + if (test.port != null && !test.port.isEmpty() && url.getPort() != null) { + assertTrue(Integer.parseInt(test.port) == url.getPort()); + } + // TODO(jprante) + //if (test.pathname != null && !test.pathname.isEmpty() && url.getPath() != null) { + // assertEquals(test.pathname, url.getPath()); + //} + System.err.println("passed: " + base + " " + input); + } else { + System.err.println("unable to resolve: " + base + " " + input); + } + } + } + } + } + + private JsonNode fromResource(String path) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ObjectReader reader = mapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true) + .readerFor(JsonNode.class); + return reader.readValue(getClass().getResourceAsStream(path)); + } + + private List readTests(JsonNode jsonNode) { + List list = new ArrayList<>(); + for (JsonNode n : jsonNode) { + if (n.isObject()) { + JsonTest jsontest = new JsonTest(); + jsontest.input = get(n, "input"); + jsontest.base = get(n, "base"); + jsontest.href = get(n, "href"); + jsontest.origin = get(n, "origin"); + jsontest.protocol = get(n, "protocol"); + jsontest.username = get(n, "username"); + jsontest.password = get(n, "password"); + jsontest.host = get(n, "host"); + jsontest.hostname = get(n, "hostname"); + jsontest.port = get(n, "port"); + jsontest.pathname = get(n, "pathname"); + jsontest.search = get(n, "search"); + jsontest.hash = get(n, "hash"); + jsontest.failure = n.has("failure"); + jsontest.skip = n.has("skip"); + list.add(jsontest); + } + } + return list; + } + + private String get(JsonNode n, String key) { + return n.has(key) ? n.get(key).textValue() : null; + } + + static class JsonTest { + String input; + String base; + String href; + String origin; + String protocol; + String username; + String password; + String host; + String hostname; + String port; + String pathname; + String search; + String hash; + boolean failure; + boolean skip; + } + +} diff --git a/src/test/java/org/xbib/net/package-info.java b/src/test/java/org/xbib/net/package-info.java new file mode 100644 index 0000000..9103c39 --- /dev/null +++ b/src/test/java/org/xbib/net/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing URL building und parsing. + */ +package org.xbib.net; diff --git a/src/test/java/org/xbib/net/path/PathDecoderTest.java b/src/test/java/org/xbib/net/path/PathDecoderTest.java new file mode 100644 index 0000000..b6082ba --- /dev/null +++ b/src/test/java/org/xbib/net/path/PathDecoderTest.java @@ -0,0 +1,40 @@ +package org.xbib.net.path; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; + +/** + */ +public class PathDecoderTest { + + @Test + public void testPlusSign() throws Exception { + PathDecoder decoder = new PathDecoder("/path?a=b+c", "d=e+f", StandardCharsets.UTF_8); + assertEquals("[b c]", decoder.params().get("a").toString()); + assertEquals("[e f]", decoder.params().get("d").toString()); + } + + @Test + public void testSlash() throws Exception { + PathDecoder decoder = new PathDecoder("path/foo/bar/?a=b+c", "d=e+f", StandardCharsets.UTF_8); + assertEquals("[b c]", decoder.params().get("a").toString()); + assertEquals("[e f]", decoder.params().get("d").toString()); + } + + @Test + public void testDoubleSlashes() throws Exception { + PathDecoder decoder = new PathDecoder("//path", "", StandardCharsets.UTF_8); + assertEquals("/path", decoder.path()); + } + + @Test + public void testSlashes() throws Exception { + PathDecoder decoder = new PathDecoder("//path?a=b+c", "d=e+f", StandardCharsets.UTF_8); + assertEquals("/path", decoder.path()); + assertEquals("[b c]", decoder.params().get("a").toString()); + assertEquals("[e f]", decoder.params().get("d").toString()); + } +} diff --git a/src/test/java/org/xbib/net/path/PathMatcherTest.java b/src/test/java/org/xbib/net/path/PathMatcherTest.java new file mode 100644 index 0000000..dfb4325 --- /dev/null +++ b/src/test/java/org/xbib/net/path/PathMatcherTest.java @@ -0,0 +1,578 @@ +package org.xbib.net.path; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.xbib.net.QueryParameters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + */ +public class PathMatcherTest { + + private PathMatcher pathMatcher = new PathMatcher(); + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void match() { + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match("/test", "/test")); + assertFalse(pathMatcher.match("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.match("test", "/test")); + assertFalse(pathMatcher.match("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("test/*", "test/Test")); + assertTrue(pathMatcher.match("test/*", "test/t")); + assertTrue(pathMatcher.match("test/*", "test/")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*.*", "test.")); + assertTrue(pathMatcher.match("*.*", "test.test")); + assertTrue(pathMatcher.match("*.*", "test.test.test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "tsttest")); + assertFalse(pathMatcher.match("test*", "test/")); + assertFalse(pathMatcher.match("test*", "test/t")); + assertFalse(pathMatcher.match("test/*", "test")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*.*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.match("/?", "/a")); + assertTrue(pathMatcher.match("/?/a", "/a/a")); + assertTrue(pathMatcher.match("/a/?", "/a/b")); + assertTrue(pathMatcher.match("/??/a", "/aa/a")); + assertTrue(pathMatcher.match("/a/??", "/a/bb")); + assertTrue(pathMatcher.match("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.match("/**", "/testing/testing")); + assertTrue(pathMatcher.match("/*/**", "/testing/testing")); + assertTrue(pathMatcher.match("/**/*", "/testing/testing")); + assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.match("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.match("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.match("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.match("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.match("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.match("/????", "/bala/bla")); + assertFalse(pathMatcher.match("/**/*bla", "/bla/bla/bla/bbb")); + + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.match("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + assertFalse(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertFalse(pathMatcher.match("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.match("", "")); + + assertTrue(pathMatcher.match("/{bla}.*", "/testing.html")); + } + + @Test + public void withMatchStart() { + // test exact matching + assertTrue(pathMatcher.matchStart("test", "test")); + assertTrue(pathMatcher.matchStart("/test", "/test")); + assertFalse(pathMatcher.matchStart("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.matchStart("test", "/test")); + assertFalse(pathMatcher.matchStart("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.matchStart("t?st", "test")); + assertTrue(pathMatcher.matchStart("??st", "test")); + assertTrue(pathMatcher.matchStart("tes?", "test")); + assertTrue(pathMatcher.matchStart("te??", "test")); + assertTrue(pathMatcher.matchStart("?es?", "test")); + assertFalse(pathMatcher.matchStart("tes?", "tes")); + assertFalse(pathMatcher.matchStart("tes?", "testt")); + assertFalse(pathMatcher.matchStart("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.matchStart("*", "test")); + assertTrue(pathMatcher.matchStart("test*", "test")); + assertTrue(pathMatcher.matchStart("test*", "testTest")); + assertTrue(pathMatcher.matchStart("test/*", "test/Test")); + assertTrue(pathMatcher.matchStart("test/*", "test/t")); + assertTrue(pathMatcher.matchStart("test/*", "test/")); + assertTrue(pathMatcher.matchStart("*test*", "AnothertestTest")); + assertTrue(pathMatcher.matchStart("*test", "Anothertest")); + assertTrue(pathMatcher.matchStart("*.*", "test.")); + assertTrue(pathMatcher.matchStart("*.*", "test.test")); + assertTrue(pathMatcher.matchStart("*.*", "test.test.test")); + assertTrue(pathMatcher.matchStart("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.matchStart("test*", "tst")); + assertFalse(pathMatcher.matchStart("test*", "test/")); + assertFalse(pathMatcher.matchStart("test*", "tsttest")); + assertFalse(pathMatcher.matchStart("test*", "test/")); + assertFalse(pathMatcher.matchStart("test*", "test/t")); + assertTrue(pathMatcher.matchStart("test/*", "test")); + assertTrue(pathMatcher.matchStart("test/t*.txt", "test")); + assertFalse(pathMatcher.matchStart("*test*", "tsttst")); + assertFalse(pathMatcher.matchStart("*test", "tsttst")); + assertFalse(pathMatcher.matchStart("*.*", "tsttst")); + assertFalse(pathMatcher.matchStart("test*aaa", "test")); + assertFalse(pathMatcher.matchStart("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.matchStart("/?", "/a")); + assertTrue(pathMatcher.matchStart("/?/a", "/a/a")); + assertTrue(pathMatcher.matchStart("/a/?", "/a/b")); + assertTrue(pathMatcher.matchStart("/??/a", "/aa/a")); + assertTrue(pathMatcher.matchStart("/a/??", "/a/bb")); + assertTrue(pathMatcher.matchStart("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.matchStart("/**", "/testing/testing")); + assertTrue(pathMatcher.matchStart("/*/**", "/testing/testing")); + assertTrue(pathMatcher.matchStart("/**/*", "/testing/testing")); + assertTrue(pathMatcher.matchStart("test*/**", "test/")); + assertTrue(pathMatcher.matchStart("test*/**", "test/t")); + assertTrue(pathMatcher.matchStart("/bla/**/bla", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.matchStart("/bla/**/bla", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.matchStart("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.matchStart("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.matchStart("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.matchStart("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.matchStart("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.matchStart("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.matchStart("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.matchStart("/????", "/bala/bla")); + assertTrue(pathMatcher.matchStart("/**/*bla", "/bla/bla/bla/bbb")); + + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + assertTrue(pathMatcher.matchStart("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.matchStart("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.matchStart("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.matchStart("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertTrue(pathMatcher.matchStart("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.matchStart("", "")); + } + + @Test + public void uniqueDeliminator() { + pathMatcher.setPathSeparator("."); + + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match(".test", ".test")); + assertFalse(pathMatcher.match(".test/jpg", "test/jpg")); + assertFalse(pathMatcher.match("test", ".test")); + assertFalse(pathMatcher.match(".test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*/*", "test/")); + assertTrue(pathMatcher.match("*/*", "test/test")); + assertTrue(pathMatcher.match("*/*", "test/test/test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "tsttest")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*/*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and .'s + assertTrue(pathMatcher.match(".?", ".a")); + assertTrue(pathMatcher.match(".?.a", ".a.a")); + assertTrue(pathMatcher.match(".a.?", ".a.b")); + assertTrue(pathMatcher.match(".??.a", ".aa.a")); + assertTrue(pathMatcher.match(".a.??", ".a.bb")); + assertTrue(pathMatcher.match(".?", ".a")); + + // test matching with **'s + assertTrue(pathMatcher.match(".**", ".testing.testing")); + assertTrue(pathMatcher.match(".*.**", ".testing.testing")); + assertTrue(pathMatcher.match(".**.*", ".testing.testing")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla.bla")); + assertTrue(pathMatcher.match(".**.test", ".bla.bla.test")); + assertTrue(pathMatcher.match(".bla.**.**.bla", ".bla.bla.bla.bla.bla.bla")); + assertTrue(pathMatcher.match(".bla*bla.test", ".blaXXXbla.test")); + assertTrue(pathMatcher.match(".*bla.test", ".XXXbla.test")); + assertFalse(pathMatcher.match(".bla*bla.test", ".blaXXXbl.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXblab.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXbl.test")); + } + + @Test + public void extractPathWithinPattern() throws Exception { + assertEquals("", + pathMatcher.extractPathWithinPattern("/docs/commit.html", "/docs/commit.html")); + + assertEquals("cvs/commit", + pathMatcher.extractPathWithinPattern("/docs/*", "/docs/cvs/commit")); + assertEquals("commit.html", + pathMatcher.extractPathWithinPattern("/docs/cvs/*.html", "/docs/cvs/commit.html")); + assertEquals("cvs/commit", + pathMatcher.extractPathWithinPattern("/docs/**", "/docs/cvs/commit")); + assertEquals("cvs/commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/*.html", "/docs/cvs/commit.html")); + assertEquals("commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/*.html", "/docs/commit.html")); + assertEquals("commit.html", + pathMatcher.extractPathWithinPattern("/*.html", "/commit.html")); + assertEquals("docs/commit.html", + pathMatcher.extractPathWithinPattern("/*.html", "/docs/commit.html")); + assertEquals("/commit.html", + pathMatcher.extractPathWithinPattern("*.html", "/commit.html")); + assertEquals("/docs/commit.html", + pathMatcher.extractPathWithinPattern("*.html", "/docs/commit.html")); + assertEquals("/docs/commit.html", + pathMatcher.extractPathWithinPattern("**/*.*", "/docs/commit.html")); + assertEquals("/docs/commit.html", + pathMatcher.extractPathWithinPattern("*", "/docs/commit.html")); + assertEquals("/docs/cvs/other/commit.html", + pathMatcher.extractPathWithinPattern("**/commit.html", "/docs/cvs/other/commit.html")); + assertEquals("cvs/other/commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/commit.html", "/docs/cvs/other/commit.html")); + assertEquals("cvs/other/commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/**/**/**", "/docs/cvs/other/commit.html")); + + assertEquals("docs/cvs/commit", + pathMatcher.extractPathWithinPattern("/d?cs/*", "/docs/cvs/commit")); + assertEquals("cvs/commit.html", + pathMatcher.extractPathWithinPattern("/docs/c?s/*.html", "/docs/cvs/commit.html")); + assertEquals("docs/cvs/commit", + pathMatcher.extractPathWithinPattern("/d?cs/**", "/docs/cvs/commit")); + assertEquals("docs/cvs/commit.html", + pathMatcher.extractPathWithinPattern("/d?cs/**/*.html", "/docs/cvs/commit.html")); + } + + @Test + public void extractUriTemplateVariables() throws Exception { + QueryParameters result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}", "/hotels/1"); + assertEquals("[hotel:1]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/h?tels/{hotel}", "/hotels/1"); + assertEquals("[hotel:1]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}/bookings/{booking}", "/hotels/1/bookings/2"); + assertEquals("[hotel:1, booking:2]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/**/hotels/**/{hotel}", "/foo/hotels/bar/1"); + assertEquals("[hotel:1]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/{page}.html", "/42.html"); + assertEquals("[page:42]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/{page}.*", "/42.html"); + assertEquals("[page:42]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/A-{B}-C", "/A-b-C"); + assertEquals("[B:b]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/{name}.{extension}", "/test.html"); + assertEquals("[name:test, extension:html]", result.toString()); + } + + @Test + public void extractUriTemplateVariablesRegex() { + QueryParameters result = pathMatcher + .extractUriTemplateVariables("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar", + "com.example-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName").get(0)); + assertEquals("1.0.0", result.get("version").get(0)); + + result = pathMatcher.extractUriTemplateVariables("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar", + "com.example-sources-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName").get(0)); + assertEquals("1.0.0", result.get("version").get(0)); + } + + @Test + public void extractUriTemplateVarsRegexQualifiers() { + QueryParameters result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar", + "com.example-sources-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName").get(0)); + assertEquals("1.0.0", result.get("version").get(0)); + result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar", + "com.example-sources-1.0.0-20100220.jar"); + assertEquals("com.example", result.get("symbolicName").get(0)); + assertEquals("1.0.0", result.get("version").get(0)); + assertEquals("2010", result.get("year").get(0)); + assertEquals("02", result.get("month").get(0)); + assertEquals("20", result.get("day").get(0)); + result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar", + "com.example-sources-1.0.0.{12}.jar"); + assertEquals("com.example", result.get("symbolicName").get(0)); + assertEquals("1.0.0.{12}", result.get("version").get(0)); + } + + @Test + public void extractUriTemplateVarsRegexCapturingGroups() { + exception.expect(IllegalArgumentException.class); + //exception.expectMessage(containsString("The number of capturing groups in the pattern")) + pathMatcher.extractUriTemplateVariables("/web/{id:foo(bar)?}", "/web/foobar"); + } + + @Test + public void combine() { + assertEquals("", pathMatcher.combine(null, null)); + assertEquals("/hotels", pathMatcher.combine("/hotels", null)); + assertEquals("/hotels", pathMatcher.combine(null, "/hotels")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "/booking")); + assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "booking")); + assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "/booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "/booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/", "booking")); + assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels/*", "{hotel}")); + assertEquals("/hotels/**/{hotel}", pathMatcher.combine("/hotels/**", "{hotel}")); + assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels", "{hotel}")); + assertEquals("/hotels/{hotel}.*", pathMatcher.combine("/hotels", "{hotel}.*")); + assertEquals("/hotels/*/booking/{booking}", pathMatcher.combine("/hotels/*/booking", "{booking}")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.html")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.*")); + assertEquals("/*.html", pathMatcher.combine("/**", "/*.html")); + assertEquals("/*.html", pathMatcher.combine("/*", "/*.html")); + assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html")); + assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); + assertEquals("/user/user", pathMatcher.combine("/user", "/user")); + assertEquals("/{foo:.*[^0-9].*}/edit/", pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); + assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); + assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); + assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); + } + + @Test + public void combineWithTwoFileExtensionPatterns() { + exception.expect(IllegalArgumentException.class); + pathMatcher.combine("/*.html", "/*.txt"); + } + + @Test + public void patternComparator() { + Comparator comparator = pathMatcher.getPatternComparator("/hotels/new"); + + assertEquals(0, comparator.compare(null, null)); + assertEquals(1, comparator.compare(null, "/hotels/new")); + assertEquals(-1, comparator.compare("/hotels/new", null)); + + assertEquals(0, comparator.compare("/hotels/new", "/hotels/new")); + + assertEquals(-1, comparator.compare("/hotels/new", "/hotels/*")); + assertEquals(1, comparator.compare("/hotels/*", "/hotels/new")); + assertEquals(0, comparator.compare("/hotels/*", "/hotels/*")); + + assertEquals(-1, comparator.compare("/hotels/new", "/hotels/{hotel}")); + assertEquals(1, comparator.compare("/hotels/{hotel}", "/hotels/new")); + assertEquals(0, comparator.compare("/hotels/{hotel}", "/hotels/{hotel}")); + assertEquals(-1, comparator.compare("/hotels/{hotel}/booking", "/hotels/{hotel}/bookings/{booking}")); + assertEquals(1, comparator.compare("/hotels/{hotel}/bookings/{booking}", "/hotels/{hotel}/booking")); + + assertEquals(-1, comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**")); + assertEquals(1, comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")); + assertEquals(0, comparator.compare("/**", "/**")); + + assertEquals(-1, comparator.compare("/hotels/{hotel}", "/hotels/*")); + assertEquals(1, comparator.compare("/hotels/*", "/hotels/{hotel}")); + + assertEquals(-1, comparator.compare("/hotels/*", "/hotels/*/**")); + assertEquals(1, comparator.compare("/hotels/*/**", "/hotels/*")); + + assertEquals(-1, comparator.compare("/hotels/new", "/hotels/new.*")); + assertEquals(2, comparator.compare("/hotels/{hotel}", "/hotels/{hotel}.*")); + + assertEquals(-1, comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/hotels/**")); + assertEquals(1, comparator.compare("/hotels/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")); + assertEquals(1, comparator.compare("/hotels/foo/bar/**", "/hotels/{hotel}")); + assertEquals(-1, comparator.compare("/hotels/{hotel}", "/hotels/foo/bar/**")); + assertEquals(2, comparator.compare("/hotels/**/bookings/**", "/hotels/**")); + assertEquals(-2, comparator.compare("/hotels/**", "/hotels/**/bookings/**")); + + assertEquals(1, comparator.compare("/**", "/hotels/{hotel}")); + + assertEquals(1, comparator.compare("/hotels", "/hotels2")); + + assertEquals(-1, comparator.compare("*", "*/**")); + assertEquals(1, comparator.compare("*/**", "*")); + } + + @Test + public void patternComparatorSort() { + Comparator comparator = pathMatcher.getPatternComparator("/hotels/new"); + List paths = new ArrayList(3); + paths.add(null); + paths.add("/hotels/new"); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0)); + assertNull(paths.get(1)); + paths.clear(); + + paths.add("/hotels/new"); + paths.add(null); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0)); + assertNull(paths.get(1)); + paths.clear(); + + paths.add("/hotels/*"); + paths.add("/hotels/new"); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0)); + assertEquals("/hotels/*", paths.get(1)); + paths.clear(); + + paths.add("/hotels/new"); + paths.add("/hotels/*"); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0)); + assertEquals("/hotels/*", paths.get(1)); + paths.clear(); + + paths.add("/hotels/**"); + paths.add("/hotels/*"); + paths.sort(comparator); + assertEquals("/hotels/*", paths.get(0)); + assertEquals("/hotels/**", paths.get(1)); + paths.clear(); + + paths.add("/hotels/*"); + paths.add("/hotels/**"); + paths.sort(comparator); + assertEquals("/hotels/*", paths.get(0)); + assertEquals("/hotels/**", paths.get(1)); + paths.clear(); + + paths.add("/hotels/{hotel}"); + paths.add("/hotels/new"); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0)); + assertEquals("/hotels/{hotel}", paths.get(1)); + paths.clear(); + + paths.add("/hotels/new"); + paths.add("/hotels/{hotel}"); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0)); + assertEquals("/hotels/{hotel}", paths.get(1)); + paths.clear(); + + paths.add("/hotels/*"); + paths.add("/hotels/{hotel}"); + paths.add("/hotels/new"); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0)); + assertEquals("/hotels/{hotel}", paths.get(1)); + assertEquals("/hotels/*", paths.get(2)); + paths.clear(); + + paths.add("/hotels/ne*"); + paths.add("/hotels/n*"); + Collections.shuffle(paths); + paths.sort(comparator); + assertEquals("/hotels/ne*", paths.get(0)); + assertEquals("/hotels/n*", paths.get(1)); + paths.clear(); + + comparator = pathMatcher.getPatternComparator("/hotels/new.html"); + paths.add("/hotels/new.*"); + paths.add("/hotels/{hotel}"); + Collections.shuffle(paths); + paths.sort(comparator); + assertEquals("/hotels/new.*", paths.get(0)); + assertEquals("/hotels/{hotel}", paths.get(1)); + paths.clear(); + + comparator = pathMatcher.getPatternComparator("/web/endUser/action/login.html"); + paths.add("/**/login.*"); + paths.add("/**/endUser/action/login.*"); + paths.sort(comparator); + assertEquals("/**/endUser/action/login.*", paths.get(0)); + assertEquals("/**/login.*", paths.get(1)); + paths.clear(); + } + + @Test + public void trimTokensOff() { + pathMatcher.setTrimTokens(false); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/group/sales/members")); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/group/ sales/members")); + assertFalse(pathMatcher.match("/group/{groupName}/members", "/Group/ Sales/Members")); + } + + @Test + public void caseInsensitive() { + pathMatcher.setCaseSensitive(false); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/group/sales/members")); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/Group/Sales/Members")); + assertTrue(pathMatcher.match("/Group/{groupName}/Members", "/group/Sales/members")); + } + + @Test + public void cachePatternsSetToFalse() { + pathMatcher.setCachePatterns(false); + match(); + assertTrue(pathMatcher.stringMatcherCache().isEmpty()); + } + + @Test + public void extensionMappingWithDotPathSeparator() { + pathMatcher.setPathSeparator("."); + assertEquals("Extension mapping should be disabled with \".\" as path separator", + "/*.html.hotel.*", pathMatcher.combine("/*.html", "hotel.*")); + } +} + diff --git a/src/test/java/org/xbib/net/path/PathNormalizerTest.java b/src/test/java/org/xbib/net/path/PathNormalizerTest.java new file mode 100644 index 0000000..185dc2e --- /dev/null +++ b/src/test/java/org/xbib/net/path/PathNormalizerTest.java @@ -0,0 +1,73 @@ +package org.xbib.net.path; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * + */ +public class PathNormalizerTest { + + @Test + public void normalizeNullPath() { + assertEquals("/", PathNormalizer.normalize(null)); + } + + @Test + public void normalizeEmptyPath() { + assertEquals("/", PathNormalizer.normalize("")); + } + + @Test + public void normalizeSlashPath() { + assertEquals("/", PathNormalizer.normalize("/")); + } + + @Test + public void normalizeDoubleSlashPath() { + assertEquals("/", PathNormalizer.normalize("//")); + } + + @Test + public void normalizeTripleSlashPath() { + assertEquals("/", PathNormalizer.normalize("///")); + } + + @Test + public void normalizePathWithPoint() { + assertEquals("/", PathNormalizer.normalize("/.")); + } + + @Test + public void normalizePathWithPointAndElement() { + assertEquals("/a", PathNormalizer.normalize("/./a")); + } + + @Test + public void normalizePathWithTwoPointsAndElement() { + assertEquals("/a", PathNormalizer.normalize("/././a")); + } + + @Test + public void normalizePathWithDoublePoint() { + assertEquals("/", PathNormalizer.normalize("/..")); + assertEquals("/", PathNormalizer.normalize("/../..")); + assertEquals("/", PathNormalizer.normalize("/../../..")); + } + + @Test + public void normalizePathWithFirstElementAndDoublePoint() { + assertEquals("/", PathNormalizer.normalize("/a/..")); + assertEquals("/", PathNormalizer.normalize("/a/../..")); + assertEquals("/", PathNormalizer.normalize("/a/../../..")); + } + + @Test + public void normalizePathWithTwoElementsAndDoublePoint() { + assertEquals("/b", PathNormalizer.normalize("/a/../b")); + assertEquals("/b", PathNormalizer.normalize("/a/../../b")); + assertEquals("/b", PathNormalizer.normalize("/a/../../../b")); + } + +} diff --git a/src/test/java/org/xbib/net/path/package-info.java b/src/test/java/org/xbib/net/path/package-info.java new file mode 100644 index 0000000..daac6e6 --- /dev/null +++ b/src/test/java/org/xbib/net/path/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing URL paths. + */ +package org.xbib.net.path; diff --git a/src/test/java/org/xbib/net/template/URITemplateTest.java b/src/test/java/org/xbib/net/template/URITemplateTest.java new file mode 100644 index 0000000..82bee8f --- /dev/null +++ b/src/test/java/org/xbib/net/template/URITemplateTest.java @@ -0,0 +1,412 @@ +package org.xbib.net.template; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import org.junit.Test; +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.expression.TemplateExpression; +import org.xbib.net.template.expression.URITemplateExpression; +import org.xbib.net.template.parse.ExpressionParser; +import org.xbib.net.template.parse.URITemplateParser; +import org.xbib.net.template.parse.VariableSpecParser; +import org.xbib.net.template.vars.Variables; +import org.xbib.net.template.vars.specs.ExplodedVariable; +import org.xbib.net.template.vars.specs.PrefixVariable; +import org.xbib.net.template.vars.specs.SimpleVariable; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.specs.VariableSpecType; +import org.xbib.net.template.vars.values.ListValue; +import org.xbib.net.template.vars.values.MapValue; +import org.xbib.net.template.vars.values.NullValue; +import org.xbib.net.template.vars.values.ScalarValue; +import org.xbib.net.template.vars.values.VariableValue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + */ +public class URITemplateTest { + + @Test + public void simpleTest() { + String[] strings = new String[]{ + "foo", "%33foo", "foo%20", "foo_%20bar", "FoOb%02ZAZE287", "foo.bar", "foo_%20bar.baz%af.r" + }; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpec varspec = VariableSpecParser.parse(buffer); + assertEquals(varspec.getName(), s); + assertSame(varspec.getType(), VariableSpecType.SIMPLE); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + public void invalidTest() { + String[] strings = new String[]{"", "%", "foo..bar", ".", "foo%ra", "foo%ar"}; + for (String s : strings) { + try { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpecParser.parse(buffer); + fail("No exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + public void literalTest() { + Variables vars = Variables.builder().build(); + String[] strings = new String[]{"foo", "%23foo", "%23foo%24", "foo%24", "f%c4oo", "http://slashdot.org", + "x?y=e", "urn:d:ze:/oize#/e/e", "ftp://ftp.foo.com/ee/z?a=b#e/dz", + "http://z.t/hello%20world"}; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + List list = URITemplateParser.parse(buffer); + assertEquals(list.get(0).expand(vars), s); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + public void parsingEmptyInputGivesEmptyList() { + CharBuffer buffer = CharBuffer.wrap("").asReadOnlyBuffer(); + List list = URITemplateParser.parse(buffer); + assertTrue(list.isEmpty()); + assertFalse(buffer.hasRemaining()); + } + + @Test + @SuppressWarnings("unchecked") + public void parseExpressions() { + List list = new ArrayList<>(); + String input; + ExpressionType type; + List varspecs; + + input = "{foo}"; + type = ExpressionType.SIMPLE; + varspecs = Collections.singletonList(new SimpleVariable("foo")); + list.add(new Object[]{input, type, varspecs}); + + input = "{foo,bar}"; + type = ExpressionType.SIMPLE; + varspecs = Arrays.asList(new SimpleVariable("foo"), new SimpleVariable("bar")); + list.add(new Object[]{input, type, varspecs}); + + input = "{+foo}"; + type = ExpressionType.RESERVED; + varspecs = Collections.singletonList(new SimpleVariable("foo")); + list.add(new Object[]{input, type, varspecs}); + + input = "{.foo:10,bar*}"; + type = ExpressionType.NAME_LABELS; + varspecs = Arrays.asList(new PrefixVariable("foo", 10), new ExplodedVariable("bar")); + list.add(new Object[]{input, type, varspecs}); + + for (Object[] o : list) { + CharBuffer buffer = CharBuffer.wrap((CharSequence) o[0]).asReadOnlyBuffer(); + URITemplateExpression actual = new ExpressionParser().parse(buffer); + assertFalse(buffer.hasRemaining()); + URITemplateExpression expected = new TemplateExpression((ExpressionType) o[1], (List) o[2]); + assertEquals(actual, expected); + } + } + + @Test + public void parseInvalidExpressions() { + try { + CharBuffer buffer = CharBuffer.wrap("{foo").asReadOnlyBuffer(); + new ExpressionParser().parse(buffer); + fail("No exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + try { + CharBuffer buffer = CharBuffer.wrap("{foo#bar}").asReadOnlyBuffer(); + new ExpressionParser().parse(buffer); + fail("No exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + + @Test + public void parsePrefixes() { + String[] strings = new String[]{"foo:323", "%33foo:323", "foo%20:323", "foo_%20bar:323", "FoOb%02ZAZE287:323", + "foo.bar:323", "foo_%20bar.baz%af.r:323"}; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpec varspec = VariableSpecParser.parse(buffer); + assertEquals(varspec.getName(), s.substring(0, s.indexOf(':'))); + assertSame(varspec.getType(), VariableSpecType.PREFIX); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + public void parseInvalidPrefixes() { + String[] strings = new String[]{"foo:", "foo:-1", "foo:a", "foo:10001", "foo:2147483648"}; + for (String s : strings) { + try { + VariableSpecParser.parse(CharBuffer.wrap(s).asReadOnlyBuffer()); + fail("No exception thrown!!"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + public void parseExploded() { + String[] strings = new String[]{"foo*", "%33foo*", "foo%20*", "foo_%20bar*", "FoOb%02ZAZE287*", "foo.bar*", + "foo_%20bar.baz%af.r*"}; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpec varspec = VariableSpecParser.parse(buffer); + assertEquals(varspec.getName(), s.substring(0, s.length() - 1)); + assertSame(varspec.getType(), VariableSpecType.EXPLODED); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + public void parseExceptions() { + String[] strings = new String[]{"foo%", "foo%r", "foo%ra", "foo%ar", "foo<", "foo{"}; + for (String s : strings) { + try { + URITemplateParser.parse(s); + fail("No exception thrown!!"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + public void testExamples() throws Exception { + JsonNode data = fromResource("/spec-examples.json"); + List> list = new ArrayList<>(); + for (JsonNode node : data) { + Variables.Builder builder = Variables.builder(); + Iterator> it = node.get("variables").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + for (JsonNode n : node.get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + m.put("resultNode", n.get(1)); + list.add(m); + } + } + for (Map e : list) { + URITemplate template = new URITemplate((String) e.get("tmpl")); + String actual = template.toString((Variables) e.get("vars")); + JsonNode resultNode = (JsonNode) e.get("resultNode"); + if (resultNode.isTextual()) { + assertEquals(resultNode.textValue(), actual); + } else { + if (!resultNode.isArray()) { + throw new IllegalArgumentException("didn't expect that"); + } + boolean found = false; + for (JsonNode node : resultNode) { + if (node.textValue().equals(actual)) { + found = true; + } + } + assertTrue(found); + } + } + } + + @Test + public void testExamplesBySection() throws Exception { + JsonNode data = fromResource("/spec-examples-by-section.json"); + List> list = new ArrayList<>(); + for (JsonNode node : data) { + Variables.Builder builder = Variables.builder(); + Iterator> it = node.get("variables").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + for (JsonNode n : node.get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + m.put("resultNode", n.get(1)); + list.add(m); + } + } + for (Map e : list) { + URITemplate template = new URITemplate((String) e.get("tmpl")); + String actual = template.toString((Variables) e.get("vars")); + JsonNode resultNode = (JsonNode) e.get("resultNode"); + if (resultNode.isTextual()) { + assertEquals(resultNode.textValue(), actual); + } else { + if (!resultNode.isArray()) { + throw new IllegalArgumentException("didn't expect that"); + } + boolean found = false; + for (JsonNode node : resultNode) { + if (node.textValue().equals(actual)) { + found = true; + } + } + assertTrue(found); + } + } + } + + @Test + public void extendedTests() throws Exception { + JsonNode data = fromResource("/extended-tests.json"); + List> list = new ArrayList<>(); + for (JsonNode node : data) { + Variables.Builder builder = Variables.builder(); + Iterator> it = node.get("variables").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + for (JsonNode n : node.get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + m.put("resultNode", n.get(1)); + list.add(m); + } + } + for (Map e : list) { + URITemplate template = new URITemplate((String) e.get("tmpl")); + String actual = template.toString((Variables) e.get("vars")); + JsonNode resultNode = (JsonNode) e.get("resultNode"); + if (resultNode.isTextual()) { + assertEquals(resultNode.textValue(), actual); + } else { + if (!resultNode.isArray()) { + throw new IllegalArgumentException("didn't expect that"); + } + boolean found = false; + for (JsonNode node : resultNode) { + if (node.textValue().equals(actual)) { + found = true; + } + } + assertTrue(found); + } + } + } + + @Test + public void negativeTests() throws Exception { + JsonNode data = fromResource("/negative-tests.json"); + JsonNode node = data.get("Failure Tests").get("variables"); + Variables.Builder builder = Variables.builder(); + Iterator> it = node.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + List> list = new ArrayList<>(); + for (JsonNode n : data.get("Failure Tests").get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + list.add(m); + } + for (Map e : list) { + try { + new URITemplate((String) e.get("tmpl")).toString((Variables) e.get("vars")); + fail("no exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + public void expansionTest() throws Exception { + String[] strings = new String[]{"/rfcExamples.json", "/strings.json", "/multipleStrings.json", + "/lists.json", "/multipleLists.json"}; + for (String s : strings) { + JsonNode data = fromResource(s); + Variables.Builder builder = Variables.builder(); + Iterator> it = data.get("vars").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (!entry.getValue().isNull()) { + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + } + List> list = new ArrayList<>(); + it = data.get("tests").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + Map m = new HashMap<>(); + m.put("tmpl", entry.getKey()); + m.put("vars", builder.build()); + m.put("expected", entry.getValue().textValue()); + list.add(m); + } + for (Map e : list) { + String actual = new URITemplate((String) e.get("tmpl")).toString((Variables) e.get("vars")); + assertEquals(e.get("expected"), actual); + } + } + } + + private JsonNode fromResource(String path) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ObjectReader reader = mapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true) + .readerFor(JsonNode.class); + return reader.readValue(getClass().getResourceAsStream(path)); + } + + private static VariableValue fromJson(JsonNode node) { + if (node.isTextual()) { + return new ScalarValue(node.textValue()); + } + if (node.isArray()) { + ListValue.Builder builder = ListValue.builder(); + for (JsonNode n : node) { + builder.add(n.textValue()); + } + return builder.build(); + } + if (node.isObject()) { + MapValue.Builder builder = MapValue.builder(); + Iterator> it = node.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.put(entry.getKey(), entry.getValue().textValue()); + } + return builder.build(); + } + if (node.isNull()) { + return new NullValue(); + } + throw new IllegalArgumentException("cannot bind JSON to variable value: " + node + " class " + node.getClass()); + } +} diff --git a/src/test/java/org/xbib/net/template/package-info.java b/src/test/java/org/xbib/net/template/package-info.java new file mode 100644 index 0000000..eb5045e --- /dev/null +++ b/src/test/java/org/xbib/net/template/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing URL templates. + */ +package org.xbib.net.template; diff --git a/src/test/resources/extended-tests.json b/src/test/resources/extended-tests.json new file mode 100644 index 0000000..dcd71bb --- /dev/null +++ b/src/test/resources/extended-tests.json @@ -0,0 +1,119 @@ +{ + "Additional Examples 1":{ + "level":4, + "variables":{ + "id" : "person", + "token" : "12345", + "fields" : ["id", "name", "picture"], + "format" : "json", + "q" : "URI Templates", + "page" : "5", + "lang" : "en", + "geocode" : ["37.76","-122.427"], + "first_name" : "John", + "last.name" : "Doe", + "Some%20Thing" : "foo", + "number" : "6", + "long" : "37.76", + "lat" : "-122.427", + "group_id" : "12345", + "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }", + "uri" : "http://example.org/?uri=http%3A%2F%2Fexample.org%2F", + "word" : "drücken", + "Stra%C3%9Fe" : "Grüner Weg", + "random" : "šö䟜ñꀣ¥‡ÑÒÓÔÕÖ×ØÙÚàáâãäåæçÿ", + "assoc_special_chars" : + { "šö䟜ñꀣ¥‡ÑÒÓÔÕ" : "Ö×ØÙÚàáâãäåæçÿ" } + }, + "testcases":[ + + [ "{/id*}" , "/person" ], + [ "{/id*}{?fields,first_name,last.name,token}" , [ + "/person?fields=id,name,picture&first_name=John&last.name=Doe&token=12345", + "/person?fields=id,picture,name&first_name=John&last.name=Doe&token=12345", + "/person?fields=picture,name,id&first_name=John&last.name=Doe&token=12345", + "/person?fields=picture,id,name&first_name=John&last.name=Doe&token=12345", + "/person?fields=name,picture,id&first_name=John&last.name=Doe&token=12345", + "/person?fields=name,id,picture&first_name=John&last.name=Doe&token=12345"] + ], + ["/search.{format}{?q,geocode,lang,locale,page,result_type}", + [ "/search.json?q=URI%20Templates&geocode=37.76,-122.427&lang=en&page=5", + "/search.json?q=URI%20Templates&geocode=-122.427,37.76&lang=en&page=5"] + ], + ["/test{/Some%20Thing}", "/test/foo" ], + ["/set{?number}", "/set?number=6"], + ["/loc{?long,lat}" , "/loc?long=37.76&lat=-122.427"], + ["/base{/group_id,first_name}/pages{/page,lang}{?format,q}","/base/12345/John/pages/5/en?format=json&q=URI%20Templates"], + ["/sparql{?query}", "/sparql?query=PREFIX%20dc%3A%20%3Chttp%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%3E%20SELECT%20%3Fbook%20%3Fwho%20WHERE%20%7B%20%3Fbook%20dc%3Acreator%20%3Fwho%20%7D"], + ["/go{?uri}", "/go?uri=http%3A%2F%2Fexample.org%2F%3Furi%3Dhttp%253A%252F%252Fexample.org%252F"], + ["/service{?word}", "/service?word=dr%C3%BCcken"], + ["/lookup{?Stra%C3%9Fe}", "/lookup?Stra%C3%9Fe=Gr%C3%BCner%20Weg"], + ["{random}" , "%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"], + ["{?assoc_special_chars*}", "?%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95=%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"] + ] + }, + "Additional Examples 2":{ + "level":4, + "variables":{ + "id" : ["person","albums"], + "token" : "12345", + "fields" : ["id", "name", "picture"], + "format" : "atom", + "q" : "URI Templates", + "page" : "10", + "start" : "5", + "lang" : "en", + "geocode" : ["37.76","-122.427"] + }, + "testcases":[ + + [ "{/id*}" , ["/person/albums","/albums/person"] ], + [ "{/id*}{?fields,token}" , [ + "/person/albums?fields=id,name,picture&token=12345", + "/person/albums?fields=id,picture,name&token=12345", + "/person/albums?fields=picture,name,id&token=12345", + "/person/albums?fields=picture,id,name&token=12345", + "/person/albums?fields=name,picture,id&token=12345", + "/person/albums?fields=name,id,picture&token=12345", + "/albums/person?fields=id,name,picture&token=12345", + "/albums/person?fields=id,picture,name&token=12345", + "/albums/person?fields=picture,name,id&token=12345", + "/albums/person?fields=picture,id,name&token=12345", + "/albums/person?fields=name,picture,id&token=12345", + "/albums/person?fields=name,id,picture&token=12345"] + ] + ] + }, + "Additional Examples 3: Empty Variables":{ + "disabled": true, + "variables" : { + "empty_list" : [], + "empty_assoc" : {} + }, + "testcases":[ + [ "{/empty_list}", [ "" ] ], + [ "{/empty_list*}", [ "" ] ], + [ "{?empty_list}", [ "?empty_list="] ], + [ "{?empty_list*}", [ "" ] ], + [ "{?empty_assoc}", [ "?empty_assoc=" ] ], + [ "{?empty_assoc*}", [ "" ] ] + ] + }, + "Additional Examples 4: Numeric Keys":{ + "variables" : { + "42" : "The Answer to the Ultimate Question of Life, the Universe, and Everything", + "1337" : ["leet", "as","it", "can","be"], + "german" : { + "11": "elf", + "12": "zwölf" + } + }, + "testcases":[ + [ "{42}", "The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], + [ "{?42}", "?42=The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], + [ "{1337}", "leet,as,it,can,be"], + [ "{?1337*}", "?1337=leet&1337=as&1337=it&1337=can&1337=be"], + [ "{?german*}", [ "?11=elf&12=zw%C3%B6lf", "?12=zw%C3%B6lf&11=elf"] ] + ] + } +} diff --git a/src/test/resources/lists.json b/src/test/resources/lists.json new file mode 100644 index 0000000..503bbe4 --- /dev/null +++ b/src/test/resources/lists.json @@ -0,0 +1,74 @@ +{ + "vars": { + "list1": [ "one", "two", "three" ], + "list2": [ "Hello", "World!" ], + "list3": [ "one", "", "three" ], + "empty": [] + }, + "tests": { + "{list1}": "one,two,three", + "{+list1}": "one,two,three", + "{.list1}": ".one,two,three", + "{/list1}": "/one,two,three", + "{;list1}": ";list1=one,two,three", + "{?list1}": "?list1=one,two,three", + "{&list1}": "&list1=one,two,three", + "{#list1}": "#one,two,three", + "{list2}": "Hello,World%21", + "{+list2}": "Hello,World!", + "{.list2}": ".Hello,World%21", + "{/list2}": "/Hello,World%21", + "{;list2}": ";list2=Hello,World%21", + "{?list2}": "?list2=Hello,World%21", + "{&list2}": "&list2=Hello,World%21", + "{#list2}": "#Hello,World!", + "{list3}": "one,,three", + "{+list3}": "one,,three", + "{.list3}": ".one,,three", + "{/list3}": "/one,,three", + "{;list3}": ";list3=one,,three", + "{?list3}": "?list3=one,,three", + "{&list3}": "&list3=one,,three", + "{#list3}": "#one,,three", + "{empty}": "", + "{+empty}": "", + "{.empty}": "", + "{/empty}": "", + "{;empty}": ";empty", + "{?empty}": "?empty=", + "{&empty}": "&empty=", + "{#empty}": "", + "{list1*}": "one,two,three", + "{+list1*}": "one,two,three", + "{.list1*}": ".one.two.three", + "{/list1*}": "/one/two/three", + "{;list1*}": ";list1=one;list1=two;list1=three", + "{?list1*}": "?list1=one&list1=two&list1=three", + "{&list1*}": "&list1=one&list1=two&list1=three", + "{#list1*}": "#one,two,three", + "{list2*}": "Hello,World%21", + "{+list2*}": "Hello,World!", + "{.list2*}": ".Hello.World%21", + "{/list2*}": "/Hello/World%21", + "{;list2*}": ";list2=Hello;list2=World%21", + "{?list2*}": "?list2=Hello&list2=World%21", + "{&list2*}": "&list2=Hello&list2=World%21", + "{#list2*}": "#Hello,World!", + "{list3*}": "one,,three", + "{+list3*}": "one,,three", + "{.list3*}": ".one..three", + "{/list3*}": "/one//three", + "{;list3*}": ";list3=one;list3;list3=three", + "{?list3*}": "?list3=one&list3=&list3=three", + "{&list3*}": "&list3=one&list3=&list3=three", + "{#list3*}": "#one,,three", + "{empty*}": "", + "{+empty*}": "", + "{.empty*}": "", + "{/empty*}": "", + "{;empty*}": "", + "{?empty*}": "", + "{&empty*}": "", + "{#empty*}": "" + } +} \ No newline at end of file diff --git a/src/test/resources/multipleLists.json b/src/test/resources/multipleLists.json new file mode 100644 index 0000000..8c4ec21 --- /dev/null +++ b/src/test/resources/multipleLists.json @@ -0,0 +1,41 @@ +{ + "vars": { + "list1": [ "one", "two", "three" ], + "list2": [ "Hello", "World!" ], + "empty": [] + }, + "tests": { + "{list1,list2}": "one,two,three,Hello,World%21", + "{+list1,list2}": "one,two,three,Hello,World!", + "{.list1,list2}": ".one,two,three.Hello,World%21", + "{/list1,list2}": "/one,two,three/Hello,World%21", + "{;list1,list2}": ";list1=one,two,three;list2=Hello,World%21", + "{?list1,list2}": "?list1=one,two,three&list2=Hello,World%21", + "{&list1,list2}": "&list1=one,two,three&list2=Hello,World%21", + "{#list1,list2}": "#one,two,three,Hello,World!", + "{list1*,list2}": "one,two,three,Hello,World%21", + "{+list1*,list2}": "one,two,three,Hello,World!", + "{.list1*,list2}": ".one.two.three.Hello,World%21", + "{/list1*,list2}": "/one/two/three/Hello,World%21", + "{;list1*,list2}": ";list1=one;list1=two;list1=three;list2=Hello,World%21", + "{?list1*,list2}": "?list1=one&list1=two&list1=three&list2=Hello,World%21", + "{&list1*,list2}": "&list1=one&list1=two&list1=three&list2=Hello,World%21", + "{#list1*,list2}": "#one,two,three,Hello,World!", + "{list1,empty}": "one,two,three", + "{+list1,empty}": "one,two,three", + "{.list1,empty}": ".one,two,three", + "{/list1,empty}": "/one,two,three", + "{;list1,empty}": ";list1=one,two,three;empty", + "{?list1,empty}": "?list1=one,two,three&empty=", + "{&list1,empty}": "&list1=one,two,three&empty=", + "{#list1,empty}": "#one,two,three", + "{list1,empty*}": "one,two,three", + "{+list1,empty*}": "one,two,three", + "{.list1,empty*}": ".one,two,three", + "{/list1,empty*}": "/one,two,three", + "{;list1,empty*}": ";list1=one,two,three", + "{?list1,empty*}": "?list1=one,two,three", + "{&list1,empty*}": "&list1=one,two,three", + "{#list1,empty*}": "#one,two,three" + } +} \ No newline at end of file diff --git a/src/test/resources/multipleStrings.json b/src/test/resources/multipleStrings.json new file mode 100644 index 0000000..9add677 --- /dev/null +++ b/src/test/resources/multipleStrings.json @@ -0,0 +1,65 @@ +{ + "vars": { + "var": "value", + "hello": "Hello World!", + "empty": "" + }, + "tests": { + "{var,undef}": "value", + "{+var,undef}": "value", + "{.var,undef}": ".value", + "{/var,undef}": "/value", + "{;var,undef}": ";var=value", + "{?var,undef}": "?var=value", + "{&var,undef}": "&var=value", + "{#var,undef}": "#value", + "{var,empty}": "value,", + "{+var,empty}": "value,", + "{.var,empty}": ".value.", + "{/var,empty}": "/value/", + "{;var,empty}": ";var=value;empty", + "{?var,empty}": "?var=value&empty=", + "{&var,empty}": "&var=value&empty=", + "{#var,empty}": "#value,", + "{var,hello}": "value,Hello%20World%21", + "{+var,hello}": "value,Hello%20World!", + "{.var,hello}": ".value.Hello%20World%21", + "{/var,hello}": "/value/Hello%20World%21", + "{;var,hello}": ";var=value;hello=Hello%20World%21", + "{?var,hello}": "?var=value&hello=Hello%20World%21", + "{&var,hello}": "&var=value&hello=Hello%20World%21", + "{#var,hello}": "#value,Hello%20World!", + "{var,undef,empty}": "value,", + "{+var,undef,empty}": "value,", + "{.var,undef,empty}": ".value.", + "{/var,undef,empty}": "/value/", + "{;var,undef,empty}": ";var=value;empty", + "{?var,undef,empty}": "?var=value&empty=", + "{&var,undef,empty}": "&var=value&empty=", + "{#var,undef,empty}": "#value,", + "{var,undef,hello}": "value,Hello%20World%21", + "{+var,undef,hello}": "value,Hello%20World!", + "{.var,undef,hello}": ".value.Hello%20World%21", + "{/var,undef,hello}": "/value/Hello%20World%21", + "{;var,undef,hello}": ";var=value;hello=Hello%20World%21", + "{?var,undef,hello}": "?var=value&hello=Hello%20World%21", + "{&var,undef,hello}": "&var=value&hello=Hello%20World%21", + "{#var,undef,hello}": "#value,Hello%20World!", + "{var,empty,undef}": "value,", + "{+var,empty,undef}": "value,", + "{.var,empty,undef}": ".value.", + "{/var,empty,undef}": "/value/", + "{;var,empty,undef}": ";var=value;empty", + "{?var,empty,undef}": "?var=value&empty=", + "{&var,empty,undef}": "&var=value&empty=", + "{#var,empty,undef}": "#value,", + "{var,hello,undef}": "value,Hello%20World%21", + "{+var,hello,undef}": "value,Hello%20World!", + "{.var,hello,undef}": ".value.Hello%20World%21", + "{/var,hello,undef}": "/value/Hello%20World%21", + "{;var,hello,undef}": ";var=value;hello=Hello%20World%21", + "{?var,hello,undef}": "?var=value&hello=Hello%20World%21", + "{&var,hello,undef}": "&var=value&hello=Hello%20World%21", + "{#var,hello,undef}": "#value,Hello%20World!" + } +} \ No newline at end of file diff --git a/src/test/resources/negative-tests.json b/src/test/resources/negative-tests.json new file mode 100644 index 0000000..e302640 --- /dev/null +++ b/src/test/resources/negative-tests.json @@ -0,0 +1,49 @@ +{ + "Failure Tests":{ + "level":4, + "variables":{ + "id" : "thing", + "var" : "value", + "hello" : "Hello World!", + "empty" : "", + "path" : "/foo/bar", + "x" : "1024", + "y" : "768", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "example" : "red", + "searchTerms" : "uri templates", + "~thing" : "some-user", + "default-graph-uri" : ["http://www.example/book/","http://www.example/papers/"], + "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }" + + }, + "testcases":[ + [ "{/id*", false ], + [ "/id*}", false ], + [ "{/?id}", false ], + [ "{var:prefix}", false ], + [ "{hello:2*}", false ] , + [ "{??hello}", false ] , + [ "{!hello}", false ] , + [ "{=path}", false ] , + [ "{$var}", false ], + [ "{|var*}", false ], + [ "{*keys?}", false ], + [ "{?empty=default,var}", false ], + [ "{var}{-prefix|/-/|var}" , false ], + [ "?q={searchTerms}&c={example:color?}" , false ], + [ "x{?empty|foo=none}" , false ], + [ "/h{#hello+}" , false ], + [ "/h#{hello+}" , false ], + [ "{keys:1}", false ], + [ "{+keys:1}", false ], + [ "{;keys:1*}", false ], + [ "?{-join|&|var,list}" , false ], + [ "/people/{~thing}", false], + [ "/sparql{?query){&default-graph-uri*}", false ], + [ "/resolution{?x, y}" , false ] + + ] + } +} \ No newline at end of file diff --git a/src/test/resources/rfcExamples.json b/src/test/resources/rfcExamples.json new file mode 100644 index 0000000..1a13d5c --- /dev/null +++ b/src/test/resources/rfcExamples.json @@ -0,0 +1,80 @@ +{ + "vars": { + "var": "value", + "hello": "Hello World!", + "path": "/foo/bar", + "empty": "", + "x": "1024", + "y": "768", + "list": [ "red", "green", "blue" ], + "keys": { "semi": ";", "dot": ".", "comma": "," } + }, + "tests": { + "{var}": "value", + "{hello}": "Hello%20World%21", + "{+var}": "value", + "{+hello}": "Hello%20World!", + "{+path}/here": "/foo/bar/here", + "here?ref={+path}": "here?ref=/foo/bar", + "X{#var}": "X#value", + "X{#hello}": "X#Hello%20World!", + "map?{x,y}": "map?1024,768", + "{x,hello,y}": "1024,Hello%20World%21,768", + "{+x,hello,y}": "1024,Hello%20World!,768", + "{+path,x}/here": "/foo/bar,1024/here", + "{#x,hello,y}": "#1024,Hello%20World!,768", + "{#path,x}/here": "#/foo/bar,1024/here", + "X{.var}": "X.value", + "X{.x,y}": "X.1024.768", + "{/var}": "/value", + "{/var,x}/here": "/value/1024/here", + "{;x,y}": ";x=1024;y=768", + "{;x,y,empty}": ";x=1024;y=768;empty", + "{?x,y}": "?x=1024&y=768", + "{?x,y,empty}": "?x=1024&y=768&empty=", + "?fixed=yes{&x}": "?fixed=yes&x=1024", + "{&x,y,empty}": "&x=1024&y=768&empty=", + "{var:3}": "val", + "{var:30}": "value", + "{list}": "red,green,blue", + "{list*}": "red,green,blue", + "{keys}": "semi,%3B,dot,.,comma,%2C", + "{keys*}": "semi=%3B,dot=.,comma=%2C", + "{+path:6}/here": "/foo/b/here", + "{+list}": "red,green,blue", + "{+list*}": "red,green,blue", + "{+keys}": "semi,;,dot,.,comma,,", + "{+keys*}": "semi=;,dot=.,comma=,", + "{#path:6}/here": "#/foo/b/here", + "{#list}": "#red,green,blue", + "{#list*}": "#red,green,blue", + "{#keys}": "#semi,;,dot,.,comma,,", + "{#keys*}": "#semi=;,dot=.,comma=,", + "X{.var:3}": "X.val", + "X{.list}": "X.red,green,blue", + "X{.list*}": "X.red.green.blue", + "X{.keys}": "X.semi,%3B,dot,.,comma,%2C", + "X{.keys*}": "X.semi=%3B.dot=..comma=%2C", + "{/var:1,var}": "/v/value", + "{/list}": "/red,green,blue", + "{/list*}": "/red/green/blue", + "{/list*,path:4}": "/red/green/blue/%2Ffoo", + "{/keys}": "/semi,%3B,dot,.,comma,%2C", + "{/keys*}": "/semi=%3B/dot=./comma=%2C", + "{;hello:5}": ";hello=Hello", + "{;list}": ";list=red,green,blue", + "{;list*}": ";list=red;list=green;list=blue", + "{;keys}": ";keys=semi,%3B,dot,.,comma,%2C", + "{;keys*}": ";semi=%3B;dot=.;comma=%2C", + "{?var:3}": "?var=val", + "{?list}": "?list=red,green,blue", + "{?list*}": "?list=red&list=green&list=blue", + "{?keys}": "?keys=semi,%3B,dot,.,comma,%2C", + "{?keys*}": "?semi=%3B&dot=.&comma=%2C", + "{&var:3}": "&var=val", + "{&list}": "&list=red,green,blue", + "{&list*}": "&list=red&list=green&list=blue", + "{&keys}": "&keys=semi,%3B,dot,.,comma,%2C", + "{&keys*}": "&semi=%3B&dot=.&comma=%2C" + } +} \ No newline at end of file diff --git a/src/test/resources/spec-examples-by-section.json b/src/test/resources/spec-examples-by-section.json new file mode 100644 index 0000000..cd54d9a --- /dev/null +++ b/src/test/resources/spec-examples-by-section.json @@ -0,0 +1,437 @@ +{ + "3.2.1 Variable Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{count}", "one,two,three"], + ["{count*}", "one,two,three"], + ["{/count}", "/one,two,three"], + ["{/count*}", "/one/two/three"], + ["{;count}", ";count=one,two,three"], + ["{;count*}", ";count=one;count=two;count=three"], + ["{?count}", "?count=one,two,three"], + ["{?count*}", "?count=one&count=two&count=three"], + ["{&count*}", "&count=one&count=two&count=three"] + ] + }, + "3.2.2 Simple String Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"], + ["{half}", "50%25"], + ["O{empty}X", "OX"], + ["O{undef}X", "OX"], + ["{x,y}", "1024,768"], + ["{x,hello,y}", "1024,Hello%20World%21,768"], + ["?{x,empty}", "?1024,"], + ["?{x,undef}", "?1024"], + ["?{undef,y}", "?768"], + ["{var:3}", "val"], + ["{var:30}", "value"], + ["{list}", "red,green,blue"], + ["{list*}", "red,green,blue"], + ["{keys}", [ + "comma,%2C,dot,.,semi,%3B", + "comma,%2C,semi,%3B,dot,.", + "dot,.,comma,%2C,semi,%3B", + "dot,.,semi,%3B,comma,%2C", + "semi,%3B,comma,%2C,dot,.", + "semi,%3B,dot,.,comma,%2C" + ]], + ["{keys*}", [ + "comma=%2C,dot=.,semi=%3B", + "comma=%2C,semi=%3B,dot=.", + "dot=.,comma=%2C,semi=%3B", + "dot=.,semi=%3B,comma=%2C", + "semi=%3B,comma=%2C,dot=.", + "semi=%3B,dot=.,comma=%2C" + ]] + ] + }, + "3.2.3 Reserved Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{+var}", "value"], + ["{+hello}", "Hello%20World!"], + ["{+half}", "50%25"], + ["{base}index", "http%3A%2F%2Fexample.com%2Fhome%2Findex"], + ["{+base}index", "http://example.com/home/index"], + ["O{+empty}X", "OX"], + ["O{+undef}X", "OX"], + ["{+path}/here", "/foo/bar/here"], + ["{+path:6}/here", "/foo/b/here"], + ["here?ref={+path}", "here?ref=/foo/bar"], + ["up{+path}{var}/here", "up/foo/barvalue/here"], + ["{+x,hello,y}", "1024,Hello%20World!,768"], + ["{+path,x}/here", "/foo/bar,1024/here"], + ["{+list}", "red,green,blue"], + ["{+list*}", "red,green,blue"], + ["{+keys}", [ + "comma,,,dot,.,semi,;", + "comma,,,semi,;,dot,.", + "dot,.,comma,,,semi,;", + "dot,.,semi,;,comma,,", + "semi,;,comma,,,dot,.", + "semi,;,dot,.,comma,," + ]], + ["{+keys*}", [ + "comma=,,dot=.,semi=;", + "comma=,,semi=;,dot=.", + "dot=.,comma=,,semi=;", + "dot=.,semi=;,comma=,", + "semi=;,comma=,,dot=.", + "semi=;,dot=.,comma=," + ]] + ] + }, + "3.2.4 Fragment Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{#var}", "#value"], + ["{#hello}", "#Hello%20World!"], + ["{#half}", "#50%25"], + ["foo{#empty}", "foo#"], + ["foo{#undef}", "foo"], + ["{#x,hello,y}", "#1024,Hello%20World!,768"], + ["{#path,x}/here", "#/foo/bar,1024/here"], + ["{#path:6}/here", "#/foo/b/here"], + ["{#list}", "#red,green,blue"], + ["{#list*}", "#red,green,blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]] + ] + }, + "3.2.5 Label Expansion with Dot-Prefix" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{.who}", ".fred"], + ["{.who,who}", ".fred.fred"], + ["{.half,who}", ".50%25.fred"], + ["www{.dom*}", "www.example.com"], + ["X{.var}", "X.value"], + ["X{.var:3}", "X.val"], + ["X{.empty}", "X."], + ["X{.undef}", "X"], + ["X{.list}", "X.red,green,blue"], + ["X{.list*}", "X.red.green.blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]], + ["{#keys*}", [ + "#comma=,,dot=.,semi=;", + "#comma=,,semi=;,dot=.", + "#dot=.,comma=,,semi=;", + "#dot=.,semi=;,comma=,", + "#semi=;,comma=,,dot=.", + "#semi=;,dot=.,comma=," + ]], + ["X{.empty_keys}", "X"], + ["X{.empty_keys*}", "X"] + ] + }, + "3.2.6 Path Segment Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{/who}", "/fred"], + ["{/who,who}", "/fred/fred"], + ["{/half,who}", "/50%25/fred"], + ["{/who,dub}", "/fred/me%2Ftoo"], + ["{/var}", "/value"], + ["{/var,empty}", "/value/"], + ["{/var,undef}", "/value"], + ["{/var,x}/here", "/value/1024/here"], + ["{/var:1,var}", "/v/value"], + ["{/list}", "/red,green,blue"], + ["{/list*}", "/red/green/blue"], + ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], + ["{/keys}", [ + "/comma,%2C,dot,.,semi,%3B", + "/comma,%2C,semi,%3B,dot,.", + "/dot,.,comma,%2C,semi,%3B", + "/dot,.,semi,%3B,comma,%2C", + "/semi,%3B,comma,%2C,dot,.", + "/semi,%3B,dot,.,comma,%2C" + ]], + ["{/keys*}", [ + "/comma=%2C/dot=./semi=%3B", + "/comma=%2C/semi=%3B/dot=.", + "/dot=./comma=%2C/semi=%3B", + "/dot=./semi=%3B/comma=%2C", + "/semi=%3B/comma=%2C/dot=.", + "/semi=%3B/dot=./comma=%2C" + ]] + ] + }, + "3.2.7 Path-Style Parameter Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{;who}", ";who=fred"], + ["{;half}", ";half=50%25"], + ["{;empty}", ";empty"], + ["{;hello:5}", ";hello=Hello"], + ["{;v,empty,who}", ";v=6;empty;who=fred"], + ["{;v,bar,who}", ";v=6;who=fred"], + ["{;x,y}", ";x=1024;y=768"], + ["{;x,y,empty}", ";x=1024;y=768;empty"], + ["{;x,y,undef}", ";x=1024;y=768"], + ["{;list}", ";list=red,green,blue"], + ["{;list*}", ";list=red;list=green;list=blue"], + ["{;keys}", [ + ";keys=comma,%2C,dot,.,semi,%3B", + ";keys=comma,%2C,semi,%3B,dot,.", + ";keys=dot,.,comma,%2C,semi,%3B", + ";keys=dot,.,semi,%3B,comma,%2C", + ";keys=semi,%3B,comma,%2C,dot,.", + ";keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{;keys*}", [ + ";comma=%2C;dot=.;semi=%3B", + ";comma=%2C;semi=%3B;dot=.", + ";dot=.;comma=%2C;semi=%3B", + ";dot=.;semi=%3B;comma=%2C", + ";semi=%3B;comma=%2C;dot=.", + ";semi=%3B;dot=.;comma=%2C" + ]] + ] + }, + "3.2.8 Form-Style Query Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{?who}", "?who=fred"], + ["{?half}", "?half=50%25"], + ["{?x,y}", "?x=1024&y=768"], + ["{?x,y,empty}", "?x=1024&y=768&empty="], + ["{?x,y,undef}", "?x=1024&y=768"], + ["{?var:3}", "?var=val"], + ["{?list}", "?list=red,green,blue"], + ["{?list*}", "?list=red&list=green&list=blue"], + ["{?keys}", [ + "?keys=comma,%2C,dot,.,semi,%3B", + "?keys=comma,%2C,semi,%3B,dot,.", + "?keys=dot,.,comma,%2C,semi,%3B", + "?keys=dot,.,semi,%3B,comma,%2C", + "?keys=semi,%3B,comma,%2C,dot,.", + "?keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{?keys*}", [ + "?comma=%2C&dot=.&semi=%3B", + "?comma=%2C&semi=%3B&dot=.", + "?dot=.&comma=%2C&semi=%3B", + "?dot=.&semi=%3B&comma=%2C", + "?semi=%3B&comma=%2C&dot=.", + "?semi=%3B&dot=.&comma=%2C" + ]] + ] + }, + "3.2.9 Form-Style Query Continuation" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{&who}", "&who=fred"], + ["{&half}", "&half=50%25"], + ["?fixed=yes{&x}", "?fixed=yes&x=1024"], + ["{&var:3}", "&var=val"], + ["{&x,y,empty}", "&x=1024&y=768&empty="], + ["{&x,y,undef}", "&x=1024&y=768"], + ["{&list}", "&list=red,green,blue"], + ["{&list*}", "&list=red&list=green&list=blue"], + ["{&keys}", [ + "&keys=comma,%2C,dot,.,semi,%3B", + "&keys=comma,%2C,semi,%3B,dot,.", + "&keys=dot,.,comma,%2C,semi,%3B", + "&keys=dot,.,semi,%3B,comma,%2C", + "&keys=semi,%3B,comma,%2C,dot,.", + "&keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{&keys*}", [ + "&comma=%2C&dot=.&semi=%3B", + "&comma=%2C&semi=%3B&dot=.", + "&dot=.&comma=%2C&semi=%3B", + "&dot=.&semi=%3B&comma=%2C", + "&semi=%3B&comma=%2C&dot=.", + "&semi=%3B&dot=.&comma=%2C" + ]] + ] + } +} diff --git a/src/test/resources/spec-examples.json b/src/test/resources/spec-examples.json new file mode 100644 index 0000000..07437de --- /dev/null +++ b/src/test/resources/spec-examples.json @@ -0,0 +1,218 @@ +{ + "Level 1 Examples" : + { + "level": 1, + "variables": { + "var" : "value", + "hello" : "Hello World!" + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"] + ] + }, + "Level 2 Examples" : + { + "level": 2, + "variables": { + "var" : "value", + "hello" : "Hello World!", + "path" : "/foo/bar" + }, + "testcases" : [ + ["{+var}", "value"], + ["{+hello}", "Hello%20World!"], + ["{+path}/here", "/foo/bar/here"], + ["here?ref={+path}", "here?ref=/foo/bar"] + ] + }, + "Level 3 Examples" : + { + "level": 3, + "variables": { + "var" : "value", + "hello" : "Hello World!", + "empty" : "", + "path" : "/foo/bar", + "x" : "1024", + "y" : "768" + }, + "testcases" : [ + ["map?{x,y}", "map?1024,768"], + ["{x,hello,y}", "1024,Hello%20World%21,768"], + ["{+x,hello,y}", "1024,Hello%20World!,768"], + ["{+path,x}/here", "/foo/bar,1024/here"], + ["{#x,hello,y}", "#1024,Hello%20World!,768"], + ["{#path,x}/here", "#/foo/bar,1024/here"], + ["X{.var}", "X.value"], + ["X{.x,y}", "X.1024.768"], + ["{/var}", "/value"], + ["{/var,x}/here", "/value/1024/here"], + ["{;x,y}", ";x=1024;y=768"], + ["{;x,y,empty}", ";x=1024;y=768;empty"], + ["{?x,y}", "?x=1024&y=768"], + ["{?x,y,empty}", "?x=1024&y=768&empty="], + ["?fixed=yes{&x}", "?fixed=yes&x=1024"], + ["{&x,y,empty}", "&x=1024&y=768&empty="] + ] + }, + "Level 4 Examples" : + { + "level": 4, + "variables": { + "var": "value", + "hello": "Hello World!", + "path": "/foo/bar", + "list": ["red", "green", "blue"], + "keys": {"semi": ";", "dot": ".", "comma":","} + }, + "testcases": [ + ["{var:3}", "val"], + ["{var:30}", "value"], + ["{list}", "red,green,blue"], + ["{list*}", "red,green,blue"], + ["{keys}", [ + "comma,%2C,dot,.,semi,%3B", + "comma,%2C,semi,%3B,dot,.", + "dot,.,comma,%2C,semi,%3B", + "dot,.,semi,%3B,comma,%2C", + "semi,%3B,comma,%2C,dot,.", + "semi,%3B,dot,.,comma,%2C" + ]], + ["{keys*}", [ + "comma=%2C,dot=.,semi=%3B", + "comma=%2C,semi=%3B,dot=.", + "dot=.,comma=%2C,semi=%3B", + "dot=.,semi=%3B,comma=%2C", + "semi=%3B,comma=%2C,dot=.", + "semi=%3B,dot=.,comma=%2C" + ]], + ["{+path:6}/here", "/foo/b/here"], + ["{+list}", "red,green,blue"], + ["{+list*}", "red,green,blue"], + ["{+keys}", [ + "comma,,,dot,.,semi,;", + "comma,,,semi,;,dot,.", + "dot,.,comma,,,semi,;", + "dot,.,semi,;,comma,,", + "semi,;,comma,,,dot,.", + "semi,;,dot,.,comma,," + ]], + ["{+keys*}", [ + "comma=,,dot=.,semi=;", + "comma=,,semi=;,dot=.", + "dot=.,comma=,,semi=;", + "dot=.,semi=;,comma=,", + "semi=;,comma=,,dot=.", + "semi=;,dot=.,comma=," + ]], + ["{#path:6}/here", "#/foo/b/here"], + ["{#list}", "#red,green,blue"], + ["{#list*}", "#red,green,blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]], + ["{#keys*}", [ + "#comma=,,dot=.,semi=;", + "#comma=,,semi=;,dot=.", + "#dot=.,comma=,,semi=;", + "#dot=.,semi=;,comma=,", + "#semi=;,comma=,,dot=.", + "#semi=;,dot=.,comma=," + ]], + ["X{.var:3}", "X.val"], + ["X{.list}", "X.red,green,blue"], + ["X{.list*}", "X.red.green.blue"], + ["X{.keys}", [ + "X.comma,%2C,dot,.,semi,%3B", + "X.comma,%2C,semi,%3B,dot,.", + "X.dot,.,comma,%2C,semi,%3B", + "X.dot,.,semi,%3B,comma,%2C", + "X.semi,%3B,comma,%2C,dot,.", + "X.semi,%3B,dot,.,comma,%2C" + ]], + ["{/var:1,var}", "/v/value"], + ["{/list}", "/red,green,blue"], + ["{/list*}", "/red/green/blue"], + ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], + ["{/keys}", [ + "/comma,%2C,dot,.,semi,%3B", + "/comma,%2C,semi,%3B,dot,.", + "/dot,.,comma,%2C,semi,%3B", + "/dot,.,semi,%3B,comma,%2C", + "/semi,%3B,comma,%2C,dot,.", + "/semi,%3B,dot,.,comma,%2C" + ]], + ["{/keys*}", [ + "/comma=%2C/dot=./semi=%3B", + "/comma=%2C/semi=%3B/dot=.", + "/dot=./comma=%2C/semi=%3B", + "/dot=./semi=%3B/comma=%2C", + "/semi=%3B/comma=%2C/dot=.", + "/semi=%3B/dot=./comma=%2C" + ]], + ["{;hello:5}", ";hello=Hello"], + ["{;list}", ";list=red,green,blue"], + ["{;list*}", ";list=red;list=green;list=blue"], + ["{;keys}", [ + ";keys=comma,%2C,dot,.,semi,%3B", + ";keys=comma,%2C,semi,%3B,dot,.", + ";keys=dot,.,comma,%2C,semi,%3B", + ";keys=dot,.,semi,%3B,comma,%2C", + ";keys=semi,%3B,comma,%2C,dot,.", + ";keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{;keys*}", [ + ";comma=%2C;dot=.;semi=%3B", + ";comma=%2C;semi=%3B;dot=.", + ";dot=.;comma=%2C;semi=%3B", + ";dot=.;semi=%3B;comma=%2C", + ";semi=%3B;comma=%2C;dot=.", + ";semi=%3B;dot=.;comma=%2C" + ]], + ["{?var:3}", "?var=val"], + ["{?list}", "?list=red,green,blue"], + ["{?list*}", "?list=red&list=green&list=blue"], + ["{?keys}", [ + "?keys=comma,%2C,dot,.,semi,%3B", + "?keys=comma,%2C,semi,%3B,dot,.", + "?keys=dot,.,comma,%2C,semi,%3B", + "?keys=dot,.,semi,%3B,comma,%2C", + "?keys=semi,%3B,comma,%2C,dot,.", + "?keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{?keys*}", [ + "?comma=%2C&dot=.&semi=%3B", + "?comma=%2C&semi=%3B&dot=.", + "?dot=.&comma=%2C&semi=%3B", + "?dot=.&semi=%3B&comma=%2C", + "?semi=%3B&comma=%2C&dot=.", + "?semi=%3B&dot=.&comma=%2C" + ]], + ["{&var:3}", "&var=val"], + ["{&list}", "&list=red,green,blue"], + ["{&list*}", "&list=red&list=green&list=blue"], + ["{&keys}", [ + "&keys=comma,%2C,dot,.,semi,%3B", + "&keys=comma,%2C,semi,%3B,dot,.", + "&keys=dot,.,comma,%2C,semi,%3B", + "&keys=dot,.,semi,%3B,comma,%2C", + "&keys=semi,%3B,comma,%2C,dot,.", + "&keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{&keys*}", [ + "&comma=%2C&dot=.&semi=%3B", + "&comma=%2C&semi=%3B&dot=.", + "&dot=.&comma=%2C&semi=%3B", + "&dot=.&semi=%3B&comma=%2C", + "&semi=%3B&comma=%2C&dot=.", + "&semi=%3B&dot=.&comma=%2C" + ]] + ] + } +} diff --git a/src/test/resources/strings.json b/src/test/resources/strings.json new file mode 100644 index 0000000..341a587 --- /dev/null +++ b/src/test/resources/strings.json @@ -0,0 +1,43 @@ +{ + "vars": { + "var": "value", + "hello": "Hello World!", + "empty": "", + "poo": "\ud83d\udca9 is a pile of poo" + }, + "tests": { + "{poo:4}": "%F0%9F%92%A9%20is", + "{var}": "value", + "{+var}": "value", + "{.var}": ".value", + "{/var}": "/value", + "{;var}": ";var=value", + "{?var}": "?var=value", + "{&var}": "&var=value", + "{#var}": "#value", + "{hello}": "Hello%20World%21", + "{+hello}": "Hello%20World!", + "{.hello}": ".Hello%20World%21", + "{/hello}": "/Hello%20World%21", + "{;hello}": ";hello=Hello%20World%21", + "{?hello}": "?hello=Hello%20World%21", + "{&hello}": "&hello=Hello%20World%21", + "{#hello}": "#Hello%20World!", + "{empty}": "", + "{+empty}": "", + "{.empty}": ".", + "{/empty}": "/", + "{;empty}": ";empty", + "{?empty}": "?empty=", + "{&empty}": "&empty=", + "{#empty}": "#", + "{undef}": "", + "{+undef}": "", + "{.undef}": "", + "{/undef}": "", + "{;undef}": "", + "{?undef}": "", + "{&undef}": "", + "{#undef}": "" + } +} \ No newline at end of file diff --git a/src/test/resources/urltestdata.json b/src/test/resources/urltestdata.json new file mode 100644 index 0000000..c0a0742 --- /dev/null +++ b/src/test/resources/urltestdata.json @@ -0,0 +1,6491 @@ +[ + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/segments.js", + { + "input": "http://example\t.\norg", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": "true" + }, + { + "input": "http://user:pass@foo:21/bar;par?b#c", + "base": "http://example.org/foo/bar", + "href": "http://user:pass@foo:21/bar;par?b#c", + "origin": "http://foo:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "foo:21", + "hostname": "foo", + "port": "21", + "pathname": "/bar;par", + "search": "?b", + "hash": "#c" + }, + { + "input": "https://test:@test", + "base": "about:blank", + "href": "https://test@test/", + "origin": "https://test", + "protocol": "https:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "https://:@test", + "base": "about:blank", + "href": "https://test/", + "origin": "https://test", + "protocol": "https:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "non-special://test:@test/x", + "base": "about:blank", + "href": "non-special://test@test/x", + "origin": "null", + "protocol": "non-special:", + "username": "test", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/x", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "non-special://:@test/x", + "base": "about:blank", + "href": "non-special://test/x", + "origin": "null", + "protocol": "non-special:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/x", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:foo.com", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "\t :foo.com \n", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com", + "search": "", + "hash": "", + "failure": "true" + }, + { + "input": " foo.com ", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "a:\t foo.com", + "base": "http://example.org/foo/bar", + "href": "a: foo.com", + "origin": "null", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": " foo.com", + "search": "", + "hash": "", + "failure": "true" + }, + { + "input": "http://f:21/ b ? d # e ", + "base": "http://example.org/foo/bar", + "href": "http://f:21/%20b%20?%20d%20# e", + "origin": "http://f:21", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:21", + "hostname": "f", + "port": "21", + "pathname": "/%20b%20", + "search": "?%20d%20", + "hash": "# e" + }, + { + "input": "lolscheme:x x#x x", + "base": "about:blank", + "href": "lolscheme:x x#x x", + "protocol": "lolscheme:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "x x", + "search": "", + "hash": "#x x" + }, + { + "input": "http://f:/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:0/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://f:00000000000000/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://f:00000000000000000000080/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:b/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: /c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:\n/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://f:fifty-two/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "non-special://f:999999/c", + "base": "http://example.org/foo/bar" + }, + { + "input": "http://f: 21 / b ? d # e ", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": " \t", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "", + "failure": true + }, + { + "input": ":foo.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": ":a", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:a", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:a", + "search": "", + "hash": "" + }, + { + "input": ":/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": "#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "#/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#/" + }, + { + "input": "#\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#\\", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#\\" + }, + { + "input": "#;?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#;?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#;?" + }, + { + "input": "?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": ":23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:23", + "search": "", + "hash": "" + }, + { + "input": "/:23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/:23", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "::", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::", + "search": "", + "hash": "" + }, + { + "input": "::23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::23", + "search": "", + "hash": "" + }, + { + "input": "foo://", + "base": "http://example.org/foo/bar", + "href": "foo://", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@c:29/d", + "base": "http://example.org/foo/bar", + "href": "http://a:b@c:29/d", + "origin": "http://c:29", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "c:29", + "hostname": "c", + "port": "29", + "pathname": "/d", + "search": "", + "hash": "" + }, + { + "input": "http::@c:29", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:@c:29", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:@c:29", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://&a:foo(b]c@d:2/", + "base": "http://example.org/foo/bar", + "href": "http://&a:foo(b%5Dc@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "&a", + "password": "foo(b%5Dc", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://::@c@d:2", + "base": "http://example.org/foo/bar", + "href": "http://:%3A%40c@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "", + "password": "%3A%40c", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com:b@d/", + "base": "http://example.org/foo/bar", + "href": "http://foo.com:b@d/", + "origin": "http://d", + "protocol": "http:", + "username": "foo.com", + "password": "b", + "host": "d", + "hostname": "d", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com/\\@", + "base": "http://example.org/foo/bar", + "href": "http://foo.com//@", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "//@", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://foo.com/", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\a\\b:c\\d@foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://a/b:c/d@foo.com/", + "origin": "http://a", + "protocol": "http:", + "username": "", + "password": "", + "host": "a", + "hostname": "a", + "port": "", + "pathname": "/b:c/d@foo.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:/", + "base": "http://example.org/foo/bar", + "href": "foo:/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "foo:/bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo:/bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo://///////", + "base": "http://example.org/foo/bar", + "href": "foo://///////", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "///////", + "search": "", + "hash": "" + }, + { + "input": "foo://///////bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo://///////bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "///////bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:////://///", + "base": "http://example.org/foo/bar", + "href": "foo:////://///", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "//://///", + "search": "", + "hash": "" + }, + { + "input": "c:/foo", + "base": "http://example.org/foo/bar", + "href": "c:/foo", + "origin": "null", + "protocol": "c:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "//foo/bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + { + "input": "http://foo/path;a??e#f#g", + "base": "http://example.org/foo/bar", + "href": "http://foo/path;a??e#f#g", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/path;a", + "search": "??e", + "hash": "#f#g" + }, + { + "input": "http://foo/abcd?efgh?ijkl", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd?efgh?ijkl", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "?efgh?ijkl", + "hash": "" + }, + { + "input": "http://foo/abcd#foo?bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd#foo?bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "", + "hash": "#foo?bar" + }, + { + "input": "[61:24:74]:98", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:24:74]:98", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:24:74]:98", + "search": "", + "hash": "", + "failure": true + + }, + { + "input": "http:[61:27]/:foo", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:27]/:foo", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:27]/:foo", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://[1::2]:3:4", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]:80", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://[2001::1]", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "2001:0:0:0:0:0:0:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[::127.0.0.1]", + "base": "http://example.org/foo/bar", + "href": "http://[::7f00:1]/", + "origin": "http://[::7f00:1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::7f00:1]", + "hostname": "0:0:0:0:0:0:7f00:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:0:0:0:0:0:13.1.68.3]", + "base": "http://example.org/foo/bar", + "href": "http://[::d01:4403]/", + "origin": "http://[::d01:4403]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::d01:4403]", + "hostname": "0:0:0:0:0:0:d01:4403", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[2001::1]:80", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "2001:0:0:0:0:0:0:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:/example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:/example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "http://example.org/foo/bar", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file://example:1/", + "base": "about:blank" + }, + { + "input": "file://example:test/", + "base": "about:blank" + }, + { + "input": "file://example%/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://[example]/", + "base": "about:blank" + }, + { + "input": "ftps:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:/example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:/example.com/", + "base": "http://example.org/foo/bar", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:/example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "mailto:/example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftps:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "gopher:example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:example.com/", + "base": "http://example.org/foo/bar", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "mailto:example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "/a/b/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/b/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/b/c", + "search": "", + "hash": "" + }, + { + "input": "/a/ /c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%20/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%20/c", + "search": "", + "hash": "" + }, + { + "input": "/a%2fc", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a%2fc", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a%2fc", + "search": "", + "hash": "" + }, + { + "input": "/a/%2f/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%2f/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%2f/c", + "search": "", + "hash": "" + }, + { + "input": "#β", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#%CE%B2", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#%CE%B2" + }, + { + "input": "data:text/html,test#test", + "base": "http://example.org/foo/bar", + "href": "data:text/html,test#test", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "text/html,test", + "search": "", + "hash": "#test" + }, + { + "input": "tel:1234567890", + "base": "http://example.org/foo/bar", + "href": "tel:1234567890", + "origin": "null", + "protocol": "tel:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "1234567890", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html", + { + "input": "file:c:\\foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:/foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/c:/foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": " File:c|////foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:////foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:////foo/bar.html", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/C|\\foo\\bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "\\\\server\\file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "/\\server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "file:///foo/bar.txt", + "base": "file:///tmp/mock/path", + "href": "file:///foo/bar.txt", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/foo/bar.txt", + "search": "", + "hash": "" + }, + { + "input": "file:///home/me", + "base": "file:///tmp/mock/path", + "href": "file:///home/me", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/home/me", + "search": "", + "hash": "" + }, + { + "input": "//", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "file://test", + "base": "file:///tmp/mock/path", + "href": "file://test/", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + { + "input": "file:test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js", + { + "input": "http://example.com/././foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/./.foo", + "base": "about:blank", + "href": "http://example.com/.foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/.foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/.", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/./", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/..bar", + "base": "about:blank", + "href": "http://example.com/foo/..bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/..bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton", + "base": "about:blank", + "href": "http://example.com/foo/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton/../../a", + "base": "about:blank", + "href": "http://example.com/a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../..", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../../ton", + "base": "about:blank", + "href": "http://example.com/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e%2", + "base": "about:blank", + "href": "http://example.com/foo/%2e%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/%2e%2", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar", + "base": "about:blank", + "href": "http://example.com/%2e.bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%2e.bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com////../..", + "base": "about:blank", + "href": "http://example.com//", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//../..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//..", + "base": "about:blank", + "href": "http://example.com/foo/bar/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/bar/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%20foo", + "base": "about:blank", + "href": "http://example.com/%20foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%20foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%", + "base": "about:blank", + "href": "http://example.com/foo%", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%2", + "base": "about:blank", + "href": "http://example.com/foo%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%2zbar", + "base": "about:blank", + "href": "http://example.com/foo%2zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2zbar", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%2©zbar", + "base": "about:blank", + "href": "http://example.com/foo%2%C3%82%C2%A9zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2%C3%82%C2%A9zbar", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%41%7a", + "base": "about:blank", + "href": "http://example.com/foo%41%7a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%41%7a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\t\u0091%91", + "base": "about:blank", + "href": "http://example.com/foo%C2%91%91", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%C2%91%91", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%00%51", + "base": "about:blank", + "href": "http://example.com/foo%00%51", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%00%51", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/(%28:%3A%29)", + "base": "about:blank", + "href": "http://example.com/(%28:%3A%29)", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/(%28:%3A%29)", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%3A%3a%3C%3c", + "base": "about:blank", + "href": "http://example.com/%3A%3a%3C%3c", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%3A%3a%3C%3c", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\tbar", + "base": "about:blank", + "href": "http://example.com/foobar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foobar", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com\\\\foo\\\\bar", + "base": "about:blank", + "href": "http://example.com//foo//bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//foo//bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "base": "about:blank", + "href": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%7Ffp3%3Eju%3Dduvgw%3Dd", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/@asdf%40", + "base": "about:blank", + "href": "http://example.com/@asdf%40", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/@asdf%40", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/你好你好", + "base": "about:blank", + "href": "http://example.com/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‥/foo", + "base": "about:blank", + "href": "http://example.com/%E2%80%A5/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%A5/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com//foo", + "base": "about:blank", + "href": "http://example.com/%EF%BB%BF/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%EF%BB%BF/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‮/foo/‭/bar", + "base": "about:blank", + "href": "http://example.com/%E2%80%AE/foo/%E2%80%AD/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%AE/foo/%E2%80%AD/bar", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js", + { + "input": "http://www.google.com/foo?bar=baz#", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz#", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "" + }, + { + "input": "http://www.google.com/foo?bar=baz# »", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz# %C2%BB", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "# %C2%BB" + }, + { + "input": "data:test# »", + "base": "about:blank", + "href": "data:test# %C2%BB", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "test", + "search": "", + "hash": "# %C2%BB" + }, + { + "input": "http://www.google.com", + "base": "about:blank", + "href": "http://www.google.com/", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.0x00A80001", + "base": "about:blank", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.0x00a80001", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo%2Ehtml", + "base": "about:blank", + "href": "http://www/foo%2Ehtml", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo%2Ehtml", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo/%2E/html", + "base": "about:blank", + "href": "http://www/foo/html", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo/html", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@/", + "base": "about:blank" + }, + { + "input": "http://%25DOMAIN:foobar@foodomain.com/", + "base": "about:blank", + "href": "http://%25DOMAIN:foobar@foodomain.com/", + "origin": "http://foodomain.com", + "protocol": "http:", + "username": "%25DOMAIN", + "password": "foobar", + "host": "foodomain.com", + "hostname": "foodomain.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\www.google.com\\foo", + "base": "about:blank", + "href": "http://www.google.com/foo", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://foo:80/", + "base": "about:blank", + "href": "http://foo/", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:81/", + "base": "about:blank", + "href": "http://foo:81/", + "origin": "http://foo:81", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "httpa://foo:80/", + "base": "about:blank", + "href": "httpa://foo:80/", + "origin": "null", + "protocol": "httpa:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:-80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://foo:443/", + "base": "about:blank", + "href": "https://foo/", + "origin": "https://foo", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://foo:80/", + "base": "about:blank", + "href": "https://foo:80/", + "origin": "https://foo:80", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:21/", + "base": "about:blank", + "href": "ftp://foo/", + "origin": "ftp://foo", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:80/", + "base": "about:blank", + "href": "ftp://foo:80/", + "origin": "ftp://foo:80", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:70/", + "base": "about:blank", + "href": "gopher://foo/", + "origin": "gopher://foo", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:443/", + "base": "about:blank", + "href": "gopher://foo:443/", + "origin": "gopher://foo:443", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:80/", + "base": "about:blank", + "href": "ws://foo/", + "origin": "ws://foo", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:81/", + "base": "about:blank", + "href": "ws://foo:81/", + "origin": "ws://foo:81", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:443/", + "base": "about:blank", + "href": "ws://foo:443/", + "origin": "ws://foo:443", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:815/", + "base": "about:blank", + "href": "ws://foo:815/", + "origin": "ws://foo:815", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:80/", + "base": "about:blank", + "href": "wss://foo:80/", + "origin": "wss://foo:80", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:81/", + "base": "about:blank", + "href": "wss://foo:81/", + "origin": "wss://foo:81", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:443/", + "base": "about:blank", + "href": "wss://foo/", + "origin": "wss://foo", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:815/", + "base": "about:blank", + "href": "wss://foo:815/", + "origin": "wss://foo:815", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:/example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:/example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:/example.com/", + "base": "about:blank", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "about:blank", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:/example.com/", + "base": "about:blank", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:/example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:/example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:/example.com/", + "base": "about:blank", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/example.com/", + "base": "about:blank", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "about:blank", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:example.com/", + "base": "about:blank", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "about:blank", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:example.com/", + "base": "about:blank", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:example.com/", + "base": "about:blank", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "mailto:example.com/", + "base": "about:blank", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html", + { + "input": "http:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@pple.com", + "base": "about:blank", + "href": "http://pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http::b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/:@/www.example.com", + "base": "about:blank" + }, + { + "input": "http://user@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:/@/www.example.com", + "base": "about:blank" + }, + { + "input": "http://@/www.example.com", + "base": "about:blank" + }, + { + "input": "https:@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:a:b@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:/a:b@/www.example.com", + "base": "about:blank" + }, + { + "input": "http://a:b@/www.example.com", + "base": "about:blank" + }, + { + "input": "http::@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www.@pple.com", + "base": "about:blank", + "href": "http://www.@pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "www.", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:@:www.example.com", + "base": "about:blank" + }, + { + "input": "http:/@:www.example.com", + "base": "about:blank" + }, + { + "input": "http://@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# Others", + { + "input": "/", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": ".", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "./test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../aaa/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/aaa/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/aaa/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "中/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/%E4%B8%AD/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/%E4%B8%AD/test.txt", + "search": "", + "hash": "" + }, + { + "input": "http://www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "//www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:...", + "base": "http://www.example.com/test", + "href": "file:///...", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/...", + "search": "", + "hash": "" + }, + { + "input": "file:..", + "base": "http://www.example.com/test", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:a", + "base": "http://www.example.com/test", + "href": "file:///a", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html", + "Basic canonicalization, uppercase should be converted to lowercase", + { + "input": "http://ExAmPlE.CoM", + "base": "http://other.com/", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example example.com", + "base": "http://other.com/" + }, + { + "input": "http://Goo%20 goo%7C|.com", + "base": "http://other.com/" + }, + { + "input": "http://[]", + "base": "http://other.com/" + }, + { + "input": "http://[:]", + "base": "http://other.com/" + }, + "U+3000 is mapped to U+0020 (space) which is disallowed", + { + "input": "http://GOO\u00a0\u3000goo.com", + "base": "http://other.com/" + }, + "Other types of space (no-break, zero-width, zero-width-no-break) are name-prepped away to nothing. U+200B, U+2060, and U+FEFF, are ignored", + { + "input": "http://GOO\u200b\u2060\ufeffgoo.com", + "base": "http://other.com/", + "href": "http://googoo.com/", + "origin": "http://googoo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "googoo.com", + "hostname": "googoo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "Leading and trailing C0 control or space", + { + "input": "\u0000\u001b\u0004\u0012 http://example.com/\u001f \u000d ", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + "Ideographic full stop (full-width period for Chinese, etc.) should be treated as a dot. U+3002 is mapped to U+002E (dot)", + { + "input": "http://www.foo。bar.com", + "base": "http://other.com/", + "href": "http://www.foo.bar.com/", + "origin": "http://www.foo.bar.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.foo.bar.com", + "hostname": "www.foo.bar.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "Invalid unicode characters should fail... U+FDD0 is disallowed; %ef%b7%90 is U+FDD0", + { + "input": "http://\ufdd0zyx.com", + "base": "http://other.com/", + "failure": true + }, + "This is the same as previous but escaped", + { + "input": "http://%ef%b7%90zyx.com", + "base": "http://other.com/", + "failure": true + }, + "U+FFFD", + { + "input": "https://\ufffd", + "base": "about:blank", + "failure": true + }, + { + "input": "https://%EF%BF%BD", + "base": "about:blank", + "failure": true + }, + { + "input": "https://x/\ufffd?\ufffd#\ufffd", + "base": "about:blank", + "href": "https://x/%EF%BF%BD?%EF%BF%BD#%EF%BF%BD", + "origin": "https://x", + "protocol": "https:", + "username": "", + "password": "", + "host": "x", + "hostname": "x", + "port": "", + "pathname": "/%EF%BF%BD", + "search": "?%EF%BF%BD", + "hash": "#%EF%BF%BD" + }, + "Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.", + { + "input": "http://Go.com", + "base": "http://other.com/", + "href": "http://go.com/", + "origin": "http://go.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "go.com", + "hostname": "go.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "URL spec forbids the following. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257", + { + "input": "http://%41.com", + "base": "http://other.com/" + }, + { + "input": "http://%ef%bc%85%ef%bc%94%ef%bc%91.com", + "base": "http://other.com/" + }, + "...%00 in fullwidth should fail (also as escaped UTF-8 input)", + { + "input": "http://%00.com", + "base": "http://other.com/" + }, + { + "input": "http://%ef%bc%85%ef%bc%90%ef%bc%90.com", + "base": "http://other.com/" + }, + "Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN", + { + "input": "http://你好你好", + "base": "http://other.com/", + "href": "http://xn--6qqa088eba/", + "origin": "http://xn--6qqa088eba", + "protocol": "http:", + "username": "", + "password": "", + "host": "xn--6qqa088eba", + "hostname": "xn--6qqa088eba", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https://faß.ExAmPlE/", + "base": "about:blank", + "href": "https://xn--fa-hia.example/", + "origin": "https://xn--fa-hia.example", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--fa-hia.example", + "hostname": "fass.example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://faß.ExAmPlE/", + "base": "about:blank", + "href": "sc://fa%C3%9F.ExAmPlE/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "fa%C3%9F.ExAmPlE", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191", + { + "input": "http://%zz%66%a.com", + "base": "http://other.com/", + "failure": true + }, + "If we get an invalid character that has been escaped.", + { + "input": "http://%25", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://hello%00", + "base": "http://other.com/" + }, + "Escaped numbers should be treated like IP addresses if they are.", + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "0xc0.0250.01", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01%2e", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "0xc0.0250.01.", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.0.257", + "base": "http://other.com/" + }, + "Invalid escaping in hosts causes failure", + { + "input": "http://%3g%78%63%30%2e%30%32%35%30%2E.01", + "base": "http://other.com/", + "failure": true + }, + "A space in a host causes failure", + { + "input": "http://192.168.0.1 hello", + "base": "http://other.com/" + }, + { + "input": "https://x x:12", + "base": "about:blank" + }, + "Fullwidth and escaped UTF-8 fullwidth should still be treated as IP", + { + "input": "http://0Xc0.0250.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "0xc0.0250.01", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Domains with empty labels", + { + "input": "http://./", + "base": "about:blank", + "href": "http://./", + "origin": "http://.", + "protocol": "http:", + "username": "", + "password": "", + "host": ".", + "hostname": ".", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://../", + "base": "about:blank", + "href": "http://../", + "origin": "http://..", + "protocol": "http:", + "username": "", + "password": "", + "host": "..", + "hostname": "..", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://0..0x300/", + "base": "about:blank", + "href": "http://0..0x300/", + "origin": "http://0..0x300", + "protocol": "http:", + "username": "", + "password": "", + "host": "0..0x300", + "hostname": "0..0x300", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + "Broken IPv6", + { + "input": "http://[www.google.com]/", + "base": "about:blank" + }, + { + "input": "http://[google.com]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.2.3.4x]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.2.3.]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.2.]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.]", + "base": "http://other.com/" + }, + "Misc Unicode", + { + "input": "http://foo:💩@example.com/bar", + "base": "http://other.com/", + "href": "http://foo:%F0%9F%92%A9@example.com/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "foo", + "password": "%F0%9F%92%A9", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + "# resolving a fragment against any scheme succeeds", + { + "input": "#", + "base": "test:test", + "href": "test:test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "test", + "search": "", + "hash": "" + }, + { + "input": "#x", + "base": "mailto:x@x.com", + "href": "mailto:x@x.com#x", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "x@x.com", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "data:,", + "href": "data:,#x", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": ",", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "about:blank", + "href": "about:blank#x", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "blank", + "search": "", + "hash": "#x" + }, + { + "input": "#", + "base": "test:test?test", + "href": "test:test?test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "test", + "search": "?test", + "hash": "" + }, + "# multiple @ in authority state", + { + "input": "https://@test@test@example:800/", + "base": "http://doesnotmatter/", + "href": "https://%40test%40test@example:800/", + "origin": "https://example:800", + "protocol": "https:", + "username": "%40test%40test", + "password": "", + "host": "example:800", + "hostname": "example", + "port": "800", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://@@@example", + "base": "http://doesnotmatter/", + "href": "https://%40%40@example/", + "origin": "https://example", + "protocol": "https:", + "username": "%40%40", + "password": "", + "host": "example", + "hostname": "example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "non-az-09 characters", + { + "input": "http://`{}:`{}@h/`{}?`{}", + "base": "http://doesnotmatter/", + "href": "http://%60%7B%7D:%60%7B%7D@h/%60%7B%7D?`{}", + "origin": "http://h", + "protocol": "http:", + "username": "%60%7B%7D", + "password": "%60%7B%7D", + "host": "h", + "hostname": "h", + "port": "", + "pathname": "/%60%7B%7D", + "search": "?`{}", + "hash": "" + }, + "# Credentials in base", + { + "input": "/some/path", + "base": "http://user@example.org/smth", + "href": "http://user@example.org/some/path", + "origin": "http://example.org", + "protocol": "http:", + "username": "user", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/smth", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/smth", + "search": "", + "hash": "" + }, + { + "input": "/some/path", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/some/path", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + "# a set of tests designed by zcorpan for relative URLs with unknown schemes", + { + "input": "i", + "base": "sc:sd" + }, + { + "input": "i", + "base": "sc:sd/sd" + }, + { + "input": "i", + "base": "sc:/pa/pa", + "href": "sc:/pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc:///pa/pa", + "href": "sc:///pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:sd" + }, + { + "input": "../i", + "base": "sc:sd/sd" + }, + { + "input": "../i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:sd" + }, + { + "input": "/i", + "base": "sc:sd/sd" + }, + { + "input": "/i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "?i", + "base": "sc:sd" + }, + { + "input": "?i", + "base": "sc:sd/sd" + }, + { + "input": "?i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc://ho/pa", + "href": "sc://ho/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "#i", + "base": "sc:sd", + "href": "sc:sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:sd/sd", + "href": "sc:sd/sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "sd/sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc://ho/pa", + "href": "sc://ho/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + "# make sure that relative URL logic works on known typically non-relative schemes too", + { + "input": "about:/../", + "base": "about:blank", + "href": "about:/", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/../", + "base": "about:blank", + "href": "data:/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/../", + "base": "about:blank", + "href": "javascript:/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/../", + "base": "about:blank", + "href": "mailto:/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# unknown schemes and their hosts", + { + "input": "sc://ñ.test/", + "base": "about:blank", + "href": "sc://%C3%B1.test/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/", + "base": "about:blank", + "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u0000/", + "base": "about:blank" + }, + { + "input": "sc:// /", + "base": "about:blank" + }, + { + "input": "sc://%/", + "base": "about:blank", + "href": "sc://%/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%", + "hostname": "%", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "sc://@/", + "base": "about:blank" + }, + { + "input": "sc://te@s:t@/", + "base": "about:blank" + }, + { + "input": "sc://:/", + "base": "about:blank" + }, + { + "input": "sc://:12/", + "base": "about:blank" + }, + { + "input": "sc://[/", + "base": "about:blank" + }, + { + "input": "sc://\\/", + "base": "about:blank" + }, + { + "input": "sc://]/", + "base": "about:blank" + }, + { + "input": "x", + "base": "sc://ñ", + "href": "sc://%C3%B1/x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + "# unknown schemes and backslashes", + { + "input": "sc:\\../", + "base": "about:blank", + "href": "sc:\\../", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "\\../", + "search": "", + "hash": "" + }, + "# unknown scheme with path looking like a password", + { + "input": "sc::a@example.net", + "base": "about:blank", + "href": "sc::a@example.net", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": ":a@example.net", + "search": "", + "hash": "" + }, + "# unknown scheme with bogus percent-encoding", + { + "input": "wow:%NBD", + "base": "about:blank", + "href": "wow:%NBD", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "%NBD", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "wow:%1G", + "base": "about:blank", + "href": "wow:%1G", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "%1G", + "search": "", + "hash": "", + "failure": true + }, + "# Hosts and percent-encoding", + { + "input": "ftp://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://%e2%98%83", + "base": "about:blank", + "href": "ftp://xn--n3h/", + "origin": "ftp://xn--n3h", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://%e2%98%83", + "base": "about:blank", + "href": "https://xn--n3h/", + "origin": "https://xn--n3h", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# tests from jsdom/whatwg-url designed for code coverage", + { + "input": "http://127.0.0.1:10100/relative_import.html", + "base": "about:blank", + "href": "http://127.0.0.1:10100/relative_import.html", + "origin": "http://127.0.0.1:10100", + "protocol": "http:", + "username": "", + "password": "", + "host": "127.0.0.1:10100", + "hostname": "localhost", + "port": "10100", + "pathname": "/relative_import.html", + "search": "", + "hash": "" + }, + { + "input": "http://facebook.com/?foo=%7B%22abc%22", + "base": "about:blank", + "href": "http://facebook.com/?foo=%7B%22abc%22", + "origin": "http://facebook.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "facebook.com", + "hostname": "facebook.com", + "port": "", + "pathname": "/", + "search": "?foo=%7B%22abc%22", + "hash": "" + }, + { + "input": "https://localhost:3000/jqueryui@1.2.3", + "base": "about:blank", + "href": "https://localhost:3000/jqueryui@1.2.3", + "origin": "https://localhost:3000", + "protocol": "https:", + "username": "", + "password": "", + "host": "localhost:3000", + "hostname": "localhost", + "port": "3000", + "pathname": "/jqueryui@1.2.3", + "search": "", + "hash": "" + }, + "# tab/LF/CR", + { + "input": "h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg", + "base": "about:blank", + "href": "http://host:9000/path?query#frag", + "origin": "http://host:9000", + "protocol": "http:", + "username": "", + "password": "", + "host": "host:9000", + "hostname": "host", + "port": "9000", + "pathname": "/path", + "search": "?query", + "hash": "#frag" + }, + "# Stringification of URL.searchParams", + { + "input": "?a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "?a=b&c=d", + "searchParams": "a=b&c=d", + "hash": "" + }, + { + "input": "??a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar??a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "??a=b&c=d", + "searchParams": "%3Fa=b&c=d", + "hash": "" + }, + "# Scheme only", + { + "input": "http:", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "searchParams": "", + "hash": "", + "skip": true + }, + { + "input": "http:", + "base": "https://example.org/foo/bar" + }, + { + "input": "sc:", + "base": "https://example.org/foo/bar", + "href": "sc:", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "searchParams": "", + "hash": "" + }, + "# Percent encoding of fragments", + { + "input": "http://foo.bar/baz?qux#foo\bbar", + "base": "about:blank", + "href": "http://foo.bar/baz?qux#foo%08bar", + "origin": "http://foo.bar", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.bar", + "hostname": "foo.bar", + "port": "", + "pathname": "/baz", + "search": "?qux", + "searchParams": "qux=", + "hash": "#foo%08bar" + }, + "# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)", + { + "input": "http://192.168.257", + "base": "http://other.com/", + "href": "http://192.168.1.1/", + "origin": "http://192.168.1.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.1.1", + "hostname": "192.168.1.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.257.com", + "base": "http://other.com/", + "href": "http://192.168.257.com/", + "origin": "http://192.168.257.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.257.com", + "hostname": "192.168.257.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256", + "base": "http://other.com/", + "href": "http://0.0.1.0/", + "origin": "http://0.0.1.0", + "protocol": "http:", + "username": "", + "password": "", + "host": "0.0.1.0", + "hostname": "0.0.1.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256.com", + "base": "http://other.com/", + "href": "http://256.com/", + "origin": "http://256.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.com", + "hostname": "256.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999", + "base": "http://other.com/", + "href": "http://59.154.201.255/", + "origin": "http://59.154.201.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "59.154.201.255", + "hostname": "59.154.201.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999.com", + "base": "http://other.com/", + "href": "http://999999999.com/", + "origin": "http://999999999.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "999999999.com", + "hostname": "999999999.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://10000000000", + "base": "http://other.com/" + }, + { + "input": "http://10000000000.com", + "base": "http://other.com/", + "href": "http://10000000000.com/", + "origin": "http://10000000000.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "10000000000.com", + "hostname": "10000000000.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967295", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "broadcasthost", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967296", + "base": "http://other.com/" + }, + { + "input": "http://0xffffffff", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "0xffffffff", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0xffffffff1", + "base": "http://other.com/" + }, + { + "input": "http://256.256.256.256", + "base": "http://other.com/" + }, + { + "input": "http://256.256.256.256.256", + "base": "http://other.com/", + "href": "http://256.256.256.256.256/", + "origin": "http://256.256.256.256.256", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.256.256.256.256", + "hostname": "256.256.256.256.256", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://0x.0x.0", + "base": "about:blank", + "href": "https://0.0.0.0/", + "origin": "https://0.0.0.0", + "protocol": "https:", + "username": "", + "password": "", + "host": "0.0.0.0", + "hostname": "0x.0x.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "More IPv4 parsing (via https://github.com/jsdom/whatwg-url/issues/92)", + { + "input": "https://0x100000000/test", + "base": "about:blank" + }, + { + "input": "https://256.0.0.1/test", + "base": "about:blank" + }, + "# file URLs containing percent-encoded Windows drive letters (shouldn't work)", + { + "input": "file:///C%3A/", + "base": "about:blank", + "href": "file:///C%3A/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C%3A/", + "search": "", + "hash": "" + }, + { + "input": "file:///C%7C/", + "base": "about:blank", + "href": "file:///C%7C/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C%7C/", + "search": "", + "hash": "" + }, + "# file URLs relative to other file URLs (via https://github.com/jsdom/whatwg-url/pull/60)", + { + "input": "pix/submit.gif", + "base": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/anchor.html", + "href": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///C:/", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# More file URL tests by zcorpan and annevk", + { + "input": "/", + "base": "file:///C:/a/b", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "//d:", + "base": "file:///C:/a/b", + "href": "file:///d:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/d:", + "search": "", + "hash": "", + "failure" : true + }, + { + "input": "//d:/..", + "base": "file:///C:/a/b", + "href": "file:///d:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/d:/", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "..", + "base": "file:///ab:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///1:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "file:", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "file:?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + { + "input": "file:#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + "# File URLs and many (back)slashes", + { + "input": "file:\\\\//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\\\\\", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\\\\\?fox", + "base": "about:blank", + "href": "file:///?fox", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "?fox", + "hash": "" + }, + { + "input": "file:\\\\\\\\#guppy", + "base": "about:blank", + "href": "file:///#guppy", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "#guppy" + }, + { + "input": "file://spider///", + "base": "about:blank", + "href": "file://spider/", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\localhost//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:///localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "file://\\/localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "file://localhost//a//../..//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/////mouse", + "base": "file:///elephant", + "href": "file:///mouse", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/mouse", + "search": "", + "hash": "" + }, + { + "input": "\\//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "\\/localhost//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "//localhost//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "/..//localhost//pig", + "base": "file://lion/", + "href": "file://lion/localhost//pig", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/localhost//pig", + "search": "", + "hash": "" + }, + { + "input": "file://", + "base": "file://ape/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# File URLs with non-empty hosts", + { + "input": "/rooibos", + "base": "file://tea/", + "href": "file://tea/rooibos", + "protocol": "file:", + "username": "", + "password": "", + "host": "tea", + "hostname": null, + "port": "", + "pathname": "/rooibos", + "search": "", + "hash": "" + }, + { + "input": "/?chai", + "base": "file://tea/", + "href": "file://tea/?chai", + "protocol": "file:", + "username": "", + "password": "", + "host": "tea", + "hostname": null, + "port": "", + "pathname": "/", + "search": "?chai", + "hash": "" + }, + "# Windows drive letter handling with the 'file:' base URL", + { + "input": "C|", + "base": "file://host/dir/file", + "href": "file:///C:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|#", + "base": "file://host/dir/file", + "href": "file:///C:#", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|?", + "base": "file://host/dir/file", + "href": "file:///C:?", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\n/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\\", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C", + "base": "file://host/dir/file", + "href": "file://host/dir/C", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": null, + "port": "", + "pathname": "/dir/C", + "search": "", + "hash": "" + }, + { + "input": "C|a", + "base": "file://host/dir/file", + "href": "file://host/dir/C|a", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": null, + "port": "", + "pathname": "/dir/C|a", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk with not empty host", + { + "input": "file://example.net/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://1.2.3.4/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://[1::8]/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk (no host)", + { + "input": "file:/C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# file URLs without base URL by Rimas Misevičius", + { + "input": "file:", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:?q=v", + "base": "about:blank", + "href": "file:///?q=v", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "?q=v", + "hash": "" + }, + { + "input": "file:#frag", + "base": "about:blank", + "href": "file:///#frag", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "#frag" + }, + "# IPv6 tests", + { + "input": "http://[1:0::]", + "base": "http://example.net/", + "href": "http://[1::]/", + "origin": "http://[1::]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1::]", + "hostname": "1:0:0:0:0:0:0:0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:1:2:3:4:5:6:7:8]", + "base": "http://example.net/" + }, + { + "input": "https://[0::0::0]", + "base": "about:blank" + }, + { + "input": "https://[0:.0]", + "base": "about:blank" + }, + { + "input": "https://[0:0:]", + "base": "about:blank" + }, + { + "input": "https://[0:1:2:3:4:5:6:7.0.0.0.1]", + "base": "about:blank" + }, + { + "input": "https://[0:1.00.0.0.0]", + "base": "about:blank" + }, + { + "input": "https://[0:1.290.0.0.0]", + "base": "about:blank" + }, + { + "input": "https://[0:1.23.23]", + "base": "about:blank" + }, + "# Empty host", + { + "input": "http://?", + "base": "about:blank" + }, + { + "input": "http://#", + "base": "about:blank" + }, + "# Non-special-URL path tests", + { + "input": "sc://ñ", + "base": "about:blank", + "href": "sc://%C3%B1", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "sc://ñ?x", + "base": "about:blank", + "href": "sc://%C3%B1?x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "?x", + "hash": "" + }, + { + "input": "sc://ñ#x", + "base": "about:blank", + "href": "sc://%C3%B1#x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "sc://ñ", + "href": "sc://%C3%B1#x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "#x" + }, + { + "input": "?x", + "base": "sc://ñ", + "href": "sc://%C3%B1?x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "?x", + "hash": "" + }, + { + "input": "sc://?", + "base": "about:blank", + "href": "sc://?", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "sc://#", + "base": "about:blank", + "href": "sc://#", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "sc://x/", + "href": "sc:///", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "////", + "base": "sc://x/", + "href": "sc:////", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "////x/", + "base": "sc://x/", + "href": "sc:////x/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "//x/", + "search": "", + "hash": "" + }, + { + "input": "tftp://foobar.com/someconfig;mode=netascii", + "base": "about:blank", + "href": "tftp://foobar.com/someconfig;mode=netascii", + "origin": "null", + "protocol": "tftp:", + "username": "", + "password": "", + "host": "foobar.com", + "hostname": "foobar.com", + "port": "", + "pathname": "/someconfig;mode=netascii", + "search": "", + "hash": "" + }, + { + "input": "telnet://user:pass@foobar.com:23/", + "base": "about:blank", + "href": "telnet://user:pass@foobar.com:23/", + "origin": "null", + "protocol": "telnet:", + "username": "user", + "password": "pass", + "host": "foobar.com:23", + "hostname": "foobar.com", + "port": "23", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ut2004://10.10.10.10:7777/Index.ut2", + "base": "about:blank", + "href": "ut2004://10.10.10.10:7777/Index.ut2", + "origin": "null", + "protocol": "ut2004:", + "username": "", + "password": "", + "host": "10.10.10.10:7777", + "hostname": null, + "port": "7777", + "pathname": "/Index.ut2", + "search": "", + "hash": "" + }, + { + "input": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "base": "about:blank", + "href": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "origin": "null", + "protocol": "redis:", + "username": "foo", + "password": "bar", + "host": "somehost:6379", + "hostname": "somehost", + "port": "6379", + "pathname": "/0", + "search": "?baz=bam&qux=baz", + "hash": "" + }, + { + "input": "rsync://foo@host:911/sup", + "base": "about:blank", + "href": "rsync://foo@host:911/sup", + "origin": "null", + "protocol": "rsync:", + "username": "foo", + "password": "", + "host": "host:911", + "hostname": "host", + "port": "911", + "pathname": "/sup", + "search": "", + "hash": "" + }, + { + "input": "git://github.com/foo/bar.git", + "base": "about:blank", + "href": "git://github.com/foo/bar.git", + "origin": "null", + "protocol": "git:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar.git", + "search": "", + "hash": "" + }, + { + "input": "irc://myserver.com:6999/channel?passwd", + "base": "about:blank", + "href": "irc://myserver.com:6999/channel?passwd", + "origin": "null", + "protocol": "irc:", + "username": "", + "password": "", + "host": "myserver.com:6999", + "hostname": "myserver.com", + "port": "6999", + "pathname": "/channel", + "search": "?passwd", + "hash": "" + }, + { + "input": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "base": "about:blank", + "href": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "origin": "null", + "protocol": "dns:", + "username": "", + "password": "", + "host": "fw.example.org:9999", + "hostname": "fw.example.org", + "port": "9999", + "pathname": "/foo.bar.org", + "search": "?type=TXT", + "hash": "" + }, + { + "input": "ldap://localhost:389/ou=People,o=JNDITutorial", + "base": "about:blank", + "href": "ldap://localhost:389/ou=People,o=JNDITutorial", + "origin": "null", + "protocol": "ldap:", + "username": "", + "password": "", + "host": "localhost:389", + "hostname": "localhost", + "port": "389", + "pathname": "/ou=People,o=JNDITutorial", + "search": "", + "hash": "" + }, + { + "input": "git+https://github.com/foo/bar", + "base": "about:blank", + "href": "git+https://github.com/foo/bar", + "origin": "null", + "protocol": "git+https:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "urn:ietf:rfc:2648", + "base": "about:blank", + "href": "urn:ietf:rfc:2648", + "origin": "null", + "protocol": "urn:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "ietf:rfc:2648", + "search": "", + "hash": "" + }, + { + "input": "tag:joe@example.org,2001:foo/bar", + "base": "about:blank", + "href": "tag:joe@example.org,2001:foo/bar", + "origin": "null", + "protocol": "tag:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "joe@example.org,2001:foo/bar", + "search": "", + "hash": "" + }, + "# percent encoded hosts in non-special-URLs", + { + "input": "non-special://%E2%80%A0/", + "base": "about:blank", + "href": "non-special://%E2%80%A0/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "%E2%80%A0", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://H%4fSt/path", + "base": "about:blank", + "href": "non-special://H%4fSt/path", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "H%4fSt", + "hostname": null, + "port": "", + "pathname": "/path", + "search": "", + "hash": "" + }, + "# IPv6 in non-special-URLs", + { + "input": "non-special://[1:2:0:0:5:0:0:0]/", + "base": "about:blank", + "href": "non-special://[1:2:0:0:5::]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2:0:0:5::]", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2:0:0:0:0:0:3]/", + "base": "about:blank", + "href": "non-special://[1:2::3]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2::3]:80/", + "base": "about:blank", + "href": "non-special://[1:2::3]:80/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]:80", + "hostname": null, + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[:80/", + "base": "about:blank" + }, + { + "input": "blob:https://example.com:443/", + "base": "about:blank", + "href": "blob:https://example.com:443/", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "https://example.com:443/", + "search": "", + "hash": "" + }, + { + "input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "base": "about:blank", + "href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "d3958f5c-0777-0845-9dcf-2cb28783acaf", + "search": "", + "hash": "" + }, + "Invalid IPv4 radix digits", + { + "input": "http://0177.0.0.0189", + "base": "about:blank", + "href": "http://0177.0.0.0189/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0177.0.0.0189", + "hostname": "177-0-0-189.cbace701.dsl.brasiltelecom.net.br", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0x7f.0.0.0x7g", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0X7F.0.0.0X7G", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid IPv4 portion of IPv6 address", + { + "input": "http://[::127.0.0.0.1]", + "base": "about:blank" + }, + "Uncompressed IPv6 addresses with 0", + { + "input": "http://[0:1:0:1:0:1:0:1]", + "base": "about:blank", + "href": "http://[0:1:0:1:0:1:0:1]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[0:1:0:1:0:1:0:1]", + "hostname": "0:1:0:1:0:1:0:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[1:0:1:0:1:0:1:0]", + "base": "about:blank", + "href": "http://[1:0:1:0:1:0:1:0]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1:0:1:0:1:0:1:0]", + "hostname": "1:0:1:0:1:0:1:0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Percent-encoded query and fragment", + { + "input": "http://example.org/test?\u0022", + "base": "about:blank", + "href": "http://example.org/test?%22", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%22", + "hash": "" + }, + { + "input": "http://example.org/test?\u0023", + "base": "about:blank", + "href": "http://example.org/test?#", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "http://example.org/test?\u003C", + "base": "about:blank", + "href": "http://example.org/test?%3C", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3C", + "hash": "" + }, + { + "input": "http://example.org/test?\u003E", + "base": "about:blank", + "href": "http://example.org/test?%3E", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3E", + "hash": "" + }, + { + "input": "http://example.org/test?\u2323", + "base": "about:blank", + "href": "http://example.org/test?%E2%8C%A3", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%E2%8C%A3", + "hash": "" + }, + { + "input": "http://example.org/test?%23%23", + "base": "about:blank", + "href": "http://example.org/test?%23%23", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%23%23", + "hash": "" + }, + { + "input": "http://example.org/test?%GH", + "base": "about:blank", + "href": "http://example.org/test?%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%GH", + "hash": "", + "failure": true + }, + { + "input": "http://example.org/test?a#%EF", + "base": "about:blank", + "href": "http://example.org/test?a#%EF", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%EF", + "failure" : true + }, + { + "input": "http://example.org/test?a#%GH", + "base": "about:blank", + "href": "http://example.org/test?a#%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%GH", + "failure": true + }, + "Bad bases", + { + "input": "test-a.html", + "base": "a", + "failure": true + }, + { + "input": "test-a-slash.html", + "base": "a/", + "failure": true + }, + { + "input": "test-a-slash-slash.html", + "base": "a//", + "failure": true + }, + { + "input": "test-a-colon.html", + "base": "a:" + }, + { + "input": "test-a-colon-slash.html", + "base": "a:/", + "href": "a:/test-a-colon-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash.html", + "base": "a://", + "href": "a:///test-a-colon-slash-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-b.html", + "base": "a:b" + }, + { + "input": "test-a-colon-slash-b.html", + "base": "a:/b", + "href": "a:/test-a-colon-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash-b.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash-b.html", + "base": "a://b", + "href": "a://b/test-a-colon-slash-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "b", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash-slash-b.html", + "search": "", + "hash": "" + }, + "Null code point in fragment", + { + "input": "http://example.org/test?a#b\u0000c", + "base": "about:blank", + "href": "http://example.org/test?a#bc", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#bc" + } +] \ No newline at end of file