├── 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 | * 41 | * merge("/some/path", "./../hello") == "/some/./../hello" 42 | * merge("/some/path/", "./../hello") == "/some/path/./../hello" 43 | * merge("/some/path/", "") == "/some/path/" 44 | * merge("", "/some/other/path") == "/some/other/path" 45 | */ 46 | private static String merge(String base, String ref) { 47 | String merged; 48 | 49 | if (ref == null || ref.isEmpty()) { 50 | merged = base; 51 | } else if (ref.charAt(0) != '/' && base != null && !base.isEmpty()) { 52 | int i = base.lastIndexOf("/"); 53 | merged = base.substring(0, i + 1) + ref; 54 | } else { 55 | merged = ref; 56 | } 57 | 58 | if (merged == null || merged.isEmpty()) { 59 | return ""; 60 | } 61 | 62 | return merged; 63 | } 64 | 65 | /** 66 | * Returns the resolved path parts. 67 | *

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 result = new ArrayList<>(); 78 | 79 | for (int i = 0; i < parts.length; i++) { 80 | switch (parts[i]) { 81 | case "": 82 | case ".": 83 | // Ignore 84 | break; 85 | case "..": 86 | if (result.size() > 0) { 87 | result.remove(result.size() - 1); 88 | } 89 | break; 90 | default: 91 | result.add(parts[i]); 92 | break; 93 | } 94 | } 95 | 96 | // Get last element, if it was '.' or '..' we need 97 | // to end in a slash. 98 | switch (parts[parts.length - 1]) { 99 | case ".": 100 | case "..": 101 | // Add an empty last string, it will be turned into 102 | // a slash when joined together. 103 | result.add(""); 104 | break; 105 | } 106 | 107 | return "/" + String.join("/", result); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/anthonynsimon/url/URLBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class URLBuilderTest { 7 | 8 | @Test 9 | public void testEmptyUrl() { 10 | URL url = new URLBuilder() 11 | .build(); 12 | Assert.assertEquals("", url.toString()); 13 | } 14 | 15 | @Test 16 | public void testSetFields() { 17 | URL url0 = new URLBuilder() 18 | .setScheme("http") 19 | .setHost("example.com") 20 | .build(); 21 | 22 | Assert.assertEquals("http://example.com", url0.toString()); 23 | 24 | URL url1 = new URLBuilder() 25 | .setScheme("https") 26 | .setHost("example.com") 27 | .setUsername("user") 28 | .setPassword("secret") 29 | .build(); 30 | 31 | Assert.assertEquals("https://user:secret@example.com", url1.toString()); 32 | 33 | URL url2 = new URLBuilder() 34 | .setScheme("https") 35 | .setHost("192.168.1.1:443") 36 | .setUsername("user") 37 | .setPassword("secret") 38 | .build(); 39 | 40 | Assert.assertEquals("https://user:secret@192.168.1.1:443", url2.toString()); 41 | 42 | URL url3 = new URLBuilder() 43 | .setFragment("testingTheFragment") 44 | .setScheme("https") 45 | .setHost("192.168.1.1:8080") 46 | .setUsername("user") 47 | .setPassword("secret") 48 | .build(); 49 | 50 | Assert.assertEquals("https://user:secret@192.168.1.1:8080#testingTheFragment", url3.toString()); 51 | 52 | URL url4 = new URLBuilder() 53 | .setFragment("testingTheFragment") 54 | .setScheme("https") 55 | .setHost("192.168.1.1:8080") 56 | .setUsername("user") 57 | .setPassword("secret") 58 | .setQuery("key=value&something=else") 59 | .build(); 60 | 61 | Assert.assertEquals("https://user:secret@192.168.1.1:8080?key=value&something=else#testingTheFragment", url4.toString()); 62 | 63 | 64 | URL url5 = new URLBuilder() 65 | .setOpaque("user@example.com") 66 | .setScheme("mailto") 67 | .setHost("192.168.1.1:8080") 68 | .setUsername("user") 69 | .setPassword("secret") 70 | .build(); 71 | 72 | Assert.assertEquals("mailto:user@example.com", url5.toString()); 73 | Assert.assertTrue(url5.isOpaque()); 74 | Assert.assertTrue(url5.isAbsolute()); 75 | 76 | URL url6 = new URLBuilder() 77 | .setPath("/path/to/file.html") 78 | .build(); 79 | 80 | Assert.assertEquals("/path/to/file.html", url6.toString()); 81 | Assert.assertFalse(url6.isOpaque()); 82 | Assert.assertFalse(url6.isAbsolute()); 83 | 84 | URL url7 = new URLBuilder() 85 | .setPath("/path/to;/file.html") 86 | .setRawPath("/path/to%3B/file.html") 87 | .build(); 88 | 89 | Assert.assertEquals("/path/to%3B/file.html", url7.toString()); 90 | Assert.assertFalse(url7.isOpaque()); 91 | Assert.assertFalse(url7.isAbsolute()); 92 | Assert.assertEquals("/path/to%3B/file.html", url7.getRawPath()); 93 | Assert.assertEquals("/path/to;/file.html", url7.getPath()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jurl 2 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/e8a7715455e44c73be897eaa131a8054)](https://app.codacy.com/app/anthonynajjars/jurl?utm_source=github.com&utm_medium=referral&utm_content=anthonynsimon/jurl&utm_campaign=badger) 3 | [![Build Status](https://travis-ci.org/anthonynsimon/jurl.svg?branch=master)](https://travis-ci.org/anthonynsimon/jurl/builds) 4 | [![Test Coverage](https://codecov.io/gh/anthonynsimon/jurl/branch/master/graph/badge.svg)](https://codecov.io/gh/anthonynsimon/jurl) 5 | [![MIT License](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/anthonynsimon/jurl/blob/master/LICENSE) 6 | [![](https://jitpack.io/v/anthonynsimon/jurl.svg)](https://jitpack.io/#anthonynsimon/jurl) 7 | 8 | Fast and simple URL parsing for Java, with UTF-8 and path resolving support. Based on Go's excellent `net/url` lib. 9 | 10 | ## Why 11 | - Easy to use API - you just want to parse a URL after all. 12 | - Fast, 4+ million URLs per second on commodity hardware. 13 | - UTF-8 encoding and decoding. 14 | - Supports path resolving between URLs (absolute and relative). 15 | - Good test coverage with plenty of edge cases. 16 | - Supports IPv4 and IPv6. 17 | - No external dependencies. 18 | 19 | ## Getting Started 20 | 21 | Example: 22 | ```java 23 | // Parse URLs 24 | URL base = URL.parse("https://user:secret@example♬.com/path/to/my/dir#about"); 25 | URL ref = URL.parse("./../file.html?search=germany&language=de_DE"); 26 | 27 | // Parsed base 28 | base.getScheme(); // https 29 | base.getUsername(); // user 30 | base.getPassword(); // secret 31 | base.getHost(); // example♬.com 32 | base.getPath(); // /path/to/my/dir 33 | base.getFragment(); // about 34 | 35 | // Parsed reference 36 | ref.getPath(); // ./../file.html 37 | ref.getQueryPairs(); // Map = {search=germany, language=de_DE} 38 | 39 | // Resolve them! 40 | URL resolved = base.resolveReference(ref); // https://user:secret@example♬.com/path/to/file.html?search=germany&language=de_DE 41 | resolved.getPath(); // /path/to/file.html 42 | 43 | // Escaped UTF-8 result 44 | resolved.toString(); // https://user:secret@example%E2%99%AC.com/path/to/file.html?search=germany&language=de_DE 45 | 46 | ``` 47 | 48 | ## Setup 49 | 50 | ### Add the JitPack repository to your build file. 51 | 52 | For gradle: 53 | ``` 54 | allprojects { 55 | repositories { 56 | maven { url 'https://jitpack.io' } 57 | } 58 | } 59 | ``` 60 | For maven: 61 | ``` 62 | 63 | 64 | jitpack.io 65 | https://jitpack.io 66 | 67 | 68 | ``` 69 | 70 | ### Add the dependency: 71 | 72 | For gradle: 73 | ``` 74 | dependencies { 75 | compile 'com.github.anthonynsimon:jurl:v0.4.2' 76 | } 77 | ``` 78 | 79 | For maven: 80 | ``` 81 | 82 | 83 | com.github.anthonynsimon 84 | jurl 85 | v0.4.2 86 | 87 | 88 | ``` 89 | 90 | ## Issues 91 | 92 | The recommended medium to report and track issues is by opening one on Github. 93 | 94 | ## Contributing 95 | 96 | Want to hack on the project? Any kind of contribution is welcome! Simply follow the next steps: 97 | 98 | - Fork the project. 99 | - Create a new branch. 100 | - Make your changes and write tests when practical. 101 | - Commit your changes to the new branch. 102 | - Send a pull request, it will be reviewed shortly. 103 | 104 | In case you want to add a feature, please create a new issue and briefly explain what the feature would consist of. For bugs or requests, before creating an issue please check if one has already been created for it. 105 | 106 | ## License 107 | 108 | This project is licensed under the MIT license. 109 | -------------------------------------------------------------------------------- /src/test/java/com/anthonynsimon/url/PercentEncoderTest.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import com.anthonynsimon.url.exceptions.MalformedURLException; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | public class PercentEncoderTest { 8 | 9 | private EncodingTestCase[] escapeCases = new EncodingTestCase[]{ 10 | new EncodingTestCase("", ""), 11 | new EncodingTestCase("onetwothree", "onetwothree"), 12 | new EncodingTestCase("ÿ", "%C3%BF"), 13 | new EncodingTestCase("ÿ/com", "%C3%BF%2Fcom"), 14 | new EncodingTestCase("one&two three", "one%26two%20three"), 15 | new EncodingTestCase("à á â ã ä å æ ç è é ê ë ì í î ï ", "%C3%A0%20%C3%A1%20%C3%A2%20%C3%A3%20%C3%A4%20%C3%A5%20%C3%A6%20%C3%A7%20%C3%A8%20%C3%A9%20%C3%AA%20%C3%AB%20%C3%AC%20%C3%AD%20%C3%AE%20%C3%AF%20"), 16 | new EncodingTestCase("a50%", "a50%25"), 17 | new EncodingTestCase(" ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;", "%20%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B"), 18 | new EncodingTestCase("ϣ%Ϧ", "%CF%A3%25%CF%A6"), 19 | new EncodingTestCase("κόσμε", "%C3%8E%C2%BA%C3%A1%C2%BD%C2%B9%C3%8F%C6%92%C3%8E%C2%BC%C3%8E%C2%B5"), 20 | new EncodingTestCase("þ", "%C3%BE"), 21 | new EncodingTestCase("𡺸", "%F0%A1%BA%B8"), 22 | new EncodingTestCase("ü€€€€€", "%C3%BC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC"), 23 | new EncodingTestCase("𡺸 𡺹 𡺺", "%F0%A1%BA%B8%20%F0%A1%BA%B9%20%F0%A1%BA%BA"), 24 | new EncodingTestCase("𡺸 𡺹 𡺺", "%F0%A1%BA%B8%20%F0%A1%BA%B9%20%F0%A1%BA%BA"), 25 | new EncodingTestCase(" 𩕵 𩕶 𩕷 𩕸 𩕹 𩕺 𩕻 𩕼 𩕽 𩕾 𩕿 𩖀 𩖁 𩖂 𩖃 𩖄 𩖅 𩖆 𩖇 𩖈 𩖉 𩖊 𩖋 𩖌 𩖍 𩖎 𩖏 𩖐 ", "%20%F0%A9%95%B5%20%F0%A9%95%B6%20%F0%A9%95%B7%20%F0%A9%95%B8%20%F0%A9%95%B9%20%F0%A9%95%BA%20%F0%A9%95%BB%20%F0%A9%95%BC%20%F0%A9%95%BD%20%F0%A9%95%BE%20%F0%A9%95%BF%20%F0%A9%96%80%20%F0%A9%96%81%20%F0%A9%96%82%20%F0%A9%96%83%20%F0%A9%96%84%20%F0%A9%96%85%20%F0%A9%96%86%20%F0%A9%96%87%20%F0%A9%96%88%20%F0%A9%96%89%20%F0%A9%96%8A%20%F0%A9%96%8B%20%F0%A9%96%8C%20%F0%A9%96%8D%20%F0%A9%96%8E%20%F0%A9%96%8F%20%F0%A9%96%90%20"), 26 | new EncodingTestCase("www.NJNj ǭǮǯǰDZDz௲ԏ ԱԲԳԴԵ௳௴௵ dz Ǵ ǵ Ƕ ǷǸ ǹnjǍ.com/^path?", "www.%C7%8A%C7%8B%20%C7%AD%C7%AE%C7%AF%C7%B0%C7%B1%C7%B2%E0%AF%B2%D4%8F%20%D4%B1%D4%B2%D4%B3%D4%B4%D4%B5%E0%AF%B3%E0%AF%B4%E0%AF%B5%20%C7%B3%20%C7%B4%20%C7%B5%20%C7%B6%20%C7%B7%C7%B8%20%C7%B9%C7%8C%C7%8D.com%2F%5Epath%3F"), 27 | }; 28 | 29 | private EncodingTestCase[] unescapeCases = new EncodingTestCase[]{ 30 | new EncodingTestCase("", ""), 31 | new EncodingTestCase("www.example.com/%20%DFhei", "www.example.com/ �hei"), 32 | new EncodingTestCase("www.example.com/%20%C3%BF%20", "www.example.com/ ÿ "), 33 | new EncodingTestCase("www.example.com/%20%c3%bF%20", "www.example.com/ ÿ "), 34 | new EncodingTestCase("one%26two%20three", "one&two three"), 35 | new EncodingTestCase("www.example.com", "www.example.com"), 36 | new EncodingTestCase("http://test.%C7%B7%C7%B8%20%C7%B9%C7%8C%C7%8D.com/foo", "http://test.ǷǸ ǹnjǍ.com/foo"), 37 | new EncodingTestCase("http%3a%2F%2F%E0%AF%B5.com", "http://௵.com"), 38 | new EncodingTestCase("_%25_~_--_/_", "_%_~_--_/_"), 39 | new EncodingTestCase("_______", "_______"), 40 | new EncodingTestCase("%20%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B", " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;"), 41 | new EncodingTestCase("%C3%BC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC%E2%82%AC", "ü€€€€€"), 42 | new EncodingTestCase("www.%C7%8A%C7%8B%20%C7%AD%C7%AE%C7%AF%C7%B0%C7%B1%C7%B2%E0%AF%B2%D4%8F%20%D4%B1%D4%B2%D4%B3%D4%B4%D4%B5%E0%AF%B3%E0%AF%B4%E0%AF%B5%20%C7%B3%20%C7%B4%20%C7%B5%20%C7%B6%20%C7%B7%C7%B8%20%C7%B9%C7%8C%C7%8D.com%2F%5Epath%3F", "www.NJNj ǭǮǯǰDZDz௲ԏ ԱԲԳԴԵ௳௴௵ dz Ǵ ǵ Ƕ ǷǸ ǹnjǍ.com/^path?"), 43 | }; 44 | 45 | @Test 46 | public void testEscaping() throws Exception { 47 | for (EncodingTestCase testCase : escapeCases) { 48 | Assert.assertEquals(testCase.expectedOutput, PercentEncoder.encode(testCase.input, URLPart.ENCODE_ZONE)); 49 | } 50 | } 51 | 52 | @Test 53 | public void testUnescaping() throws Exception { 54 | for (EncodingTestCase testCase : unescapeCases) { 55 | Assert.assertEquals(testCase.expectedOutput, PercentEncoder.decode(testCase.input)); 56 | } 57 | } 58 | 59 | @Test(expected = MalformedURLException.class) 60 | public void testUnescapeInvalidHex() throws Exception { 61 | PercentEncoder.decode("http://www.domain.com/path%C3%##"); 62 | } 63 | 64 | @Test(expected = MalformedURLException.class) 65 | public void testMalformedUnescape() throws Exception { 66 | PercentEncoder.decode("_______%"); 67 | } 68 | 69 | @Test(expected = MalformedURLException.class) 70 | public void testMalformedUnescapeWithUnicodeMultiple() throws Exception { 71 | PercentEncoder.decode("!__!__%Ƿabasda"); 72 | } 73 | 74 | @Test(expected = MalformedURLException.class) 75 | public void testMalformedUnescapeWithUnicodeSingle() throws Exception { 76 | PercentEncoder.decode("!__!__%Ƿ"); 77 | } 78 | 79 | class EncodingTestCase { 80 | public String input; 81 | public String expectedOutput; 82 | 83 | public EncodingTestCase(String input, String expectedOutput) { 84 | this.input = input; 85 | this.expectedOutput = expectedOutput; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/anthonynsimon/url/PathResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class PathResolverTest { 7 | 8 | 9 | private PathResolveTestCase[] pathResolveCases = new PathResolveTestCase[]{ 10 | new PathResolveTestCase( 11 | "", 12 | "", 13 | "" 14 | ), 15 | new PathResolveTestCase( 16 | "abc", 17 | "", 18 | "/abc" 19 | ), 20 | new PathResolveTestCase( 21 | "abc", 22 | "456", 23 | "/456" 24 | ), 25 | new PathResolveTestCase( 26 | "/abc", 27 | "123", 28 | "/123" 29 | ), 30 | new PathResolveTestCase( 31 | "/abc/abc", 32 | "/123", 33 | "/123" 34 | ), 35 | new PathResolveTestCase( 36 | "abc/def", 37 | ".", 38 | "/abc/" 39 | ), 40 | new PathResolveTestCase( 41 | "abc/def", 42 | "123", 43 | "/abc/123" 44 | ), 45 | new PathResolveTestCase( 46 | "abc/def", 47 | "..", 48 | "/" 49 | ), 50 | new PathResolveTestCase( 51 | "abc/", 52 | "..", 53 | "/" 54 | ), 55 | new PathResolveTestCase( 56 | "abc/", 57 | "../..", 58 | "/" 59 | ), 60 | new PathResolveTestCase( 61 | "abc/def/hij", 62 | "..", 63 | "/abc/" 64 | ), 65 | new PathResolveTestCase( 66 | "abc/def/hij", 67 | ".", 68 | "/abc/def/" 69 | ), 70 | new PathResolveTestCase( 71 | "abc/def/hij", 72 | "../123", 73 | "/abc/123" 74 | ), 75 | new PathResolveTestCase( 76 | "abc/def/hij", 77 | ".././123", 78 | "/abc/123" 79 | ), 80 | new PathResolveTestCase( 81 | "abc/def/hij", 82 | "../../123", 83 | "/123" 84 | ), 85 | new PathResolveTestCase( 86 | "abc/def/hij", 87 | "./../123", 88 | "/abc/123" 89 | ), 90 | new PathResolveTestCase( 91 | "abc/../hij", 92 | "", 93 | "/hij" 94 | ), 95 | new PathResolveTestCase( 96 | "abc/hij", 97 | "./..", 98 | "/" 99 | ), 100 | new PathResolveTestCase( 101 | "abc/./hij", 102 | ".", 103 | "/abc/" 104 | ), 105 | new PathResolveTestCase( 106 | "abc/./hij", 107 | "", 108 | "/abc/hij" 109 | ), 110 | new PathResolveTestCase( 111 | "abc/../hij", 112 | "", 113 | "/hij" 114 | ), 115 | new PathResolveTestCase( 116 | "abc/../hij", 117 | ".", 118 | "/" 119 | ), 120 | new PathResolveTestCase( 121 | "", 122 | "../../../././../", 123 | "/" 124 | ), 125 | new PathResolveTestCase( 126 | "../../../././../", 127 | "../../../././../", 128 | "/" 129 | ), 130 | new PathResolveTestCase( 131 | "../////../.././/./../", 132 | "../../../././../", 133 | "/" 134 | ), 135 | new PathResolveTestCase( 136 | "abc", 137 | "////", 138 | "/" 139 | ), 140 | new PathResolveTestCase( 141 | "/////", 142 | "abc", 143 | "/abc" 144 | ), 145 | new PathResolveTestCase( 146 | "abc/.././123", 147 | "x", 148 | "/x" 149 | ), 150 | }; 151 | 152 | @Test 153 | public void testResolvePath() throws Exception { 154 | for (PathResolveTestCase testCase : pathResolveCases) { 155 | String resolved = PathResolver.resolve(testCase.inputBase, testCase.inputReference); 156 | Assert.assertEquals(testCase.expected, resolved); 157 | } 158 | } 159 | 160 | 161 | private class PathResolveTestCase { 162 | public String inputBase; 163 | public String inputReference; 164 | public String expected; 165 | 166 | public PathResolveTestCase(String inputBase, String inputReference, String expected) { 167 | this.inputBase = inputBase; 168 | this.inputReference = inputReference; 169 | this.expected = expected; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/com/anthonynsimon/url/PercentEncoder.java: -------------------------------------------------------------------------------- 1 | package com.anthonynsimon.url; 2 | 3 | import com.anthonynsimon.url.exceptions.InvalidHexException; 4 | import com.anthonynsimon.url.exceptions.MalformedURLException; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | 8 | /** 9 | * PercentEncoder handles the escaping and unescaping of characters in URLs. 10 | * It escapes character based on the context (part of the URL) that is being dealt with. 11 | *

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> parsedQueryPairs; 49 | 50 | /** 51 | * Cached string representation of the URL. 52 | * Do not serialize. 53 | */ 54 | private transient String stringRepresentation; 55 | 56 | /** 57 | * Protect instantiation of class. Use public parse method instead to construct URLs. 58 | * Builder is for protected use only. 59 | */ 60 | URL(String scheme, String username, String password, String host, String path, String query, String fragment, String opaque) { 61 | this.scheme = mapToNullIfEmpty(scheme); 62 | this.username = mapToNullIfEmpty(username); 63 | this.password = mapToNullIfEmpty(password); 64 | this.host = mapToNullIfEmpty(host); 65 | this.hostname = extractHostname(host); 66 | this.port = extractPort(host); 67 | this.path = mapToNullIfEmpty(path); 68 | this.rawPath = null; 69 | this.query = mapToNullIfEmpty(query); 70 | this.fragment = mapToNullIfEmpty(fragment); 71 | this.opaque = mapToNullIfEmpty(opaque); 72 | } 73 | 74 | URL(String scheme, String username, String password, String host, String path, String rawPath, String query, String fragment, String opaque) { 75 | this.scheme = mapToNullIfEmpty(scheme); 76 | this.username = mapToNullIfEmpty(username); 77 | this.password = mapToNullIfEmpty(password); 78 | this.host = mapToNullIfEmpty(host); 79 | this.hostname = extractHostname(host); 80 | this.port = extractPort(host); 81 | this.path = mapToNullIfEmpty(path); 82 | this.rawPath = mapToNullIfEmpty(rawPath); 83 | this.query = mapToNullIfEmpty(query); 84 | this.fragment = mapToNullIfEmpty(fragment); 85 | this.opaque = mapToNullIfEmpty(opaque); 86 | } 87 | 88 | URL(String scheme, String username, String password, String hostname, Integer port, String path, String rawPath, String query, String fragment, String opaque) { 89 | this.scheme = mapToNullIfEmpty(scheme); 90 | this.username = mapToNullIfEmpty(username); 91 | this.password = mapToNullIfEmpty(password); 92 | this.host = mergeHostPortIfSet(hostname, port); 93 | this.hostname = mapToNullIfEmpty(hostname); 94 | this.port = port; 95 | this.path = mapToNullIfEmpty(path); 96 | this.rawPath = mapToNullIfEmpty(rawPath); 97 | this.query = mapToNullIfEmpty(query); 98 | this.fragment = mapToNullIfEmpty(fragment); 99 | this.opaque = mapToNullIfEmpty(opaque); 100 | } 101 | 102 | 103 | /** 104 | * Returns a new URL object after parsing the provided URL string. 105 | */ 106 | public static URL parse(String url) throws MalformedURLException { 107 | return URL_PARSER.parse(url); 108 | } 109 | 110 | /** 111 | * Returns the scheme ('http' or 'file' or 'ftp' etc...) of the URL if it exists. 112 | */ 113 | public String getScheme() { 114 | return scheme; 115 | } 116 | 117 | /** 118 | * Returns the username part of the userinfo if it exists. 119 | */ 120 | public String getUsername() { 121 | return username; 122 | } 123 | 124 | /** 125 | * Returns the password part of the userinfo if it exists. 126 | */ 127 | public String getPassword() { 128 | return password; 129 | } 130 | 131 | /** 132 | * Returns the host ('www.example.com' or '192.168.0.1:8080' or '[fde2:d7de:302::]') of the URL if it exists. 133 | */ 134 | public String getHost() { 135 | return host; 136 | } 137 | 138 | /** 139 | * Returns the hostname part of the host ('www.example.com' or '192.168.0.1' or '[fde2:d7de:302::]') if it exists. 140 | */ 141 | public String getHostname() { 142 | return hostname; 143 | } 144 | 145 | /** 146 | * Returns the port part of the host (i.e. 80 or 443 or 3000) if it exists. 147 | */ 148 | public Integer getPort() { 149 | return port; 150 | } 151 | 152 | /** 153 | * Returns the unescaped path ('/path/to/the;/file.html') of the URL if it exists. 154 | */ 155 | public String getPath() { 156 | return path; 157 | } 158 | 159 | 160 | /** 161 | * Returns the raw path ('/path/to/the%3B/file.html') of the URL if it exists. 162 | */ 163 | public String getRawPath() { 164 | return rawPath; 165 | } 166 | 167 | /** 168 | * Returns the query ('?q=foo{@literal &}bar') of the URL if it exists. 169 | */ 170 | public String getQuery() { 171 | return query; 172 | } 173 | 174 | /** 175 | * Returns the fragment ('#foo{@literal &}bar') of the URL if it exists. 176 | */ 177 | public String getFragment() { 178 | return fragment; 179 | } 180 | 181 | /** 182 | * Returns the opaque part of the URL if it exists. 183 | */ 184 | public String getOpaque() { 185 | return opaque; 186 | } 187 | 188 | /** 189 | * Returns a java.net.URL object from the parsed url. 190 | * 191 | * @throws java.net.MalformedURLException if something went wrong while created the new object. 192 | */ 193 | public java.net.URL toJavaURL() throws java.net.MalformedURLException { 194 | return new java.net.URL(toString()); 195 | } 196 | 197 | /** 198 | * Returns a java.net.URI object from the parsed url. 199 | * 200 | * @throws java.net.URISyntaxException if something went wrong while created the new object. 201 | */ 202 | public java.net.URI toJavaURI() throws URISyntaxException { 203 | return new URI(toString()); 204 | } 205 | 206 | 207 | /** 208 | * Returns a map of key-value pairs from the parsed query string. 209 | */ 210 | public Map> getQueryPairs() { 211 | if (parsedQueryPairs != null) { 212 | return parsedQueryPairs; 213 | } 214 | parsedQueryPairs = new HashMap<>(); 215 | 216 | if (!nullOrEmpty(query) && !query.equals("?")) { 217 | String[] pairs = query.split("&"); 218 | for (String pair : pairs) { 219 | String[] parts = pair.split("="); 220 | if (parts.length > 0 && !parts[0].isEmpty()) { 221 | Collection existing = parsedQueryPairs.getOrDefault(parts[0], new ArrayList<>()); 222 | if (parts.length == 2) { 223 | existing.add(parts[1]); 224 | } 225 | parsedQueryPairs.put(parts[0], existing); 226 | } 227 | } 228 | } 229 | 230 | return parsedQueryPairs; 231 | } 232 | 233 | /** 234 | * Returns true if the two Objects are instances of URL and their string representations match. 235 | */ 236 | @Override 237 | public boolean equals(Object other) { 238 | if (other == null) { 239 | return false; 240 | } 241 | if (!(other instanceof URL)) { 242 | return false; 243 | } 244 | return toString().equals(other.toString()); 245 | } 246 | 247 | /** 248 | * Returns a string representation of the all parts of the URL that are not null. 249 | */ 250 | @Override 251 | public String toString() { 252 | if (stringRepresentation != null) { 253 | return stringRepresentation; 254 | } 255 | 256 | boolean hasScheme = !nullOrEmpty(scheme); 257 | StringBuffer sb = new StringBuffer(); 258 | if (hasScheme) { 259 | sb.append(scheme); 260 | sb.append(":"); 261 | } 262 | if (!nullOrEmpty(opaque)) { 263 | sb.append(opaque); 264 | } else { 265 | if (hasScheme || !nullOrEmpty(host)) { 266 | if (hasScheme) { 267 | sb.append("//"); 268 | } 269 | if (!nullOrEmpty(username)) { 270 | sb.append(PercentEncoder.encode(username, URLPart.CREDENTIALS)); 271 | if (!nullOrEmpty(password)) { 272 | sb.append(":"); 273 | sb.append(PercentEncoder.encode(password, URLPart.CREDENTIALS)); 274 | } 275 | sb.append("@"); 276 | } 277 | if (!nullOrEmpty(host)) { 278 | sb.append(PercentEncoder.encode(host, URLPart.HOST)); 279 | } 280 | } 281 | if (!nullOrEmpty(rawPath)) { 282 | sb.append(rawPath); 283 | } else if (!nullOrEmpty(path)) { 284 | if (path.indexOf('/') != 0 && !"*".equals(path)) { 285 | sb.append("/"); 286 | } 287 | sb.append(path); 288 | } 289 | } 290 | if (!nullOrEmpty(query)) { 291 | sb.append("?"); 292 | // Only append '?' if that's all there is to the query string 293 | if (!"?".equals(query)) { 294 | sb.append(query); 295 | } 296 | } 297 | if (!nullOrEmpty(fragment)) { 298 | sb.append("#"); 299 | sb.append(fragment); 300 | } 301 | 302 | stringRepresentation = sb.toString(); 303 | 304 | return stringRepresentation; 305 | } 306 | 307 | /** 308 | * Returns the hash code of the URL. 309 | */ 310 | @Override 311 | public int hashCode() { 312 | return toString().hashCode(); 313 | } 314 | 315 | /** 316 | * Returns true if the parameter string is neither null nor empty (""). 317 | */ 318 | private boolean nullOrEmpty(String str) { 319 | return str == null || str.isEmpty(); 320 | } 321 | 322 | /** 323 | * Returns true if URL is opaque. 324 | */ 325 | public boolean isOpaque() { 326 | return !nullOrEmpty(opaque); 327 | } 328 | 329 | /** 330 | * Returns true if URL is absolute. 331 | */ 332 | public boolean isAbsolute() { 333 | return !nullOrEmpty(scheme); 334 | } 335 | 336 | public URL resolveReference(String ref) throws MalformedURLException, InvalidURLReferenceException { 337 | URL url = URL_PARSER.parse(ref); 338 | return resolveReference(url); 339 | } 340 | 341 | /** 342 | * Returns the resolved reference URL using the instance URL as a base. 343 | *

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>() {{ 1075 | put("key", Arrays.asList("value")); 1076 | }} 1077 | ), 1078 | new QueryStringTestCase( 1079 | "http://example.com?key=", 1080 | new HashMap>() {{ 1081 | put("key", Collections.emptyList()); 1082 | }} 1083 | ), 1084 | new QueryStringTestCase( 1085 | "http://example.com?one=uno&two=dos", 1086 | new HashMap>() {{ 1087 | put("one", Arrays.asList("uno")); 1088 | put("two", Arrays.asList("dos")); 1089 | }} 1090 | ), 1091 | new QueryStringTestCase( 1092 | "http://example.com?one=uno&two=dos&", 1093 | new HashMap>() {{ 1094 | put("one", Arrays.asList("uno")); 1095 | put("two", Arrays.asList("dos")); 1096 | }} 1097 | ), 1098 | new QueryStringTestCase( 1099 | "http://example.com?one=uno&two=dos&three=", 1100 | new HashMap>() {{ 1101 | put("one", Arrays.asList("uno")); 1102 | put("two", Arrays.asList("dos")); 1103 | put("three", Collections.emptyList()); 1104 | }} 1105 | ), 1106 | new QueryStringTestCase( 1107 | "http://example.com?one=uno&two=dos&three", 1108 | new HashMap>() {{ 1109 | put("one", Arrays.asList("uno")); 1110 | put("two", Arrays.asList("dos")); 1111 | put("three", Collections.emptyList()); 1112 | }} 1113 | ), 1114 | new QueryStringTestCase( 1115 | "http://example.com?one=uno&two=dos&three=tres&three=drei", 1116 | new HashMap>() {{ 1117 | put("one", Arrays.asList("uno")); 1118 | put("two", Arrays.asList("dos")); 1119 | put("three", Arrays.asList("tres", "drei")); 1120 | }} 1121 | ), 1122 | new QueryStringTestCase( 1123 | "http://example.com?one=uno&two=dos&=", 1124 | new HashMap>() {{ 1125 | put("one", Arrays.asList("uno")); 1126 | put("two", Arrays.asList("dos")); 1127 | }} 1128 | ), 1129 | new QueryStringTestCase( 1130 | "http://example.com?one=uno&two=dos&===", 1131 | new HashMap>() {{ 1132 | put("one", Arrays.asList("uno")); 1133 | put("two", Arrays.asList("dos")); 1134 | }} 1135 | ), 1136 | new QueryStringTestCase( 1137 | "http://example.com?one=uno&two=dos&===&three=tres", 1138 | new HashMap>() {{ 1139 | put("one", Arrays.asList("uno")); 1140 | put("two", Arrays.asList("dos")); 1141 | put("three", Arrays.asList("tres")); 1142 | }} 1143 | ), 1144 | new QueryStringTestCase( 1145 | "http://example.com?one=uno&two=dos&===&&=&&three=tres", 1146 | new HashMap>() {{ 1147 | put("one", Arrays.asList("uno")); 1148 | put("two", Arrays.asList("dos")); 1149 | put("three", Arrays.asList("tres")); 1150 | }} 1151 | ), 1152 | new QueryStringTestCase( 1153 | "http://example.com?one=uno&two=dos&===&&=&&three=tres#fragment=hello", 1154 | new HashMap>() {{ 1155 | put("one", Arrays.asList("uno")); 1156 | put("two", Arrays.asList("dos")); 1157 | put("three", Arrays.asList("tres")); 1158 | }} 1159 | ), 1160 | }; 1161 | 1162 | @Test 1163 | public void testUrls() throws Exception { 1164 | for (URLTestCase testCase : urlTestCases) { 1165 | URL url = URL.parse(testCase.input); 1166 | Assert.assertEquals(testCase.expectedScheme, url.getScheme()); 1167 | Assert.assertEquals(testCase.expectedUsername, url.getUsername()); 1168 | Assert.assertEquals(testCase.expectedPassword, url.getPassword()); 1169 | Assert.assertEquals(testCase.expectedHost, url.getHost()); 1170 | Assert.assertEquals(testCase.expectedPath, url.getPath()); 1171 | Assert.assertEquals(testCase.expectedQuery, url.getQuery()); 1172 | Assert.assertEquals(testCase.expectedFragment, url.getFragment()); 1173 | Assert.assertEquals(testCase.expectedStringRepr, url.toString()); 1174 | } 1175 | } 1176 | 1177 | @Test 1178 | public void testToURL() throws Exception { 1179 | for (String testCase : toJavaClassCases) { 1180 | URL url = URL.parse(testCase); 1181 | java.net.URL javaURL = url.toJavaURL(); 1182 | Assert.assertEquals(testCase, javaURL.toString()); 1183 | } 1184 | } 1185 | 1186 | 1187 | @Test 1188 | public void testToURI() throws Exception { 1189 | for (String testCase : toJavaClassCases) { 1190 | URL url = URL.parse(testCase); 1191 | java.net.URI javaURI = url.toJavaURI(); 1192 | Assert.assertEquals(testCase, javaURI.toString()); 1193 | } 1194 | } 1195 | 1196 | @Test 1197 | public void testQueryStringValues() throws Exception { 1198 | for (QueryStringTestCase testCase : queryStringCases) { 1199 | URL url = URL.parse(testCase.input); 1200 | Assert.assertEquals(testCase.expected, url.getQueryPairs()); 1201 | } 1202 | } 1203 | 1204 | @Test(expected = MalformedURLException.class) 1205 | public void testNullParam() throws MalformedURLException { 1206 | URL url = URL.parse(""); 1207 | } 1208 | 1209 | @Test(expected = MalformedURLException.class) 1210 | public void testEmpty() throws MalformedURLException { 1211 | URL url = URL.parse(""); 1212 | } 1213 | 1214 | 1215 | @Test(expected = MalformedURLException.class) 1216 | public void testIPv6WithoutClosingtag() throws MalformedURLException { 1217 | URL url = URL.parse("http://[::1"); 1218 | } 1219 | 1220 | @Test(expected = MalformedURLException.class) 1221 | public void testIPv6InvalidPort() throws MalformedURLException { 1222 | URL url = URL.parse("http://[::1]:123abc"); 1223 | } 1224 | 1225 | @Test(expected = MalformedURLException.class) 1226 | public void testIPv6InvalidPort2() throws MalformedURLException { 1227 | URL url = URL.parse("http://[::1]:"); 1228 | } 1229 | 1230 | @Test 1231 | public void testCachedQueryPairs() throws MalformedURLException { 1232 | URL url = URL.parse("http://example.com?one=uno&two=dos&three"); 1233 | Map> expected = new HashMap>() {{ 1234 | put("one", Arrays.asList("uno")); 1235 | put("two", Arrays.asList("dos")); 1236 | put("three", Collections.emptyList()); 1237 | }}; 1238 | 1239 | Map> first = url.getQueryPairs(); 1240 | // Second time, it should come from a cached value 1241 | Map> second = url.getQueryPairs(); 1242 | 1243 | // Test expected values 1244 | Assert.assertEquals(expected, first); 1245 | 1246 | // Test referential equality 1247 | Assert.assertTrue(first == second); 1248 | } 1249 | 1250 | @Test 1251 | public void testIsPortValid() throws MalformedURLException { 1252 | Assert.assertFalse(new DefaultURLParser().isPortValid("12345")); 1253 | Assert.assertFalse(new DefaultURLParser().isPortValid("abcde")); 1254 | Assert.assertFalse(new DefaultURLParser().isPortValid(":abcde")); 1255 | Assert.assertFalse(new DefaultURLParser().isPortValid(":")); 1256 | Assert.assertFalse(new DefaultURLParser().isPortValid(":::")); 1257 | Assert.assertFalse(new DefaultURLParser().isPortValid("123:456")); 1258 | 1259 | Assert.assertTrue(new DefaultURLParser().isPortValid(":1234")); 1260 | Assert.assertTrue(new DefaultURLParser().isPortValid(":888888")); 1261 | Assert.assertTrue(new DefaultURLParser().isPortValid(":443")); 1262 | } 1263 | 1264 | 1265 | @Test(expected = MalformedURLException.class) 1266 | public void testColonWithoutPort() throws MalformedURLException { 1267 | URL url = URL.parse("http://host:/abc/[one_two]"); 1268 | } 1269 | 1270 | @Test(expected = MalformedURLException.class) 1271 | public void testMalformedIPv6() throws MalformedURLException { 1272 | URL url = URL.parse("http://e34::1"); 1273 | } 1274 | 1275 | @Test 1276 | public void testAbsoluteURLs() throws Exception { 1277 | Assert.assertTrue(URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section").isAbsolute()); 1278 | Assert.assertTrue(URL.parse("http://domain.com").isAbsolute()); 1279 | Assert.assertTrue(URL.parse("http://www.domain.com?key=value").isAbsolute()); 1280 | Assert.assertTrue(URL.parse("http://192.168.0.1/path/to/RESOURCE.html?q=abc#section").isAbsolute()); 1281 | Assert.assertTrue(URL.parse("http://192.168.0.1").isAbsolute()); 1282 | Assert.assertTrue(URL.parse("file:///home/user").isAbsolute()); 1283 | Assert.assertFalse(URL.parse("///home/user").isAbsolute()); 1284 | Assert.assertFalse(URL.parse("/home/user").isAbsolute()); 1285 | Assert.assertFalse(URL.parse("home/user").isAbsolute()); 1286 | Assert.assertFalse(URL.parse("home").isAbsolute()); 1287 | } 1288 | 1289 | @Test 1290 | public void testResolveReferences() throws Exception { 1291 | for (URLReferenceTestCase testCase : resolveReferenceCases) { 1292 | URL base = URL.parse(testCase.inputBase); 1293 | URL urlRef = URL.parse(testCase.inputReference); 1294 | 1295 | URL resolvedUrlRef = base.resolveReference(urlRef); 1296 | URL resolvedStringRef = base.resolveReference(testCase.inputReference); 1297 | 1298 | Assert.assertEquals(testCase.expectedResolvedReference, resolvedUrlRef.toString()); 1299 | Assert.assertEquals(testCase.expectedResolvedReference, resolvedStringRef.toString()); 1300 | } 1301 | } 1302 | 1303 | 1304 | @Test 1305 | public void testHashCode() throws Exception { 1306 | URL urlA = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section"); 1307 | URL urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section"); 1308 | Assert.assertTrue(urlA.hashCode() == urlB.hashCode()); 1309 | 1310 | urlA = URL.parse("http://www.domain.com"); 1311 | urlB = URL.parse("http://www.domain.com/"); 1312 | Assert.assertFalse(urlA.hashCode() == urlB.hashCode()); 1313 | 1314 | urlA = URL.parse("http://www.domain.com"); 1315 | urlB = URL.parse("http://www.domain.de"); 1316 | Assert.assertFalse(urlA.hashCode() == urlB.hashCode()); 1317 | } 1318 | 1319 | @Test(expected = InvalidURLReferenceException.class) 1320 | public void testInvalidResolveNotAbsoluteBase() throws Exception { 1321 | URL base = URL.parse("/path/to/RESOURCE.html"); 1322 | URL ref = URL.parse("http://www.domain.com/path/to/ANOTHER_RESOURCE.html?q=abc#section"); 1323 | base.resolveReference(ref); 1324 | } 1325 | 1326 | @Test(expected = InvalidURLReferenceException.class) 1327 | public void testInvalidResolveNullRef() throws Exception { 1328 | URL base = URL.parse("http://www.domain.com/path"); 1329 | URL ref = null; 1330 | base.resolveReference(ref); 1331 | } 1332 | 1333 | @Test(expected = MalformedURLException.class) 1334 | public void testMissingScheme() throws Exception { 1335 | URL url = URL.parse("://www.domain.com/path"); 1336 | } 1337 | 1338 | 1339 | @Test 1340 | public void testRoundtrip() throws Exception { 1341 | URL url = URL.parse("http://www.domain.com/?"); 1342 | Assert.assertEquals("http://www.domain.com/?", url.toString()); 1343 | } 1344 | 1345 | 1346 | @Test 1347 | public void testSeparateHostnameAndPort() throws Exception { 1348 | URL simple = new URL("https", null, null, "localhost", null, "/", null, null, null, null); 1349 | Assert.assertTrue(simple.toString().equals("https://localhost/")); 1350 | Assert.assertEquals(simple.getHost(), "localhost"); 1351 | Assert.assertEquals(simple.getHostname(), "localhost"); 1352 | Assert.assertEquals(simple.getPort(), null); 1353 | 1354 | URL withPort = new URL("https", null, null, "localhost", 8080, "/", null, null, null, null); 1355 | Assert.assertTrue(withPort.toString().equals("https://localhost:8080/")); 1356 | Assert.assertEquals(withPort.getHost(), "localhost:8080"); 1357 | Assert.assertEquals(withPort.getHostname(), "localhost"); 1358 | Assert.assertTrue(withPort.getPort().equals(8080)); 1359 | 1360 | URL withPortNoPath = new URL("https", null, null, "localhost", 8080, null, null, null, null, null); 1361 | Assert.assertTrue(withPortNoPath.toString().equals("https://localhost:8080")); 1362 | Assert.assertEquals(withPortNoPath.getHost(), "localhost:8080"); 1363 | Assert.assertEquals(withPortNoPath.getHostname(), "localhost"); 1364 | Assert.assertTrue(withPortNoPath.getPort().equals(8080)); 1365 | 1366 | URL noHostname = new URL("https", null, null, null, 8080, "/", null, null, null, null); 1367 | Assert.assertTrue(noHostname.toString().equals("https://:8080/")); 1368 | Assert.assertEquals(noHostname.getHost(), ":8080"); 1369 | Assert.assertEquals(noHostname.getHostname(), null); 1370 | Assert.assertTrue(noHostname.getPort().equals(8080)); 1371 | 1372 | URL justPort = new URL(null, null, null, null, 3000, null, null, null, null, null); 1373 | Assert.assertEquals(justPort.getHost(), ":3000"); 1374 | Assert.assertEquals(justPort.getHostname(), null); 1375 | Assert.assertTrue(justPort.getPort().equals(3000)); 1376 | Assert.assertTrue(justPort.toString().equals(":3000")); 1377 | 1378 | URL networkInterface = new URL(null, null, null, "0.0.0.0", 443, null, null, null, null, null); 1379 | Assert.assertEquals(networkInterface.getHost(), "0.0.0.0:443"); 1380 | Assert.assertEquals(networkInterface.getHostname(), "0.0.0.0"); 1381 | Assert.assertTrue(networkInterface.getPort().equals(443)); 1382 | Assert.assertEquals(networkInterface.toString(), "0.0.0.0:443"); 1383 | 1384 | URL url = URL.parse("https://example.com:443"); 1385 | Assert.assertTrue(url.toString().equals("https://example.com:443")); 1386 | Assert.assertEquals(url.getHost(), "example.com:443"); 1387 | Assert.assertEquals(url.getHostname(), "example.com"); 1388 | Assert.assertTrue(url.getPort() == 443); 1389 | 1390 | 1391 | URL url2 = URL.parse("https://example.com/path"); 1392 | Assert.assertTrue(url2.toString().equals("https://example.com/path")); 1393 | Assert.assertEquals(url2.getHost(), "example.com"); 1394 | Assert.assertEquals(url2.getHostname(), "example.com"); 1395 | Assert.assertEquals(url2.getPath(), "/path"); 1396 | Assert.assertTrue(url2.getPort() == null); 1397 | } 1398 | 1399 | 1400 | @Test(expected = MalformedURLException.class) 1401 | public void testCantParsePort() throws Exception { 1402 | URL missingPort = URL.parse("http://localhost:"); 1403 | } 1404 | 1405 | @Test 1406 | public void testEquals() throws Exception { 1407 | URL urlA = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section"); 1408 | URL urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section"); 1409 | Assert.assertTrue(urlA.equals(urlB)); 1410 | 1411 | urlA = URL.parse("http://domain.com/path/to/RESOURCE.html?q=abc#section"); 1412 | urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section"); 1413 | Assert.assertFalse(urlA.equals(urlB)); 1414 | 1415 | urlA = URL.parse("http://www.DOMAIN.com/path/to/RESOURCE.html?q=abc#section"); 1416 | urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section"); 1417 | Assert.assertTrue(urlA.equals(urlB)); 1418 | 1419 | urlA = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc"); 1420 | urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc#section"); 1421 | Assert.assertFalse(urlA.equals(urlB)); 1422 | 1423 | urlA = URL.parse("http://domain.com/path/to/RESOURCE.html?q=abc"); 1424 | urlB = URL.parse("http://domain.com/path/to/RESOURCE.html?q=abc#"); 1425 | Assert.assertTrue(urlA.equals(urlB)); 1426 | 1427 | urlA = URL.parse("http://www.domain.de/path/to/RESOURCE.html?q=abc"); 1428 | urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc"); 1429 | Assert.assertFalse(urlA.equals(urlB)); 1430 | 1431 | urlA = URL.parse("http://www.domain.com:8080/path/to/RESOURCE.html?q=abc"); 1432 | urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc"); 1433 | Assert.assertFalse(urlA.equals(urlB)); 1434 | 1435 | urlA = URL.parse("http://www.domain.com/path/to/ANOTHER.html?q=abc"); 1436 | urlB = URL.parse("http://www.domain.com/path/to/RESOURCE.html?q=abc"); 1437 | Assert.assertFalse(urlA.equals(urlB)); 1438 | 1439 | Assert.assertFalse(urlA == null); 1440 | Assert.assertFalse(urlA.equals(null)); 1441 | Assert.assertFalse("http://www.domain.com/path/to/ANOTHER.html?q=abc".equals(urlA)); 1442 | Assert.assertFalse(new Integer(123).equals(urlA)); 1443 | 1444 | urlA = URL.parse("8080"); 1445 | Assert.assertFalse(urlA.equals(8080)); 1446 | Assert.assertFalse("8080".equals(urlA)); 1447 | } 1448 | 1449 | private class URLReferenceTestCase { 1450 | public String inputBase; 1451 | public String inputReference; 1452 | public String expectedResolvedReference; 1453 | 1454 | public URLReferenceTestCase(String inputBase, String inputReference, String expectedResolvedReference) { 1455 | this.inputBase = inputBase; 1456 | this.inputReference = inputReference; 1457 | this.expectedResolvedReference = expectedResolvedReference; 1458 | } 1459 | } 1460 | 1461 | private class URLTestCase { 1462 | public String input; 1463 | public String expectedScheme; 1464 | public String expectedUsername; 1465 | public String expectedPassword; 1466 | public String expectedHost; 1467 | public String expectedPath; 1468 | public String expectedQuery; 1469 | public String expectedFragment; 1470 | public String expectedStringRepr; 1471 | 1472 | public URLTestCase(String input, String expectedScheme, String expectedUsername, String expectedPassword, String expectedHost, String expectedPath, String expectedQuery, String expectedFragment, String expectedStringRepr) { 1473 | this.input = input; 1474 | this.expectedScheme = expectedScheme; 1475 | this.expectedUsername = expectedUsername; 1476 | this.expectedPassword = expectedPassword; 1477 | this.expectedHost = expectedHost; 1478 | this.expectedPath = expectedPath; 1479 | this.expectedQuery = expectedQuery; 1480 | this.expectedFragment = expectedFragment; 1481 | this.expectedStringRepr = expectedStringRepr; 1482 | } 1483 | } 1484 | 1485 | private class QueryStringTestCase { 1486 | public String input; 1487 | public Map> expected; 1488 | 1489 | public QueryStringTestCase(String input, Map> expected) { 1490 | this.input = input; 1491 | this.expected = expected; 1492 | } 1493 | } 1494 | 1495 | private class ToJavaClassTestCase { 1496 | public String input; 1497 | public String expected; 1498 | 1499 | public ToJavaClassTestCase(String input, String expected) { 1500 | this.input = input; 1501 | this.expected = expected; 1502 | } 1503 | } 1504 | } --------------------------------------------------------------------------------