├── settings.gradle ├── .codacy.yaml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .travis.yml ├── src ├── main │ └── java │ │ └── com │ │ └── anthonynsimon │ │ └── url │ │ ├── URLPart.java │ │ ├── URLParser.java │ │ ├── exceptions │ │ ├── InvalidHexException.java │ │ ├── MalformedURLException.java │ │ └── InvalidURLReferenceException.java │ │ ├── URLBuilder.java │ │ ├── PathResolver.java │ │ ├── PercentEncoder.java │ │ ├── DefaultURLParser.java │ │ └── URL.java ├── test │ └── java │ │ └── com │ │ └── anthonynsimon │ │ └── url │ │ ├── exceptions │ │ ├── InvalidHexExceptionTest.java │ │ ├── MalformedURLExecptionTest.java │ │ └── InvalidURLReferenceExceptionTest.java │ │ ├── URLBuilderTest.java │ │ ├── PercentEncoderTest.java │ │ ├── PathResolverTest.java │ │ └── URLTest.java └── jmh │ └── java │ └── com │ └── anthonynsimon │ └── url │ ├── BenchmarkJavaURLClass.java │ └── BenchmarkDefaultURLParser.java ├── LICENSE ├── .gitignore ├── gradlew.bat ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jurl' 2 | 3 | -------------------------------------------------------------------------------- /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - 'benchmarks/**/*' 4 | - 'src/test/**' 5 | - 'src/jmh/**' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/jurl/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | before_script: 5 | - chmod +x gradlew 6 | script: 7 | - ./gradlew check 8 | - ./gradlew jacocoTestReport 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip 6 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/URLPart.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | /** 4 | * URLPart is used to distinguish between the parts of the url when encoding/decoding. 5 | */ 6 | enum URLPart { 7 | CREDENTIALS, 8 | HOST, 9 | PATH, 10 | QUERY, 11 | FRAGMENT, 12 | ENCODE_ZONE, 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/URLParser.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import com.anthonynsimon.url.exceptions.MalformedURLException; 4 | 5 | /** 6 | * URLParser handles the parsing of a URL string into a URL object. 7 | */ 8 | interface URLParser { 9 | /** 10 | * Returns a the URL with the new values after parsing the provided URL string. 11 | */ 12 | URL parse(String url) throws MalformedURLException; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/exceptions/InvalidHexException.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url.exceptions; 2 | 3 | /** 4 | * InvalidHexException is thrown when parsing a Hexadecimal was not 5 | * possible due to bad input. 6 | */ 7 | public class InvalidHexException extends Exception { 8 | public InvalidHexException() { 9 | super(); 10 | } 11 | 12 | public InvalidHexException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/exceptions/MalformedURLException.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url.exceptions; 2 | 3 | /** 4 | * MalformedURLException is thrown when parsing a URL or part of it and it was not 5 | * possible to complete the operation due to bad input. 6 | */ 7 | public class MalformedURLException extends Exception { 8 | public MalformedURLException() { 9 | super(); 10 | } 11 | 12 | public MalformedURLException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/exceptions/InvalidURLReferenceException.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url.exceptions; 2 | 3 | /** 4 | * InvalidURLReferenceException is thrown when attempting to resolve a relative URL against an 5 | * absolute URL and something went wrong. 6 | */ 7 | public class InvalidURLReferenceException extends Exception { 8 | public InvalidURLReferenceException() { 9 | super(); 10 | } 11 | 12 | public InvalidURLReferenceException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/anthonynsimon/url/exceptions/InvalidHexExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url.exceptions; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class InvalidHexExceptionTest { 8 | 9 | @Test(expected = InvalidHexException.class) 10 | public void testNoMessage() throws Exception { 11 | throw new InvalidHexException(); 12 | } 13 | 14 | 15 | @Test(expected = InvalidHexException.class) 16 | public void testWithMessage() throws Exception { 17 | try { 18 | throw new InvalidHexException("with message"); 19 | } catch (InvalidHexException e) { 20 | assertEquals("with message", e.getMessage()); 21 | } 22 | throw new InvalidHexException("with message"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/anthonynsimon/url/exceptions/MalformedURLExecptionTest.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url.exceptions; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class MalformedURLExecptionTest { 8 | 9 | @Test(expected = MalformedURLException.class) 10 | public void testNoMessage() throws Exception { 11 | throw new MalformedURLException(); 12 | } 13 | 14 | 15 | @Test(expected = MalformedURLException.class) 16 | public void testWithMessage() throws Exception { 17 | try { 18 | throw new MalformedURLException("with message"); 19 | } catch (MalformedURLException e) { 20 | assertEquals("with message", e.getMessage()); 21 | } 22 | throw new MalformedURLException("with message"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/anthonynsimon/url/exceptions/InvalidURLReferenceExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url.exceptions; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class InvalidURLReferenceExceptionTest { 8 | 9 | @Test(expected = InvalidURLReferenceException.class) 10 | public void testNoMessage() throws Exception { 11 | throw new InvalidURLReferenceException(); 12 | } 13 | 14 | 15 | @Test(expected = InvalidURLReferenceException.class) 16 | public void testWithMessage() throws Exception { 17 | try { 18 | throw new InvalidURLReferenceException("with message"); 19 | } catch (InvalidURLReferenceException e) { 20 | assertEquals("with message", e.getMessage()); 21 | } 22 | throw new InvalidURLReferenceException("with message"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anthony N. Simon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/jmh/java/com/anthonynsimon/url/BenchmarkJavaURLClass.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import org.openjdk.jmh.annotations.*; 4 | import org.openjdk.jmh.infra.Blackhole; 5 | 6 | import java.net.MalformedURLException; 7 | import java.net.URL; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | @Fork(1) 11 | @Warmup(iterations = 3, time = 3000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Measurement(iterations = 5, time = 3000, timeUnit = TimeUnit.MILLISECONDS) 13 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 14 | @State(Scope.Benchmark) 15 | public class BenchmarkJavaURLClass { 16 | 17 | @State(Scope.Benchmark) 18 | public static class BenchmarkState { 19 | String[] urls = new String[] { 20 | "https://github.com/anthonynsimon/jurl", 21 | "/video.download.akamai.com/2d2c1/Something_Something_(112344_ISMUSP)_v3.ism/QualityLevels(940000)/Fragments(video_eng=5880000000)" 22 | }; 23 | } 24 | 25 | @Benchmark 26 | public void benchmarkParser(BenchmarkState state, Blackhole bh) throws MalformedURLException { 27 | for(int i = 0; i < state.urls.length; i++) { 28 | URL url = new URL(state.urls[i]); 29 | bh.consume(url); 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/jmh/java/com/anthonynsimon/url/BenchmarkDefaultURLParser.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import org.openjdk.jmh.annotations.*; 6 | import org.openjdk.jmh.infra.Blackhole; 7 | 8 | import com.anthonynsimon.url.exceptions.MalformedURLException; 9 | 10 | @Fork(1) 11 | @Warmup(iterations = 3, time = 3000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Measurement(iterations = 5, time = 3000, timeUnit = TimeUnit.MILLISECONDS) 13 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 14 | @State(Scope.Benchmark) 15 | public class BenchmarkDefaultURLParser { 16 | 17 | final static DefaultURLParser parser = new DefaultURLParser(); 18 | 19 | @State(Scope.Benchmark) 20 | public static class BenchmarkState { 21 | String[] urls = new String[] { 22 | "https://github.com/anthonynsimon/jurl", 23 | "/video.download.akamai.com/2d2c1/Something_Something_(112344_ISMUSP)_v3.ism/QualityLevels(940000)/Fragments(video_eng=5880000000)" 24 | }; 25 | } 26 | 27 | @Benchmark 28 | public void benchmarkParser(BenchmarkState state, Blackhole bh) throws MalformedURLException { 29 | for(int i = 0; i < state.urls.length; i++) { 30 | URL url = parser.parse(state.urls[i]); 31 | bh.consume(url); 32 | } 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/URLBuilder.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | /** 4 | * URLBuilder is a helper class for the construction of a URL object. 5 | */ 6 | final class URLBuilder { 7 | private String scheme; 8 | private String username; 9 | private String password; 10 | private String host; 11 | private String path; 12 | private String rawPath; 13 | private String query; 14 | private String fragment; 15 | private String opaque; 16 | 17 | public URL build() { 18 | return new URL(scheme, username, password, host, path, rawPath, query, fragment, opaque); 19 | } 20 | 21 | public URLBuilder setScheme(String scheme) { 22 | this.scheme = scheme; 23 | return this; 24 | } 25 | 26 | public URLBuilder setUsername(String username) { 27 | this.username = username; 28 | return this; 29 | } 30 | 31 | public URLBuilder setPassword(String password) { 32 | this.password = password; 33 | return this; 34 | } 35 | 36 | public URLBuilder setHost(String host) { 37 | this.host = host; 38 | return this; 39 | } 40 | 41 | public URLBuilder setPath(String path) { 42 | this.path = path; 43 | return this; 44 | } 45 | 46 | public URLBuilder setRawPath(String rawPath) { 47 | this.rawPath = rawPath; 48 | return this; 49 | } 50 | 51 | public URLBuilder setQuery(String query) { 52 | this.query = query; 53 | return this; 54 | } 55 | 56 | public URLBuilder setFragment(String fragment) { 57 | this.fragment = fragment; 58 | return this; 59 | } 60 | 61 | public URLBuilder setOpaque(String opaque) { 62 | this.opaque = opaque; 63 | return this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven ### 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | .classpath 12 | .project 13 | org.eclipse.buildship.core.prefs 14 | 15 | # Exclude maven wrapper 16 | !/.mvn/wrapper/maven-wrapper.jar 17 | 18 | ### editors 19 | .vscode 20 | 21 | 22 | ### Intellij ### 23 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 24 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 25 | 26 | # All .idea related files 27 | .idea 28 | *.iml 29 | 30 | # User-specific stuff: 31 | .idea/workspace.xml 32 | .idea/tasks.xml 33 | 34 | # Sensitive or high-churn files: 35 | .idea/dataSources/ 36 | .idea/dataSources.ids 37 | .idea/dataSources.xml 38 | .idea/dataSources.local.xml 39 | .idea/sqlDataSources.xml 40 | .idea/dynamic.xml 41 | .idea/uiDesigner.xml 42 | 43 | # Gradle: 44 | .idea/gradle.xml 45 | .idea/libraries 46 | 47 | # Mongo Explorer plugin: 48 | .idea/mongoSettings.xml 49 | 50 | ## File-based project format: 51 | *.iws 52 | 53 | ## Plugin-specific files: 54 | 55 | # IntelliJ 56 | /out/ 57 | 58 | # mpeltonen/sbt-idea plugin 59 | .idea_modules/ 60 | 61 | # JIRA plugin 62 | atlassian-ide-plugin.xml 63 | 64 | # Crashlytics plugin (for Android Studio and IntelliJ) 65 | com_crashlytics_export_strings.xml 66 | crashlytics.properties 67 | crashlytics-build.properties 68 | fabric.properties 69 | 70 | ### Intellij Patch ### 71 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 72 | 73 | # *.iml 74 | # modules.xml 75 | # .idea/misc.xml 76 | # *.ipr 77 | 78 | 79 | ### Java ### 80 | *.class 81 | 82 | # BlueJ files 83 | *.ctxt 84 | 85 | # Mobile Tools for Java (J2ME) 86 | .mtj.tmp/ 87 | 88 | # Package Files # 89 | *.jar 90 | *.war 91 | *.ear 92 | 93 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 94 | hs_err_pid* 95 | 96 | 97 | ### Gradle ### 98 | .gradle 99 | /build/ 100 | 101 | # Ignore Gradle GUI config 102 | gradle-app.setting 103 | 104 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 105 | !gradle-wrapper.jar 106 | 107 | # Cache of project 108 | .gradletasknamecache 109 | 110 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 111 | # gradle/wrapper/gradle-wrapper.properties 112 | 113 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/PathResolver.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * PathResolver is a utility class that resolves a reference path against a base path. 8 | */ 9 | final class PathResolver { 10 | 11 | /** 12 | * Disallow instantiation of class. 13 | */ 14 | private PathResolver() { 15 | } 16 | 17 | /** 18 | * Returns a resolved path. 19 | *
20 | * For example: 21 | *
22 | * resolve("/some/path", "..") == "/some" 23 | * resolve("/some/path", ".") == "/some/" 24 | * resolve("/some/path", "./here") == "/some/here" 25 | * resolve("/some/path", "../here") == "/here" 26 | */ 27 | public static String resolve(String base, String ref) { 28 | String merged = merge(base, ref); 29 | if (merged == null || merged.isEmpty()) { 30 | return ""; 31 | } 32 | String[] parts = merged.split("/", -1); 33 | return resolve(parts); 34 | } 35 | 36 | /** 37 | * Returns the two path strings merged into one. 38 | *
39 | * For example:
40 | *
68 | * Example:
69 | *
70 | * resolve(String[]{"some", "path", "..", "hello"}) == "/some/hello"
71 | */
72 | private static String resolve(String[] parts) {
73 | if (parts.length == 0) {
74 | return "";
75 | }
76 |
77 | List
12 | * Supports UTF-8 escaping and unescaping.
13 | */
14 | final class PercentEncoder {
15 |
16 | /**
17 | * Reserved characters, allowed in certain parts of the URL. Must be escaped in most cases.
18 | */
19 | private static final char[] reservedChars = {'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"'};
20 | /**
21 | * Unreserved characters do not need to be escaped.
22 | */
23 | private static final char[] unreservedChars = {'-', '_', '.', '~'};
24 | /**
25 | * Byte masks to aid in the decoding of UTF-8 byte arrays.
26 | */
27 | private static final short[] utf8Masks = new short[]{0b00000000, 0b11000000, 0b11100000, 0b11110000};
28 |
29 | /**
30 | * Character set for Hex Strings
31 | */
32 | private static final String hexSet = "0123456789ABCDEF";
33 |
34 | /**
35 | * Disallow instantiation of class.
36 | */
37 | private PercentEncoder() {
38 | }
39 |
40 | /**
41 | * Returns true if escaping is required based on the character and encode zone provided.
42 | */
43 | private static boolean shouldEscapeChar(char c, URLPart zone) {
44 | if ('A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9') {
45 | return false;
46 | }
47 |
48 | if (zone == URLPart.HOST || zone == URLPart.PATH) {
49 | if (c == '%') {
50 | return true;
51 | }
52 | for (char reserved : reservedChars) {
53 | if (reserved == c) {
54 | return false;
55 | }
56 | }
57 | }
58 |
59 | for (char unreserved : unreservedChars) {
60 | if (unreserved == c) {
61 | return false;
62 | }
63 | }
64 |
65 | for (char reserved : new char[]{'$', '&', '+', ',', '/', ':', ';', '=', '?', '@'}) {
66 | if (reserved == c) {
67 | switch (zone) {
68 | case PATH:
69 | return c == '?';
70 | case CREDENTIALS:
71 | return c == '@' || c == '/' || c == '?' || c == ':';
72 | case QUERY:
73 | return true;
74 | case FRAGMENT:
75 | return false;
76 | default:
77 | return true;
78 | }
79 | }
80 | }
81 |
82 | return true;
83 | }
84 |
85 | private static boolean needsEscaping(String str, URLPart zone) {
86 | char[] chars = str.toCharArray();
87 | for (char c : chars) {
88 | if (shouldEscapeChar(c, zone)) {
89 | return true;
90 | }
91 | }
92 | return false;
93 | }
94 |
95 | private static boolean needsUnescaping(String str) {
96 | return (str.indexOf('%') >= 0);
97 | }
98 |
99 | /**
100 | * Returns a percent-escaped string. Each character will be evaluated in case it needs to be escaped
101 | * based on the provided EncodeZone.
102 | */
103 | public static String encode(String str, URLPart zone) {
104 | // The string might not need escaping at all, check first.
105 | if (!needsEscaping(str, zone)) {
106 | return str;
107 | }
108 |
109 | byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
110 |
111 | int i = 0;
112 | String result = "";
113 | while (i < bytes.length) {
114 | int readBytes = 0;
115 | for (short mask : utf8Masks) {
116 | if ((bytes[i] & mask) == mask) {
117 | readBytes++;
118 | } else {
119 | break;
120 | }
121 | }
122 | for (int j = 0; j < readBytes; j++) {
123 | char c = (char) bytes[i];
124 | if (shouldEscapeChar(c, zone)) {
125 | result += "%" + hexSet.charAt((bytes[i] & 0xFF) >> 4) + hexSet.charAt((bytes[i] & 0xFF) & 15);
126 | } else {
127 | result += c;
128 | }
129 | i++;
130 | }
131 | }
132 |
133 | return result;
134 | }
135 |
136 | /**
137 | * Returns an unescaped string.
138 | *
139 | * @throws MalformedURLException if an invalid escape sequence is found.
140 | */
141 | public static String decode(String str) throws MalformedURLException {
142 | // The string might not need unescaping at all, check first.
143 | if (!needsUnescaping(str)) {
144 | return str;
145 | }
146 |
147 | char[] chars = str.toCharArray();
148 | String result = "";
149 | int len = str.length();
150 | int i = 0;
151 | while (i < chars.length) {
152 | char c = chars[i];
153 | if (c != '%') {
154 | result += c;
155 | i++;
156 | } else {
157 | if (i + 2 >= len) {
158 | throw new MalformedURLException("invalid escape sequence");
159 | }
160 | byte code;
161 | try {
162 | code = unhex(str.substring(i + 1, i + 3).toCharArray());
163 | } catch (InvalidHexException e) {
164 | throw new MalformedURLException(e.getMessage());
165 | }
166 | int readBytes = 0;
167 | for (short mask : utf8Masks) {
168 | if ((code & mask) == mask) {
169 | readBytes++;
170 | } else {
171 | break;
172 | }
173 | }
174 | byte[] buffer = new byte[readBytes];
175 | for (int j = 0; j < readBytes; j++) {
176 | if (str.charAt(i) != '%') {
177 | byte[] currentBuffer = new byte[j];
178 | for (int h = 0; h < j; h++) {
179 | currentBuffer[h] = buffer[h];
180 | }
181 | buffer = currentBuffer;
182 | break;
183 | }
184 | if (i + 3 > len) {
185 | buffer = "\uFFFD".getBytes();
186 | break;
187 | }
188 | try {
189 | buffer[j] = unhex(str.substring(i + 1, i + 3).toCharArray());
190 | } catch (InvalidHexException e) {
191 | throw new MalformedURLException(e.getMessage());
192 | }
193 | i += 3;
194 | }
195 | result += new String(buffer);
196 | }
197 | }
198 | return result;
199 | }
200 |
201 | /**
202 | * Returns a byte representation of a parsed array of hex chars.
203 | *
204 | * @throws InvalidHexException if the provided array of hex characters is invalid.
205 | */
206 | private static byte unhex(char[] hex) throws InvalidHexException {
207 | int result = 0;
208 | for (int i = 0; i < hex.length; i++) {
209 | char c = hex[hex.length - i - 1];
210 | int index = -1;
211 | if ('0' <= c && c <= '9') {
212 | index = c - '0';
213 | } else if ('a' <= c && c <= 'f') {
214 | index = c - 'a' + 10;
215 | } else if ('A' <= c && c <= 'F') {
216 | index = c - 'A' + 10;
217 | }
218 | if (index < 0 || index >= 16) {
219 | throw new InvalidHexException("not a valid hex char: " + c);
220 | }
221 | result += index * pow(16, i);
222 | }
223 | return (byte) result;
224 | }
225 |
226 | private static int pow(int base, int exp) {
227 | int result = 1;
228 | int expRemaining = exp;
229 | while (expRemaining > 0) {
230 | result *= base;
231 | expRemaining--;
232 | }
233 | return result;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/main/java/com/anthonynsimon/url/DefaultURLParser.java:
--------------------------------------------------------------------------------
1 | package com.anthonynsimon.url;
2 |
3 | import com.anthonynsimon.url.exceptions.MalformedURLException;
4 |
5 | /**
6 | * A default URL parser implementation.
7 | */
8 | final class DefaultURLParser implements URLParser {
9 |
10 | /**
11 | * Returns a the URL with the new values after parsing the provided URL string.
12 | */
13 | public URL parse(String rawUrl) throws MalformedURLException {
14 | if (rawUrl == null || rawUrl.isEmpty()) {
15 | throw new MalformedURLException("raw url string is empty");
16 | }
17 |
18 | URLBuilder builder = new URLBuilder();
19 | String remaining = rawUrl;
20 |
21 | int index = remaining.lastIndexOf('#');
22 | if (index >= 0) {
23 | String frag = remaining.substring(index + 1, remaining.length());
24 | builder.setFragment(frag.isEmpty() ? null : frag);
25 | remaining = remaining.substring(0, index);
26 | }
27 |
28 | if (remaining.isEmpty()) {
29 | return builder.build();
30 | }
31 |
32 | if ("*".equals(remaining)) {
33 | builder.setPath("*");
34 | return builder.build();
35 | }
36 |
37 | index = remaining.indexOf('?');
38 | if (index > 0) {
39 | String query = remaining.substring(index + 1, remaining.length());
40 | if (query.isEmpty()) {
41 | builder.setQuery("?");
42 | } else {
43 | builder.setQuery(query);
44 | }
45 | remaining = remaining.substring(0, index);
46 | }
47 |
48 | PartialParseResult parsedScheme = parseScheme(remaining);
49 | String scheme = parsedScheme.result;
50 | boolean hasScheme = scheme != null && !scheme.isEmpty();
51 | builder.setScheme(scheme);
52 | remaining = parsedScheme.remaining;
53 |
54 | if (hasScheme && !remaining.startsWith("/")) {
55 | builder.setOpaque(remaining);
56 | return builder.build();
57 | }
58 | if ((hasScheme || !remaining.startsWith("///")) && remaining.startsWith("//")) {
59 | remaining = remaining.substring(2, remaining.length());
60 |
61 | String authority = remaining;
62 | int i = remaining.indexOf('/');
63 | if (i >= 0) {
64 | authority = remaining.substring(0, i);
65 | remaining = remaining.substring(i, remaining.length());
66 | } else {
67 | remaining = "";
68 | }
69 |
70 | if (!authority.isEmpty()) {
71 | UserInfoResult userInfoResult = parseUserInfo(authority);
72 | builder.setUsername(userInfoResult.user);
73 | builder.setPassword(userInfoResult.password);
74 | authority = userInfoResult.remaining;
75 | }
76 |
77 | PartialParseResult hostResult = parseHost(authority);
78 | builder.setHost(hostResult.result);
79 | }
80 |
81 | if (!remaining.isEmpty()) {
82 | builder.setPath(PercentEncoder.decode(remaining));
83 | builder.setRawPath(remaining);
84 | }
85 |
86 | return builder.build();
87 | }
88 |
89 | /**
90 | * Parses the scheme from the provided string.
91 | *
92 | * * @throws MalformedURLException if there was a problem parsing the input string.
93 | */
94 | private PartialParseResult parseScheme(String remaining) throws MalformedURLException {
95 | int indexColon = remaining.indexOf(':');
96 | if (indexColon == 0) {
97 | throw new MalformedURLException("missing scheme");
98 | }
99 | if (indexColon < 0) {
100 | return new PartialParseResult("", remaining);
101 | }
102 |
103 | // if first char is special then its not a scheme
104 | char first = remaining.charAt(0);
105 | if ('0' <= first && first <= '9' || first == '+' || first == '-' || first == '.') {
106 | return new PartialParseResult("", remaining);
107 | }
108 |
109 | String scheme = remaining.substring(0, indexColon).toLowerCase();
110 | String rest = remaining.substring(indexColon + 1, remaining.length());
111 |
112 | return new PartialParseResult(scheme, rest);
113 | }
114 |
115 | /**
116 | * Parses the authority (user:password@host:port) from the provided string.
117 | *
118 | * @throws MalformedURLException if there was a problem parsing the input string.
119 | */
120 | private UserInfoResult parseUserInfo(String str) throws MalformedURLException {
121 | int i = str.lastIndexOf('@');
122 | String username = null;
123 | String password = null;
124 | String rest = str;
125 | if (i >= 0) {
126 | String credentials = str.substring(0, i);
127 | if (credentials.indexOf(':') >= 0) {
128 | String[] parts = credentials.split(":", 2);
129 | username = PercentEncoder.decode(parts[0]);
130 | password = PercentEncoder.decode(parts[1]);
131 | } else {
132 | username = PercentEncoder.decode(credentials);
133 | }
134 | rest = str.substring(i + 1, str.length());
135 | }
136 |
137 | return new UserInfoResult(username, password, rest);
138 | }
139 |
140 | /**
141 | * Parses the host from the provided string. The port is considered part of the host and
142 | * will be checked to ensure that it's a numeric value.
143 | *
144 | * @throws MalformedURLException if there was a problem parsing the input string.
145 | */
146 | private PartialParseResult parseHost(String str) throws MalformedURLException {
147 | if (str.length() == 0) {
148 | return new PartialParseResult("", "");
149 | }
150 | if (str.charAt(0) == '[') {
151 | int i = str.lastIndexOf(']');
152 | if (i < 0) {
153 | throw new MalformedURLException("IPv6 detected, but missing closing ']' token");
154 | }
155 | String portPart = str.substring(i + 1, str.length());
156 | if (!isPortValid(portPart)) {
157 | throw new MalformedURLException("invalid port");
158 | }
159 | } else {
160 | if (str.indexOf(':') != -1) {
161 | String[] parts = str.split(":", -1);
162 | if (parts.length > 2) {
163 | throw new MalformedURLException("invalid host in: " + str);
164 | }
165 | if (parts.length == 2) {
166 | try {
167 | Integer.valueOf(parts[1]);
168 | } catch (NumberFormatException e) {
169 | throw new MalformedURLException("invalid port");
170 | }
171 | }
172 | }
173 | }
174 | return new PartialParseResult(PercentEncoder.decode(str.toLowerCase()), "");
175 | }
176 |
177 | /**
178 | * Returns true if the provided port string contains a valid port number.
179 | * Note that an empty string is a valid port number since it's optional.
180 | *
181 | * For example:
182 | *
183 | * '' => TRUE
184 | * null => TRUE
185 | * ':8080' => TRUE
186 | * ':ab80' => FALSE
187 | * ':abc' => FALSE
188 | */
189 | protected boolean isPortValid(String portStr) {
190 | if (portStr == null || portStr.isEmpty()) {
191 | return true;
192 | }
193 | int i = portStr.indexOf(':');
194 | // Port format must be ':8080'
195 | if (i != 0) {
196 | return false;
197 | }
198 | String segment = portStr.substring(i + 1, portStr.length());
199 | try {
200 | Integer.valueOf(segment);
201 | } catch (NumberFormatException e) {
202 | return false;
203 | }
204 | return true;
205 | }
206 |
207 | private class PartialParseResult {
208 | public final String result;
209 | public final String remaining;
210 |
211 |
212 | public PartialParseResult(String result, String remaining) {
213 | this.result = result;
214 | this.remaining = remaining;
215 | }
216 | }
217 |
218 | private class UserInfoResult {
219 | public final String user;
220 | public final String password;
221 | public final String remaining;
222 |
223 |
224 | public UserInfoResult(String user, String password, String remaining) {
225 | this.user = user;
226 | this.password = password;
227 | this.remaining = remaining;
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/main/java/com/anthonynsimon/url/URL.java:
--------------------------------------------------------------------------------
1 | package com.anthonynsimon.url;
2 |
3 | import com.anthonynsimon.url.exceptions.InvalidURLReferenceException;
4 | import com.anthonynsimon.url.exceptions.MalformedURLException;
5 |
6 | import java.io.Serializable;
7 | import java.net.URI;
8 | import java.net.URISyntaxException;
9 | import java.util.*;
10 |
11 | /**
12 | * URL is a reference to a web resource. This class implements functionality for parsing and
13 | * manipulating the various parts that make up a URL.
14 | *
15 | * Once parsed it is of the form:
16 | *
17 | * scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
18 | */
19 | public final class URL implements Serializable {
20 |
21 | /**
22 | * Unique ID for serialization purposes.
23 | */
24 | private static final long serialVersionUID = 80443L;
25 |
26 | /**
27 | * URLParser to be used to parse the URL string into the URL object.
28 | * Do not serialize.
29 | */
30 | private transient final static URLParser URL_PARSER = new DefaultURLParser();
31 |
32 | private final String scheme;
33 | private final String username;
34 | private final String password;
35 | private final String host;
36 | private final String hostname;
37 | private final Integer port;
38 | private final String path;
39 | private final String rawPath;
40 | private final String query;
41 | private final String fragment;
42 | private final String opaque;
43 |
44 | /**
45 | * Cached parsed query string key-value pairs.
46 | * Do not serialize.
47 | */
48 | private transient Map
344 | * If the reference URL is absolute, then it simply creates a new URL that is identical to it
345 | * and returns it. If the reference and the base URLs are identical, a new instance of the reference is returned.
346 | *
347 | * @throws InvalidURLReferenceException if the provided ref URL is invalid or if the base URL is not absolute.
348 | */
349 | public URL resolveReference(URL ref) throws InvalidURLReferenceException {
350 | if (!isAbsolute()) {
351 | throw new InvalidURLReferenceException("base url is not absolute");
352 | }
353 | if (ref == null) {
354 | throw new InvalidURLReferenceException("reference url is null");
355 | }
356 |
357 | URLBuilder builder = new URLBuilder()
358 | .setScheme(ref.getScheme())
359 | .setUsername(ref.getUsername())
360 | .setPassword(ref.getPassword())
361 | .setHost(ref.getHost())
362 | .setPath(ref.getPath())
363 | .setQuery(ref.getQuery())
364 | .setFragment(ref.getFragment())
365 | .setOpaque(ref.getOpaque());
366 |
367 | if (!ref.isAbsolute()) {
368 | builder.setScheme(scheme);
369 | }
370 |
371 | if (!nullOrEmpty(ref.scheme) || !nullOrEmpty(ref.host)) {
372 | builder.setPath(PathResolver.resolve(ref.path, ""));
373 | return builder.build();
374 | }
375 |
376 | if (ref.isOpaque() || isOpaque()) {
377 | return builder.build();
378 | }
379 |
380 | return builder
381 | .setHost(host)
382 | .setUsername(username)
383 | .setPassword(password)
384 | .setPath(PathResolver.resolve(path, ref.path))
385 | .build();
386 | }
387 |
388 |
389 | private static String mapToNullIfEmpty(String str) {
390 | return str != null && !str.isEmpty() ? str : null;
391 | }
392 |
393 | /**
394 | * Returns the full host (hostname:port), maps to null if none are set.
395 | */
396 | private static String mergeHostPortIfSet(String hostname, Integer port) {
397 | StringBuilder sb = new StringBuilder();
398 | boolean exists = false;
399 | if (hostname != null) {
400 | sb.append(hostname);
401 | exists = true;
402 | }
403 | if (port != null) {
404 | sb.append(":");
405 | sb.append(port);
406 | exists = true;
407 | }
408 | if (exists) {
409 | return sb.toString();
410 | }
411 | return null;
412 | }
413 |
414 | /**
415 | * Returns the hostname part of the host ('www.example.com' or '192.168.0.1' or '[fde2:d7de:302::]') if it exists.
416 | */
417 | private static String extractHostname(String host) {
418 | if (host != null) {
419 | int separator = host.lastIndexOf(":");
420 | if (separator > -1) {
421 | return host.substring(0, separator);
422 | }
423 | return host;
424 | }
425 | return null;
426 | }
427 |
428 | /**
429 | * Returns the port part of the host (i.e. 8080 or 443 or 3000) if it exists.
430 | */
431 | private static Integer extractPort(String host) {
432 | if (host != null) {
433 | int separator = host.lastIndexOf(":");
434 | if (separator > -1) {
435 | String part = host.substring(separator + 1, host.length());
436 | if (part != null && part != "") {
437 | try {
438 | return Integer.parseInt(part);
439 | } catch (NumberFormatException exception) {
440 | return null;
441 | }
442 | }
443 | }
444 | }
445 | return null;
446 | }
447 | }
448 |
--------------------------------------------------------------------------------
/src/test/java/com/anthonynsimon/url/URLTest.java:
--------------------------------------------------------------------------------
1 | package com.anthonynsimon.url;
2 |
3 | import com.anthonynsimon.url.exceptions.InvalidURLReferenceException;
4 | import com.anthonynsimon.url.exceptions.MalformedURLException;
5 | import org.junit.Assert;
6 | import org.junit.Test;
7 |
8 | import java.util.*;
9 |
10 | public class URLTest {
11 |
12 | private URLTestCase[] urlTestCases = {
13 | // No path
14 | new URLTestCase(
15 | "http://www.example.com",
16 | "http",
17 | null,
18 | null,
19 | "www.example.com",
20 | null,
21 | null,
22 | null,
23 | "http://www.example.com"
24 | ),
25 | // With path
26 | new URLTestCase(
27 | "http://www.example.com/",
28 | "http",
29 | null,
30 | null,
31 | "www.example.com",
32 | "/",
33 | null,
34 | null,
35 | "http://www.example.com/"
36 | ),
37 | // Path with hex escaping
38 | new URLTestCase(
39 | "http://www.example.com/path%20one%20two%26three",
40 | "http",
41 | null,
42 | null,
43 | "www.example.com",
44 | "/path one two&three",
45 | null,
46 | null,
47 | "http://www.example.com/path%20one%20two%26three"
48 | ),
49 | // Non - ASCII
50 | new URLTestCase(
51 | "http://test.ü€€€€€𡺸.com/foo",
52 | "http",
53 | null,
54 | null,
55 | "test.ü€€€€€𡺸.com",
56 | "/foo",
57 | null,
58 | null,
59 | "http://test.%C3%BC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%F0%A1%BA%B8.com/foo"
60 | ),
61 | // Non-ASCII
62 | new URLTestCase(
63 | "http://test.%C3%BC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%F0%A1%BA%B8.com/foo",
64 | "http",
65 | null,
66 | null,
67 | "test.ü€€€€€𡺸.com",
68 | "/foo",
69 | null,
70 | null,
71 | "http://test.%C3%BC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%F0%A1%BA%B8.com/foo"
72 | ),
73 | // Username
74 | new URLTestCase(
75 | "ftp://me@www.example.com/",
76 | "ftp",
77 | "me",
78 | null,
79 | "www.example.com",
80 | "/",
81 | null,
82 | null,
83 | "ftp://me@www.example.com/"
84 | ),
85 | // Username with escaping
86 | new URLTestCase(
87 | "ftp://me%20again@www.example.com/",
88 | "ftp",
89 | "me again",
90 | null,
91 | "www.example.com",
92 | "/",
93 | null,
94 | null,
95 | "ftp://me%20again@www.example.com/"
96 | ),
97 | // Empty query string
98 | new URLTestCase(
99 | "http://www.example.com/?",
100 | "http",
101 | null,
102 | null,
103 | "www.example.com",
104 | "/",
105 | "?",
106 | null,
107 | "http://www.example.com/?"
108 | ),
109 | // Query string ending in query char
110 | new URLTestCase(
111 | "http://www.example.com/?foo=bar?",
112 | "http",
113 | null,
114 | null,
115 | "www.example.com",
116 | "/",
117 | "foo=bar?",
118 | null,
119 | "http://www.example.com/?foo=bar?"
120 | ),
121 | // Query string
122 | new URLTestCase(
123 | "http://www.example.com/?q=one+two",
124 | "http",
125 | null,
126 | null,
127 | "www.example.com",
128 | "/",
129 | "q=one+two",
130 | null,
131 | "http://www.example.com/?q=one+two"
132 | ),
133 | // Query string with multiple values
134 | new URLTestCase(
135 | "http://www.example.com/?q=one+two&key=value&another",
136 | "http",
137 | null,
138 | null,
139 | "www.example.com",
140 | "/",
141 | "q=one+two&key=value&another",
142 | null,
143 | "http://www.example.com/?q=one+two&key=value&another"
144 | ),
145 | // Query with hex escaping
146 | new URLTestCase(
147 | "http://www.example.com/?q=one%20two",
148 | "http",
149 | null,
150 | null,
151 | "www.example.com",
152 | "/",
153 | "q=one%20two",
154 | null,
155 | "http://www.example.com/?q=one%20two"
156 | ),
157 | // Hex escaping outside query
158 | new URLTestCase(
159 | "http://www.example.com/one%20two?q=three+four",
160 | "http",
161 | null,
162 | null,
163 | "www.example.com",
164 | "/one two",
165 | "q=three+four",
166 | null,
167 | "http://www.example.com/one%20two?q=three+four"
168 | ),
169 | // Path without leading /
170 | new URLTestCase(
171 | "http:www.example.com/one%20two?q=three+four",
172 | "http",
173 | null,
174 | null,
175 | null,
176 | null,
177 | "q=three+four",
178 | null,
179 | "http:www.example.com/one%20two?q=three+four"
180 | ),
181 | // Path without leading /, escaped
182 | new URLTestCase(
183 | "http:%2f%2fwww.example.com/one%20two?q=three+four",
184 | "http",
185 | null,
186 | null,
187 | null,
188 | null,
189 | "q=three+four",
190 | null,
191 | "http:%2f%2fwww.example.com/one%20two?q=three+four"
192 | ),
193 | // Opaque with fragment
194 | new URLTestCase(
195 | "http:%2f%2fwww.example.com/one%20two?q=three+four#flag",
196 | "http",
197 | null,
198 | null,
199 | null,
200 | null,
201 | "q=three+four",
202 | "flag",
203 | "http:%2f%2fwww.example.com/one%20two?q=three+four#flag"
204 | ),
205 | // Non-authority with path
206 | new URLTestCase(
207 | "mailto:/admin@example.com",
208 | "mailto",
209 | null,
210 | null,
211 | null,
212 | "/admin@example.com",
213 | null,
214 | null,
215 | "mailto:///admin@example.com"
216 | ),
217 | // Non-authority
218 | new URLTestCase(
219 | "mailto:admin@example.com",
220 | "mailto",
221 | null,
222 | null,
223 | null,
224 | null,
225 | null,
226 | null,
227 | "mailto:admin@example.com"
228 | ),
229 | // Unescaped :// should not create scheme
230 | new URLTestCase(
231 | "/foo?q=http://something",
232 | null,
233 | null,
234 | null,
235 | null,
236 | "/foo",
237 | "q=http://something",
238 | null,
239 | "/foo?q=http://something"
240 | ),
241 | // Leading // without scheme should create an authority
242 | new URLTestCase(
243 | "//foo",
244 | null,
245 | null,
246 | null,
247 | "foo",
248 | null,
249 | null,
250 | null,
251 | "foo"
252 | ),
253 | // Leading // without scheme, with credentials and query
254 | new URLTestCase(
255 | "//user@foo/path?a=b",
256 | null,
257 | "user",
258 | null,
259 | "foo",
260 | "/path",
261 | "a=b",
262 | null,
263 | "user@foo/path?a=b"
264 | ),
265 | // Three leading slashes
266 | new URLTestCase(
267 | "///hello",
268 | null,
269 | null,
270 | null,
271 | null,
272 | "///hello",
273 | null,
274 | null,
275 | "///hello"
276 | ),
277 | // Don't try to resolve path
278 | new URLTestCase(
279 | "http://example.com/abc/..",
280 | "http",
281 | null,
282 | null,
283 | "example.com",
284 | "/abc/..",
285 | null,
286 | null,
287 | "http://example.com/abc/.."
288 | ),
289 | // Username and password
290 | new URLTestCase(
291 | "https://user:password@example.com",
292 | "https",
293 | "user",
294 | "password",
295 | "example.com",
296 | null,
297 | null,
298 | null,
299 | "https://user:password@example.com"
300 | ),
301 | // Unescaped @ in username
302 | new URLTestCase(
303 | "https://us@r:password@example.com",
304 | "https",
305 | "us@r",
306 | "password",
307 | "example.com",
308 | null,
309 | null,
310 | null,
311 | "https://us%40r:password@example.com"
312 | ),
313 | // Unescaped @ in password
314 | new URLTestCase(
315 | "https://user:p@ssword@example.com",
316 | "https",
317 | "user",
318 | "p@ssword",
319 | "example.com",
320 | null,
321 | null,
322 | null,
323 | "https://user:p%40ssword@example.com"
324 | ),
325 | // Unescaped @ in everywhere
326 | new URLTestCase(
327 | "https://us@r:p@ssword@example.com/p@th?q=@here",
328 | "https",
329 | "us@r",
330 | "p@ssword",
331 | "example.com",
332 | "/p@th",
333 | "q=@here",
334 | null,
335 | "https://us%40r:p%40ssword@example.com/p@th?q=@here"
336 | ),
337 | // Query string and fragment
338 | new URLTestCase(
339 | "https://www.example.de/?q=foo#q=bar",
340 | "https",
341 | null,
342 | null,
343 | "www.example.de",
344 | "/",
345 | "q=foo",
346 | "q=bar",
347 | "https://www.example.de/?q=foo#q=bar"
348 | ),
349 | // Query string and fragment with escaped chars
350 | new URLTestCase(
351 | "https://www.example.de/?q%26foo#a%26b",
352 | "https",
353 | null,
354 | null,
355 | "www.example.de",
356 | "/",
357 | "q%26foo",
358 | "a%26b",
359 | "https://www.example.de/?q%26foo#a%26b"
360 | ),
361 | // File path
362 | new URLTestCase(
363 | "file:///user/docs/recent",
364 | "file",
365 | null,
366 | null,
367 | null,
368 | "/user/docs/recent",
369 | null,
370 | null,
371 | "file:///user/docs/recent"
372 | ),
373 | // Windows file path
374 | new URLTestCase(
375 | "file:///C:/User/Docs/recent.xlsx",
376 | "file",
377 | null,
378 | null,
379 | null,
380 | "/C:/User/Docs/recent.xlsx",
381 | null,
382 | null,
383 | "file:///C:/User/Docs/recent.xlsx"
384 | ),
385 | // Case-insensitive scheme
386 | new URLTestCase(
387 | "HTTP://example.com",
388 | "http",
389 | null,
390 | null,
391 | "example.com",
392 | null,
393 | null,
394 | null,
395 | "http://example.com"
396 | ),
397 | // Relative path
398 | new URLTestCase(
399 | "abc/123/xyz",
400 | null,
401 | null,
402 | null,
403 | null,
404 | "abc/123/xyz",
405 | null,
406 | null,
407 | "abc/123/xyz"
408 | ),
409 | // '*' path
410 | new URLTestCase(
411 | "*",
412 | null,
413 | null,
414 | null,
415 | null,
416 | "*",
417 | null,
418 | null,
419 | "*"
420 | ),
421 | // '*' path with query and fragment
422 | new URLTestCase(
423 | "*?key=value#frag",
424 | null,
425 | null,
426 | null,
427 | null,
428 | "*",
429 | "key=value",
430 | "frag",
431 | "*?key=value#frag"
432 | ),
433 | // Escaped ? in credentials
434 | new URLTestCase(
435 | "https://us%3Fer:p%3fssword@example.com",
436 | "https",
437 | "us?er",
438 | "p?ssword",
439 | "example.com",
440 | null,
441 | null,
442 | null,
443 | "https://us%3Fer:p%3Fssword@example.com"
444 | ),
445 | // IPv4 address
446 | new URLTestCase(
447 | "http://192.168.0.1",
448 | "http",
449 | null,
450 | null,
451 | "192.168.0.1",
452 | null,
453 | null,
454 | null,
455 | "http://192.168.0.1"
456 | ),
457 | // IPv4 address with port
458 | new URLTestCase(
459 | "http://192.168.0.1:8080",
460 | "http",
461 | null,
462 | null,
463 | "192.168.0.1:8080",
464 | null,
465 | null,
466 | null,
467 | "http://192.168.0.1:8080"
468 | ),
469 | // IPv4 address with path
470 | new URLTestCase(
471 | "http://192.168.0.1/",
472 | "http",
473 | null,
474 | null,
475 | "192.168.0.1",
476 | "/",
477 | null,
478 | null,
479 | "http://192.168.0.1/"
480 | ),
481 | // IPv4 address with port and path
482 | new URLTestCase(
483 | "http://192.168.0.1:8080/",
484 | "http",
485 | null,
486 | null,
487 | "192.168.0.1:8080",
488 | "/",
489 | null,
490 | null,
491 | "http://192.168.0.1:8080/"
492 | ),
493 | // IPv6 address with port and path
494 | new URLTestCase(
495 | "http://[fe80::1]:8080/",
496 | "http",
497 | null,
498 | null,
499 | "[fe80::1]:8080",
500 | "/",
501 | null,
502 | null,
503 | "http://[fe80::1]:8080/"
504 | ),
505 | // IPv6 address
506 | new URLTestCase(
507 | "http://[fe80::1]",
508 | "http",
509 | null,
510 | null,
511 | "[fe80::1]",
512 | null,
513 | null,
514 | null,
515 | "http://[fe80::1]"
516 | ),
517 | // IPv6 address with port
518 | new URLTestCase(
519 | "http://[fe80::1]:8080",
520 | "http",
521 | null,
522 | null,
523 | "[fe80::1]:8080",
524 | null,
525 | null,
526 | null,
527 | "http://[fe80::1]:8080"
528 | ),
529 | // IPv6 address with port, path and query
530 | new URLTestCase(
531 | "http://[fe80::1]:8080/?q=foo",
532 | "http",
533 | null,
534 | null,
535 | "[fe80::1]:8080",
536 | "/",
537 | "q=foo",
538 | null,
539 | "http://[fe80::1]:8080/?q=foo"
540 | ),
541 | // IPv6 address with zone identifier, port, path and query
542 | new URLTestCase(
543 | "http://[fe80::1%25en0]:8080/?q=foo",
544 | "http",
545 | null,
546 | null,
547 | "[fe80::1%en0]:8080",
548 | "/",
549 | "q=foo",
550 | null,
551 | "http://[fe80::1%25en0]:8080/?q=foo"
552 | ),
553 | // IPv6 address with zone identifier special chars
554 | new URLTestCase(
555 | "http://[fe80::1%25%65%6e%301-._~]/",
556 | "http",
557 | null,
558 | null,
559 | "[fe80::1%en01-._~]",
560 | "/",
561 | null,
562 | null,
563 | "http://[fe80::1%25en01-._~]/"
564 | ),
565 | // IPv6 address with zone identifier special chars and port
566 | new URLTestCase(
567 | "http://[fe80::1%25%65%6e%301-._~]:8080/",
568 | "http",
569 | null,
570 | null,
571 | "[fe80::1%en01-._~]:8080",
572 | "/",
573 | null,
574 | null,
575 | "http://[fe80::1%25en01-._~]:8080/"
576 | ),
577 | // Alternate escape
578 | new URLTestCase(
579 | "http://rest.rsc.io/foo%2fbar/baz%2Fquux?alt=media",
580 | "http",
581 | null,
582 | null,
583 | "rest.rsc.io",
584 | "/foo/bar/baz/quux",
585 | "alt=media",
586 | null,
587 | "http://rest.rsc.io/foo%2fbar/baz%2Fquux?alt=media"
588 | ),
589 | // Commas in host
590 | new URLTestCase(
591 | "psql://a,b,c/foo",
592 | "psql",
593 | null,
594 | null,
595 | "a,b,c",
596 | "/foo",
597 | null,
598 | null,
599 | "psql://a,b,c/foo"
600 | ),
601 | // Difficult case host
602 | new URLTestCase(
603 | "http://!$&'()*+,;=hello!:8080/path",
604 | "http",
605 | null,
606 | null,
607 | "!$&'()*+,;=hello!:8080",
608 | "/path",
609 | null,
610 | null,
611 | "http://!$&'()*+,;=hello!:8080/path"
612 | ),
613 | // Difficult case path
614 | new URLTestCase(
615 | "http://host/!$&'()*+,;=:@[hello]",
616 | "http",
617 | null,
618 | null,
619 | "host",
620 | "/!$&'()*+,;=:@[hello]",
621 | null,
622 | null,
623 | "http://host/!$&'()*+,;=:@[hello]"
624 | ),
625 | // Special chars in path
626 | new URLTestCase(
627 | "http://host/abc/[one_two]",
628 | "http",
629 | null,
630 | null,
631 | "host",
632 | "/abc/[one_two]",
633 | null,
634 | null,
635 | "http://host/abc/[one_two]"
636 | ),
637 | // IPv6
638 | new URLTestCase(
639 | "http://[2001:1890:1112:1::20]/foo",
640 | "http",
641 | null,
642 | null,
643 | "[2001:1890:1112:1::20]",
644 | "/foo",
645 | null,
646 | null,
647 | "http://[2001:1890:1112:1::20]/foo"
648 | ),
649 | // IPv6
650 | new URLTestCase(
651 | "http://[fde2:d7de:302::]",
652 | "http",
653 | null,
654 | null,
655 | "[fde2:d7de:302::]",
656 | null,
657 | null,
658 | null,
659 | "http://[fde2:d7de:302::]"
660 | ),
661 | // IPv6 with spaces in scope IDs are the place where they're allowed
662 | new URLTestCase(
663 | "http://[fde2:d7de:302::%25are%20you%20being%20serious]",
664 | "http",
665 | null,
666 | null,
667 | "[fde2:d7de:302::%are you being serious]",
668 | null,
669 | null,
670 | null,
671 | "http://[fde2:d7de:302::%25are%20you%20being%20serious]"
672 | ),
673 | new URLTestCase(
674 | "http://test.com//foo",
675 | "http",
676 | null,
677 | null,
678 | "test.com",
679 | "//foo",
680 | null,
681 | null,
682 | "http://test.com//foo"
683 | ),
684 | // Lowercase escape hex digits
685 | new URLTestCase(
686 | "http://TEST.%e4%b8%96%e7%95%8c.com/foo",
687 | "http",
688 | null,
689 | null,
690 | "test.世界.com",
691 | "/foo",
692 | null,
693 | null,
694 | "http://test.%E4%B8%96%E7%95%8C.com/foo"
695 | ),
696 | // More UTF-8
697 | new URLTestCase(
698 | "https://user:secret@example♬.com/path/to/my/dir?search=one+two#about",
699 | "https",
700 | "user",
701 | "secret",
702 | "example♬.com",
703 | "/path/to/my/dir",
704 | "search=one+two",
705 | "about",
706 | "https://user:secret@example%E2%99%AC.com/path/to/my/dir?search=one+two#about"
707 | ),
708 |
709 | // Percent encoding in url path
710 | new URLTestCase(
711 | "http://abc.net/1160x%3E/quality/",
712 | "http",
713 | null,
714 | null,
715 | "abc.net",
716 | "/1160x>/quality/",
717 | null,
718 | null,
719 | "http://abc.net/1160x%3E/quality/"
720 | ),
721 |
722 | // Percent encoding in url path
723 | new URLTestCase(
724 | "http://db-engines.com/en/system/PostgreSQL%3BRocksDB",
725 | "http",
726 | null,
727 | null,
728 | "db-engines.com",
729 | "/en/system/PostgreSQL;RocksDB",
730 | null,
731 | null,
732 | "http://db-engines.com/en/system/PostgreSQL%3BRocksDB"
733 | ),
734 |
735 | // Percent encoding in url path
736 | new URLTestCase(
737 | "http://xzy.org/test/hei%DFfl",
738 | "http",
739 | null,
740 | null,
741 | "xzy.org",
742 | "/test/hei�fl",
743 | null,
744 | null,
745 | "http://xzy.org/test/hei%DFfl"),
746 |
747 | // Percent encoding in url path
748 | new URLTestCase(
749 | "http://www.net/decom/category/AA/A_%26_BBB/AAA_%26_BBB/",
750 | "http",
751 | null,
752 | null,
753 | "www.net",
754 | "/decom/category/AA/A_&_BBB/AAA_&_BBB/",
755 | null,
756 | null,
757 | "http://www.net/decom/category/AA/A_%26_BBB/AAA_%26_BBB/"
758 | ),
759 |
760 | // Percent encoding in url path
761 | new URLTestCase(
762 | "https://en.wikipedia.org/wiki/Eat_one%27s_own_dog_food",
763 | "https",
764 | null,
765 | null,
766 | "en.wikipedia.org",
767 | "/wiki/Eat_one's_own_dog_food",
768 | null,
769 | null,
770 | "https://en.wikipedia.org/wiki/Eat_one%27s_own_dog_food"
771 | ),
772 | };
773 |
774 | private String[] toJavaClassCases = {
775 | "http://www.example.com",
776 | "http://www.example.com/path/to/my/file.html",
777 | "http://www.example.com/path/to/my/file.html?q=key/value",
778 | "http://www.example.com/path/to/my/file.html?q=key/value#fragment",
779 | "http://example/path/to/my/file?q=key/value#fragment",
780 | "http://example/path/to/my/file?q=http://testing/value#fragment",
781 | "https://username:password@host.com:8080/path/goes/here?search=for+this,and+this&another=true#fragment",
782 | "https://192.168.1.1:443",
783 | "http://[::1]:8080"
784 | };
785 |
786 | private URLReferenceTestCase[] resolveReferenceCases = new URLReferenceTestCase[]{
787 | new URLReferenceTestCase(
788 | "http://www.domain.com/path/to/RESOURCE.html",
789 | "http://www.domain.com/path/to/ANOTHER_RESOURCE.html?q=abc#section",
790 | "http://www.domain.com/path/to/ANOTHER_RESOURCE.html?q=abc#section"
791 | ),
792 | new URLReferenceTestCase(
793 | "http://www.domain.com/path/to/RESOURCE.html",
794 | "/path/to/ANOTHER_RESOURCE.html?q=abc#section",
795 | "http://www.domain.com/path/to/ANOTHER_RESOURCE.html?q=abc#section"
796 | ),
797 | new URLReferenceTestCase(
798 | "http://www.domain.com/?q=foo",
799 | "/path?q=abc#section",
800 | "http://www.domain.com/path?q=abc#section"
801 | ),
802 | new URLReferenceTestCase(
803 | "http://www.domain.com/?q=foo",
804 | "#section",
805 | "http://www.domain.com/#section"
806 | ),
807 | new URLReferenceTestCase(
808 | "http://www.domain.com/bar",
809 | "/foo",
810 | "http://www.domain.com/foo"
811 | ),
812 | new URLReferenceTestCase(
813 | "http://www.domain.com/bar?q#y",
814 | "/foo",
815 | "http://www.domain.com/foo"
816 | ),
817 | new URLReferenceTestCase(
818 | "http://www.domain.com/bar?q#y",
819 | "/foo?k#z",
820 | "http://www.domain.com/foo?k#z"
821 | ),
822 | new URLReferenceTestCase(
823 | "mailto:user@example.com",
824 | "//example.com",
825 | "mailto://example.com"
826 | ),
827 | new URLReferenceTestCase(
828 | "http://www.domain.com/bar?q#y",
829 | "//example.com",
830 | "http://example.com"
831 | ),
832 | new URLReferenceTestCase(
833 | "http://www.domain.com",
834 | "/path",
835 | "http://www.domain.com/path"
836 | ),
837 | new URLReferenceTestCase(
838 | "http://www.domain.com",
839 | "home",
840 | "http://www.domain.com/home"
841 | ),
842 | new URLReferenceTestCase(
843 | "http://www.domain.com/",
844 | "path",
845 | "http://www.domain.com/path"
846 | ),
847 | new URLReferenceTestCase(
848 | "http://www.domain.com/here/there",
849 | "/here/there/that",
850 | "http://www.domain.com/here/there/that"
851 | ),
852 | new URLReferenceTestCase(
853 | "http://www.domain.com/one/two",
854 | "three",
855 | "http://www.domain.com/one/three"
856 | ),
857 | new URLReferenceTestCase(
858 | "http://www.domain.com/one/two",
859 | "//example.com/three",
860 | "http://example.com/three"
861 | ),
862 | new URLReferenceTestCase(
863 | "http://192.168.0.1/one",
864 | "http://192.168.0.1/three",
865 | "http://192.168.0.1/three"
866 | ),
867 | new URLReferenceTestCase(
868 | "http://192.168.0.1/one",
869 | "/three",
870 | "http://192.168.0.1/three"
871 | ),
872 | new URLReferenceTestCase(
873 | "http://192.168.0.1:44/one",
874 | "/three",
875 | "http://192.168.0.1:44/three"
876 | ),
877 | new URLReferenceTestCase(
878 | "http://192.168.0.1/one",
879 | "three",
880 | "http://192.168.0.1/three"
881 | ),
882 | new URLReferenceTestCase(
883 | "http://192.168.0.1",
884 | "three",
885 | "http://192.168.0.1/three"
886 | ),
887 | new URLReferenceTestCase(
888 | "http://[1080::8:800:200C:417A]/foo",
889 | "three",
890 | "http://[1080::8:800:200c:417a]/three"
891 | ),
892 | new URLReferenceTestCase(
893 | "http://[1080::8:800:200C:417A]:9090/foo",
894 | "three",
895 | "http://[1080::8:800:200c:417a]:9090/three"
896 | ),
897 | new URLReferenceTestCase(
898 | "http://[1080::8:800:200C:417A]:9090/foo/",
899 | "three",
900 | "http://[1080::8:800:200c:417a]:9090/foo/three"
901 | ),
902 | new URLReferenceTestCase(
903 | "http://[1080::8:800:200C:417A]:9090/foo/a",
904 | "three",
905 | "http://[1080::8:800:200c:417a]:9090/foo/three"
906 | ),
907 | // Opaque base
908 | new URLReferenceTestCase(
909 | "http:www.domain.com/",
910 | "path",
911 | "http:///path"
912 | ),
913 | new URLReferenceTestCase(
914 | "http://www.domain.com/",
915 | "mailto:user@domain.com",
916 | "mailto:user@domain.com"
917 | ),
918 | new URLReferenceTestCase(
919 | "mailto:user@domain.com",
920 | "mailto:another@test.de",
921 | "mailto:another@test.de"
922 | ),
923 | new URLReferenceTestCase(
924 | "file://",
925 | "/documents",
926 | "file:///documents"
927 | ),
928 | new URLReferenceTestCase(
929 | "file:///",
930 | "/documents",
931 | "file:///documents"
932 | ),
933 | new URLReferenceTestCase(
934 | "file:///home",
935 | "/documents",
936 | "file:///documents"
937 | ),
938 | new URLReferenceTestCase(
939 | "file:///user/documents",
940 | "pictures",
941 | "file:///user/pictures"
942 | ),
943 | new URLReferenceTestCase(
944 | "file:///user/documents/one",
945 | "pictures",
946 | "file:///user/documents/pictures"
947 | ),
948 | new URLReferenceTestCase(
949 | "file:///",
950 | "pictures",
951 | "file:///pictures"
952 | ),
953 | new URLReferenceTestCase(
954 | "file:///home/user/",
955 | "../here",
956 | "file:///home/here"
957 | ),
958 | new URLReferenceTestCase(
959 | "file:///home/user/",
960 | "..",
961 | "file:///home/"
962 | ),
963 | new URLReferenceTestCase(
964 | "file:///home/user/",
965 | ".",
966 | "file:///home/user/"
967 | ),
968 | new URLReferenceTestCase(
969 | "file:///home/user",
970 | ".",
971 | "file:///home/"
972 | ),
973 | new URLReferenceTestCase(
974 | "http://home.com/user",
975 | "../../../",
976 | "http://home.com/"
977 | ),
978 | new URLReferenceTestCase(
979 | "http://home.com/user/test",
980 | "foo/bar/../last",
981 | "http://home.com/user/foo/last"
982 | ),
983 | new URLReferenceTestCase(
984 | "http://home.com/user/test",
985 | "foo/bar/../last/..",
986 | "http://home.com/user/foo/"
987 | ),
988 | new URLReferenceTestCase(
989 | "http://home.com/user/test",
990 | "./..",
991 | "http://home.com/"
992 | ),
993 | new URLReferenceTestCase(
994 | "http://home.com/user/test",
995 | ".",
996 | "http://home.com/user/"
997 | ),
998 | new URLReferenceTestCase(
999 | "http://home.com/user/test",
1000 | "..",
1001 | "http://home.com/"
1002 | ),
1003 | new URLReferenceTestCase(
1004 | "http://home.com",
1005 | ".",
1006 | "http://home.com/"
1007 | ),
1008 | new URLReferenceTestCase(
1009 | "http://home.com/",
1010 | ".",
1011 | "http://home.com/"
1012 | ),
1013 | new URLReferenceTestCase(
1014 | "http://home.com/foo",
1015 | ".",
1016 | "http://home.com/"
1017 | ),
1018 | new URLReferenceTestCase(
1019 | "http://home.com/foo/",
1020 | ".",
1021 | "http://home.com/foo/"
1022 | ),
1023 | new URLReferenceTestCase(
1024 | "http://home.com/foo/",
1025 | "../../../../bar",
1026 | "http://home.com/bar"
1027 | ),
1028 | new URLReferenceTestCase(
1029 | "http://home.com/foo/",
1030 | "./../../.././../bar",
1031 | "http://home.com/bar"
1032 | ),
1033 | new URLReferenceTestCase(
1034 | "http://home.com/foo/",
1035 | "./../../.././../bar/",
1036 | "http://home.com/bar"
1037 | ),
1038 | new URLReferenceTestCase(
1039 | "http://home.com/foo/bar",
1040 | "a/./b/../c/../d/./last/..",
1041 | "http://home.com/foo/a/d/"
1042 | ),
1043 | // Triple dots do not affect the path
1044 | new URLReferenceTestCase(
1045 | "http://home.com/foo/bar/",
1046 | "...",
1047 | "http://home.com/foo/bar/..."
1048 | ),
1049 | // Triple dots do not affect the path
1050 | new URLReferenceTestCase(
1051 | "http://home.com/foo/bar/",
1052 | "/...",
1053 | "http://home.com/..."
1054 | ),
1055 | // Triple dots do not affect the path
1056 | new URLReferenceTestCase(
1057 | "http://home.com/foo/bar/",
1058 | "./...",
1059 | "http://home.com/foo/bar/..."
1060 | ),
1061 | };
1062 |
1063 | private QueryStringTestCase[] queryStringCases = new QueryStringTestCase[]{
1064 | new QueryStringTestCase(
1065 | "http://example.com",
1066 | Collections.emptyMap()
1067 | ),
1068 | new QueryStringTestCase(
1069 | "http://example.com?",
1070 | Collections.emptyMap()
1071 | ),
1072 | new QueryStringTestCase(
1073 | "http://example.com?key=value",
1074 | new HashMap