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.protocol.http;
018
019import java.io.IOException;
020import java.lang.reflect.InvocationTargetException;
021import java.util.HashSet;
022import java.util.Set;
023
024import javax.servlet.Filter;
025import javax.servlet.FilterChain;
026import javax.servlet.FilterConfig;
027import javax.servlet.ServletException;
028import javax.servlet.ServletRequest;
029import javax.servlet.ServletResponse;
030import javax.servlet.annotation.WebFilter;
031import javax.servlet.annotation.WebServlet;
032import javax.servlet.http.HttpServletRequest;
033import javax.servlet.http.HttpServletResponse;
034
035import org.apache.wicket.Session;
036import org.apache.wicket.ThreadContext;
037import org.apache.wicket.WicketRuntimeException;
038import org.apache.wicket.protocol.http.servlet.ResponseIOException;
039import org.apache.wicket.request.cycle.RequestCycle;
040import org.apache.wicket.request.http.WebRequest;
041import org.apache.wicket.request.http.WebResponse;
042import org.apache.wicket.util.file.WebXmlFile;
043import org.apache.wicket.util.lang.Args;
044import org.apache.wicket.util.string.Strings;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048/**
049 * Filter for initiating handling of Wicket requests.
050 * <p>
051 * The advantage of a filter is that, unlike a servlet, it can choose not to process the request and
052 * let whatever is next in chain try. So when using a Wicket filter and a request comes in for
053 * foo.gif the filter can choose not to process it because it knows it is not a wicket-related
054 * request. Since the filter didn't process it, it falls on to the application server to try, and
055 * then it works."
056 * 
057 * @see WicketServlet for documentation
058 * 
059 * @author Jonathan Locke
060 * @author Timur Mehrvarz
061 * @author Juergen Donnerstag
062 * @author Igor Vaynberg (ivaynberg)
063 * @author Al Maw
064 * @author jcompagner
065 * @author Matej Knopp
066 */
067public class WicketFilter implements Filter
068{
069        private static final Logger log = LoggerFactory.getLogger(WicketFilter.class);
070
071        /** The name of the root path parameter that specifies the root dir of the app. */
072        public static final String FILTER_MAPPING_PARAM = "filterMappingUrlPattern";
073
074        /** The name of the context parameter that specifies application factory class */
075        public static final String APP_FACT_PARAM = "applicationFactoryClassName";
076
077        /**
078         * Name of parameter used to express a comma separated list of paths that should be ignored
079         */
080        public static final String IGNORE_PATHS_PARAM = "ignorePaths";
081
082        // Wicket's Application object
083        private WebApplication application;
084
085        /** the factory used to create the web aplication instance */
086        private IWebApplicationFactory applicationFactory;
087
088        private FilterConfig filterConfig;
089
090        private String filterPath;
091
092        // filterPath length without trailing "/"
093        private int filterPathLength = -1;
094
095        /** set of paths that should be ignored by the wicket filter */
096        private final Set<String> ignorePaths = new HashSet<String>();
097
098        /**
099         * A flag indicating whether WicketFilter is used directly or through WicketServlet
100         */
101        private boolean isServlet = false;
102
103        /**
104         * default constructor, usually invoked through the servlet container by the web.xml
105         * configuration
106         */
107        public WicketFilter()
108        {
109        }
110
111        /**
112         * constructor supporting programmatic setup of the filter
113         * <p/>
114         * this can be useful for programmatically creating and appending the wicket filter to the
115         * servlet context using servlet 3 features.
116         * 
117         * @param application
118         *            web application
119         */
120        public WicketFilter(WebApplication application)
121        {
122                this.application = Args.notNull(application, "application");
123        }
124
125        /**
126         * @return The class loader
127         */
128        protected ClassLoader getClassLoader()
129        {
130                return Thread.currentThread().getContextClassLoader();
131        }
132
133        /**
134         * This is Wicket's main method to execute a request
135         * 
136         * @param request
137         * @param response
138         * @param chain
139         * @return false, if the request could not be processed
140         * @throws IOException
141         * @throws ServletException
142         */
143        boolean processRequest(ServletRequest request, final ServletResponse response,
144                final FilterChain chain) throws IOException, ServletException
145        {
146                final ThreadContext previousThreadContext = ThreadContext.detach();
147
148                // Assume we are able to handle the request
149                boolean res = true;
150
151                final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
152                final ClassLoader newClassLoader = getClassLoader();
153
154                HttpServletRequest httpServletRequest = (HttpServletRequest)request;
155                HttpServletResponse httpServletResponse = (HttpServletResponse)response;
156
157                boolean ioExceptionOccurred = false;
158                try
159                {
160                        if (previousClassLoader != newClassLoader)
161                        {
162                                Thread.currentThread().setContextClassLoader(newClassLoader);
163                        }
164
165                        // Make sure getFilterPath() gets called before checkIfRedirectRequired()
166                        String filterPath = getFilterPath(httpServletRequest);
167
168                        if (filterPath == null)
169                        {
170                                throw new IllegalStateException("filter path was not configured");
171                        }
172
173                        if (shouldIgnorePath(httpServletRequest))
174                        {
175                                log.debug("Ignoring request {}", httpServletRequest.getRequestURL());
176                                if (chain != null)
177                                {
178                                        // invoke next filter from within Wicket context
179                                        chain.doFilter(request, response);
180                                }
181                                return false;
182                        }
183
184                        if ("OPTIONS".equalsIgnoreCase(httpServletRequest.getMethod()))
185                        {
186                                // handle the OPTIONS request outside of normal request processing.
187                                // wicket pages normally only support GET and POST methods, but resources and
188                                // special pages acting like REST clients can also support other methods, so
189                                // we include them all.
190                                httpServletResponse.setStatus(HttpServletResponse.SC_OK);
191                                httpServletResponse.setHeader("Allow",
192                                        "GET,POST,OPTIONS,PUT,HEAD,PATCH,DELETE,TRACE");
193                                httpServletResponse.setHeader("Content-Length", "0");
194                                return true;
195                        }
196
197                        String redirectURL = checkIfRedirectRequired(httpServletRequest);
198                        if (redirectURL == null)
199                        {
200                                // No redirect; process the request
201                                ThreadContext.setApplication(application);
202
203                                WebRequest webRequest = application.createWebRequest(httpServletRequest, filterPath);
204                                WebResponse webResponse = application.createWebResponse(webRequest,
205                                        httpServletResponse);
206
207                                RequestCycle requestCycle = application.createRequestCycle(webRequest, webResponse);
208                                res = processRequestCycle(requestCycle, webResponse, httpServletRequest,
209                                        httpServletResponse, chain);
210                        }
211                        else
212                        {
213                                if (Strings.isEmpty(httpServletRequest.getQueryString()) == false)
214                                {
215                                        redirectURL += "?" + httpServletRequest.getQueryString();
216                                }
217
218                                // send redirect - this will discard POST parameters if the request is POST
219                                // - still better than getting an error because of lacking trailing slash
220                                httpServletResponse.sendRedirect(httpServletResponse.encodeRedirectURL(redirectURL));
221                        }
222                }
223                catch (IOException e)
224                {
225                        ioExceptionOccurred = true;
226                        throw e;
227                }
228                catch (ResponseIOException e)
229                {
230                        ioExceptionOccurred = true;
231                        throw e.getCause();
232                }
233                finally
234                {
235                        ThreadContext.restore(previousThreadContext);
236
237                        if (newClassLoader != previousClassLoader)
238                        {
239                                Thread.currentThread().setContextClassLoader(previousClassLoader);
240                        }
241
242                        if (!ioExceptionOccurred && response.isCommitted() &&
243                                !httpServletRequest.isAsyncStarted())
244                        {
245                                try
246                                {
247                                        response.flushBuffer();
248                                }
249                                catch (ResponseIOException e)
250                                {
251                                        throw e.getCause();
252                                }
253                        }
254                }
255                return res;
256        }
257
258        /**
259         * Process the request cycle
260         * 
261         * @param requestCycle
262         * @param webResponse
263         * @param httpServletRequest
264         * @param httpServletResponse
265         * @param chain
266         * @return false, if the request could not be processed
267         * @throws IOException
268         * @throws ServletException
269         */
270        protected boolean processRequestCycle(RequestCycle requestCycle, WebResponse webResponse,
271                HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
272                final FilterChain chain) throws IOException, ServletException
273        {
274                boolean reqProcessed;
275                try
276                {
277                        reqProcessed = requestCycle.processRequest();
278                        if (reqProcessed)
279                        {
280                                webResponse.flush();
281                        }
282                }
283                finally
284                {
285                        requestCycle.detach();
286                }
287
288                if (!reqProcessed)
289                {
290                        if (chain != null)
291                        {
292                                // invoke next filter from within Wicket context
293                                chain.doFilter(httpServletRequest, httpServletResponse);
294                        }
295                }
296                return reqProcessed;
297        }
298
299        /**
300         * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
301         *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
302         */
303        @Override
304        public void doFilter(final ServletRequest request, final ServletResponse response,
305                final FilterChain chain) throws IOException, ServletException
306        {
307                processRequest(request, response, chain);
308        }
309
310        /**
311         * Creates the web application factory instance.
312         * 
313         * If no APP_FACT_PARAM is specified in web.xml ContextParamWebApplicationFactory will be used
314         * by default.
315         * 
316         * @see ContextParamWebApplicationFactory
317         * 
318         * @return application factory instance
319         */
320        protected IWebApplicationFactory getApplicationFactory()
321        {
322                final String appFactoryClassName = filterConfig.getInitParameter(APP_FACT_PARAM);
323
324                if (appFactoryClassName == null)
325                {
326                        // If no context param was specified we return the default factory
327                        return new ContextParamWebApplicationFactory();
328                }
329                else
330                {
331                        try
332                        {
333                                // Try to find the specified factory class
334                                // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6500212
335                                // final Class<?> factoryClass = Thread.currentThread()
336                                // .getContextClassLoader()
337                                // .loadClass(appFactoryClassName);
338                                final Class<?> factoryClass = Class.forName(appFactoryClassName, false,
339                                        Thread.currentThread().getContextClassLoader());
340
341                                // Instantiate the factory
342                                return (IWebApplicationFactory)factoryClass.getDeclaredConstructor().newInstance();
343                        }
344                        catch (ClassCastException e)
345                        {
346                                throw new WicketRuntimeException("Application factory class " +
347                                        appFactoryClassName + " must implement IWebApplicationFactory");
348                        }
349                        catch (ClassNotFoundException | InstantiationException | IllegalAccessException | SecurityException
350                                        | NoSuchMethodException | InvocationTargetException e)
351                        {
352                                throw new WebApplicationFactoryCreationException(appFactoryClassName, e);
353                        }
354                }
355        }
356
357        /**
358         * If you do have a need to subclass, you may subclass {@link #init(boolean, FilterConfig)}
359         * 
360         * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
361         */
362        @Override
363        public final void init(final FilterConfig filterConfig) throws ServletException
364        {
365                init(false, filterConfig);
366        }
367
368        /**
369         * Servlets and Filters are treated essentially the same with Wicket. This is the entry point
370         * for both of them.
371         * 
372         * @see #init(FilterConfig)
373         * 
374         * @param isServlet
375         *            True if Servlet, false if Filter
376         * @param filterConfig
377         * @throws ServletException
378         */
379        public void init(final boolean isServlet, final FilterConfig filterConfig)
380                throws ServletException
381        {
382                this.filterConfig = filterConfig;
383                this.isServlet = isServlet;
384                initIgnorePaths(filterConfig);
385
386                final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
387                final ClassLoader newClassLoader = getClassLoader();
388                try
389                {
390                        if (previousClassLoader != newClassLoader)
391                        {
392                                Thread.currentThread().setContextClassLoader(newClassLoader);
393                        }
394
395                        // locate application instance unless it was already specified during construction
396                        if (application == null)
397                        {
398                                applicationFactory = getApplicationFactory();
399                                application = applicationFactory.createApplication(this);
400                        }
401
402                        if (application.getName() == null)
403                        {
404                                application.setName(filterConfig.getFilterName());
405                        }
406                        application.setWicketFilter(this);
407
408                        // Allow the filterPath to be preset via setFilterPath()
409                        String configureFilterPath = getFilterPath();
410
411                        if (configureFilterPath == null)
412                        {
413                                configureFilterPath = getFilterPathFromConfig(filterConfig);
414
415                                if (configureFilterPath == null)
416                                {
417                                        configureFilterPath = getFilterPathFromWebXml(isServlet, filterConfig);
418
419                                        if (configureFilterPath == null)
420                                        {
421                                                configureFilterPath = getFilterPathFromAnnotation(isServlet);
422                                        }
423                                }
424
425                                if (configureFilterPath != null)
426                                {
427                                        setFilterPath(configureFilterPath);
428                                }
429                        }
430
431                        if (getFilterPath() == null)
432                        {
433                                log.warn("Unable to determine filter path from filter init-param, web.xml, "
434                                        + "or servlet 3.0 annotations. Assuming user will set filter path "
435                                        + "manually by calling setFilterPath(String)");
436                        }
437
438                        ThreadContext.setApplication(application);
439                        try
440                        {
441                                application.initApplication();
442
443                                // Give the application the option to log that it is started
444                                application.logStarted();
445                        }
446                        finally
447                        {
448                                ThreadContext.detach();
449                        }
450                }
451                catch (Exception e)
452                {
453                        // #destroy() might not be called by the web container when #init() fails,
454                        // so destroy now
455                        log.error(String.format("The initialization of an application with name '%s' has failed.",
456                                        filterConfig.getFilterName()), e);
457
458                        try
459                        {
460                                destroy();
461                        }
462                        catch (Exception destroyException)
463                        {
464                                log.error("Unable to destroy after initialization failure", destroyException);
465                        }
466
467                        throw new ServletException(e);
468                }
469                finally
470                {
471                        if (newClassLoader != previousClassLoader)
472                        {
473                                Thread.currentThread().setContextClassLoader(previousClassLoader);
474                        }
475                }
476        }
477
478        /**
479         * Stub method that lets subclasses configure filter path from annotations.
480         * 
481         * @param isServlet
482         * @return Filter path from annotation
483         */
484        protected String getFilterPathFromAnnotation(boolean isServlet)
485        {
486                String[] patterns = null;
487
488                if (isServlet)
489                {
490                        WebServlet servlet = getClass().getAnnotation(WebServlet.class);
491                        if (servlet != null)
492                        {
493                                if (servlet.urlPatterns().length > 0)
494                                {
495                                        patterns = servlet.urlPatterns();
496                                }
497                                else
498                                {
499                                        patterns = servlet.value();
500                                }
501                        }
502                }
503                else
504                {
505                        WebFilter filter = getClass().getAnnotation(WebFilter.class);
506                        if (filter != null)
507                        {
508                                if (filter.urlPatterns().length > 0)
509                                {
510                                        patterns = filter.urlPatterns();
511                                }
512                                else
513                                {
514                                        patterns = filter.value();
515                                }
516                        }
517                }
518
519                if (patterns != null && patterns.length > 0)
520                {
521                        String pattern = patterns[0];
522                        if (patterns.length > 1)
523                        {
524                                log.warn(
525                                                "Multiple url patterns defined for Wicket filter/servlet, using the first: {}",
526                                                pattern);
527                        }
528
529                        if ("/*".equals(pattern))
530                        {
531                                pattern = "";
532                        }
533
534                        if (pattern.endsWith("*"))
535                        {
536                                pattern = pattern.substring(0, pattern.length() - 1);
537                        }
538                        return pattern;
539                }
540                return null;
541        }
542
543        /**
544         * 
545         * @param isServlet
546         * @param filterConfig
547         * @return filter path from web.xml
548         */
549        protected String getFilterPathFromWebXml(final boolean isServlet,
550                final FilterConfig filterConfig)
551        {
552                return new WebXmlFile().getUniqueFilterPath(isServlet, filterConfig);
553        }
554
555        /**
556         * @return filter config
557         */
558        public FilterConfig getFilterConfig()
559        {
560                return filterConfig;
561        }
562
563        /**
564         * Either get the filterPath retrieved from web.xml, or if not found the old (1.3) way via a
565         * filter mapping param.
566         * 
567         * @param request
568         * @return filterPath
569         */
570        protected String getFilterPath(final HttpServletRequest request)
571        {
572                return filterPath;
573        }
574
575        /**
576         * Provide a standard getter for filterPath.
577         * 
578         * @return The configured filterPath.
579         */
580        public String getFilterPath()
581        {
582                return filterPath;
583        }
584
585        /**
586         * 
587         * @param filterConfig
588         * @return filter path
589         */
590        protected String getFilterPathFromConfig(FilterConfig filterConfig)
591        {
592                String result = filterConfig.getInitParameter(FILTER_MAPPING_PARAM);
593                if (result != null)
594                {
595                        if (result.equals("/*"))
596                        {
597                                result = "";
598                        }
599                        else if (!result.startsWith("/") || !result.endsWith("/*"))
600                        {
601                                throw new WicketRuntimeException("Your " + FILTER_MAPPING_PARAM +
602                                        " must start with \"/\" and end with \"/*\". It is: " + result);
603                        }
604                        else
605                        {
606                                // remove leading "/" and trailing "*"
607                                result = result.substring(1, result.length() - 1);
608                        }
609                }
610                return result;
611        }
612
613        /**
614         * @see javax.servlet.Filter#destroy()
615         */
616        @Override
617        public void destroy()
618        {
619                if (application != null)
620                {
621                        try
622                        {
623                                ThreadContext.setApplication(application);
624                                application.internalDestroy();
625                        }
626                        finally
627                        {
628                                ThreadContext.detach();
629                                application = null;
630                        }
631                }
632
633                if (applicationFactory != null)
634                {
635                        try
636                        {
637                                applicationFactory.destroy(this);
638                        }
639                        finally
640                        {
641                                applicationFactory = null;
642                        }
643                }
644        }
645
646        /**
647         * Try to determine as fast as possible if a redirect is necessary
648         * 
649         * @param request
650         * @return null, if no redirect is necessary. Else the redirect URL
651         */
652        private String checkIfRedirectRequired(final HttpServletRequest request)
653        {
654                return checkIfRedirectRequired(request.getRequestURI(), request.getContextPath());
655        }
656
657        /**
658         * Try to determine as fast as possible if a redirect is necessary
659         * 
660         * @param requestURI
661         * @param contextPath
662         * @return null, if no redirect is necessary. Else the redirect URL
663         */
664        protected final String checkIfRedirectRequired(final String requestURI, final String contextPath)
665        {
666                // length without jsessionid (http://.../abc;jsessionid=...?param)
667                int uriLength = requestURI.indexOf(';');
668                if (uriLength == -1)
669                {
670                        uriLength = requestURI.length();
671                }
672
673                // request.getContextPath() + "/" + filterPath. But without any trailing "/".
674                int homePathLength = contextPath.length() +
675                        (filterPathLength > 0 ? 1 + filterPathLength : 0);
676                if (uriLength != homePathLength)
677                {
678                        // requestURI and homePath are different (in length)
679                        // => continue with standard request processing. No redirect.
680                        return null;
681                }
682
683                // Fail fast failed. Revert to "slow" but exact check
684                String uri = Strings.stripJSessionId(requestURI);
685
686                // home page without trailing slash URI
687                String homePageUri = contextPath + '/' + getFilterPath();
688                if (homePageUri.endsWith("/"))
689                {
690                        homePageUri = homePageUri.substring(0, homePageUri.length() - 1);
691                }
692
693                // If both are equal => redirect
694                if (uri.equals(homePageUri))
695                {
696                        uri += "/";
697                        return uri;
698                }
699
700                // no match => standard request processing; no redirect
701                return null;
702        }
703
704        /**
705         * Sets the filter path instead of reading it from web.xml.
706         * 
707         * Please note that you must subclass WicketFilter.init(FilterConfig) and set your filter path
708         * before you call super.init(filterConfig).
709         * 
710         * @param filterPath
711         */
712        public final void setFilterPath(String filterPath)
713        {
714                // see https://issues.apache.org/jira/browse/WICKET-701
715                if (this.filterPath != null)
716                {
717                        throw new IllegalStateException(
718                                "Filter path is write-once. You can not change it. Current value='" + filterPath +
719                                        '\'');
720                }
721                if (filterPath != null)
722                {
723                        filterPath = canonicaliseFilterPath(filterPath);
724
725                        // We only need to determine it once. It'll not change.
726                        if (filterPath.endsWith("/"))
727                        {
728                                filterPathLength = filterPath.length() - 1;
729                        }
730                        else
731                        {
732                                filterPathLength = filterPath.length();
733                        }
734                }
735                this.filterPath = filterPath;
736        }
737
738        /**
739         * Returns a relative path to the filter path and context root from an HttpServletRequest - use
740         * this to resolve a Wicket request.
741         * 
742         * @param request
743         * @return Path requested, minus query string, context path, and filterPath. Relative, no
744         *         leading '/'.
745         */
746        public String getRelativePath(HttpServletRequest request)
747        {
748                String path = Strings.stripJSessionId(request.getRequestURI());
749                String contextPath = request.getContextPath();
750                path = path.substring(contextPath.length());
751                if (isServlet)
752                {
753                        String servletPath = request.getServletPath();
754                        path = path.substring(servletPath.length());
755                }
756
757                if (path.length() > 0)
758                {
759                        path = path.substring(1);
760                }
761
762                // We should always be under the rootPath, except
763                // for the special case of someone landing on the
764                // home page without a trailing slash.
765                String filterPath = getFilterPath();
766                if (!path.startsWith(filterPath))
767                {
768                        if (filterPath.equals(path + "/"))
769                        {
770                                path += "/";
771                        }
772                }
773                if (path.startsWith(filterPath))
774                {
775                        path = path.substring(filterPath.length());
776                }
777
778                return path;
779
780        }
781
782        protected WebApplication getApplication()
783        {
784                return application;
785        }
786
787        /**
788         * Checks whether this is a request to an ignored path
789         * 
790         * @param request
791         *            the current http request
792         * @return {@code true} when the request should be ignored, {@code false} - otherwise
793         */
794        private boolean shouldIgnorePath(final HttpServletRequest request)
795        {
796                boolean ignore = false;
797                if (ignorePaths.size() > 0)
798                {
799                        String relativePath = getRelativePath(request);
800                        if (Strings.isEmpty(relativePath) == false)
801                        {
802                                for (String path : ignorePaths)
803                                {
804                                        if (relativePath.startsWith(path))
805                                        {
806                                                ignore = true;
807                                                break;
808                                        }
809                                }
810                        }
811                }
812
813                return ignore;
814        }
815
816        /**
817         * initializes the ignore paths parameter
818         * 
819         * @param filterConfig
820         */
821        private void initIgnorePaths(final FilterConfig filterConfig)
822        {
823                String paths = filterConfig.getInitParameter(IGNORE_PATHS_PARAM);
824                if (Strings.isEmpty(paths) == false)
825                {
826                        String[] parts = Strings.split(paths, ',');
827                        for (String path : parts)
828                        {
829                                path = path.trim();
830                                if (path.startsWith("/"))
831                                {
832                                        path = path.substring(1);
833                                }
834                                ignorePaths.add(path);
835                        }
836                }
837        }
838
839        /**
840         * A filterPath should have all leading slashes removed and exactly one trailing slash. A
841         * wildcard asterisk character has no special meaning. If your intention is to mean the top
842         * level "/" then an empty string should be used instead.
843         * 
844         * @param filterPath
845         * @return canonic filter path
846         */
847        static String canonicaliseFilterPath(String filterPath)
848        {
849                if (Strings.isEmpty(filterPath))
850                {
851                        return filterPath;
852                }
853
854                int beginIndex = 0;
855                int endIndex = filterPath.length();
856                while (beginIndex < endIndex)
857                {
858                        char c = filterPath.charAt(beginIndex);
859                        if (c != '/')
860                        {
861                                break;
862                        }
863                        beginIndex++;
864                }
865                int o;
866                int i = o = beginIndex;
867                while (i < endIndex)
868                {
869                        char c = filterPath.charAt(i);
870                        i++;
871                        if (c != '/')
872                        {
873                                o = i;
874                        }
875                }
876                if (o < endIndex)
877                {
878                        o++; // include exactly one trailing slash
879                        filterPath = filterPath.substring(beginIndex, o);
880                }
881                else
882                {
883                        // ensure to append trailing slash
884                        filterPath = filterPath.substring(beginIndex) + '/';
885                }
886
887                if (filterPath.equals("/"))
888                {
889                        return "";
890                }
891                return filterPath;
892        }
893}