View Javadoc

1   /**
2    * This file Copyright (c) 2003-2010 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 info.magnolia.objectfactory.Classes;
40  
41  import java.io.IOException;
42  import java.util.Collection;
43  import java.util.Enumeration;
44  import java.util.Iterator;
45  import java.util.LinkedList;
46  import java.util.Map;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  import javax.servlet.FilterChain;
51  import javax.servlet.FilterConfig;
52  import javax.servlet.Servlet;
53  import javax.servlet.ServletConfig;
54  import javax.servlet.ServletContext;
55  import javax.servlet.ServletException;
56  import javax.servlet.ServletRequest;
57  import javax.servlet.http.HttpServletRequest;
58  import javax.servlet.http.HttpServletRequestWrapper;
59  import javax.servlet.http.HttpServletResponse;
60  
61  import org.apache.commons.lang.StringUtils;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  
66  /**
67   * A filter that dispatches requests to a wrapped servlet.
68   *
69   * TODO : cache matching URIs ?
70   *
71   * @author vsteller
72   * @version $Id: ServletDispatchingFilter.java 32667 2010-03-13 00:37:06Z gjoseph $
73   */
74  public class ServletDispatchingFilter extends AbstractMgnlFilter {
75  
76      private static final Logger log = LoggerFactory.getLogger(ServletDispatchingFilter.class);
77  
78      private static final String METACHARACTERS = "([\\^\\(\\)\\{\\}\\[\\]*$+])";
79  
80      private String servletName;
81  
82      private String servletClass;
83  
84      private Collection mappings;
85  
86      private Map parameters;
87  
88      private String comment;
89  
90      private Servlet servlet;
91  
92      public ServletDispatchingFilter() {
93          mappings = new LinkedList();
94      }
95  
96      public String getName() {
97          return "Wrapper for " + servletName + " servlet";
98      }
99  
100     /**
101      * Initializes the servlet and its mappings. ServletConfig is wrapped to take init parameters into account.
102      */
103     public void init(final FilterConfig filterConfig) throws ServletException {
104         super.init(filterConfig);
105 
106         if (servletClass != null) {
107             try {
108                 servlet = Classes.newInstance(servletClass);
109                 servlet.init(new WrappedServletConfig(servletName, filterConfig, parameters));
110             }
111             catch (Throwable e) {
112                 log.error("Unable to load servlet " + servletClass + " : " + e.getMessage(), e);
113             }
114         }
115     }
116 
117     /**
118      * Delegates the destroy() call to the wrapper servlet, then to this filter itself.
119      */
120     public void destroy() {
121         if (servlet != null) {
122             servlet.destroy();
123         }
124         super.destroy();
125     }
126 
127     /**
128      * Bypasses if the current request does not match any of the mappings of the servlet. Explicit bypasses defined in
129      * the bypasses content node of this filter are taken into account as well.
130      */
131     public boolean bypasses(HttpServletRequest request) {
132         return determineMatchingEnd(request) < 0 || super.bypasses(request);
133     }
134 
135     /**
136      * Determines the index of the first pathInfo character. If the uri does not match any mapping this method returns
137      * -1.
138      */
139     protected int determineMatchingEnd(HttpServletRequest request) {
140         final Matcher matcher = findMatcher(request);
141         if (matcher == null) {
142             return -1;
143         } else {
144             if (matcher.groupCount() > 0) {
145                 return matcher.end(1);
146             } else {
147                 return matcher.end();
148             }
149         }
150     }
151 
152     protected Matcher findMatcher(HttpServletRequest request) {
153         WebContext ctx = MgnlContext.getWebContextOrNull();
154         final String uri;
155         if (ctx != null) {
156             uri = MgnlContext.getWebContext().getAggregationState().getCurrentURI();
157         } else {
158             // the web context is not available during installation
159             uri = StringUtils.substringAfter(request.getRequestURI(), request.getContextPath());
160         }
161         return findMatcher(uri);
162     }
163 
164     protected Matcher findMatcher(String uri) {
165         for (Iterator iter = mappings.iterator(); iter.hasNext();) {
166             final Matcher matcher = ((Pattern) iter.next()).matcher(uri);
167 
168             if (matcher.find()) {
169                 return matcher;
170             }
171         }
172 
173         return null;
174     }
175 
176     /**
177      * Dispatches the request to the servlet if not already bypassed. The request is wrapped for properly setting the
178      * pathInfo.
179      */
180     public void doFilter(final HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
181         log.debug("Dispatching to servlet {}", getServletClass());
182         final Matcher matcher = findMatcher(request);
183         servlet.service(new WrappedRequest(request, matcher), response);
184     }
185 
186     public String getServletName() {
187         return servletName;
188     }
189 
190     public void setServletName(String servletName) {
191         this.servletName = servletName;
192     }
193 
194     public String getServletClass() {
195         return servletClass;
196     }
197 
198     public void setServletClass(String servletClass) {
199         this.servletClass = servletClass;
200     }
201 
202     public Collection getMappings() {
203         return mappings;
204     }
205 
206     public void setMappings(Collection mappings) {
207         this.mappings = mappings;
208     }
209 
210     /**
211      * See SRV.11.2 Specification of Mappings in the Servlet Specification
212      * for the syntax of mappings. Additionally, you can also use plain regular
213      * expressions to map your servlets, by prefix the mapping by "regex:". (in
214      * which case anything in the request url following the expression's match
215      * will be the pathInfo - if your pattern ends with a $, extra pathInfo won't
216      * match)
217      */
218     public void addMapping(final String mapping) {
219         final String pattern;
220 
221         // we're building a Pattern with 3 groups: (1) servletPath (2) ignored (3) pathInfo
222 
223         if (isDefaultMapping(mapping)) {
224             // the mapping is exactly '/*', the servlet path should be
225             // an empty string and everything else should be the path info
226             pattern = "^()(/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
227         } else if (isPathMapping(mapping)) {
228             // the pattern ends with /*, escape out metacharacters for
229             // use in a regex, and replace the ending * with MULTIPLE_CHAR_PATTERN
230             final String mappingWithoutSuffix = StringUtils.removeEnd(mapping, "/*");
231             pattern = "^(" + escapeMetaCharacters(mappingWithoutSuffix) + ")(/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
232         } else if (isExtensionMapping(mapping)) {
233             // something like '*.jsp', everything should be the servlet path
234             // and the path info should be null
235             final String regexedMapping = StringUtils.replace(mapping, "*.", SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + "\\.");
236             pattern = "^(" + regexedMapping + ")$";
237         } else if (isRegexpMapping(mapping)) {
238             final String mappingWithoutPrefix = StringUtils.removeStart(mapping, "regex:");
239             pattern = "^(" + mappingWithoutPrefix + ")($|/)(" + SimpleUrlPattern.MULTIPLE_CHAR_PATTERN + ")";
240         } else {
241             // just literal text, ensure metacharacters are escaped, and that only
242             // the exact string is matched.
243             pattern = "^(" + escapeMetaCharacters(mapping) + ")$";
244         }
245         log.debug("Adding new mapping for {}", mapping);
246 
247         mappings.add(Pattern.compile(pattern));
248     }
249 
250     static String escapeMetaCharacters(String str) {
251         return str.replaceAll(METACHARACTERS, "\\\\$1");
252     }
253 
254     /**
255      * This is order specific, this method should not be called until
256      * after the isDefaultMapping() method else it will return true
257      * for a default mapping.
258      */
259     private boolean isPathMapping(String mapping) {
260         return mapping.startsWith("/") && mapping.endsWith("/*");
261     }
262 
263     private boolean isExtensionMapping(String mapping) {
264         return mapping.startsWith("*.");
265     }
266 
267     private boolean isDefaultMapping(String mapping) {
268         // TODO : default mapping per spec is "/" - do we really want to support this? is there a point ?
269         return mapping.equals("/");
270     }
271 
272     private boolean isRegexpMapping(String mapping) {
273         return mapping.startsWith("regex:");
274     }
275 
276     public Map getParameters() {
277         return parameters;
278     }
279 
280     public void setParameters(Map parameters) {
281         this.parameters = parameters;
282     }
283 
284     public String getComment() {
285         return comment;
286     }
287 
288     public void setComment(String comment) {
289         this.comment = comment;
290     }
291 
292     private final static class WrappedServletConfig implements ServletConfig {
293 
294         private final String servletName;
295 
296         private final FilterConfig filterConfig;
297 
298         private final Map parameters;
299 
300         public WrappedServletConfig(String servletName, FilterConfig filterConfig, Map parameters) {
301             this.servletName = servletName;
302             this.filterConfig = filterConfig;
303             this.parameters = parameters;
304         }
305 
306         public String getInitParameter(String name) {
307             return (String) parameters.get(name);
308         }
309 
310         public Enumeration getInitParameterNames() {
311             return new Enumeration() {
312 
313                 private Iterator iter = parameters.keySet().iterator();
314 
315                 public boolean hasMoreElements() {
316                     return iter.hasNext();
317                 }
318 
319                 public Object nextElement() {
320                     return iter.next();
321                 }
322             };
323         }
324 
325         public ServletContext getServletContext() {
326             return filterConfig.getServletContext();
327         }
328 
329         public String getServletName() {
330             return servletName;
331         }
332 
333     }
334 
335     private class WrappedRequest extends HttpServletRequestWrapper {
336 
337         private Matcher matcher;
338 
339         /**
340          * This is set to true when the original request object passed
341          * in is changed by the setRequest() method.  This can indicate
342          * that a forward occurred and the values pulled from the
343          * matcher should no longer be used.
344          */
345         private boolean requestReplaced = false;
346 
347         /**
348          * The given Matcher should be built from a Pattern containing two groups:
349          * (1) servletPath (2) ignored (3) pathInfo
350          */
351         public WrappedRequest(HttpServletRequest request, Matcher matcher) {
352             super(request);
353             this.matcher = matcher;
354         }
355 
356         public String getPathInfo() {
357             if (requestReplaced) {
358                 return super.getPathInfo();
359             }
360             if (matcher.groupCount() > 2) {
361                 String pathInfo = matcher.group(3);
362                 if (pathInfo.equals("")) {
363                     return null;
364                 }
365                 // according to the servlet spec the pathInfo should contain a leading slash
366                 return (pathInfo.startsWith("/") ? pathInfo : "/" + pathInfo);
367             }
368             return null;
369         }
370 
371         public String getServletPath() {
372             if (requestReplaced) {
373                 return super.getServletPath();
374             }
375             return matcher.group(1);
376         }
377 
378         public void setRequest(ServletRequest request) {
379             requestReplaced = true;
380             super.setRequest(request);
381         }
382 
383     }
384 }