View Javadoc
1   /**
2    * This file Copyright (c) 2010-2014 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.filters;
35  
36  import info.magnolia.cms.util.ServletUtil;
37  import info.magnolia.cms.util.SimpleUrlPattern;
38  import info.magnolia.context.MgnlContext;
39  import info.magnolia.context.WebContext;
40  
41  import java.util.ArrayList;
42  import java.util.Collection;
43  import java.util.Iterator;
44  import java.util.regex.Matcher;
45  import java.util.regex.Pattern;
46  
47  import javax.servlet.http.HttpServletRequest;
48  
49  import org.apache.commons.lang.StringUtils;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  
54  /**
55   * A URI mapping as configured for filters and servlets.
56   * @version $Id$
57   *
58   */
59  public class Mapping {
60  
61      private static Logger log = LoggerFactory.getLogger(Mapping.class);
62  
63      private static final String METACHARACTERS = "([\\^\\(\\)\\{\\}\\[\\]*$+.])";
64  
65      protected static String escapeMetaCharacters(String str) {
66          return str.replaceAll(METACHARACTERS, "\\\\$1");
67      }
68  
69      protected Collection<Pattern> mappings = new ArrayList<Pattern>();
70  
71      public MatchingResult match(HttpServletRequest request) {
72          Matcher matcher = findMatcher(request);
73          boolean matches = matcher != null;
74          int matchingEndPosition = determineMatchingEnd(matcher);
75          return new MatchingResult(matcher, matches, matchingEndPosition);
76      }
77  
78      /**
79       * Determines the index of the first pathInfo character. If the uri does not match any mapping
80       * this method returns -1.
81       */
82      private int determineMatchingEnd(Matcher matcher) {
83          if (matcher == null) {
84              return -1;
85          }
86          if (matcher.groupCount() > 0) {
87              return matcher.end(1);
88          }
89          return matcher.end();
90      }
91  
92      private Matcher findMatcher(HttpServletRequest request) {
93          String uri = null;
94          WebContext ctx = MgnlContext.getWebContextOrNull();
95          if (ctx != null) {
96              uri = ctx.getAggregationState().getCurrentURI();
97          }
98          if (uri == null) {
99              // the web context is not available during installation
100             uri = ServletUtil.stripPathParameters(request.getRequestURI());
101             String contextPath = request.getContextPath();
102             // The requestUri should always start with the context path
103             if (uri.startsWith(contextPath + "/")) {
104                 uri = uri.substring(contextPath.length());
105             }
106         }
107         return findMatcher(uri);
108     }
109 
110     private Matcher findMatcher(String uri) {
111         for (Iterator<Pattern> iter = getMappings().iterator(); iter.hasNext();) {
112             final Matcher matcher = iter.next().matcher(uri);
113 
114             if (matcher.find()) {
115                 return matcher;
116             }
117         }
118 
119         return null;
120     }
121 
122     public Collection<Pattern> getMappings() {
123         return mappings;
124     }
125 
126     /**
127      * See SRV.11.2 Specification of Mappings in the Servlet Specification for the syntax of
128      * mappings. Additionally, you can also use plain regular expressions to map your servlets, by
129      * prefix the mapping by "regex:". (in which case anything in the request url following the
130      * expression's match will be the pathInfo - if your pattern ends with a $, extra pathInfo won't
131      * match)
132      */
133     public void addMapping(final String mapping) {
134         final String pattern;
135 
136         // we're building a Pattern with 3 groups: (1) servletPath (2) ignored (3) pathInfo
137 
138         if (isDefaultMapping(mapping)) {
139             // the mapping is exactly '/*', the servlet path should be
140             // an empty string and everything else should be the path info
141             pattern = "^()(/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
142         }
143         else if (isPathMapping(mapping)) {
144             // the pattern ends with /*, escape out metacharacters for
145             // use in a regex, and replace the ending * with MULTIPLE_CHAR_PATTERN
146             final String mappingWithoutSuffix = StringUtils.removeEnd(mapping, "/*");
147             pattern = "^(" + escapeMetaCharacters(mappingWithoutSuffix) + ")(/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
148         }
149         else if (isExtensionMapping(mapping)) {
150             // something like '*.jsp', everything should be the servlet path
151             // and the path info should be null
152             final String regexedMapping = StringUtils.replace(mapping, "*.", SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + "\\.");
153             pattern = "^(" + regexedMapping + ")$";
154         }
155         else if (isRegexpMapping(mapping)) {
156             final String mappingWithoutPrefix = StringUtils.removeStart(mapping, "regex:");
157             pattern = "^(" + mappingWithoutPrefix + ")($|/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
158         }
159         else {
160             // just literal text, ensure metacharacters are escaped, and that only
161             // the exact string is matched.
162             pattern = "^(" + escapeMetaCharacters(mapping) + ")$";
163         }
164         log.debug("Adding new mapping for {}", mapping);
165 
166         mappings.add(Pattern.compile(pattern));
167     }
168 
169     /**
170      * This is order specific, this method should not be called until after the isDefaultMapping()
171      * method else it will return true for a default mapping.
172      */
173     private boolean isPathMapping(String mapping) {
174         return mapping.startsWith("/") && mapping.endsWith("/*");
175     }
176 
177     private boolean isExtensionMapping(String mapping) {
178         return mapping.startsWith("*.");
179     }
180 
181     private boolean isDefaultMapping(String mapping) {
182         // TODO : default mapping per spec is "/" - do we really want to support this? is there a
183         // point ?
184         return mapping.equals("/");
185     }
186 
187     private boolean isRegexpMapping(String mapping) {
188         return mapping.startsWith("regex:");
189     }
190 
191     /**
192      * Result of {@link ThemeReader} {@link Mapping#match(HttpServletRequest)} method.
193      * @version $Id$
194      */
195     public static class MatchingResult {
196 
197         Matcher matcher;
198 
199         boolean matches;
200 
201         int matchingEndPosition;
202 
203         public MatchingResult(Matcher matcher, boolean matches, int matchingEndPosition) {
204             this.matcher = matcher;
205             this.matches = matches;
206             this.matchingEndPosition = matchingEndPosition;
207         }
208 
209         public Matcher getMatcher() {
210             return matcher;
211         }
212 
213         public boolean isMatching() {
214             return matches;
215         }
216 
217         public int getMatchingEndPosition() {
218             return matchingEndPosition;
219         }
220     }
221 
222 }