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.rendering.engine;
35  
36  import info.magnolia.cms.core.AggregationState;
37  import info.magnolia.cms.filters.AbstractMgnlFilter;
38  import info.magnolia.context.MgnlContext;
39  import info.magnolia.jcr.wrapper.ChannelVisibilityContentDecorator;
40  import info.magnolia.objectfactory.Components;
41  import info.magnolia.registry.RegistrationException;
42  import info.magnolia.rendering.listeners.AbstractRenderingListener.RenderingListenerReturnCode;
43  import info.magnolia.rendering.template.TemplateDefinition;
44  import info.magnolia.rendering.template.registry.TemplateDefinitionRegistry;
45  
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.util.Collection;
49  import java.util.Collections;
50  
51  import javax.jcr.Node;
52  import javax.jcr.Property;
53  import javax.jcr.RepositoryException;
54  import javax.jcr.Session;
55  import javax.servlet.FilterChain;
56  import javax.servlet.ServletException;
57  import javax.servlet.ServletOutputStream;
58  import javax.servlet.http.HttpServletRequest;
59  import javax.servlet.http.HttpServletResponse;
60  
61  import org.apache.commons.io.IOUtils;
62  import org.apache.commons.lang3.StringUtils;
63  import org.apache.commons.lang3.math.NumberUtils;
64  import org.apache.jackrabbit.JcrConstants;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  /**
69   * Filter responsible for rendering the current aggregation state,
70   * by delegating to the appropriate TemplateRenderer or by serving
71   * binary content.
72   */
73  public class RenderingFilter extends AbstractMgnlFilter {
74  
75      private static final Logger log = LoggerFactory.getLogger(RenderingFilter.class);
76  
77      private final RenderingEngine renderingEngine;
78  
79      private final TemplateDefinitionRegistry templateDefinitionRegistry;
80  
81      // backward compatible by default
82      private boolean terminateChain = true;
83  
84      public RenderingFilter(RenderingEngine renderingEngine, TemplateDefinitionRegistry templateDefinitionRegistry) {
85          this.renderingEngine = renderingEngine;
86          this.templateDefinitionRegistry = templateDefinitionRegistry;
87      }
88  
89      @Override
90      public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
91          final AggregationState aggregationState = MgnlContext.getAggregationState();
92  
93          // render using template
94          boolean rendered = handleTemplateRequest(aggregationState, request, response);
95          // not rendered, try direct request
96          if (!rendered) {
97              rendered = handleResourceRequest(aggregationState, request, response);
98          }
99  
100         if (!rendered) {
101 
102             if (isTerminateChain()) {
103                 if (!response.isCommitted()) {
104                     response.sendError(HttpServletResponse.SC_NOT_FOUND);
105                 } else {
106                     log.info("Unable to redirect to 404 page for {}, response is already committed", request.getRequestURI());
107                 }
108             } else {
109                 log.debug("Resource {} not found, giving over to next filter. You might want to consider configuring bypass for this URI to speed up the processing.", request.getRequestURI());
110                 chain.doFilter(request, response);
111             }
112         }
113     }
114 
115     protected boolean handleTemplateRequest(AggregationState aggregationState, HttpServletRequest request, HttpServletResponse response) throws ServletException {
116         String templateName = aggregationState.getTemplateName();
117         if (StringUtils.isEmpty(templateName)) {
118             return false;
119         }
120         try {
121             // don't reset any existing status code, see MAGNOLIA-2005
122             // response.setStatus(HttpServletResponse.SC_OK);
123             if (response != MgnlContext.getWebContext().getResponse()) {
124                 log.warn("Context response not synced. This may lead to discrepancies in rendering.");
125             }
126 
127             Node content = aggregationState.getMainContentNode();
128 
129             // if the content isn't visible output a 404
130             if (!isVisible(content, request, response, aggregationState)) {
131                 if (!response.isCommitted()) {
132                     response.sendError(HttpServletResponse.SC_NOT_FOUND);
133                     log.info("The requested resource ('{}') is not available on channel '{}'", aggregationState.getCurrentURI(), aggregationState.getChannel().getName());
134                 } else {
135                     log.info("Unable to redirect to 404 page for {}, response is already committed", request.getRequestURI());
136                 }
137                 return true;
138             }
139 
140             if (!render(content, templateName, response)) {
141                 return false;
142             }
143 
144             try {
145                 response.flushBuffer();
146             } catch (IOException e) {
147                 // don't log at error level since tomcat typically throws a
148                 // org.apache.catalina.connector.ClientAbortException if the user stops loading the page
149                 log.debug("Exception flushing response {}: {}", e.getClass().getName(), e.getMessage(), e);
150             }
151         } catch (RenderException e) {
152             // TODO better handling of rendering exception
153             // TODO dlipp: why not move this section up to the actual call to render() -> that's the only place where a RenderException could occur...
154             log.error(e.getMessage(), e);
155             throw new ServletException(e);
156         } catch (Exception e) {
157             // TODO dlipp: there's no other checked exceptions thrown in the code above - is it correct to react like that???
158             log.error(e.getMessage(), e);
159             if (!response.isCommitted()) {
160                 response.setContentType("text/html");
161             }
162             throw new RuntimeException(e);
163         }
164         return true;
165     }
166 
167     protected boolean isVisible(Node content, HttpServletRequest request, HttpServletResponse response, AggregationState aggregationState) {
168 
169         // if there's a channel set test if the content is excluded for the current channel
170         if (aggregationState.getChannel() != null) {
171             String currentChannel = aggregationState.getChannel().getName();
172             if (StringUtils.isNotEmpty(currentChannel) && !currentChannel.equalsIgnoreCase("all")) {
173                 ChannelVisibilityContentDecorator decorator = new ChannelVisibilityContentDecorator(currentChannel);
174                 return decorator.evaluateNode(content);
175             }
176         }
177 
178         return true;
179     }
180 
181     protected boolean render(Node content, String templateName, HttpServletResponse response) throws RenderException {
182 
183         TemplateDefinition templateDefinition;
184         try {
185             templateDefinition = templateDefinitionRegistry.getTemplateDefinition(templateName);
186         } catch (RegistrationException e) {
187             throw new RenderException(e);
188         }
189 
190         OutputProvider out = Components.newInstance(OutputProvider.class, response);
191         Collection<RenderingListenerReturnCode> listenerResults = renderingEngine.initListeners(out, response);
192         if (listenerResults.contains(RenderingListenerReturnCode.NOT_FOUND)) {
193             return false; // this will result into 404 (e.g. request for direct area rendering of a nonexisting area)
194         }
195         renderingEngine.render(content, templateDefinition, Collections.<String, Object>emptyMap(), out);
196         return true;
197     }
198 
199     /**
200      * Get the requested resource and copy it to the ServletOutputStream, bit by bit.
201      *
202      * @param request HttpServletRequest as given by the servlet container
203      * @param response HttpServletResponse as given by the servlet container
204      * @throws IOException standard servlet exception
205      */
206     protected boolean handleResourceRequest(AggregationState aggregationState, HttpServletRequest request, HttpServletResponse response) throws IOException {
207 
208         final String resourceHandle = aggregationState.getHandle();
209 
210         log.debug("handleResourceRequest, resourceHandle=\"{}\"", resourceHandle);
211 
212         if (StringUtils.isNotEmpty(resourceHandle)) {
213             InputStream is = null;
214             try {
215                 Session session = MgnlContext.getJCRSession(aggregationState.getRepository());
216                 is = getNodedataAsStream(resourceHandle, session, response);
217                 if (null != is) {
218                     // don't reset any existing status code, see MAGNOLIA-2005
219                     // response.setStatus(HttpServletResponse.SC_OK);
220                     sendUnCompressed(is, response);
221                     IOUtils.closeQuietly(is);
222                 } else {
223                     return false;
224                 }
225             } catch (IOException e) {
226                 // don't log at error level since tomcat typically throws a
227                 // org.apache.catalina.connector.ClientAbortException if the user stops loading the page
228                 log.debug("Exception while dispatching resource {}: {}", e.getClass().getName(), e.getMessage(), e);
229             } catch (Exception e) {
230                 log.error("Exception while dispatching resource  {}: {}", e.getClass().getName(), e.getMessage(), e);
231             } finally {
232                 IOUtils.closeQuietly(is);
233             }
234             return true;
235         }
236         return false;
237     }
238 
239     /**
240      * Send data as is.
241      *
242      * @param is Input stream for the resource
243      * @param response HttpServletResponse as received by the service method
244      * @throws IOException standard servlet exception
245      */
246     private void sendUnCompressed(InputStream is, HttpServletResponse response) throws IOException {
247         ServletOutputStream os = response.getOutputStream();
248         byte[] buffer = new byte[8192];
249         int read;
250         while ((read = is.read(buffer)) > 0) {
251             os.write(buffer, 0, read);
252         }
253         os.flush();
254         IOUtils.closeQuietly(os);
255     }
256 
257     private InputStream getNodedataAsStream(String path, Session session, HttpServletResponse res) {
258 
259         log.debug("getNodedataAstream for path \"{}\"", path);
260 
261         try {
262             Node atom = session.getNode(path);
263             if (atom != null) {
264                 if (atom.hasProperty(JcrConstants.JCR_DATA)) {
265                     String sizeString = atom.hasProperty("size") ? atom.getProperty("size").getString() : "";
266                     if (NumberUtils.isNumber(sizeString)) {
267                         res.setContentLength(Integer.parseInt(sizeString));
268                     }
269                     Property streamProperty = atom.getProperty(JcrConstants.JCR_DATA);
270                     return streamProperty.getStream();
271                 }
272             }
273 
274             log.warn("Resource not found: [{}?{}]", MgnlContext.getWebContext().getRequest().getRequestURI(), MgnlContext.getWebContext().getRequest().getQueryString());
275         } catch (RepositoryException e) {
276             log.error("RepositoryException while reading Resource [{}]", path, e);
277         }
278         return null;
279     }
280 
281     public boolean isTerminateChain() {
282         return terminateChain;
283     }
284 
285     public void setTerminateChain(boolean terminateChain) {
286         this.terminateChain = terminateChain;
287     }
288 
289 }