View Javadoc

1   /**
2    * This file Copyright (c) 2010-2011 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.SimpleUrlPattern;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.context.WebContext;
39  import org.apache.commons.lang.StringUtils;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  import javax.servlet.http.HttpServletRequest;
44  import java.util.ArrayList;
45  import java.util.Collection;
46  import java.util.Iterator;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  
51  /**
52   * A URI mapping as configured for filters and servlets.
53   * @version $Id$
54   *
55   */
56  public class Mapping {
57  
58      private static Logger log = LoggerFactory.getLogger(Mapping.class);
59  
60      private static final String METACHARACTERS = "([\\^\\(\\)\\{\\}\\[\\]*$+])";
61  
62      protected static String escapeMetaCharacters(String str) {
63          return str.replaceAll(METACHARACTERS, "\\\\$1");
64      }
65  
66      protected Collection<Pattern> mappings = new ArrayList<Pattern>();
67  
68      public MatchingResult match(HttpServletRequest request) {
69          Matcher matcher = findMatcher(request);
70          boolean matches = matcher != null;
71          int matchingEndPosition = determineMatchingEnd(matcher);
72          return new MatchingResult(matcher, matches, matchingEndPosition);
73      }
74  
75      /**
76       * Determines the index of the first pathInfo character. If the uri does not match any mapping
77       * this method returns -1.
78       */
79      private int determineMatchingEnd(Matcher matcher) {
80          if (matcher == null) {
81              return -1;
82          }
83          if (matcher.groupCount() > 0) {
84              return matcher.end(1);
85          }
86          return matcher.end();
87      }
88  
89      private Matcher findMatcher(HttpServletRequest request) {
90          String uri = null;
91          WebContext ctx = MgnlContext.getWebContextOrNull();
92          if (ctx != null) {
93              uri = ctx.getAggregationState().getCurrentURI();
94          }
95          if (uri == null) {
96              // the web context is not available during installation
97              uri = StringUtils.substringAfter(request.getRequestURI(), request.getContextPath());
98          }
99          return findMatcher(uri);
100     }
101 
102     private Matcher findMatcher(String uri) {
103         for (Iterator iter = mappings.iterator(); iter.hasNext();) {
104             final Matcher matcher = ((Pattern) iter.next()).matcher(uri);
105 
106             if (matcher.find()) {
107                 return matcher;
108             }
109         }
110 
111         return null;
112     }
113 
114     public Collection<Pattern> getMappings() {
115         return mappings;
116     }
117 
118     public void setMappings(Collection<String> mappings) {
119         for (String mapping : mappings) {
120             this.addMapping(mapping);
121         }
122     }
123 
124     /**
125      * See SRV.11.2 Specification of Mappings in the Servlet Specification for the syntax of
126      * mappings. Additionally, you can also use plain regular expressions to map your servlets, by
127      * prefix the mapping by "regex:". (in which case anything in the request url following the
128      * expression's match will be the pathInfo - if your pattern ends with a $, extra pathInfo won't
129      * match)
130      */
131     public void addMapping(final String mapping) {
132         final String pattern;
133 
134         // we're building a Pattern with 3 groups: (1) servletPath (2) ignored (3) pathInfo
135 
136         if (isDefaultMapping(mapping)) {
137             // the mapping is exactly '/*', the servlet path should be
138             // an empty string and everything else should be the path info
139             pattern = "^()(/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
140         }
141         else if (isPathMapping(mapping)) {
142             // the pattern ends with /*, escape out metacharacters for
143             // use in a regex, and replace the ending * with MULTIPLE_CHAR_PATTERN
144             final String mappingWithoutSuffix = StringUtils.removeEnd(mapping, "/*");
145             pattern = "^(" + escapeMetaCharacters(mappingWithoutSuffix) + ")(/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
146         }
147         else if (isExtensionMapping(mapping)) {
148             // something like '*.jsp', everything should be the servlet path
149             // and the path info should be null
150             final String regexedMapping = StringUtils.replace(mapping, "*.", SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + "\\.");
151             pattern = "^(" + regexedMapping + ")$";
152         }
153         else if (isRegexpMapping(mapping)) {
154             final String mappingWithoutPrefix = StringUtils.removeStart(mapping, "regex:");
155             pattern = "^(" + mappingWithoutPrefix + ")($|/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
156         }
157         else {
158             // just literal text, ensure metacharacters are escaped, and that only
159             // the exact string is matched.
160             pattern = "^(" + escapeMetaCharacters(mapping) + ")$";
161         }
162         log.debug("Adding new mapping for {}", mapping);
163 
164         mappings.add(Pattern.compile(pattern));
165     }
166 
167     /**
168      * This is order specific, this method should not be called until after the isDefaultMapping()
169      * method else it will return true for a default mapping.
170      */
171     private boolean isPathMapping(String mapping) {
172         return mapping.startsWith("/") && mapping.endsWith("/*");
173     }
174 
175     private boolean isExtensionMapping(String mapping) {
176         return mapping.startsWith("*.");
177     }
178 
179     private boolean isDefaultMapping(String mapping) {
180         // TODO : default mapping per spec is "/" - do we really want to support this? is there a
181         // point ?
182         return mapping.equals("/");
183     }
184 
185     private boolean isRegexpMapping(String mapping) {
186         return mapping.startsWith("regex:");
187     }
188 
189     /**
190      * Result of {@link ThemeReader} {@link Mapping#match(HttpServletRequest)} method.
191      * @version $Id$
192      */
193     public static class MatchingResult {
194 
195         Matcher matcher;
196 
197         boolean matches;
198 
199         int matchingEndPosition;
200 
201         public MatchingResult(Matcher matcher, boolean matches, int matchingEndPosition) {
202             this.matcher = matcher;
203             this.matches = matches;
204             this.matchingEndPosition = matchingEndPosition;
205         }
206 
207         public Matcher getMatcher() {
208             return matcher;
209         }
210 
211         public boolean isMatching() {
212             return matches;
213         }
214 
215         public int getMatchingEndPosition() {
216             return matchingEndPosition;
217         }
218     }
219 
220 }