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: [&quot;foo&quot;,&quot;bar&quot;], fragment: baz </li>
058 *     <li>http://hostname:1234/foo/bar?a=b - protocol: http, host: hostname, port: 1234, segments: [&quot;foo&quot;,&quot;bar&quot;] </li>
059 *     <li>//hostname:1234/foo/bar?a=b - protocol: null, host: hostname, port: 1234, segments: [&quot;foo&quot;,&quot;bar&quot;] </li>
060 *     <li>foo/bar/baz?a=1&amp;b=5    - segments: [&quot;foo&quot;,&quot;bar&quot;,&quot;baz&quot;], query parameters: [&quot;a&quot;=&quot;1&quot;, &quot;b&quot;=&quot;5&quot;]</li>
061 *     <li>foo/bar//baz?=4&amp;6      - segments: [&quot;foo&quot;, &quot;bar&quot;, &quot;&quot;, &quot;baz&quot;], query parameters: [&quot;&quot;=&quot;4&quot;, &quot;6&quot;=&quot;&quot;]</li>
062 *     <li>/foo/bar/              - segments: [&quot;&quot;, &quot;foo&quot;, &quot;bar&quot;, &quot;&quot;]</li>
063 *     <li>foo/bar//              - segments: [&quot;foo&quot;, &quot;bar&quot;, &quot;&quot;, &quot;&quot;]</li>
064 *     <li>?a=b                   - segments: [ ], query parameters: [&quot;a&quot;=&quot;b&quot;]</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.length() == 0) && !(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}