001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.util;
018
019import java.io.UnsupportedEncodingException;
020import java.net.URLDecoder;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025
026import javax.servlet.http.HttpServletRequest;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.util.Assert;
032import org.springframework.util.LinkedMultiValueMap;
033import org.springframework.util.MultiValueMap;
034import org.springframework.util.StringUtils;
035
036/**
037 * Helper class for URL path matching. Provides support for URL paths in
038 * {@code RequestDispatcher} includes and support for consistent URL decoding.
039 *
040 * <p>Used by {@link org.springframework.web.servlet.handler.AbstractUrlHandlerMapping},
041 * {@link org.springframework.web.servlet.mvc.multiaction.AbstractUrlMethodNameResolver}
042 * and {@link org.springframework.web.servlet.support.RequestContext} for path matching
043 * and/or URI determination.
044 *
045 * @author Juergen Hoeller
046 * @author Rob Harrop
047 * @author Rossen Stoyanchev
048 * @since 14.01.2004
049 * @see #getLookupPathForRequest
050 * @see javax.servlet.RequestDispatcher
051 */
052public class UrlPathHelper {
053
054        /**
055         * Special WebSphere request attribute, indicating the original request URI.
056         * Preferable over the standard Servlet 2.4 forward attribute on WebSphere,
057         * simply because we need the very first URI in the request forwarding chain.
058         */
059        private static final String WEBSPHERE_URI_ATTRIBUTE = "com.ibm.websphere.servlet.uri_non_decoded";
060
061        private static final Log logger = LogFactory.getLog(UrlPathHelper.class);
062
063        static volatile Boolean websphereComplianceFlag;
064
065
066        private boolean alwaysUseFullPath = false;
067
068        private boolean urlDecode = true;
069
070        private boolean removeSemicolonContent = true;
071
072        private String defaultEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
073
074        private boolean readOnly = false;
075
076
077        /**
078         * Whether URL lookups should always use the full path within the current
079         * web application context, i.e. within
080         * {@link javax.servlet.ServletContext#getContextPath()}.
081         * <p>If set to {@literal false} the path within the current servlet mapping
082         * is used instead if applicable (i.e. in the case of a prefix based Servlet
083         * mapping such as "/myServlet/*").
084         * <p>By default this is set to "false".
085         */
086        public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
087                checkReadOnly();
088                this.alwaysUseFullPath = alwaysUseFullPath;
089        }
090
091        /**
092         * Whether the context path and request URI should be decoded -- both of
093         * which are returned <i>undecoded</i> by the Servlet API, in contrast to
094         * the servlet path.
095         * <p>Either the request encoding or the default Servlet spec encoding
096         * (ISO-8859-1) is used when set to "true".
097         * <p>By default this is set to {@literal true}.
098         * <p><strong>Note:</strong> Be aware the servlet path will not match when
099         * compared to encoded paths. Therefore use of {@code urlDecode=false} is
100         * not compatible with a prefix-based Servlet mapping and likewise implies
101         * also setting {@code alwaysUseFullPath=true}.
102         * @see #getServletPath
103         * @see #getContextPath
104         * @see #getRequestUri
105         * @see WebUtils#DEFAULT_CHARACTER_ENCODING
106         * @see javax.servlet.ServletRequest#getCharacterEncoding()
107         * @see java.net.URLDecoder#decode(String, String)
108         */
109        public void setUrlDecode(boolean urlDecode) {
110                checkReadOnly();
111                this.urlDecode = urlDecode;
112        }
113
114        /**
115         * Whether to decode the request URI when determining the lookup path.
116         * @since 4.3.13
117         */
118        public boolean isUrlDecode() {
119                return this.urlDecode;
120        }
121
122        /**
123         * Set if ";" (semicolon) content should be stripped from the request URI.
124         * <p>Default is "true".
125         */
126        public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
127                checkReadOnly();
128                this.removeSemicolonContent = removeSemicolonContent;
129        }
130
131        /**
132         * Whether configured to remove ";" (semicolon) content from the request URI.
133         */
134        public boolean shouldRemoveSemicolonContent() {
135                checkReadOnly();
136                return this.removeSemicolonContent;
137        }
138
139        /**
140         * Set the default character encoding to use for URL decoding.
141         * Default is ISO-8859-1, according to the Servlet spec.
142         * <p>If the request specifies a character encoding itself, the request
143         * encoding will override this setting. This also allows for generically
144         * overriding the character encoding in a filter that invokes the
145         * {@code ServletRequest.setCharacterEncoding} method.
146         * @param defaultEncoding the character encoding to use
147         * @see #determineEncoding
148         * @see javax.servlet.ServletRequest#getCharacterEncoding()
149         * @see javax.servlet.ServletRequest#setCharacterEncoding(String)
150         * @see WebUtils#DEFAULT_CHARACTER_ENCODING
151         */
152        public void setDefaultEncoding(String defaultEncoding) {
153                checkReadOnly();
154                this.defaultEncoding = defaultEncoding;
155        }
156
157        /**
158         * Return the default character encoding to use for URL decoding.
159         */
160        protected String getDefaultEncoding() {
161                return this.defaultEncoding;
162        }
163
164        /**
165         * Switch to read-only mode where further configuration changes are not allowed.
166         */
167        private void setReadOnly() {
168                this.readOnly = true;
169        }
170
171        private void checkReadOnly() {
172                Assert.isTrue(!this.readOnly, "This instance cannot be modified");
173        }
174
175
176        /**
177         * Return the mapping lookup path for the given request, within the current
178         * servlet mapping if applicable, else within the web application.
179         * <p>Detects include request URL if called within a RequestDispatcher include.
180         * @param request current HTTP request
181         * @return the lookup path
182         * @see #getPathWithinServletMapping
183         * @see #getPathWithinApplication
184         */
185        public String getLookupPathForRequest(HttpServletRequest request) {
186                // Always use full path within current servlet context?
187                if (this.alwaysUseFullPath) {
188                        return getPathWithinApplication(request);
189                }
190                // Else, use path within current servlet mapping if applicable
191                String rest = getPathWithinServletMapping(request);
192                if (!"".equals(rest)) {
193                        return rest;
194                }
195                else {
196                        return getPathWithinApplication(request);
197                }
198        }
199
200        /**
201         * Return the path within the servlet mapping for the given request,
202         * i.e. the part of the request's URL beyond the part that called the servlet,
203         * or "" if the whole URL has been used to identify the servlet.
204         * <p>Detects include request URL if called within a RequestDispatcher include.
205         * <p>E.g.: servlet mapping = "/*"; request URI = "/test/a" -> "/test/a".
206         * <p>E.g.: servlet mapping = "/"; request URI = "/test/a" -> "/test/a".
207         * <p>E.g.: servlet mapping = "/test/*"; request URI = "/test/a" -> "/a".
208         * <p>E.g.: servlet mapping = "/test"; request URI = "/test" -> "".
209         * <p>E.g.: servlet mapping = "/*.test"; request URI = "/a.test" -> "".
210         * @param request current HTTP request
211         * @return the path within the servlet mapping, or ""
212         * @see #getLookupPathForRequest
213         */
214        public String getPathWithinServletMapping(HttpServletRequest request) {
215                String pathWithinApp = getPathWithinApplication(request);
216                String servletPath = getServletPath(request);
217                String sanitizedPathWithinApp = getSanitizedPath(pathWithinApp);
218                String path;
219
220                // If the app container sanitized the servletPath, check against the sanitized version
221                if (servletPath.contains(sanitizedPathWithinApp)) {
222                        path = getRemainingPath(sanitizedPathWithinApp, servletPath, false);
223                }
224                else {
225                        path = getRemainingPath(pathWithinApp, servletPath, false);
226                }
227
228                if (path != null) {
229                        // Normal case: URI contains servlet path.
230                        return path;
231                }
232                else {
233                        // Special case: URI is different from servlet path.
234                        String pathInfo = request.getPathInfo();
235                        if (pathInfo != null) {
236                                // Use path info if available. Indicates index page within a servlet mapping?
237                                // e.g. with index page: URI="/", servletPath="/index.html"
238                                return pathInfo;
239                        }
240                        if (!this.urlDecode) {
241                                // No path info... (not mapped by prefix, nor by extension, nor "/*")
242                                // For the default servlet mapping (i.e. "/"), urlDecode=false can
243                                // cause issues since getServletPath() returns a decoded path.
244                                // If decoding pathWithinApp yields a match just use pathWithinApp.
245                                path = getRemainingPath(decodeInternal(request, pathWithinApp), servletPath, false);
246                                if (path != null) {
247                                        return pathWithinApp;
248                                }
249                        }
250                        // Otherwise, use the full servlet path.
251                        return servletPath;
252                }
253        }
254
255        /**
256         * Return the path within the web application for the given request.
257         * <p>Detects include request URL if called within a RequestDispatcher include.
258         * @param request current HTTP request
259         * @return the path within the web application
260         * @see #getLookupPathForRequest
261         */
262        public String getPathWithinApplication(HttpServletRequest request) {
263                String contextPath = getContextPath(request);
264                String requestUri = getRequestUri(request);
265                String path = getRemainingPath(requestUri, contextPath, true);
266                if (path != null) {
267                        // Normal case: URI contains context path.
268                        return (StringUtils.hasText(path) ? path : "/");
269                }
270                else {
271                        return requestUri;
272                }
273        }
274
275        /**
276         * Match the given "mapping" to the start of the "requestUri" and if there
277         * is a match return the extra part. This method is needed because the
278         * context path and the servlet path returned by the HttpServletRequest are
279         * stripped of semicolon content unlike the requesUri.
280         */
281        private String getRemainingPath(String requestUri, String mapping, boolean ignoreCase) {
282                int index1 = 0;
283                int index2 = 0;
284                for (; (index1 < requestUri.length()) && (index2 < mapping.length()); index1++, index2++) {
285                        char c1 = requestUri.charAt(index1);
286                        char c2 = mapping.charAt(index2);
287                        if (c1 == ';') {
288                                index1 = requestUri.indexOf('/', index1);
289                                if (index1 == -1) {
290                                        return null;
291                                }
292                                c1 = requestUri.charAt(index1);
293                        }
294                        if (c1 == c2 || (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2)))) {
295                                continue;
296                        }
297                        return null;
298                }
299                if (index2 != mapping.length()) {
300                        return null;
301                }
302                else if (index1 == requestUri.length()) {
303                        return "";
304                }
305                else if (requestUri.charAt(index1) == ';') {
306                        index1 = requestUri.indexOf('/', index1);
307                }
308                return (index1 != -1 ? requestUri.substring(index1) : "");
309        }
310
311        /**
312         * Sanitize the given path. Uses the following rules:
313         * <ul>
314         * <li>replace all "//" by "/"</li>
315         * </ul>
316         */
317        private String getSanitizedPath(final String path) {
318                String sanitized = path;
319                while (true) {
320                        int index = sanitized.indexOf("//");
321                        if (index < 0) {
322                                break;
323                        }
324                        else {
325                                sanitized = sanitized.substring(0, index) + sanitized.substring(index + 1);
326                        }
327                }
328                return sanitized;
329        }
330
331        /**
332         * Return the request URI for the given request, detecting an include request
333         * URL if called within a RequestDispatcher include.
334         * <p>As the value returned by {@code request.getRequestURI()} is <i>not</i>
335         * decoded by the servlet container, this method will decode it.
336         * <p>The URI that the web container resolves <i>should</i> be correct, but some
337         * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
338         * in the URI. This method cuts off such incorrect appendices.
339         * @param request current HTTP request
340         * @return the request URI
341         */
342        public String getRequestUri(HttpServletRequest request) {
343                String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);
344                if (uri == null) {
345                        uri = request.getRequestURI();
346                }
347                return decodeAndCleanUriString(request, uri);
348        }
349
350        /**
351         * Return the context path for the given request, detecting an include request
352         * URL if called within a RequestDispatcher include.
353         * <p>As the value returned by {@code request.getContextPath()} is <i>not</i>
354         * decoded by the servlet container, this method will decode it.
355         * @param request current HTTP request
356         * @return the context path
357         */
358        public String getContextPath(HttpServletRequest request) {
359                String contextPath = (String) request.getAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE);
360                if (contextPath == null) {
361                        contextPath = request.getContextPath();
362                }
363                if ("/".equals(contextPath)) {
364                        // Invalid case, but happens for includes on Jetty: silently adapt it.
365                        contextPath = "";
366                }
367                return decodeRequestString(request, contextPath);
368        }
369
370        /**
371         * Return the servlet path for the given request, regarding an include request
372         * URL if called within a RequestDispatcher include.
373         * <p>As the value returned by {@code request.getServletPath()} is already
374         * decoded by the servlet container, this method will not attempt to decode it.
375         * @param request current HTTP request
376         * @return the servlet path
377         */
378        public String getServletPath(HttpServletRequest request) {
379                String servletPath = (String) request.getAttribute(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE);
380                if (servletPath == null) {
381                        servletPath = request.getServletPath();
382                }
383                if (servletPath.length() > 1 && servletPath.endsWith("/") && shouldRemoveTrailingServletPathSlash(request)) {
384                        // On WebSphere, in non-compliant mode, for a "/foo/" case that would be "/foo"
385                        // on all other servlet containers: removing trailing slash, proceeding with
386                        // that remaining slash as final lookup path...
387                        servletPath = servletPath.substring(0, servletPath.length() - 1);
388                }
389                return servletPath;
390        }
391
392
393        /**
394         * Return the request URI for the given request. If this is a forwarded request,
395         * correctly resolves to the request URI of the original request.
396         */
397        public String getOriginatingRequestUri(HttpServletRequest request) {
398                String uri = (String) request.getAttribute(WEBSPHERE_URI_ATTRIBUTE);
399                if (uri == null) {
400                        uri = (String) request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE);
401                        if (uri == null) {
402                                uri = request.getRequestURI();
403                        }
404                }
405                return decodeAndCleanUriString(request, uri);
406        }
407
408        /**
409         * Return the context path for the given request, detecting an include request
410         * URL if called within a RequestDispatcher include.
411         * <p>As the value returned by {@code request.getContextPath()} is <i>not</i>
412         * decoded by the servlet container, this method will decode it.
413         * @param request current HTTP request
414         * @return the context path
415         */
416        public String getOriginatingContextPath(HttpServletRequest request) {
417                String contextPath = (String) request.getAttribute(WebUtils.FORWARD_CONTEXT_PATH_ATTRIBUTE);
418                if (contextPath == null) {
419                        contextPath = request.getContextPath();
420                }
421                return decodeRequestString(request, contextPath);
422        }
423
424        /**
425         * Return the servlet path for the given request, detecting an include request
426         * URL if called within a RequestDispatcher include.
427         * @param request current HTTP request
428         * @return the servlet path
429         */
430        public String getOriginatingServletPath(HttpServletRequest request) {
431                String servletPath = (String) request.getAttribute(WebUtils.FORWARD_SERVLET_PATH_ATTRIBUTE);
432                if (servletPath == null) {
433                        servletPath = request.getServletPath();
434                }
435                return servletPath;
436        }
437
438        /**
439         * Return the query string part of the given request's URL. If this is a forwarded request,
440         * correctly resolves to the query string of the original request.
441         * @param request current HTTP request
442         * @return the query string
443         */
444        public String getOriginatingQueryString(HttpServletRequest request) {
445                if ((request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE) != null) ||
446                        (request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) {
447                        return (String) request.getAttribute(WebUtils.FORWARD_QUERY_STRING_ATTRIBUTE);
448                }
449                else {
450                        return request.getQueryString();
451                }
452        }
453
454        /**
455         * Decode the supplied URI string and strips any extraneous portion after a ';'.
456         */
457        private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
458                uri = removeSemicolonContent(uri);
459                uri = decodeRequestString(request, uri);
460                uri = getSanitizedPath(uri);
461                return uri;
462        }
463
464        /**
465         * Decode the given source string with a URLDecoder. The encoding will be taken
466         * from the request, falling back to the default "ISO-8859-1".
467         * <p>The default implementation uses {@code URLDecoder.decode(input, enc)}.
468         * @param request current HTTP request
469         * @param source the String to decode
470         * @return the decoded String
471         * @see WebUtils#DEFAULT_CHARACTER_ENCODING
472         * @see javax.servlet.ServletRequest#getCharacterEncoding
473         * @see java.net.URLDecoder#decode(String, String)
474         * @see java.net.URLDecoder#decode(String)
475         */
476        public String decodeRequestString(HttpServletRequest request, String source) {
477                if (this.urlDecode && source != null) {
478                        return decodeInternal(request, source);
479                }
480                return source;
481        }
482
483        @SuppressWarnings("deprecation")
484        private String decodeInternal(HttpServletRequest request, String source) {
485                String enc = determineEncoding(request);
486                try {
487                        return UriUtils.decode(source, enc);
488                }
489                catch (UnsupportedEncodingException ex) {
490                        if (logger.isWarnEnabled()) {
491                                logger.warn("Could not decode request string [" + source + "] with encoding '" + enc +
492                                                "': falling back to platform default encoding; exception message: " + ex.getMessage());
493                        }
494                        return URLDecoder.decode(source);
495                }
496        }
497
498        /**
499         * Determine the encoding for the given request.
500         * Can be overridden in subclasses.
501         * <p>The default implementation checks the request encoding,
502         * falling back to the default encoding specified for this resolver.
503         * @param request current HTTP request
504         * @return the encoding for the request (never {@code null})
505         * @see javax.servlet.ServletRequest#getCharacterEncoding()
506         * @see #setDefaultEncoding
507         */
508        protected String determineEncoding(HttpServletRequest request) {
509                String enc = request.getCharacterEncoding();
510                if (enc == null) {
511                        enc = getDefaultEncoding();
512                }
513                return enc;
514        }
515
516        /**
517         * Remove ";" (semicolon) content from the given request URI if the
518         * {@linkplain #setRemoveSemicolonContent removeSemicolonContent}
519         * property is set to "true". Note that "jsessionid" is always removed.
520         * @param requestUri the request URI string to remove ";" content from
521         * @return the updated URI string
522         */
523        public String removeSemicolonContent(String requestUri) {
524                return (this.removeSemicolonContent ?
525                                removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri));
526        }
527
528        private String removeSemicolonContentInternal(String requestUri) {
529                int semicolonIndex = requestUri.indexOf(';');
530                while (semicolonIndex != -1) {
531                        int slashIndex = requestUri.indexOf('/', semicolonIndex);
532                        String start = requestUri.substring(0, semicolonIndex);
533                        requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;
534                        semicolonIndex = requestUri.indexOf(';', semicolonIndex);
535                }
536                return requestUri;
537        }
538
539        private String removeJsessionid(String requestUri) {
540                String key = ";jsessionid=";
541                int index = requestUri.toLowerCase().indexOf(key);
542                if (index == -1) {
543                        return requestUri;
544                }
545                String start = requestUri.substring(0, index);
546                for (int i = index + key.length(); i < requestUri.length(); i++) {
547                        char c = requestUri.charAt(i);
548                        if (c == ';' || c == '/') {
549                                return start + requestUri.substring(i);
550                        }
551                }
552                return start;
553        }
554
555        /**
556         * Decode the given URI path variables via {@link #decodeRequestString} unless
557         * {@link #setUrlDecode} is set to {@code true} in which case it is assumed
558         * the URL path from which the variables were extracted is already decoded
559         * through a call to {@link #getLookupPathForRequest(HttpServletRequest)}.
560         * @param request current HTTP request
561         * @param vars the URI variables extracted from the URL path
562         * @return the same Map or a new Map instance
563         */
564        public Map<String, String> decodePathVariables(HttpServletRequest request, Map<String, String> vars) {
565                if (this.urlDecode) {
566                        return vars;
567                }
568                else {
569                        Map<String, String> decodedVars = new LinkedHashMap<String, String>(vars.size());
570                        for (Map.Entry<String, String> entry : vars.entrySet()) {
571                                decodedVars.put(entry.getKey(), decodeInternal(request, entry.getValue()));
572                        }
573                        return decodedVars;
574                }
575        }
576
577        /**
578         * Decode the given matrix variables via {@link #decodeRequestString} unless
579         * {@link #setUrlDecode} is set to {@code true} in which case it is assumed
580         * the URL path from which the variables were extracted is already decoded
581         * through a call to {@link #getLookupPathForRequest(HttpServletRequest)}.
582         * @param request current HTTP request
583         * @param vars the URI variables extracted from the URL path
584         * @return the same Map or a new Map instance
585         */
586        public MultiValueMap<String, String> decodeMatrixVariables(
587                        HttpServletRequest request, MultiValueMap<String, String> vars) {
588
589                if (this.urlDecode) {
590                        return vars;
591                }
592                else {
593                        MultiValueMap<String, String> decodedVars = new LinkedMultiValueMap<String, String>(vars.size());
594                        for (Map.Entry<String, List<String>> entry : vars.entrySet()) {
595                                for (String value : entry.getValue()) {
596                                        decodedVars.add(entry.getKey(), decodeInternal(request, value));
597                                }
598                        }
599                        return decodedVars;
600                }
601        }
602
603        private boolean shouldRemoveTrailingServletPathSlash(HttpServletRequest request) {
604                if (request.getAttribute(WEBSPHERE_URI_ATTRIBUTE) == null) {
605                        // Regular servlet container: behaves as expected in any case,
606                        // so the trailing slash is the result of a "/" url-pattern mapping.
607                        // Don't remove that slash.
608                        return false;
609                }
610                if (websphereComplianceFlag == null) {
611                        ClassLoader classLoader = UrlPathHelper.class.getClassLoader();
612                        String className = "com.ibm.ws.webcontainer.WebContainer";
613                        String methodName = "getWebContainerProperties";
614                        String propName = "com.ibm.ws.webcontainer.removetrailingservletpathslash";
615                        boolean flag = false;
616                        try {
617                                Class<?> cl = classLoader.loadClass(className);
618                                Properties prop = (Properties) cl.getMethod(methodName).invoke(null);
619                                flag = Boolean.parseBoolean(prop.getProperty(propName));
620                        }
621                        catch (Throwable ex) {
622                                if (logger.isDebugEnabled()) {
623                                        logger.debug("Could not introspect WebSphere web container properties: " + ex);
624                                }
625                        }
626                        websphereComplianceFlag = flag;
627                }
628                // Don't bother if WebSphere is configured to be fully Servlet compliant.
629                // However, if it is not compliant, do remove the improper trailing slash!
630                return !websphereComplianceFlag;
631        }
632
633
634        /**
635         * Shared, read-only instance with defaults. The following apply:
636         * <ul>
637         * <li>{@code alwaysUseFullPath=false}
638         * <li>{@code urlDecode=true}
639         * <li>{@code removeSemicolon=true}
640         * <li>{@code defaultEncoding=}{@link WebUtils#DEFAULT_CHARACTER_ENCODING}
641         * </ul>
642         */
643        public static final UrlPathHelper defaultInstance = new UrlPathHelper();
644
645        static {
646                defaultInstance.setReadOnly();
647        }
648
649
650        /**
651         * Shared, read-only instance for the full, encoded path. The following apply:
652         * <ul>
653         * <li>{@code alwaysUseFullPath=true}
654         * <li>{@code urlDecode=false}
655         * <li>{@code removeSemicolon=false}
656         * <li>{@code defaultEncoding=}{@link WebUtils#DEFAULT_CHARACTER_ENCODING}
657         * </ul>
658         */
659        public static final UrlPathHelper rawPathInstance = new UrlPathHelper() {
660
661                @Override
662                public String removeSemicolonContent(String requestUri) {
663                        return requestUri;
664                }
665        };
666
667        static {
668                rawPathInstance.setAlwaysUseFullPath(true);
669                rawPathInstance.setUrlDecode(false);
670                rawPathInstance.setRemoveSemicolonContent(false);
671                rawPathInstance.setReadOnly();
672        }
673
674}