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 jakarta.servlet.Filter; 025import jakarta.servlet.FilterChain; 026import jakarta.servlet.FilterConfig; 027import jakarta.servlet.ServletException; 028import jakarta.servlet.ServletRequest; 029import jakarta.servlet.ServletResponse; 030import jakarta.servlet.annotation.WebFilter; 031import jakarta.servlet.annotation.WebServlet; 032import jakarta.servlet.http.HttpServletRequest; 033import jakarta.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 jakarta.servlet.Filter#doFilter(jakarta.servlet.ServletRequest, 301 * jakarta.servlet.ServletResponse, jakarta.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 jakarta.servlet.Filter#init(jakarta.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 jakarta.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}