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                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}