001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.request; 018 019import java.io.Serializable; 020import java.nio.charset.Charset; 021import java.nio.charset.StandardCharsets; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Locale; 027 028import org.apache.wicket.util.encoding.UrlDecoder; 029import org.apache.wicket.util.encoding.UrlEncoder; 030import org.apache.wicket.util.lang.Args; 031import org.apache.wicket.util.lang.Generics; 032import org.apache.wicket.util.lang.Objects; 033import org.apache.wicket.util.string.StringValue; 034import org.apache.wicket.util.string.Strings; 035 036/** 037 * Represents the URL to an external resource or internal resource/component. 038 * <p> 039 * A url could be: 040 * <ul> 041 * <li>full - consists of an optional protocol/scheme, a host name, an optional port, 042 * optional segments and and optional query parameters.</li> 043 * <li>non-full: 044 * <ul> 045 * <li>absolute - a url relative to the host name. Such url may escape from the application by using 046 * different context path and/or different filter path. For example: <code>/foo/bar</code></li> 047 * <li>relative - a url relative to the current base url. The base url is the url of the currently rendered page. 048 * For example: <code>foo/bar</code>, <code>../foo/bar</code></li> 049 * </ul> 050 * </ul> 051 * 052 * </p> 053 * 054 * Example URLs: 055 * 056 * <ul> 057 * <li>http://hostname:1234/foo/bar?a=b#baz - protocol: http, host: hostname, port: 1234, segments: ["foo","bar"], fragment: baz </li> 058 * <li>http://hostname:1234/foo/bar?a=b - protocol: http, host: hostname, port: 1234, segments: ["foo","bar"] </li> 059 * <li>//hostname:1234/foo/bar?a=b - protocol: null, host: hostname, port: 1234, segments: ["foo","bar"] </li> 060 * <li>foo/bar/baz?a=1&b=5 - segments: ["foo","bar","baz"], query parameters: ["a"="1", "b"="5"]</li> 061 * <li>foo/bar//baz?=4&6 - segments: ["foo", "bar", "", "baz"], query parameters: [""="4", "6"=""]</li> 062 * <li>/foo/bar/ - segments: ["", "foo", "bar", ""]</li> 063 * <li>foo/bar// - segments: ["foo", "bar", "", ""]</li> 064 * <li>?a=b - segments: [ ], query parameters: ["a"="b"]</li> 065 * <li></li> 066 * </ul> 067 * 068 * The Url class takes care of encoding and decoding of the segments and parameters. 069 * 070 * @author Matej Knopp 071 * @author Igor Vaynberg 072 */ 073public class Url implements Serializable 074{ 075 private static final long serialVersionUID = 1L; 076 077 private final List<String> segments; 078 079 private final List<QueryParameter> parameters; 080 081 private String charsetName; 082 private transient Charset _charset; 083 084 private String protocol; 085 private Integer port; 086 private String host; 087 private String fragment; 088 089 /** 090 * Flags the URL as relative to the application context. It is a special type of URL, used 091 * internally to hold the client data: protocol + host + port + relative segments 092 * 093 * Unlike absolute URLs, a context relative one has no context or filter mapping segments 094 */ 095 private boolean contextRelative; 096 097 /** 098 * A flag indicating that the Url is created from a full url, i.e. 099 * with scheme, host and optional port. 100 * Wicket usually works with relative urls. If a client wants to parse 101 * a full url then most probably it also expects this url to be rendered 102 * as full by {@link UrlRenderer#renderUrl(Url)} 103 */ 104 private boolean shouldRenderAsFull; 105 106 public boolean shouldRenderAsFull() 107 { 108 return shouldRenderAsFull; 109 } 110 111 /** 112 * Modes with which urls can be stringized 113 * 114 * @author igor 115 */ 116 public enum StringMode { 117 /** local urls are rendered without the host name */ 118 LOCAL, 119 /** 120 * full urls are written with hostname. if the hostname is not set or one of segments is 121 * {@literal ..} an {@link IllegalStateException} is thrown. 122 */ 123 FULL; 124 } 125 126 /** 127 * Construct. 128 */ 129 public Url() 130 { 131 segments = Generics.newArrayList(); 132 parameters = Generics.newArrayList(); 133 } 134 135 /** 136 * Construct. 137 * 138 * @param charset 139 */ 140 public Url(final Charset charset) 141 { 142 this(); 143 setCharset(charset); 144 } 145 146 147 /** 148 * copy constructor 149 * 150 * @param url 151 * url being copied 152 */ 153 public Url(final Url url) 154 { 155 Args.notNull(url, "url"); 156 157 protocol = url.protocol; 158 host = url.host; 159 port = url.port; 160 segments = new ArrayList<>(url.segments); 161 parameters = new ArrayList<>(url.parameters); 162 charsetName = url.charsetName; 163 _charset = url._charset; 164 shouldRenderAsFull = url.shouldRenderAsFull; 165 } 166 167 /** 168 * Construct. 169 * 170 * @param segments 171 * @param parameters 172 */ 173 public Url(final List<String> segments, final List<QueryParameter> parameters) 174 { 175 this(segments, parameters, null); 176 } 177 178 /** 179 * Construct. 180 * 181 * @param segments 182 * @param charset 183 */ 184 public Url(final List<String> segments, final Charset charset) 185 { 186 this(segments, Collections.emptyList(), charset); 187 } 188 189 /** 190 * Construct. 191 * 192 * @param segments 193 * @param parameters 194 * @param charset 195 */ 196 public Url(final List<String> segments, final List<QueryParameter> parameters, 197 final Charset charset) 198 { 199 Args.notNull(segments, "segments"); 200 Args.notNull(parameters, "parameters"); 201 202 this.segments = new ArrayList<>(segments); 203 this.parameters = new ArrayList<>(parameters); 204 setCharset(charset); 205 } 206 207 /** 208 * Parses the given URL string. 209 * 210 * @param url 211 * absolute or relative url with query string 212 * @return Url object 213 */ 214 public static Url parse(final CharSequence url) 215 { 216 return parse(url, null); 217 } 218 219 /** 220 * Parses the given URL string. 221 * 222 * @param _url 223 * absolute or relative url with query string 224 * @param charset 225 * @return Url object 226 */ 227 public static Url parse(CharSequence _url, Charset charset) 228 { 229 return parse(_url, charset, true); 230 } 231 232 /** 233 * Parses the given URL string. 234 * 235 * @param _url 236 * absolute or relative url with query string 237 * @param charset 238 * @param isFullHint 239 * a hint whether to try to parse the protocol, host and port part of the url 240 * @return Url object 241 */ 242 public static Url parse(CharSequence _url, Charset charset, boolean isFullHint) 243 { 244 Args.notNull(_url, "_url"); 245 246 final Url result = new Url(charset); 247 248 // the url object resolved the charset, use that 249 charset = result.getCharset(); 250 251 String url = _url.toString(); 252 // extract query string part 253 final String queryString; 254 final String absoluteUrl; 255 256 final int fragmentAt = url.indexOf('#'); 257 258 // matches url fragment, but doesn't match optional path parameter (e.g. .../#{optional}/...) 259 if (fragmentAt > -1 && url.length() > fragmentAt + 1 && url.charAt(fragmentAt + 1) != '{') 260 { 261 result.fragment = url.substring(fragmentAt + 1); 262 url = url.substring(0, fragmentAt); 263 } 264 265 final int queryAt = url.indexOf('?'); 266 267 if (queryAt == -1) 268 { 269 queryString = ""; 270 absoluteUrl = url; 271 } 272 else 273 { 274 absoluteUrl = url.substring(0, queryAt); 275 queryString = url.substring(queryAt + 1); 276 } 277 278 // get absolute / relative part of url 279 String relativeUrl; 280 281 final int idxOfFirstSlash = absoluteUrl.indexOf('/'); 282 final int protocolAt = absoluteUrl.indexOf("://"); 283 284 // full urls start either with a "scheme://" or with "//" 285 boolean protocolLess = absoluteUrl.startsWith("//"); 286 final boolean isFull = (protocolAt > 1 && (protocolAt < idxOfFirstSlash)) || protocolLess; 287 288 if (isFull && isFullHint) 289 { 290 result.shouldRenderAsFull = true; 291 292 if (protocolLess == false) 293 { 294 result.protocol = absoluteUrl.substring(0, protocolAt).toLowerCase(Locale.US); 295 } 296 297 final String afterProto = absoluteUrl.substring(protocolAt + 3); 298 final String hostAndPort; 299 300 int relativeAt = afterProto.indexOf('/'); 301 if (relativeAt == -1) 302 { 303 relativeAt = afterProto.indexOf(';'); 304 } 305 if (relativeAt == -1) 306 { 307 relativeUrl = ""; 308 hostAndPort = afterProto; 309 } 310 else 311 { 312 relativeUrl = afterProto.substring(relativeAt); 313 hostAndPort = afterProto.substring(0, relativeAt); 314 } 315 316 final int credentialsAt = hostAndPort.lastIndexOf('@') + 1; 317 //square brackets are used for ip6 URLs 318 final int closeSqrBracketAt = hostAndPort.lastIndexOf(']') + 1; 319 final int portAt = hostAndPort.substring(credentialsAt) 320 .substring(closeSqrBracketAt) 321 .lastIndexOf(':'); 322 323 if (portAt == -1) 324 { 325 result.host = hostAndPort; 326 result.port = getDefaultPortForProtocol(result.protocol); 327 } 328 else 329 { 330 final int portOffset = portAt + credentialsAt + closeSqrBracketAt; 331 332 result.host = hostAndPort.substring(0, portOffset); 333 result.port = Integer.parseInt(hostAndPort.substring(portOffset + 1)); 334 } 335 336 if (relativeAt < 0) 337 { 338 relativeUrl = "/"; 339 } 340 } 341 else 342 { 343 relativeUrl = absoluteUrl; 344 } 345 346 if (relativeUrl.length() > 0) 347 { 348 boolean removeLast = false; 349 if (relativeUrl.endsWith("/")) 350 { 351 // we need to append something and remove it after splitting 352 // because otherwise the 353 // trailing slashes will be lost 354 relativeUrl += "/x"; 355 removeLast = true; 356 } 357 358 String segmentArray[] = Strings.split(relativeUrl, '/'); 359 360 if (removeLast) 361 { 362 segmentArray[segmentArray.length - 1] = null; 363 } 364 365 for (String s : segmentArray) 366 { 367 if (s != null) 368 { 369 result.segments.add(decodeSegment(s, charset)); 370 } 371 } 372 } 373 374 if (queryString.length() > 0) 375 { 376 String queryArray[] = Strings.split(queryString, '&'); 377 for (String s : queryArray) 378 { 379 if (Strings.isEmpty(s) == false) 380 { 381 result.parameters.add(parseQueryParameter(s, charset)); 382 } 383 } 384 } 385 386 return result; 387 } 388 389 /** 390 * 391 * @param qp 392 * @param charset 393 * @return query parameters 394 */ 395 private static QueryParameter parseQueryParameter(final String qp, final Charset charset) 396 { 397 int idxOfEquals = qp.indexOf('='); 398 if (idxOfEquals == -1) 399 { 400 // name => empty value 401 return new QueryParameter(decodeParameter(qp, charset), ""); 402 } 403 404 String parameterName = qp.substring(0, idxOfEquals); 405 String parameterValue = qp.substring(idxOfEquals + 1); 406 return new QueryParameter(decodeParameter(parameterName, charset), decodeParameter(parameterValue, charset)); 407 } 408 409 /** 410 * get default port number for protocol 411 * 412 * @param protocol 413 * name of protocol 414 * @return default port for protocol or <code>null</code> if unknown 415 */ 416 private static Integer getDefaultPortForProtocol(String protocol) 417 { 418 if ("http".equals(protocol)) 419 { 420 return 80; 421 } 422 else if ("https".equals(protocol)) 423 { 424 return 443; 425 } 426 else if ("ftp".equals(protocol)) 427 { 428 return 21; 429 } 430 else 431 { 432 return null; 433 } 434 } 435 436 /** 437 * 438 * @return charset 439 */ 440 public Charset getCharset() 441 { 442 if (_charset == null) 443 { 444 if (Strings.isEmpty(charsetName)) 445 { 446 _charset = StandardCharsets.UTF_8; 447 } else { 448 _charset = Charset.forName(charsetName); 449 } 450 } 451 return _charset; 452 } 453 454 /** 455 * 456 * @param charset 457 */ 458 private void setCharset(final Charset charset) 459 { 460 if (charset == null) 461 { 462 charsetName = null; 463 _charset = null; 464 } 465 else 466 { 467 charsetName = charset.name(); 468 _charset = charset; 469 } 470 } 471 472 /** 473 * Returns segments of the URL. Segments form the part before query string. 474 * 475 * @return mutable list of segments 476 */ 477 public List<String> getSegments() 478 { 479 return segments; 480 } 481 482 /** 483 * Returns query parameters of the URL. 484 * 485 * @return mutable list of query parameters 486 */ 487 public List<QueryParameter> getQueryParameters() 488 { 489 return parameters; 490 } 491 492 /** 493 * 494 * @return fragment 495 */ 496 public String getFragment() 497 { 498 return fragment; 499 } 500 501 /** 502 * 503 * @param fragment 504 */ 505 public void setFragment(String fragment) 506 { 507 this.fragment = fragment; 508 } 509 510 /** 511 * Returns whether the Url is context absolute. Absolute Urls start with a '{@literal /}'. 512 * 513 * @return <code>true</code> if Url starts with the context path, <code>false</code> otherwise. 514 */ 515 public boolean isContextAbsolute() 516 { 517 return !contextRelative && !isFull() && !getSegments().isEmpty() && Strings.isEmpty(getSegments().get(0)); 518 } 519 520 /** 521 * Returns whether the Url is a CSS data uri. Data uris start with '{@literal data:}'. 522 * 523 * @return <code>true</code> if Url starts with 'data:', <code>false</code> otherwise. 524 */ 525 public boolean isDataUrl() 526 { 527 return (getProtocol() != null && getProtocol().equals("data")) || (!getSegments().isEmpty() && getSegments() 528 .get(0).startsWith("data")); 529 } 530 531 /** 532 * Returns whether the Url has a <em>host</em> attribute. 533 * The scheme is optional because the url may be <code>//host/path</code>. 534 * The port is also optional because there are defaults for the different protocols. 535 * 536 * @return <code>true</code> if Url has a <em>host</em> attribute, <code>false</code> otherwise. 537 */ 538 public boolean isFull() 539 { 540 return !contextRelative && getHost() != null; 541 } 542 543 /** 544 * Convenience method that removes all query parameters with given name. 545 * 546 * @param name 547 * query parameter name 548 */ 549 public void removeQueryParameters(final String name) 550 { 551 for (Iterator<QueryParameter> i = getQueryParameters().iterator(); i.hasNext();) 552 { 553 QueryParameter param = i.next(); 554 if (Objects.equal(name, param.getName())) 555 { 556 i.remove(); 557 } 558 } 559 } 560 561 /** 562 * Convenience method that removes <code>count</code> leading segments 563 * 564 * @param count 565 */ 566 public void removeLeadingSegments(final int count) 567 { 568 Args.withinRange(0, segments.size(), count, "count"); 569 for (int i = 0; i < count; i++) 570 { 571 segments.remove(0); 572 } 573 } 574 575 /** 576 * Convenience method that prepends <code>segments</code> to the segments collection 577 * 578 * @param newSegments 579 */ 580 public void prependLeadingSegments(final List<String> newSegments) 581 { 582 Args.notNull(newSegments, "segments"); 583 segments.addAll(0, newSegments); 584 } 585 586 /** 587 * Convenience method that removes all query parameters with given name and adds new query 588 * parameter with specified name and value 589 * 590 * @param name 591 * @param value 592 */ 593 public void setQueryParameter(final String name, final Object value) 594 { 595 removeQueryParameters(name); 596 addQueryParameter(name, value); 597 } 598 599 /** 600 * Convenience method that removes adds a query parameter with given name 601 * 602 * @param name 603 * @param value 604 */ 605 public void addQueryParameter(final String name, final Object value) 606 { 607 if (value != null) 608 { 609 QueryParameter parameter = new QueryParameter(name, value.toString()); 610 getQueryParameters().add(parameter); 611 } 612 } 613 614 /** 615 * Returns first query parameter with specified name or null if such query parameter doesn't 616 * exist. 617 * 618 * @param name 619 * @return query parameter or <code>null</code> 620 */ 621 public QueryParameter getQueryParameter(final String name) 622 { 623 for (QueryParameter parameter : parameters) 624 { 625 if (Objects.equal(name, parameter.getName())) 626 { 627 return parameter; 628 } 629 } 630 return null; 631 } 632 633 /** 634 * Returns the value of first query parameter with specified name. Note that this method never 635 * returns <code>null</code>. Not even if the parameter does not exist. 636 * 637 * @see StringValue#isNull() 638 * 639 * @param name 640 * @return {@link StringValue} instance wrapping the parameter value 641 */ 642 public StringValue getQueryParameterValue(final String name) 643 { 644 QueryParameter parameter = getQueryParameter(name); 645 if (parameter == null) 646 { 647 return StringValue.valueOf((String)null); 648 } 649 else 650 { 651 return StringValue.valueOf(parameter.getValue()); 652 } 653 } 654 655 /** 656 * {@inheritDoc} 657 */ 658 @Override 659 public boolean equals(final Object obj) 660 { 661 if (this == obj) 662 { 663 return true; 664 } 665 if ((obj instanceof Url) == false) 666 { 667 return false; 668 } 669 Url rhs = (Url)obj; 670 671 return getSegments().equals(rhs.getSegments()) && 672 getQueryParameters().equals(rhs.getQueryParameters()) && 673 Objects.isEqual(getFragment(), rhs.getFragment()); 674 } 675 676 /** 677 * {@inheritDoc} 678 */ 679 @Override 680 public int hashCode() 681 { 682 return Objects.hashCode(getSegments(), getQueryParameters(), getFragment()); 683 } 684 685 /** 686 * 687 * @param string 688 * @param charset 689 * @return encoded segment 690 */ 691 private static String encodeSegment(final String string, final Charset charset) 692 { 693 return UrlEncoder.PATH_INSTANCE.encode(string, charset); 694 } 695 696 /** 697 * 698 * @param string 699 * @param charset 700 * @return decoded segment 701 */ 702 private static String decodeSegment(final String string, final Charset charset) 703 { 704 return UrlDecoder.PATH_INSTANCE.decode(string, charset); 705 } 706 707 /** 708 * 709 * @param string 710 * @param charset 711 * @return encoded parameter 712 */ 713 private static String encodeParameter(final String string, final Charset charset) 714 { 715 return UrlEncoder.QUERY_INSTANCE.encode(string, charset); 716 } 717 718 /** 719 * 720 * @param string 721 * @param charset 722 * @return decoded parameter 723 */ 724 private static String decodeParameter(final String string, final Charset charset) 725 { 726 return UrlDecoder.QUERY_INSTANCE.decode(string, charset); 727 } 728 729 /** 730 * Renders a url with {@link StringMode#LOCAL} using the url's charset 731 */ 732 @Override 733 public String toString() 734 { 735 return toString(getCharset()); 736 } 737 738 /** 739 * Stringizes this url 740 * 741 * @param mode 742 * {@link StringMode} that determins how to stringize the url 743 * @param charset 744 * charset 745 * @return sringized version of this url 746 * 747 */ 748 public String toString(StringMode mode, Charset charset) 749 { 750 // this method is rarely called with StringMode == FULL. 751 752 final CharSequence path = getPathInternal(charset); 753 final String queryString = getQueryString(charset); 754 String _fragment = getFragment(); 755 756 // short circuit all the processing in the most common cases 757 if (StringMode.FULL != mode && Strings.isEmpty(_fragment)) 758 { 759 if (queryString == null) 760 { 761 return path.toString(); 762 } 763 else 764 { 765 return path + "?" + queryString; 766 } 767 } 768 769 // fall through into the traditional code path 770 771 StringBuilder result = new StringBuilder(64); 772 773 if (StringMode.FULL == mode) 774 { 775 if (Strings.isEmpty(host)) 776 { 777 throw new IllegalStateException("Cannot render this url in " + 778 StringMode.FULL.name() + " mode because it does not have a host set."); 779 } 780 781 if (Strings.isEmpty(protocol) == false) 782 { 783 result.append(protocol); 784 result.append("://"); 785 } 786 else if (Strings.isEmpty(protocol) && Strings.isEmpty(host) == false) 787 { 788 result.append("//"); 789 } 790 result.append(host); 791 792 if (port != null && port.equals(getDefaultPortForProtocol(protocol)) == false) 793 { 794 result.append(':'); 795 result.append(port); 796 } 797 798 if (segments.contains("..")) 799 { 800 throw new IllegalStateException("Cannot render this url in " + 801 StringMode.FULL.name() + " mode because it has a `..` segment: " + toString()); 802 } 803 804 if (!path.isEmpty() && !(path.charAt(0) == '/')) 805 { 806 result.append('/'); 807 } 808 } 809 810 result.append(path); 811 812 if (queryString != null) 813 { 814 result.append('?').append(queryString); 815 } 816 817 if (Strings.isEmpty(_fragment) == false) 818 { 819 result.append('#').append(_fragment); 820 } 821 822 return result.toString(); 823 } 824 825 /** 826 * Stringizes this url using the specific {@link StringMode} and url's charset 827 * 828 * @param mode 829 * {@link StringMode} that determines how to stringize the url 830 * @return stringized url 831 */ 832 public String toString(StringMode mode) 833 { 834 return toString(mode, getCharset()); 835 } 836 837 /** 838 * Stringizes this url using {@link StringMode#LOCAL} and the specified charset 839 * 840 * @param charset 841 * @return stringized url 842 */ 843 public String toString(final Charset charset) 844 { 845 return toString(StringMode.LOCAL, charset); 846 } 847 848 /** 849 * 850 * @return true if last segment contains a name and not something like "." or "..". 851 */ 852 private boolean isLastSegmentReal() 853 { 854 if (segments.isEmpty()) 855 { 856 return false; 857 } 858 String last = segments.get(segments.size() - 1); 859 return (last.length() > 0) && !".".equals(last) && !"..".equals(last); 860 } 861 862 /** 863 * @param segments 864 * @return true if last segment is empty 865 */ 866 private boolean isLastSegmentEmpty(final List<String> segments) 867 { 868 if (segments.isEmpty()) 869 { 870 return false; 871 } 872 String last = segments.get(segments.size() - 1); 873 return last.length() == 0; 874 } 875 876 /** 877 * 878 * @return true, if last segement is empty 879 */ 880 private boolean isLastSegmentEmpty() 881 { 882 return isLastSegmentEmpty(segments); 883 } 884 885 /** 886 * 887 * @param segments 888 * @return true if at least one segement is real 889 */ 890 private boolean isAtLeastOneSegmentReal(final List<String> segments) 891 { 892 for (String s : segments) 893 { 894 if ((s.length() > 0) && !".".equals(s) && !"..".equals(s)) 895 { 896 return true; 897 } 898 } 899 return false; 900 } 901 902 /** 903 * Concatenate the specified segments; The segments can be relative - begin with "." or "..". 904 * 905 * @param segments 906 */ 907 public void concatSegments(List<String> segments) 908 { 909 boolean checkedLastSegment = false; 910 911 if (!isAtLeastOneSegmentReal(segments) && !isLastSegmentEmpty(segments)) 912 { 913 segments = new ArrayList<>(segments); 914 segments.add(""); 915 } 916 917 for (String s : segments) 918 { 919 if (".".equals(s)) 920 { 921 continue; 922 } 923 else if ("..".equals(s) && !this.segments.isEmpty()) 924 { 925 this.segments.remove(this.segments.size() - 1); 926 } 927 else 928 { 929 if (!checkedLastSegment) 930 { 931 if (isLastSegmentReal() || isLastSegmentEmpty()) 932 { 933 this.segments.remove(this.segments.size() - 1); 934 } 935 checkedLastSegment = true; 936 } 937 this.segments.add(s); 938 } 939 } 940 941 if ((this.segments.size() == 1) && (this.segments.get(0).length() == 0)) 942 { 943 this.segments.clear(); 944 } 945 } 946 947 /** 948 * Represents a single query parameter 949 * 950 * @author Matej Knopp 951 */ 952 public final static class QueryParameter implements Serializable 953 { 954 private static final long serialVersionUID = 1L; 955 956 private final String name; 957 private final String value; 958 959 /** 960 * Creates new {@link QueryParameter} instance. The <code>name</code> and <code>value</code> 961 * parameters must not be <code>null</code>, though they can be empty strings. 962 * 963 * @param name 964 * parameter name 965 * @param value 966 * parameter value 967 */ 968 public QueryParameter(final String name, final String value) 969 { 970 Args.notNull(name, "name"); 971 Args.notNull(value, "value"); 972 973 this.name = name; 974 this.value = value; 975 } 976 977 /** 978 * Returns query parameter name. 979 * 980 * @return query parameter name 981 */ 982 public String getName() 983 { 984 return name; 985 } 986 987 /** 988 * Returns query parameter value. 989 * 990 * @return query parameter value 991 */ 992 public String getValue() 993 { 994 return value; 995 } 996 997 /** 998 * {@inheritDoc} 999 */ 1000 @Override 1001 public boolean equals(final Object obj) 1002 { 1003 if (this == obj) 1004 { 1005 return true; 1006 } 1007 if ((obj instanceof QueryParameter) == false) 1008 { 1009 return false; 1010 } 1011 QueryParameter rhs = (QueryParameter)obj; 1012 return Objects.equal(getName(), rhs.getName()) && 1013 Objects.equal(getValue(), rhs.getValue()); 1014 } 1015 1016 /** 1017 * {@inheritDoc} 1018 */ 1019 @Override 1020 public int hashCode() 1021 { 1022 return Objects.hashCode(getName(), getValue()); 1023 } 1024 1025 /** 1026 * {@inheritDoc} 1027 */ 1028 @Override 1029 public String toString() 1030 { 1031 return toString(StandardCharsets.UTF_8); 1032 } 1033 1034 /** 1035 * 1036 * @param charset 1037 * @return see toString() 1038 */ 1039 public String toString(final Charset charset) 1040 { 1041 String value = getValue(); 1042 if (Strings.isEmpty(value)) 1043 { 1044 return encodeParameter(getName(), charset); 1045 } 1046 else 1047 { 1048 return encodeParameter(getName(), charset) + "=" + encodeParameter(value, charset); 1049 } 1050 } 1051 } 1052 1053 /** 1054 * Makes this url the result of resolving the {@code relative} url against this url. 1055 * <p> 1056 * Segments will be properly resolved, handling any {@code ..} references, while the query 1057 * parameters will be completely replaced with {@code relative}'s query parameters. 1058 * </p> 1059 * <p> 1060 * For example: 1061 * 1062 * <pre> 1063 * wicket/page/render?foo=bar 1064 * </pre> 1065 * 1066 * resolved with 1067 * 1068 * <pre> 1069 * ../component/render?a=b 1070 * </pre> 1071 * 1072 * will become 1073 * 1074 * <pre> 1075 * wicket/component/render?a=b 1076 * </pre> 1077 * 1078 * </p> 1079 * 1080 * @param relative 1081 * relative url 1082 */ 1083 public void resolveRelative(final Url relative) 1084 { 1085 if (getSegments().size() > 0) 1086 { 1087 // strip the first non-folder segment (if it is not empty) 1088 getSegments().remove(getSegments().size() - 1); 1089 } 1090 1091 // remove leading './' (current folder) and empty segments, process any ../ segments from 1092 // the relative url 1093 final List<String> relativeSegments = relative.getSegments(); 1094 while (!relativeSegments.isEmpty()) 1095 { 1096 final String firstSegment = relativeSegments.get(0); 1097 if (".".equals(firstSegment)) 1098 { 1099 relativeSegments.remove(0); 1100 } 1101 else if (firstSegment.isEmpty()) 1102 { 1103 relativeSegments.remove(0); 1104 } 1105 else if ("..".equals(firstSegment)) 1106 { 1107 relativeSegments.remove(0); 1108 if (getSegments().isEmpty() == false) 1109 { 1110 getSegments().remove(getSegments().size() - 1); 1111 } 1112 } 1113 else 1114 { 1115 break; 1116 } 1117 } 1118 1119 if (!getSegments().isEmpty() && relativeSegments.isEmpty()) 1120 { 1121 getSegments().add(""); 1122 } 1123 1124 // append the remaining relative segments 1125 getSegments().addAll(relativeSegments); 1126 1127 // replace query params with the ones from relative 1128 parameters.clear(); 1129 parameters.addAll(relative.getQueryParameters()); 1130 } 1131 1132 /** 1133 * Gets the protocol of this url (http/https/etc) 1134 * 1135 * @return protocol or {@code null} if none has been set 1136 */ 1137 public String getProtocol() 1138 { 1139 return protocol; 1140 } 1141 1142 /** 1143 * Sets the protocol of this url (http/https/etc) 1144 * 1145 * @param protocol 1146 */ 1147 public void setProtocol(final String protocol) 1148 { 1149 this.protocol = protocol; 1150 } 1151 1152 /** 1153 * 1154 * Flags the URL as relative to the application context. 1155 * 1156 * @param contextRelative 1157 */ 1158 public void setContextRelative(boolean contextRelative) 1159 { 1160 this.contextRelative = contextRelative; 1161 } 1162 1163 /** 1164 * Tests if the URL is relative to the application context. If so, it holds all the information 1165 * an absolute URL would have, minus the context and filter mapping segments 1166 * 1167 * @return contextRelative 1168 */ 1169 public boolean isContextRelative() 1170 { 1171 return contextRelative; 1172 } 1173 1174 /** 1175 * Gets the port of this url 1176 * 1177 * @return port or {@code null} if none has been set 1178 */ 1179 public Integer getPort() 1180 { 1181 return port; 1182 } 1183 1184 /** 1185 * Sets the port of this url 1186 * 1187 * @param port 1188 */ 1189 public void setPort(final Integer port) 1190 { 1191 this.port = port; 1192 } 1193 1194 /** 1195 * Gets the host name of this url 1196 * 1197 * @return host name or {@code null} if none is seto 1198 */ 1199 public String getHost() 1200 { 1201 return host; 1202 } 1203 1204 /** 1205 * Sets the host name of this url 1206 * 1207 * @param host 1208 */ 1209 public void setHost(final String host) 1210 { 1211 this.host = host; 1212 } 1213 1214 /** 1215 * return path for current url in given encoding 1216 * 1217 * @param charset 1218 * character set for encoding 1219 * 1220 * @return path string 1221 */ 1222 public String getPath(Charset charset) 1223 { 1224 return getPathInternal(charset).toString(); 1225 } 1226 1227 /** 1228 * return path for current url in given encoding, with optimizations for common use to avoid excessive object creation 1229 * and resizing of StringBuilders. Used internally by Url 1230 * 1231 * @param charset 1232 * character set for encoding 1233 * 1234 * @return path string 1235 */ 1236 private CharSequence getPathInternal(Charset charset) 1237 { 1238 Args.notNull(charset, "charset"); 1239 1240 List<String> segments = getSegments(); 1241 // these two common cases can be handled with no additional overhead, so do that. 1242 if (segments.isEmpty()) 1243 return ""; 1244 if (segments.size() == 1) 1245 return encodeSegment(segments.get(0), charset); 1246 1247 int length = 0; 1248 for (String segment : getSegments()) 1249 length += segment.length() + 4; 1250 1251 StringBuilder path = new StringBuilder(length); 1252 boolean slash = false; 1253 1254 for (String segment : getSegments()) 1255 { 1256 if (slash) 1257 { 1258 path.append('/'); 1259 } 1260 path.append(encodeSegment(segment, charset)); 1261 slash = true; 1262 } 1263 return path; 1264 } 1265 1266 /** 1267 * return path for current url in original encoding 1268 * 1269 * @return path string 1270 */ 1271 public String getPath() 1272 { 1273 return getPath(getCharset()); 1274 } 1275 1276 /** 1277 * return query string part of url in given encoding 1278 * 1279 * @param charset 1280 * character set for encoding 1281 * @since Wicket 7 1282 * the return value does not contain any "?" and could be null 1283 * @return query string (null if empty) 1284 */ 1285 public String getQueryString(Charset charset) 1286 { 1287 Args.notNull(charset, "charset"); 1288 1289 List<QueryParameter> queryParameters = getQueryParameters(); 1290 if (queryParameters.isEmpty()) 1291 return null; 1292 if (queryParameters.size() == 1) 1293 return queryParameters.get(0).toString(charset); 1294 1295 // make a reasonable guess at a size for this builder 1296 StringBuilder query = new StringBuilder(16 * parameters.size()); 1297 for (QueryParameter parameter : queryParameters) 1298 { 1299 if (query.length() != 0) { 1300 query.append('&'); 1301 } 1302 query.append(parameter.toString(charset)); 1303 } 1304 1305 return query.toString(); 1306 } 1307 1308 /** 1309 * return query string part of url in original encoding 1310 * 1311 * @since Wicket 7 1312 * the return value does not contain any "?" and could be null 1313 * @return query string (null if empty) 1314 */ 1315 public String getQueryString() 1316 { 1317 return getQueryString(getCharset()); 1318 } 1319 1320 /** 1321 * Try to reduce url by eliminating '..' and '.' from the path where appropriate (this is 1322 * somehow similar to {@link java.io.File#getCanonicalPath()}). Either by different / unexpected 1323 * browser behavior or by malicious attacks it can happen that these kind of redundant urls are 1324 * processed by wicket. These urls can cause some trouble when mapping the request. 1325 * <p/> 1326 * <strong>example:</strong> 1327 * 1328 * the url 1329 * 1330 * <pre> 1331 * /example/..;jsessionid=234792?0 1332 * </pre> 1333 * 1334 * will not get normalized by the browser due to the ';jsessionid' string that gets appended by 1335 * the servlet container. After wicket strips the jsessionid part the resulting internal url 1336 * will be 1337 * 1338 * <pre> 1339 * /example/.. 1340 * </pre> 1341 * 1342 * instead of 1343 * 1344 * <pre> 1345 * / 1346 * </pre> 1347 * 1348 * <p/> 1349 * 1350 * This code correlates to <a 1351 * href="https://issues.apache.org/jira/browse/WICKET-4303">WICKET-4303</a> 1352 * 1353 * @return canonical url 1354 */ 1355 public Url canonical() 1356 { 1357 Url url = new Url(this); 1358 url.segments.clear(); 1359 1360 for (int i = 0; i < segments.size(); i++) 1361 { 1362 final String segment = segments.get(i); 1363 1364 // drop '.' from path 1365 if (".".equals(segment)) 1366 { 1367 // skip 1368 } 1369 else if ("..".equals(segment) && url.segments.isEmpty() == false) 1370 { 1371 url.segments.remove(url.segments.size() - 1); 1372 } 1373 // skip segment if following segment is a '..' 1374 else if ((i + 1) < segments.size() && "..".equals(segments.get(i + 1))) 1375 { 1376 i++; 1377 } 1378 else 1379 { 1380 url.segments.add(segment); 1381 } 1382 } 1383 return url; 1384 } 1385}