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