View Javadoc
1   /**
2    * This file Copyright (c) 2003-2018 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.CustomServletConfig;
37  import info.magnolia.objectfactory.ComponentProvider;
38  
39  import java.io.IOException;
40  import java.util.Collection;
41  import java.util.Map;
42  import java.util.regex.Matcher;
43  import java.util.regex.Pattern;
44  
45  import javax.inject.Inject;
46  import javax.servlet.FilterChain;
47  import javax.servlet.FilterConfig;
48  import javax.servlet.Servlet;
49  import javax.servlet.ServletException;
50  import javax.servlet.http.HttpServletRequest;
51  import javax.servlet.http.HttpServletRequestWrapper;
52  import javax.servlet.http.HttpServletResponse;
53  
54  import org.apache.commons.lang3.StringUtils;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import com.google.common.collect.ImmutableList;
59  
60  /**
61   * A filter that dispatches requests to a wrapped servlet.
62   *
63   * TODO : cache matching URIs ?
64   */
65  public class ServletDispatchingFilter extends AbstractMgnlFilter {
66  
67      private static final Logger log = LoggerFactory.getLogger(ServletDispatchingFilter.class);
68  
69      private final ComponentProvider componentProvider;
70  
71      private final Mapping servletMapping;
72  
73      private String servletName;
74  
75      private Class<? extends Servlet> servletClass;
76  
77      private Map parameters;
78  
79      private String comment;
80  
81      private Servlet servlet;
82  
83      @Inject
84      public ServletDispatchingFilter(ComponentProvider componentProvider) {
85          this.componentProvider = componentProvider;
86          this.servletMapping = new ServletMapping();
87      }
88  
89      @Override
90      public String getName() {
91          return "Wrapper for " + servletName + " servlet";
92      }
93  
94      /**
95       * Initializes the servlet and its mappings. ServletConfig is wrapped to take init parameters into account.
96       */
97      @Override
98      public void init(final FilterConfig filterConfig) throws ServletException {
99          super.init(filterConfig);
100 
101         if (getServletClass() != null) {
102             try {
103                 initializeServlet(filterConfig);
104             } catch (Throwable e) {
105                 log.error("Unable to load servlet {} : {}", getServletClass(), e.getMessage(), e);
106             }
107         }
108     }
109 
110     protected void initializeServlet(FilterConfig filterConfig) throws ClassNotFoundException, ServletException {
111         servlet = newServletInstance();
112         servlet.init(new CustomServletConfig(servletName, filterConfig.getServletContext(), parameters));
113     }
114 
115     protected Servlet newServletInstance() throws ClassNotFoundException {
116         return componentProvider.newInstance(getServletClass());
117     }
118 
119     protected Servlet getServlet() {
120         return servlet;
121     }
122 
123     @Override
124     protected Mapping getMapping() {
125         return servletMapping;
126     }
127 
128     /**
129      * Delegates the destroy() call to the wrapper servlet, then to this filter itself.
130      */
131     @Override
132     public void destroy() {
133         if (servlet != null) {
134             servlet.destroy();
135         }
136         super.destroy();
137     }
138 
139     /**
140      * Dispatches the request to the servlet if not already bypassed. The request is wrapped for properly setting the
141      * pathInfo.
142      */
143     @Override
144     public void doFilter(final HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
145         if (!getMapping().getMappings().isEmpty()) {
146             log.debug("Dispatching to servlet {}", getServletClass());
147             final Matcher matcher = getMapping().match(request).getMatcher();
148             servlet.service(new WrappedRequest(request, matcher), response);
149         } else {
150             log.debug("No mappings found to {}", getServletClass());
151             setEnabled(false);
152         }
153     }
154 
155     public String getServletName() {
156         return servletName;
157     }
158 
159     public void setServletName(String servletName) {
160         this.servletName = servletName;
161     }
162 
163     public Class<? extends Servlet> getServletClass() {
164         return servletClass;
165     }
166 
167     public void setServletClass(Class<? extends Servlet> servletClass) {
168         this.servletClass = servletClass;
169     }
170 
171     public Map getParameters() {
172         return parameters;
173     }
174 
175     public void setParameters(Map parameters) {
176         this.parameters = parameters;
177     }
178 
179     public String getComment() {
180         return comment;
181     }
182 
183     public void setComment(String comment) {
184         this.comment = comment;
185     }
186 
187     /**
188      * Request wrapper that overrides servletPath and pathInfo with new values. If any of the path elements changes in
189      * a wrapper behind it then it returns them instead of the overridden values. This happens on forwards. It's
190      * necessary to check all four since they act as a group, if any of them changes we cannot override any of them.
191      */
192     private static class WrappedRequest extends HttpServletRequestWrapper {
193 
194         private String originalRequestUri;
195         private String originalServletPath;
196         private String originalPathInfo;
197         private String originalQueryString;
198 
199         private String newServletPath;
200         private String newPathInfo;
201 
202         /**
203          * The given Matcher should be built from a Pattern containing two groups:
204          * (1) servletPath (2) ignored (3) pathInfo.
205          */
206         public WrappedRequest(HttpServletRequest request, Matcher matcher) {
207             super(request);
208 
209             this.originalRequestUri = request.getRequestURI();
210             this.originalServletPath = request.getServletPath();
211             this.originalPathInfo = request.getPathInfo();
212             this.originalQueryString = request.getQueryString();
213 
214             this.newServletPath = matcher.group(1);
215             if (matcher.groupCount() > 2) {
216                 String pathInfo = matcher.group(3);
217                 // pathInfo should be null when empty
218                 if (!pathInfo.equals("")) {
219                     // according to the servlet spec the pathInfo should contain a leading slash
220                     this.newPathInfo = (pathInfo.startsWith("/") ? pathInfo : "/" + pathInfo);
221                 }
222             }
223         }
224 
225         @Override
226         public String getPathInfo() {
227             String current = super.getPathInfo();
228             if (!StringUtils.equals(super.getRequestURI(), originalRequestUri)) {
229                 return current;
230             }
231             if (!StringUtils.equals(super.getServletPath(), originalServletPath)) {
232                 return current;
233             }
234             if (!StringUtils.equals(current, originalPathInfo)) {
235                 return current;
236             }
237             if (!StringUtils.equals(super.getQueryString(), originalQueryString)) {
238                 return current;
239             }
240             return newPathInfo;
241         }
242 
243         @Override
244         public String getServletPath() {
245             String current = super.getServletPath();
246             if (!StringUtils.equals(super.getRequestURI(), originalRequestUri)) {
247                 return current;
248             }
249             if (!StringUtils.equals(current, originalServletPath)) {
250                 return current;
251             }
252             if (!StringUtils.equals(super.getPathInfo(), originalPathInfo)) {
253                 return current;
254             }
255             if (!StringUtils.equals(super.getQueryString(), originalQueryString)) {
256                 return current;
257             }
258             return newServletPath;
259         }
260     }
261 
262     /**
263      * A subclass of Mapping which allows adding *one* specific mapping, in this case provided by the Servlet,
264      * if it implements {@link SelfMappingServlet}.
265      */
266     private class ServletMapping extends Mapping {
267         private Collection<Pattern> mergedMappings;
268         private String lastUsedSelfMapping;
269 
270         /**
271          * Returns mapping that potentially include an entry for the servlet's self mapping.
272          * Does some preemptive optimization to avoid re-instantiating the collection at every call,
273          * while still getting the "latest" self mapping (i.e allows the servlet to delegate that call
274          * to some dynamic component)
275          */
276         @Override
277         public Collection<Pattern> getMappings() {
278             final Collection<Pattern> configuredMappings = super.getMappings();
279             if (!(getServlet() instanceof SelfMappingServlet)) {
280                 return configuredMappings;
281             }
282 
283             final String selfMapping = ((SelfMappingServlet) getServlet()).getSelfMappingPath();
284             if (selfMapping == null) {
285                 return configuredMappings;
286             }
287 
288             if (lastUsedSelfMapping == null || !lastUsedSelfMapping.equals(selfMapping)) {
289                 final Pattern selfMappingPattern = servletMappingToRegex(selfMapping);
290                 // Using ImmutableList and its builder - which may or may not optimize this, and will avoid leaking write operations in the list.
291                 mergedMappings = ImmutableList.<Pattern>builder().add(selfMappingPattern).addAll(configuredMappings).build();
292                 lastUsedSelfMapping = selfMapping;
293             }
294             return mergedMappings;
295         }
296 
297 
298     }
299 }