1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit.util;
16
17 import static java.nio.charset.StandardCharsets.US_ASCII;
18 import static java.nio.charset.StandardCharsets.UTF_8;
19
20 import java.io.ByteArrayOutputStream;
21 import java.net.MalformedURLException;
22 import java.net.URI;
23 import java.net.URISyntaxException;
24 import java.net.URL;
25 import java.net.URLEncoder;
26 import java.net.URLStreamHandler;
27 import java.nio.charset.Charset;
28 import java.util.BitSet;
29 import java.util.Locale;
30 import java.util.Objects;
31
32 import org.htmlunit.WebAssert;
33 import org.htmlunit.protocol.AnyHandler;
34 import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
35
36 /**
37 * URL utilities class that makes it easy to create new URLs based off of old URLs
38 * without having to assemble or parse them yourself.
39 *
40 * @author Daniel Gredler
41 * @author Martin Tamme
42 * @author Sudhan Moghe
43 * @author Marc Guillemot
44 * @author Ahmed Ashour
45 * @author Ronald Brill
46 * @author Joerg Werner
47 * @author Hartmut Arlt
48 */
49 public final class UrlUtils {
50
51 /** "about". */
52 public static final String ABOUT = "about";
53 /** "about:". */
54 public static final String ABOUT_SCHEME = ABOUT + ":";
55 /** "about:blank". */
56 public static final String ABOUT_BLANK = ABOUT_SCHEME + "blank";
57 /** URL for "about:blank". */
58 public static final URL URL_ABOUT_BLANK;
59
60 private static final URLStreamHandler JS_HANDLER;
61 private static final URLStreamHandler ABOUT_HANDLER;
62 private static final URLStreamHandler DATA_HANDLER;
63
64 private static final BitSet PATH_ALLOWED_CHARS = new BitSet(256);
65 private static final BitSet QUERY_ALLOWED_CHARS = new BitSet(256);
66 private static final BitSet ANCHOR_ALLOWED_CHARS = new BitSet(256);
67 private static final BitSet HASH_ALLOWED_CHARS = new BitSet(256);
68
69 /*
70 URI allowed char initialization; based on HttpClient 3.1's URI bit sets.
71 */
72 static {
73 // make sure the handlers are available first (before calling toUrlSafe())
74 JS_HANDLER = new org.htmlunit.protocol.javascript.Handler();
75 ABOUT_HANDLER = new org.htmlunit.protocol.about.Handler();
76 DATA_HANDLER = new org.htmlunit.protocol.data.Handler();
77
78 try {
79 URL_ABOUT_BLANK = new URL(null, ABOUT_BLANK, ABOUT_HANDLER);
80 }
81 catch (final MalformedURLException e) {
82 // should never happen
83 throw new RuntimeException(e);
84 }
85
86 final BitSet reserved = new BitSet(256);
87 reserved.set(';');
88 reserved.set('/');
89 reserved.set('?');
90 reserved.set(':');
91 reserved.set('@');
92 reserved.set('&');
93 reserved.set('=');
94 reserved.set('+');
95 reserved.set('$');
96 reserved.set(',');
97
98 final BitSet mark = new BitSet(256);
99 mark.set('-');
100 mark.set('_');
101 mark.set('.');
102 mark.set('!');
103 mark.set('~');
104 mark.set('*');
105 mark.set('\'');
106 mark.set('(');
107 mark.set(')');
108
109 final BitSet alpha = new BitSet(256);
110 for (int i = 'a'; i <= 'z'; i++) {
111 alpha.set(i);
112 }
113 for (int i = 'A'; i <= 'Z'; i++) {
114 alpha.set(i);
115 }
116
117 final BitSet digit = new BitSet(256);
118 for (int i = '0'; i <= '9'; i++) {
119 digit.set(i);
120 }
121
122 final BitSet alphanumeric = new BitSet(256);
123 alphanumeric.or(alpha);
124 alphanumeric.or(digit);
125
126 final BitSet unreserved = new BitSet(256);
127 unreserved.or(alphanumeric);
128 unreserved.or(mark);
129
130 final BitSet hex = new BitSet(256);
131 hex.or(digit);
132 for (int i = 'a'; i <= 'f'; i++) {
133 hex.set(i);
134 }
135 for (int i = 'A'; i <= 'F'; i++) {
136 hex.set(i);
137 }
138
139 final BitSet escaped = new BitSet(256);
140 escaped.set('%');
141 escaped.or(hex);
142
143 final BitSet uric = new BitSet(256);
144 uric.or(reserved);
145 uric.or(unreserved);
146 uric.or(escaped);
147
148 final BitSet pchar = new BitSet(256);
149 pchar.or(unreserved);
150 pchar.or(escaped);
151 pchar.set(':');
152 pchar.set('@');
153 pchar.set('&');
154 pchar.set('=');
155 pchar.set('+');
156 pchar.set('$');
157 pchar.set(',');
158
159 final BitSet segment = new BitSet(256);
160 segment.set(';');
161 segment.or(pchar);
162
163 final BitSet pathSegments = new BitSet(256);
164 pathSegments.set('/');
165 pathSegments.or(segment);
166
167 final BitSet allowedAbsPath = new BitSet(256);
168 allowedAbsPath.or(pathSegments);
169
170 final BitSet allowedFragment = new BitSet(256);
171 allowedFragment.or(uric);
172
173 final BitSet allowedQuery = new BitSet(256);
174 allowedQuery.or(uric);
175
176 final BitSet allowedHash = new BitSet(256);
177 allowedHash.or(uric);
178
179 PATH_ALLOWED_CHARS.or(allowedAbsPath);
180 QUERY_ALLOWED_CHARS.or(allowedQuery);
181 ANCHOR_ALLOWED_CHARS.or(allowedFragment);
182 HASH_ALLOWED_CHARS.or(allowedHash);
183 }
184
185 /**
186 * Disallow instantiation of this class.
187 */
188 private UrlUtils() {
189 // Empty.
190 }
191
192 /**
193 * <p>Constructs a URL instance based on the specified URL string, taking into account the fact that the
194 * specified URL string may represent an <code>"about:..."</code> URL, a <code>"javascript:..."</code> URL, or
195 * a <code>data:...</code> URL.</p>
196 *
197 * <p>The caller should be sure that URL strings passed to this method will parse correctly as URLs, as
198 * this method never expects to have to handle {@link MalformedURLException}s.</p>
199 *
200 * @param url the URL string to convert into a URL instance
201 * @return the constructed URL instance
202 */
203 public static URL toUrlSafe(final String url) {
204 try {
205 return toUrlUnsafe(url);
206 }
207 catch (final MalformedURLException e) {
208 // Should never happen.
209 throw new RuntimeException(e);
210 }
211 }
212
213 /**
214 * <p>Constructs a URL instance based on the specified URL string, taking into account the fact that the
215 * specified URL string may represent an <code>"about:..."</code> URL, a <code>"javascript:..."</code> URL, or
216 * a <code>data:...</code> URL.</p>
217 *
218 * <p>Unlike {@link #toUrlSafe(String)}, the caller need not be sure that URL strings passed to this
219 * method will parse correctly as URLs.</p>
220 *
221 * @param url the URL string to convert into a URL instance
222 * @return the constructed URL instance
223 * @throws MalformedURLException if the URL string cannot be converted to a URL instance
224 */
225 public static URL toUrlUnsafe(final String url) throws MalformedURLException {
226 WebAssert.notNull("url", url);
227
228 final String protocol = StringUtils.substringBefore(url, ":").toLowerCase(Locale.ROOT);
229
230 if (protocol.isEmpty() || UrlUtils.isNormalUrlProtocol(protocol)) {
231 final URL response = new URL(url);
232 if (response.getProtocol().startsWith("http")
233 && StringUtils.isEmptyOrNull(response.getHost())) {
234 throw new MalformedURLException("Missing host name in url: " + url);
235 }
236 return response;
237 }
238
239 if (JavaScriptURLConnection.JAVASCRIPT_PREFIX.equals(protocol + ":")) {
240 return new URL(null, url, JS_HANDLER);
241 }
242
243 if (ABOUT.equals(protocol)) {
244 if (ABOUT_BLANK.equalsIgnoreCase(url)) {
245 return URL_ABOUT_BLANK;
246 }
247 return new URL(null, url, ABOUT_HANDLER);
248 }
249
250 if ("data".equals(protocol)) {
251 return new URL(null, url, DATA_HANDLER);
252 }
253
254 return new URL(null, url, AnyHandler.INSTANCE);
255 }
256
257 /**
258 * <p>Encodes illegal characters in the specified URL's path, query string and anchor according to the URL
259 * encoding rules observed in real browsers.</p>
260 *
261 * <p>For example, this method changes
262 * <code>"http://first/?a=b c"</code> to <code>"http://first/?a=b%20c"</code>.</p>
263 *
264 * @param url the URL to encode
265 * @param charset the charset
266 * @return the encoded URL
267 */
268 public static URL encodeUrl(final URL url, final Charset charset) {
269 if (!isNormalUrlProtocol(url.getProtocol())) {
270 return url; // javascript:, about:, data: and anything not supported like foo:
271 }
272
273 try {
274 String path = url.getPath();
275 if (path != null) {
276 path = encode(path, PATH_ALLOWED_CHARS, UTF_8);
277 }
278 String query = url.getQuery();
279 if (query != null) {
280 query = encode(query, QUERY_ALLOWED_CHARS, charset);
281 }
282 String anchor = url.getRef();
283 if (anchor != null) {
284 anchor = encode(anchor, ANCHOR_ALLOWED_CHARS, UTF_8);
285 }
286 return createNewUrl(url.getProtocol(), url.getUserInfo(), url.getHost(),
287 url.getPort(), path, anchor, query);
288 }
289 catch (final MalformedURLException e) {
290 // Impossible... I think.
291 throw new RuntimeException(e);
292 }
293 }
294
295 /**
296 * Encodes and escapes the specified URI anchor string.
297 *
298 * @param anchor the anchor string to encode and escape
299 * @return the encoded and escaped anchor string
300 */
301 public static String encodeAnchor(final String anchor) {
302 if (anchor == null) {
303 return null;
304 }
305 return encode(anchor, ANCHOR_ALLOWED_CHARS, UTF_8);
306 }
307
308 /**
309 * Encodes and escapes the specified URI hash string.
310 *
311 * @param hash the anchor string to encode and escape
312 * @return the encoded and escaped hash string
313 */
314 public static String encodeHash(final String hash) {
315 if (hash == null) {
316 return null;
317 }
318 return encode(hash, HASH_ALLOWED_CHARS, UTF_8);
319 }
320
321 /**
322 * Encodes and escapes the specified URI hash string.
323 *
324 * @param query the query string to encode and escape
325 * @return the encoded and escaped hash string
326 */
327 public static String encodeQuery(final String query) {
328 if (query == null) {
329 return null;
330 }
331 return encode(query, QUERY_ALLOWED_CHARS, UTF_8);
332 }
333
334 /**
335 * Unescapes and decodes the specified string.
336 *
337 * @param escaped the string to be unescaped and decoded
338 * @return the unescaped and decoded string
339 */
340 public static String decode(final String escaped) {
341 try {
342 final byte[] bytes = escaped.getBytes(US_ASCII);
343 final byte[] bytes2 = decodeUrl(bytes);
344 return new String(bytes2, UTF_8);
345 }
346 catch (final IllegalArgumentException e) {
347 // Should never happen.
348 throw new RuntimeException(e);
349 }
350 }
351
352 /**
353 * Escapes and encodes the specified string. Based on HttpClient 3.1's <code>URIUtil.encode()</code> method.
354 *
355 * @param unescaped the string to encode
356 * @param allowed allowed characters that shouldn't be escaped
357 * @param charset the charset to use
358 * @return the escaped string
359 */
360 private static String encode(final String unescaped, final BitSet allowed, final Charset charset) {
361 final byte[] bytes = unescaped.getBytes(charset);
362 final byte[] bytes2 = encodeUrl(allowed, bytes);
363 return encodePercentSign(bytes2);
364 }
365
366 /**
367 * Encodes every occurrence of the escape character '%' in the given input
368 * string that is not followed by two hexadecimal characters.
369 * @param input the input bytes
370 * @return the given input string where every occurrence of <code>%</code> in
371 * invalid escape sequences has been replace by <code>%25</code>
372 */
373 private static String encodePercentSign(final byte[] input) {
374 if (input == null) {
375 return null;
376 }
377
378 final StringBuilder result = new StringBuilder(new String(input, US_ASCII));
379 int state = 0;
380 int offset = 0;
381 for (int i = 0; i < input.length; i++) {
382 final byte b = input[i];
383 if (state == 0 && b == '%') {
384 state = 1;
385 }
386 else if (state == 1 || state == 2) {
387 if (('0' <= b && b <= '9')
388 || ('A' <= b && b <= 'F')
389 || ('a' <= b && b <= 'f')) {
390 state++;
391 if (state == 3) {
392 state = 0;
393 }
394 }
395 else {
396 final int st = i - state + offset;
397 result.replace(st, st + 1, "%25");
398 offset = offset + 2;
399 state = b == '%' ? 1 : 0;
400 }
401 }
402 }
403 if (state == 1 || state == 2) {
404 final int st = input.length - state + offset;
405 result.replace(st, st + 1, "%25");
406 }
407 return result.toString();
408 }
409
410 /**
411 * Creates and returns a new URL using only the protocol and authority from the given one.
412 * @param u the URL on which to base the returned URL
413 * @return a new URL using only the protocol and authority from the given one
414 * @throws MalformedURLException if there is a problem creating the new URL
415 */
416 public static URL getUrlWithoutPathRefQuery(final URL u) throws MalformedURLException {
417 return createNewUrl(u.getProtocol(), u.getAuthority(), null, null, null);
418 }
419
420 /**
421 * Creates and returns a new URL using only the protocol, authority and path
422 * from the given one.
423 * @param u the URL on which to base the returned URL
424 * @return a new URL using only the protocol and authority from the given one
425 * @throws MalformedURLException if there is a problem creating the new URL
426 */
427 public static URL getUrlWithoutRef(final URL u) throws MalformedURLException {
428 return createNewUrl(u.getProtocol(), u.getAuthority(), u.getPath(), null, u.getQuery());
429 }
430
431 /**
432 * Creates and returns a new URL identical to the specified URL, except using the specified protocol.
433 * @param u the URL on which to base the returned URL
434 * @param newProtocol the new protocol to use in the returned URL
435 * @return a new URL identical to the specified URL, except using the specified protocol
436 * @throws MalformedURLException if there is a problem creating the new URL
437 */
438 public static URL getUrlWithNewProtocol(final URL u, final String newProtocol) throws MalformedURLException {
439 return createNewUrl(newProtocol, u.getAuthority(), u.getPath(), u.getRef(), u.getQuery());
440 }
441
442 /**
443 * Creates and returns a new URL identical to the specified URL, except using the specified host.
444 * @param u the URL on which to base the returned URL
445 * @param newHost the new host to use in the returned URL
446 * @return a new URL identical to the specified URL, except using the specified host
447 * @throws MalformedURLException if there is a problem creating the new URL
448 */
449 public static URL getUrlWithNewHost(final URL u, final String newHost)
450 throws MalformedURLException {
451 return createNewUrl(u.getProtocol(), u.getUserInfo(), newHost,
452 u.getPort(), u.getPath(), u.getRef(), u.getQuery());
453 }
454
455 /**
456 * Creates and returns a new URL identical to the specified URL, except using the specified host.
457 * @param u the URL on which to base the returned URL
458 * @param newHost the new host to use in the returned URL
459 * @param newPort the new port to use in the returned URL
460 * @return a new URL identical to the specified URL, except using the specified host
461 * @throws MalformedURLException if there is a problem creating the new URL
462 */
463 public static URL getUrlWithNewHostAndPort(final URL u, final String newHost, final int newPort)
464 throws MalformedURLException {
465 return createNewUrl(u.getProtocol(), u.getUserInfo(), newHost, newPort, u.getPath(), u.getRef(), u.getQuery());
466 }
467
468 /**
469 * Creates and returns a new URL identical to the specified URL, except using the specified port.
470 * @param u the URL on which to base the returned URL
471 * @param newPort the new port to use in the returned URL or -1 to remove it
472 * @return a new URL identical to the specified URL, except using the specified port
473 * @throws MalformedURLException if there is a problem creating the new URL
474 */
475 public static URL getUrlWithNewPort(final URL u, final int newPort) throws MalformedURLException {
476 return createNewUrl(u.getProtocol(), u.getUserInfo(), u.getHost(),
477 newPort, u.getPath(), u.getRef(), u.getQuery());
478 }
479
480 /**
481 * Creates and returns a new URL identical to the specified URL, except using the specified path.
482 * @param u the URL on which to base the returned URL
483 * @param newPath the new path to use in the returned URL
484 * @return a new URL identical to the specified URL, except using the specified path
485 * @throws MalformedURLException if there is a problem creating the new URL
486 */
487 public static URL getUrlWithNewPath(final URL u, final String newPath) throws MalformedURLException {
488 return createNewUrl(u.getProtocol(), u.getAuthority(), newPath, u.getRef(), u.getQuery());
489 }
490
491 /**
492 * Creates and returns a new URL identical to the specified URL, except using the specified reference.
493 * @param u the URL on which to base the returned URL
494 * @param newRef the new reference to use in the returned URL or null to remove it
495 * @return a new URL identical to the specified URL, except using the specified reference
496 * @throws MalformedURLException if there is a problem creating the new URL
497 */
498 public static URL getUrlWithNewRef(final URL u, final String newRef) throws MalformedURLException {
499 return createNewUrl(u.getProtocol(), u.getAuthority(), u.getPath(), newRef, u.getQuery());
500 }
501
502 /**
503 * Creates and returns a new URL identical to the specified URL, except using the specified query string.
504 * @param u the URL on which to base the returned URL
505 * @param newQuery the new query string to use in the returned URL
506 * @return a new URL identical to the specified URL, except using the specified query string
507 * @throws MalformedURLException if there is a problem creating the new URL
508 */
509 public static URL getUrlWithNewQuery(final URL u, final String newQuery) throws MalformedURLException {
510 return createNewUrl(u.getProtocol(), u.getAuthority(), u.getPath(), u.getRef(), newQuery);
511 }
512
513 /**
514 * Creates and returns a new URL identical to the specified URL, ignoring path, protocol and query.
515 * @param u the URL on which to base the returned URL
516 * @return a new URL identical to the specified URL, ignoring path, protocol and query
517 * @throws MalformedURLException if there is a problem creating the new URL
518 */
519 public static URL getUrlWithProtocolAndAuthority(final URL u) throws MalformedURLException {
520 return createNewUrl(u.getProtocol(), u.getAuthority(), null, null, null);
521 }
522
523 /**
524 * Creates and returns a new URL identical to the specified URL but with a changed user name.
525 * @param u the URL on which to base the returned URL
526 * @param newUserName the new user name or null to remove it
527 * @return a new URL identical to the specified URL; only user name updated
528 * @throws MalformedURLException if there is a problem creating the new URL
529 */
530 public static URL getUrlWithNewUserName(final URL u, final String newUserName) throws MalformedURLException {
531 String newUserInfo = newUserName == null ? "" : newUserName;
532 final String userInfo = u.getUserInfo();
533 if (StringUtils.isNotBlank(userInfo)) {
534 final int colonIdx = userInfo.indexOf(':');
535 if (colonIdx > -1) {
536 newUserInfo = newUserInfo + userInfo.substring(colonIdx);
537 }
538 }
539 return createNewUrl(u.getProtocol(), newUserInfo.isEmpty() ? null : newUserInfo,
540 u.getHost(), u.getPort(), u.getPath(), u.getRef(), u.getQuery());
541 }
542
543 /**
544 * Creates and returns a new URL identical to the specified URL but with a changed user password.
545 * @param u the URL on which to base the returned URL
546 * @param newUserPassword the new user password or null to remove it
547 * @return a new URL identical to the specified URL; only user name updated
548 * @throws MalformedURLException if there is a problem creating the new URL
549 */
550 public static URL getUrlWithNewUserPassword(final URL u, final String newUserPassword)
551 throws MalformedURLException {
552 final StringBuilder newUserInfo = new StringBuilder();
553
554 final String userInfo = u.getUserInfo();
555 if (StringUtils.isNotBlank(userInfo)) {
556 final int colonIdx = userInfo.indexOf(':');
557 if (colonIdx > -1) {
558 newUserInfo.append(userInfo, 0, colonIdx);
559 }
560 else {
561 newUserInfo.append(userInfo);
562 }
563 }
564
565 if (newUserPassword != null) {
566 newUserInfo.append(':').append(newUserPassword);
567 }
568
569 return createNewUrl(u.getProtocol(), newUserInfo.isEmpty() ? null : newUserInfo.toString(),
570 u.getHost(), u.getPort(), u.getPath(), u.getRef(), u.getQuery());
571 }
572
573 /**
574 * Creates a new URL based on the specified fragments.
575 * @param protocol the protocol to use (may not be {@code null})
576 * @param userInfo the user info to use (may be {@code null})
577 * @param host the host to use (may not be {@code null})
578 * @param port the port to use (may be <code>-1</code> if no port is specified)
579 * @param path the path to use (may be {@code null} and may omit the initial <code>'/'</code>)
580 * @param ref the reference to use (may be {@code null} and must not include the <code>'#'</code>)
581 * @param query the query to use (may be {@code null} and must not include the <code>'?'</code>)
582 * @return a new URL based on the specified fragments
583 * @throws MalformedURLException if there is a problem creating the new URL
584 */
585 private static URL createNewUrl(final String protocol, final String userInfo, final String host, final int port,
586 final String path, final String ref, final String query) throws MalformedURLException {
587 final StringBuilder s = new StringBuilder();
588 s.append(protocol).append("://");
589 if (userInfo != null) {
590 s.append(userInfo).append('@');
591 }
592 s.append(host);
593 if (port != -1) {
594 s.append(':').append(port);
595 }
596 if (path != null && !path.isEmpty()) {
597 if ('/' != path.charAt(0)) {
598 s.append('/');
599 }
600 s.append(path);
601 }
602 if (query != null) {
603 s.append('?').append(query);
604 }
605 if (ref != null) {
606 if (ref.isEmpty() || ref.charAt(0) != '#') {
607 s.append('#');
608 }
609 s.append(ref);
610 }
611
612 return new URL(s.toString());
613 }
614
615 /**
616 * Creates a new URL based on the specified fragments.
617 * @param protocol the protocol to use (may not be {@code null})
618 * @param authority the authority to use (may not be {@code null})
619 * @param path the path to use (may be {@code null} and may omit the initial <code>'/'</code>)
620 * @param ref the reference to use (may be {@code null} and must not include the <code>'#'</code>)
621 * @param query the query to use (may be {@code null} and must not include the <code>'?'</code>)
622 * @return a new URL based on the specified fragments
623 * @throws MalformedURLException if there is a problem creating the new URL
624 */
625 private static URL createNewUrl(final String protocol, final String authority,
626 final String path, final String ref, final String query) throws MalformedURLException {
627
628 // pre-compute length of StringBuilder
629 int len = protocol.length() + 1;
630 if (authority != null && !authority.isEmpty()) {
631 len += 2 + authority.length();
632 }
633 if (path != null) {
634 len += path.length();
635 }
636 if (query != null) {
637 len += 1 + query.length();
638 }
639 if (ref != null) {
640 len += 1 + ref.length();
641 }
642
643 final StringBuilder s = new StringBuilder(len);
644 s.append(protocol).append(':');
645 if (authority != null && !authority.isEmpty()) {
646 s.append("//").append(authority);
647 }
648 if (path != null) {
649 s.append(path);
650 }
651 if (query != null) {
652 s.append('?').append(query);
653 }
654 if (ref != null) {
655 if (ref.isEmpty() || ref.charAt(0) != '#') {
656 s.append('#');
657 }
658 s.append(ref);
659 }
660
661 return toUrlSafe(s.toString());
662 }
663
664 /**
665 * Resolves a given relative URL against a base URL. See
666 * <a href="http://www.faqs.org/rfcs/rfc1808.html">RFC1808</a>
667 * Section 4 for more details.
668 *
669 * @param baseUrl The base URL in which to resolve the specification.
670 * @param relativeUrl The relative URL to resolve against the base URL.
671 * @return the resolved specification.
672 */
673 public static String resolveUrl(final String baseUrl, final String relativeUrl) {
674 if (baseUrl == null) {
675 throw new IllegalArgumentException("Base URL must not be null");
676 }
677 if (relativeUrl == null) {
678 throw new IllegalArgumentException("Relative URL must not be null");
679 }
680 final Url url = resolveUrl(parseUrl(baseUrl), relativeUrl);
681
682 return url.toString();
683 }
684
685 /**
686 * Resolves a given relative URL against a base URL. See
687 * <a href="http://www.faqs.org/rfcs/rfc1808.html">RFC1808</a>
688 * Section 4 for more details.
689 *
690 * @param baseUrl The base URL in which to resolve the specification.
691 * @param relativeUrl The relative URL to resolve against the base URL.
692 * @return the resolved specification.
693 */
694 public static String resolveUrl(final URL baseUrl, final String relativeUrl) {
695 if (baseUrl == null) {
696 throw new IllegalArgumentException("Base URL must not be null");
697 }
698 return resolveUrl(baseUrl.toExternalForm(), relativeUrl);
699 }
700
701 /**
702 * Parses a given specification using the algorithm depicted in
703 * <a href="http://www.faqs.org/rfcs/rfc1808.html">RFC1808</a>.
704 * <p>
705 * Section 2.4: Parsing a URL
706 * </p>
707 * <p>
708 * An accepted method for parsing URLs is useful to clarify the
709 * generic-RL syntax of Section 2.2 and to describe the algorithm for
710 * resolving relative URLs presented in Section 4. This section
711 * describes the parsing rules for breaking down a URL (relative or
712 * absolute) into the component parts described in Section 2.1. The
713 * rules assume that the URL has already been separated from any
714 * surrounding text and copied to a "parse string". The rules are
715 * listed in the order in which they would be applied by the parser.
716 * </p>
717 * @param spec The specification to parse.
718 * @return the parsed specification.
719 */
720 private static Url parseUrl(String spec) {
721 final Url url = new Url();
722 int startIndex = 0;
723 int endIndex = spec.length();
724
725 // see https://url.spec.whatwg.org/#concept-basic-url-parser
726 // * If input contains any leading or trailing C0 control or space, validation error.
727 // Remove any leading and trailing C0 control or space from input.
728 // * If input contains any ASCII tab or newline, validation error.
729 // Remove all ASCII tab or newline from input.
730
731 if (endIndex > startIndex) {
732 StringBuilder sb = null;
733 boolean before = true;
734 int trailing = 0;
735
736 for (int i = 0; i < endIndex; i++) {
737 final char c = spec.charAt(i);
738 boolean remove = false;
739
740 if (c == '\t' | c == '\r' | c == '\n') {
741 remove = true;
742 }
743 else if ('\u0000' <= c && c <= '\u0020') {
744 if (before) {
745 remove = true;
746 }
747 else {
748 trailing++;
749 }
750 }
751 else {
752 before = false;
753 trailing = 0;
754 }
755
756 if (remove) {
757 if (sb == null) {
758 sb = new StringBuilder(spec.substring(0, i));
759 }
760 }
761 else if (sb != null) {
762 sb.append(c);
763 }
764 }
765
766 if (sb == null) {
767 if (trailing > 0) {
768 endIndex = spec.length() - trailing;
769 spec = spec.substring(0, endIndex);
770 }
771 }
772 else {
773 if (trailing > 0) {
774 spec = sb.substring(0, sb.length() - trailing);
775 }
776 else {
777 spec = sb.toString();
778 }
779 endIndex = spec.length();
780 }
781 }
782
783 // Section 2.4.1: Parsing the Fragment Identifier
784 //
785 // If the parse string contains a crosshatch "#" character, then the
786 // substring after the first (left-most) crosshatch "#" and up to the
787 // end of the parse string is the <fragment> identifier. If the
788 // crosshatch is the last character, or no crosshatch is present, then
789 // the fragment identifier is empty. The matched substring, including
790 // the crosshatch character, is removed from the parse string before
791 // continuing.
792 //
793 // Note that the fragment identifier is not considered part of the URL.
794 // However, since it is often attached to the URL, parsers must be able
795 // to recognize and set aside fragment identifiers as part of the
796 // process.
797 final int crosshatchIndex = StringUtils.indexOf(spec, '#', startIndex, endIndex);
798
799 if (crosshatchIndex >= 0) {
800 url.fragment_ = spec.substring(crosshatchIndex + 1, endIndex);
801 endIndex = crosshatchIndex;
802 }
803 // Section 2.4.2: Parsing the Scheme
804 //
805 // If the parse string contains a colon ":" after the first character
806 // and before any characters not allowed as part of a scheme name (i.e.,
807 // any not an alphanumeric, plus "+", period ".", or hyphen "-"), the
808 // <scheme> of the URL is the substring of characters up to but not
809 // including the first colon. These characters and the colon are then
810 // removed from the parse string before continuing.
811 final int colonIndex = StringUtils.indexOf(spec, ':', startIndex, endIndex);
812
813 if (colonIndex > 0) {
814 final String scheme = spec.substring(startIndex, colonIndex);
815 if (isValidScheme(scheme)) {
816 url.scheme_ = scheme;
817 startIndex = colonIndex + 1;
818 }
819 }
820 // Section 2.4.3: Parsing the Network Location/Login
821 //
822 // If the parse string begins with a double-slash "//", then the
823 // substring of characters after the double-slash and up to, but not
824 // including, the next slash "/" character is the network location/login
825 // (<net_loc>) of the URL. If no trailing slash "/" is present, the
826 // entire remaining parse string is assigned to <net_loc>. The double-
827 // slash and <net_loc> are removed from the parse string before
828 // continuing.
829 //
830 // Note: We also accept a question mark "?" or a semicolon ";" character as
831 // delimiters for the network location/login (<net_loc>) of the URL.
832 final int locationStartIndex;
833 int locationEndIndex;
834
835 if (spec.startsWith("//", startIndex)) {
836 locationStartIndex = startIndex + 2;
837 locationEndIndex = StringUtils.indexOf(spec, '/', locationStartIndex, endIndex);
838 if (locationEndIndex >= 0) {
839 startIndex = locationEndIndex;
840 }
841 }
842 else {
843 locationStartIndex = -1;
844 locationEndIndex = -1;
845 }
846 // Section 2.4.4: Parsing the Query Information
847 //
848 // If the parse string contains a question mark "?" character, then the
849 // substring after the first (left-most) question mark "?" and up to the
850 // end of the parse string is the <query> information. If the question
851 // mark is the last character, or no question mark is present, then the
852 // query information is empty. The matched substring, including the
853 // question mark character, is removed from the parse string before
854 // continuing.
855 final int questionMarkIndex = StringUtils.indexOf(spec, '?', startIndex, endIndex);
856
857 if (questionMarkIndex >= 0) {
858 if ((locationStartIndex >= 0) && (locationEndIndex < 0)) {
859 // The substring of characters after the double-slash and up to, but not
860 // including, the question mark "?" character is the network location/login
861 // (<net_loc>) of the URL.
862 locationEndIndex = questionMarkIndex;
863 startIndex = questionMarkIndex;
864 }
865 url.query_ = spec.substring(questionMarkIndex + 1, endIndex);
866 endIndex = questionMarkIndex;
867 }
868 // Section 2.4.5: Parsing the Parameters
869 //
870 // If the parse string contains a semicolon ";" character, then the
871 // substring after the first (left-most) semicolon ";" and up to the end
872 // of the parse string is the parameters (<params>). If the semicolon
873 // is the last character, or no semicolon is present, then <params> is
874 // empty. The matched substring, including the semicolon character, is
875 // removed from the parse string before continuing.
876 final int semicolonIndex = StringUtils.indexOf(spec, ';', startIndex, endIndex);
877
878 if (semicolonIndex >= 0) {
879 if ((locationStartIndex >= 0) && (locationEndIndex < 0)) {
880 // The substring of characters after the double-slash and up to, but not
881 // including, the semicolon ";" character is the network location/login
882 // (<net_loc>) of the URL.
883 locationEndIndex = semicolonIndex;
884 startIndex = semicolonIndex;
885 }
886 url.parameters_ = spec.substring(semicolonIndex + 1, endIndex);
887 endIndex = semicolonIndex;
888 }
889 // Section 2.4.6: Parsing the Path
890 //
891 // After the above steps, all that is left of the parse string is the
892 // URL <path> and the slash "/" that may precede it. Even though the
893 // initial slash is not part of the URL path, the parser must remember
894 // whether or not it was present so that later processes can
895 // differentiate between relative and absolute paths. Often this is
896 // done by simply storing the preceding slash along with the path.
897 if ((locationStartIndex >= 0) && (locationEndIndex < 0)) {
898 // The entire remaining parse string is assigned to the network
899 // location/login (<net_loc>) of the URL.
900 locationEndIndex = endIndex;
901 }
902 else if (startIndex < endIndex) {
903 url.path_ = spec.substring(startIndex, endIndex);
904 }
905 // Set the network location/login (<net_loc>) of the URL.
906 if ((locationStartIndex >= 0) && (locationEndIndex >= 0)) {
907 url.location_ = spec.substring(locationStartIndex, locationEndIndex);
908 }
909 return url;
910 }
911
912 /**
913 * Returns true if specified string is a valid scheme name.
914 * <p>
915 * https://tools.ietf.org/html/rfc1738
916 * </p>
917 * <p>
918 * Scheme names consist of a sequence of characters. The lower case
919 * letters "a"--"z", digits, and the characters plus ("+"), period
920 * ("."), and hyphen ("-") are allowed. For resiliency, programs
921 * interpreting URLs should treat upper case letters as equivalent to
922 * lower case in scheme names (e.g., allow "HTTP" as well as "http").
923 * </p>
924 *
925 * @param scheme the scheme string to check
926 * @return true if valid
927 */
928 public static boolean isValidScheme(final String scheme) {
929 final int length = scheme.length();
930 if (length < 1) {
931 return false;
932 }
933
934 char c = scheme.charAt(0);
935 boolean isValid = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z');
936 if (!isValid) {
937 return false;
938 }
939
940 for (int i = 1; i < length; i++) {
941 c = scheme.charAt(i);
942 isValid =
943 ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
944 || ('0' <= c && c <= '9')
945 || c == '+'
946 || c == '.'
947 || c == '-';
948 if (!isValid) {
949 return false;
950 }
951 }
952
953 return true;
954 }
955
956 /**
957 * Returns true if specified string is a special scheme.
958 * see <a href='https://url.spec.whatwg.org/#special-scheme'>
959 * https://url.spec.whatwg.org/#special-scheme</a>
960 *
961 * @param scheme the scheme string to check
962 * @return true if special
963 */
964 public static boolean isSpecialScheme(final String scheme) {
965 final int length = scheme.length();
966 if (length < 2 || length > 5) {
967 return false;
968 }
969
970 final String schemeLC = scheme.toLowerCase(Locale.ROOT);
971 return "ftp".equals(schemeLC)
972 || "file".equals(schemeLC)
973 || "http".equals(schemeLC)
974 || "https".equals(schemeLC)
975 || "ws".equals(schemeLC)
976 || "wss".equals(schemeLC);
977 }
978
979 /**
980 * Resolves a given relative URL against a base URL using the algorithm
981 * depicted in <a href="http://www.faqs.org/rfcs/rfc1808.html">RFC1808</a>.
982 * <p>
983 * Section 4: Resolving Relative URLs
984 * </p>
985 * <p>
986 * This section describes an example algorithm for resolving URLs within
987 * a context in which the URLs may be relative, such that the result is
988 * always a URL in absolute form. Although this algorithm cannot
989 * guarantee that the resulting URL will equal that intended by the
990 * original author, it does guarantee that any valid URL (relative or
991 * absolute) can be consistently transformed to an absolute form given a
992 * valid base URL.
993 * </p>
994 *
995 * @param baseUrl The base URL in which to resolve the specification.
996 * @param relativeUrl The relative URL to resolve against the base URL.
997 * @return the resolved specification.
998 */
999 private static Url resolveUrl(final Url baseUrl, final String relativeUrl) {
1000 final Url url = parseUrl(relativeUrl);
1001
1002 // Step 1: The base URL is established according to the rules of
1003 // Section 3. If the base URL is the empty string (unknown),
1004 // the embedded URL is interpreted as an absolute URL and
1005 // we are done.
1006 if (baseUrl == null) {
1007 return url;
1008 }
1009
1010 // Step 2: Both the base and embedded URLs are parsed into their
1011 // component parts as described in Section 2.4.
1012 // a) If the embedded URL is entirely empty, it inherits the
1013 // entire base URL (i.e., is set equal to the base URL)
1014 // and we are done.
1015 if (relativeUrl.isEmpty()) {
1016 return new Url(baseUrl);
1017 }
1018 // b) If the embedded URL starts with a scheme name, it is
1019 // interpreted as an absolute URL and we are done.
1020 if (url.scheme_ != null) {
1021 return url;
1022 }
1023 // c) Otherwise, the embedded URL inherits the scheme of
1024 // the base URL.
1025 url.scheme_ = baseUrl.scheme_;
1026
1027 // Step 3: If the embedded URL's <net_loc> is non-empty, we skip to
1028 // Step 7. Otherwise, the embedded URL inherits the <net_loc>
1029 // (if any) of the base URL.
1030 if (url.location_ != null) {
1031 return url;
1032 }
1033 url.location_ = baseUrl.location_;
1034
1035 // Step 4: If the embedded URL path is preceded by a slash "/", the
1036 // path is not relative and we skip to Step 7.
1037 if (url.path_ != null && !url.path_.isEmpty() && url.path_.charAt(0) == '/') {
1038 url.path_ = normalizeDotSegments(url.path_);
1039 return url;
1040 }
1041
1042 // Step 5: If the embedded URL path is empty (and not preceded by a
1043 // slash), then the embedded URL inherits the base URL path,
1044 // and
1045 if (url.path_ == null) {
1046 url.path_ = baseUrl.path_;
1047 // a) if the embedded URL's <params> is non-empty, we skip to
1048 // step 7; otherwise, it inherits the <params> of the base
1049 // URL (if any) and
1050 if (url.parameters_ != null) {
1051 return url;
1052 }
1053 url.parameters_ = baseUrl.parameters_;
1054 // b) if the embedded URL's <query> is non-empty, we skip to
1055 // step 7; otherwise, it inherits the <query> of the base
1056 // URL (if any) and we skip to step 7.
1057 if (url.query_ != null) {
1058 return url;
1059 }
1060 url.query_ = baseUrl.query_;
1061 return url;
1062 }
1063
1064 // Step 6: The last segment of the base URL's path (anything
1065 // following the rightmost slash "/", or the entire path if no
1066 // slash is present) is removed and the embedded URL's path is
1067 // appended in its place. The following operations are
1068 // then applied, in order, to the new path:
1069 final String basePath = baseUrl.path_;
1070 String path = "";
1071
1072 if (basePath == null) {
1073 path = "/";
1074 }
1075 else {
1076 final int lastSlashIndex = basePath.lastIndexOf('/');
1077
1078 if (lastSlashIndex >= 0) {
1079 path = basePath.substring(0, lastSlashIndex + 1);
1080 }
1081 }
1082
1083 path = path.concat(url.path_);
1084 url.path_ = normalizeDotSegments(path);
1085
1086 // Step 7: The resulting URL components, including any inherited from
1087 // the base URL, are recombined to give the absolute form of
1088 // the embedded URL.
1089 return url;
1090 }
1091
1092 /**
1093 * Applies the RFC1808 Section 4, Step 6 dot-segment removal rules (a)-(d)
1094 * to {@code path}, followed by the browser-specific leading-"../" collapse.
1095 * Used both for the merged base+relative path in Step 6 itself, and for an
1096 * embedded URL whose own path is already absolute (Step 4) -- an absolute
1097 * path reference needs the same normalization a merged path gets, it just
1098 * isn't merged with anything first.
1099 *
1100 * @param path the path to normalize; must already be the final path to
1101 * clean up (already merged with the base path, if applicable)
1102 * @return the normalized path
1103 */
1104 private static String normalizeDotSegments(String path) {
1105 // a) All occurrences of "./", where "." is a complete path
1106 // segment, are removed.
1107 int pathSegmentIndex;
1108 while ((pathSegmentIndex = path.indexOf("/./")) >= 0) {
1109 path = path.substring(0, pathSegmentIndex + 1).concat(path.substring(pathSegmentIndex + 3));
1110 }
1111 // b) If the path ends with "." as a complete path segment,
1112 // that "." is removed.
1113 if (path.endsWith("/.")) {
1114 path = path.substring(0, path.length() - 1);
1115 }
1116 // c) All occurrences of "<segment>/../", where <segment> is a
1117 // complete path segment not equal to "..", are removed.
1118 // Removal of these path segments is performed iteratively,
1119 // removing the leftmost matching pattern on each iteration,
1120 // until no matching pattern remains.
1121 while ((pathSegmentIndex = path.indexOf("/../")) > 0) {
1122 final String pathSegment = path.substring(0, pathSegmentIndex);
1123 final int slashIndex = pathSegment.lastIndexOf('/');
1124
1125 if (slashIndex >= 0) {
1126 if (!"..".equals(pathSegment.substring(slashIndex))) {
1127 path = path.substring(0, slashIndex + 1).concat(path.substring(pathSegmentIndex + 4));
1128 }
1129 }
1130 else {
1131 path = path.substring(pathSegmentIndex + 4);
1132 }
1133 }
1134 // d) If the path ends with "<segment>/..", where <segment> is a
1135 // complete path segment not equal to "..", that
1136 // "<segment>/.." is removed.
1137 if (path.endsWith("/..")) {
1138 final String pathSegment = path.substring(0, path.length() - 3);
1139 final int slashIndex = pathSegment.lastIndexOf('/');
1140
1141 if (slashIndex >= 0) {
1142 path = path.substring(0, slashIndex + 1);
1143 }
1144 }
1145
1146 return removeLeadingSlashPoints(path);
1147 }
1148
1149 /**
1150 * "../" after the leading "/" should be removed as browsers do (not in RFC).
1151 */
1152 private static String removeLeadingSlashPoints(final String path) {
1153 int i = 1;
1154 while (path.startsWith("../", i)) {
1155 i = i + 3;
1156 }
1157
1158 if (i > 1) {
1159 return "/" + path.substring(i);
1160 }
1161
1162 return path;
1163 }
1164
1165 /**
1166 * Class <code>Url</code> represents a Uniform Resource Locator.
1167 */
1168 private static class Url {
1169
1170 private String scheme_;
1171 private String location_;
1172 private String path_;
1173 private String parameters_;
1174 private String query_;
1175 private String fragment_;
1176
1177 /**
1178 * Creates a <code>Url</code> object.
1179 */
1180 Url() {
1181 super();
1182 }
1183
1184 /**
1185 * Creates a <code>Url</code> object from the specified
1186 * <code>Url</code> object.
1187 *
1188 * @param url a <code>Url</code> object.
1189 */
1190 Url(final Url url) {
1191 scheme_ = url.scheme_;
1192 location_ = url.location_;
1193 path_ = url.path_;
1194 parameters_ = url.parameters_;
1195 query_ = url.query_;
1196 fragment_ = url.fragment_;
1197 }
1198
1199 /**
1200 * Returns a string representation of the <code>Url</code> object.
1201 *
1202 * @return a string representation of the <code>Url</code> object.
1203 */
1204 @Override
1205 public String toString() {
1206 final StringBuilder sb = new StringBuilder();
1207
1208 if (scheme_ != null) {
1209 sb.append(scheme_).append(':');
1210 }
1211 if (location_ != null) {
1212 sb.append("//").append(location_);
1213 }
1214 if (path_ != null) {
1215 sb.append(path_);
1216 }
1217 if (parameters_ != null) {
1218 sb.append(';').append(parameters_);
1219 }
1220 if (query_ != null) {
1221 sb.append('?').append(query_);
1222 }
1223 if (fragment_ != null) {
1224 sb.append('#').append(fragment_);
1225 }
1226 return sb.toString();
1227 }
1228 }
1229
1230 static boolean isNormalUrlProtocol(final String protocol) {
1231 return "http".equals(protocol) || "https".equals(protocol) || "file".equals(protocol);
1232 }
1233
1234 /**
1235 * More or less the same as sameFile(URL, URL) but without
1236 * resolving the host to an IP address for comparing.
1237 * Additionally we do some path normalization.
1238 *
1239 * @param u1 a URL object
1240 * @param u2 a URL object
1241 * @return true if u1 and u2 refer to the same file
1242 */
1243 public static boolean sameFile(final URL u1, final URL u2) {
1244 if (u1 == u2) {
1245 return true;
1246 }
1247 if (u1 == null || u2 == null) {
1248 return false;
1249 }
1250
1251 // Compare the protocols.
1252 final String p1 = u1.getProtocol();
1253 final String p2 = u2.getProtocol();
1254 if (!(p1 == p2 || (p1 != null && p1.equalsIgnoreCase(p2)))) {
1255 return false;
1256 }
1257
1258 // Compare the ports.
1259 final int port1 = (u1.getPort() == -1) ? u1.getDefaultPort() : u1.getPort();
1260 final int port2 = (u2.getPort() == -1) ? u2.getDefaultPort() : u2.getPort();
1261 if (port1 != port2) {
1262 return false;
1263 }
1264
1265 // Compare the hosts.
1266 final String h1 = u1.getHost();
1267 final String h2 = u2.getHost();
1268 if (!(h1 == h2 || (h1 != null && h1.equalsIgnoreCase(h2)))) {
1269 return false;
1270 }
1271
1272 // Compare the files.
1273 String f1 = u1.getFile();
1274 if (f1.isEmpty()) {
1275 f1 = "/";
1276 }
1277 String f2 = u2.getFile();
1278 if (f2.isEmpty()) {
1279 f2 = "/";
1280 }
1281 if (f1.indexOf('.') > 0 || f2.indexOf('.') > 0) {
1282 try {
1283 f1 = u1.toURI().normalize().toURL().getFile();
1284 f2 = u2.toURI().normalize().toURL().getFile();
1285 }
1286 catch (final RuntimeException e) {
1287 throw e;
1288 }
1289 catch (final Exception ignored) {
1290 // ignore
1291 }
1292 }
1293
1294 return Objects.equals(f1, f2);
1295 }
1296
1297 /**
1298 * Helper that constructs a normalized url string
1299 * usable as cache key.
1300 *
1301 * @param url a URL object
1302 * @return the normalized string
1303 */
1304 public static String normalize(final URL url) {
1305 final StringBuilder result = new StringBuilder();
1306 result.append(url.getProtocol())
1307 .append("://")
1308 .append(url.getHost())
1309 .append(':')
1310 .append((url.getPort() == -1) ? url.getDefaultPort() : url.getPort());
1311
1312 // Compare the files.
1313 String f = url.getFile();
1314 if (f.isEmpty()) {
1315 result.append('/');
1316 }
1317 else {
1318 if (f.indexOf('.') > 0) {
1319 try {
1320 f = url.toURI().normalize().toURL().getFile();
1321 }
1322 catch (final Exception ignored) {
1323 // ignore
1324 }
1325 }
1326 result.append(f);
1327 }
1328
1329 return result.toString();
1330 }
1331
1332 /**
1333 * Constructs a {@link URI} using the specified URL.
1334 *
1335 * @param url the URL
1336 * @param query the query
1337 *
1338 * @throws URISyntaxException
1339 * If both a scheme and a path are given but the path is
1340 * relative, if the URI string constructed from the given
1341 * components violates RFC 2396, or if the authority
1342 * component of the string is present but cannot be parsed
1343 * as a server-based authority
1344 * @return the URI
1345 */
1346 public static URI toURI(final URL url, final String query) throws URISyntaxException {
1347 final String scheme = url.getProtocol();
1348 final String host = url.getHost();
1349 final int port = url.getPort();
1350 final String path = url.getPath();
1351 final StringBuilder buffer = new StringBuilder();
1352 if (host != null) {
1353 if (scheme != null) {
1354 buffer.append(scheme).append("://");
1355 }
1356 buffer.append(host);
1357 if (port > 0) {
1358 buffer.append(':').append(port);
1359 }
1360 }
1361 if (path == null || path.isEmpty() || path.charAt(0) != '/') {
1362 buffer.append('/');
1363 }
1364 if (path != null) {
1365 buffer.append(path);
1366 }
1367 if (query != null) {
1368 buffer.append('?').append(query);
1369 }
1370 return new URI(buffer.toString());
1371 }
1372
1373 /**
1374 * Returns the encoded string.
1375 *
1376 * @param part the part to encode
1377 * @return the encoded string
1378 */
1379 public static String encodeQueryPart(final String part) {
1380 if (part == null || part.isEmpty()) {
1381 return "";
1382 }
1383
1384 return URLEncoder.encode(part, UTF_8);
1385 }
1386
1387 /**
1388 * Removes the well known ports if it can be deduced from protocol.
1389 * @param url the url to clean up
1390 * @return a new URL without the port or the given one
1391 * @throws MalformedURLException if the URL string cannot be converted to a URL instance
1392 */
1393 public static URL removeRedundantPort(final URL url) throws MalformedURLException {
1394 if (("https".equals(url.getProtocol()) && url.getPort() == 443)
1395 || ("http".equals(url.getProtocol()) && url.getPort() == 80)) {
1396 return getUrlWithNewPort(url, -1);
1397 }
1398 return url;
1399 }
1400
1401 /**
1402 * Decodes an array of URL safe 7-bit characters into an array of original bytes.
1403 * Escaped characters are converted back to their original representation.
1404 * @param bytes array of URL safe characters
1405 * @param removeWhitespace if true don't add whitespace chars to the output
1406 * @return array of original bytes
1407 * @throws IllegalArgumentException in case of error
1408 */
1409 public static byte[] decodeDataUrl(final byte[] bytes, final boolean removeWhitespace)
1410 throws IllegalArgumentException {
1411 // adapted from apache commons codec
1412 if (bytes == null) {
1413 return null;
1414 }
1415 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
1416 for (int i = 0; i < bytes.length; i++) {
1417 int b = bytes[i];
1418 if (b == '%') {
1419 try {
1420 final int u = digit16(bytes[++i]);
1421 final int l = digit16(bytes[++i]);
1422 b = (u << 4) + l;
1423 }
1424 catch (final ArrayIndexOutOfBoundsException e) {
1425 throw new IllegalArgumentException("Invalid URL encoding: ", e);
1426 }
1427 }
1428 if (removeWhitespace
1429 && (b == 9 || b == 10 || b == 12 || b == 13 || b == 32)) {
1430 continue;
1431 }
1432
1433 buffer.write(b);
1434 }
1435 return buffer.toByteArray();
1436 }
1437
1438 /**
1439 * Decodes an array of URL safe 7-bit characters into an array of original bytes.
1440 * Escaped characters are converted back to their original representation.
1441 * @param bytes array of URL safe characters
1442 * @return array of original bytes
1443 * @throws IllegalArgumentException in case of error
1444 */
1445 public static byte[] decodeUrl(final byte[] bytes) throws IllegalArgumentException {
1446 // adapted from apache commons codec
1447 if (bytes == null) {
1448 return null;
1449 }
1450 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
1451 for (int i = 0; i < bytes.length; i++) {
1452 final int b = bytes[i];
1453 if (b == '+') {
1454 buffer.write(' ');
1455 }
1456 else if (b == '%') {
1457 try {
1458 final int u = digit16(bytes[++i]);
1459 final int l = digit16(bytes[++i]);
1460 buffer.write((char) ((u << 4) + l));
1461 }
1462 catch (final ArrayIndexOutOfBoundsException e) {
1463 throw new IllegalArgumentException("Invalid URL encoding: ", e);
1464 }
1465 }
1466 else {
1467 buffer.write(b);
1468 }
1469 }
1470 return buffer.toByteArray();
1471 }
1472
1473 private static int digit16(final byte b) throws IllegalArgumentException {
1474 final int i = Character.digit((char) b, 16);
1475 if (i == -1) {
1476 throw new IllegalArgumentException("Invalid URL encoding: not a valid digit (radix 16): " + b);
1477 }
1478 return i;
1479 }
1480
1481 /**
1482 * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped.
1483 * @param urlsafe bitset of characters deemed URL safe
1484 * @param bytes array of bytes to convert to URL safe characters
1485 * @return array of bytes containing URL safe characters
1486 */
1487 public static byte[] encodeUrl(final BitSet urlsafe, final byte[] bytes) {
1488 // adapted from apache commons codec
1489 if (bytes == null) {
1490 return null;
1491 }
1492
1493 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
1494 for (final byte c : bytes) {
1495 int b = c;
1496 if (b < 0) {
1497 b = 256 + b;
1498 }
1499 if (urlsafe.get(b)) {
1500 if (b == ' ') {
1501 b = '+';
1502 }
1503 buffer.write(b);
1504 }
1505 else {
1506 buffer.write('%');
1507 final char hex1 = hexDigit(b >> 4);
1508 final char hex2 = hexDigit(b);
1509 buffer.write(hex1);
1510 buffer.write(hex2);
1511 }
1512 }
1513 return buffer.toByteArray();
1514 }
1515
1516 private static char hexDigit(final int b) {
1517 return Character.toUpperCase(Character.forDigit(b & 0xF, 16));
1518 }
1519
1520 /**
1521 * Determines whether two URLs share the same origin according to the Same-Origin Policy.
1522 * Two URLs are considered to have the same origin if they have the same protocol (scheme),
1523 * host, and port.
1524 *
1525 * <p>The method handles default ports correctly by using the URL's default port when
1526 * the explicit port is -1 (indicating no port was specified).
1527 * </p>
1528 *
1529 * @param originUrl the first URL to compare (must not be null)
1530 * @param newUrl the second URL to compare (must not be null)
1531 * @return {@code true} if both URLs have the same host and effective port; {@code false} otherwise
1532 */
1533 public static boolean isSameOrigin(final URL originUrl, final URL newUrl) {
1534 if (!originUrl.getProtocol().equals(newUrl.getProtocol())) {
1535 return false;
1536 }
1537
1538 if (!originUrl.getHost().equalsIgnoreCase(newUrl.getHost())) {
1539 return false;
1540 }
1541
1542 int originPort = originUrl.getPort();
1543 if (originPort == -1) {
1544 originPort = originUrl.getDefaultPort();
1545 }
1546 int newPort = newUrl.getPort();
1547 if (newPort == -1) {
1548 newPort = newUrl.getDefaultPort();
1549 }
1550 return originPort == newPort;
1551 }
1552 }