View Javadoc
1   /**
2    * This file Copyright (c) 2003-2015 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.servlets;
35  
36  import info.magnolia.cms.util.ClasspathResourcesUtil;
37  
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.net.URL;
41  import java.net.URLConnection;
42  import java.util.Hashtable;
43  import java.util.Map;
44  
45  import javax.servlet.ServletException;
46  import javax.servlet.ServletOutputStream;
47  import javax.servlet.http.HttpServlet;
48  import javax.servlet.http.HttpServletRequest;
49  import javax.servlet.http.HttpServletResponse;
50  
51  import org.apache.commons.io.IOUtils;
52  import org.apache.commons.lang.StringUtils;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  
57  /**
58   * A simple spool servlet that load resources from the classpath. Resources folder is configurable via the servlet <code>resourcesRoot</code> init parameter.
59   * If none is provided, it defaults to <code>/mgnl-resources</code>. Files in this folder will be loaded by this servlet (corresponding to the configured mapped url,
60   * e.g. <code>/.resources/*</code> or <code>/VAADIN/*</code>, etc. This servlet should be used for authoring-only resources, like rich editor images and
61   * scripts. It's not suggested for public website resources. Content length and last modification date are not set on
62   * files returned from the classpath.
63   */
64  public class ClasspathSpool extends HttpServlet {
65  
66      /**
67       * Default root directory for resources streamed from the classpath. Resources folder is configurable via the servlet <code>resourcesRoot</code> init parameter.
68       */
69      public static final String MGNL_DEFAULT_RESOURCES_ROOT = "/mgnl-resources";
70  
71      private final static Logger log = LoggerFactory.getLogger(ClasspathSpool.class);
72  
73      private String resourcesRoot;
74  
75      @Override
76      protected long getLastModified(HttpServletRequest req) {
77          String filePath = this.getFilePath(req);
78          try {
79              URL url = ClasspathResourcesUtil.getResource(resourcesRoot + filePath);
80              if(url != null){
81                  URLConnection connection = url.openConnection();
82  
83                  connection.setDoInput(false);
84                  connection.setDoOutput(false);
85  
86                  long lastModified = connection.getLastModified();
87                  InputStream is = null;
88                  try{
89                      is = connection.getInputStream();
90                  }
91                  finally{
92                      IOUtils.closeQuietly(is);
93                  }
94                  return lastModified;
95              }
96          }
97          catch (IOException e) {
98              // just ignore
99          }
100 
101         return -1;
102     }
103 
104     /**
105      * All static resource requests are handled here.
106      * @throws IOException for error in accessing the resource or the servlet output stream
107      */
108     @Override
109     public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
110 
111         String filePath = getFilePath(request);
112 
113         if (StringUtils.contains(filePath, "*")) {
114             streamMultipleFile(response, filePath);
115         }
116         else if (StringUtils.contains(filePath, "|")) {
117             String[] paths = StringUtils.split(filePath, "|");
118             streamMultipleFile(response, paths);
119         }
120         else {
121             streamSingleFile(response, filePath);
122         }
123     }
124 
125     protected String getFilePath(HttpServletRequest request) {
126         // handle includes
127         String filePath = (String) request.getAttribute("javax.servlet.include.path_info");
128 
129         // handle forwards
130         if (StringUtils.isEmpty(filePath)) {
131             filePath = (String) request.getAttribute("javax.servlet.forward.path_info");
132         }
133 
134         // standard request
135         if (StringUtils.isEmpty(filePath)) {
136             filePath = request.getPathInfo();
137         }
138         return filePath;
139     }
140 
141     private Map<String, String[]> multipleFilePathsCache;
142 
143     /**
144      * @see javax.servlet.GenericServlet#init()
145      */
146     @Override
147     public void init() throws ServletException {
148         super.init();
149         multipleFilePathsCache = new Hashtable<String, String[]>();
150         resourcesRoot = StringUtils.defaultIfEmpty(getInitParameter("resourcesRoot"), MGNL_DEFAULT_RESOURCES_ROOT);
151         //test if the folder is really there, else log warning and fall back to default.
152         URL url = ClasspathResourcesUtil.getResource(resourcesRoot);
153         log.debug("resources root is {}", resourcesRoot);
154         if(url == null) {
155             log.warn("Resource classpath root {} does not seem to exist. Some resources might not be available, please check your configuration. Falling back to default resources root {}", resourcesRoot, MGNL_DEFAULT_RESOURCES_ROOT);
156             // in case of misconfiguration, this should mitigate the risk of ending up with an unusable Magnolia instance.
157             resourcesRoot = MGNL_DEFAULT_RESOURCES_ROOT;
158         }
159     }
160 
161     /**
162      * @see javax.servlet.GenericServlet#destroy()
163      */
164     @Override
165     public void destroy() {
166         super.destroy();
167         multipleFilePathsCache.clear();
168     }
169 
170     /**
171      * Join and stream multiple files, using a "regexp-like" pattern. Only a single "*" is allowed as keyword in the
172      * request URI.
173      */
174     private void streamMultipleFile(HttpServletResponse response, String filePath) throws IOException {
175         log.debug("aggregating files for request {}", filePath);
176 
177         String[] paths = multipleFilePathsCache.get(filePath);
178         if (paths == null) {
179             final String startsWith = resourcesRoot + StringUtils.substringBefore(filePath, "*");
180             final String endsWith = StringUtils.substringAfterLast(filePath, "*");
181 
182             paths = ClasspathResourcesUtil.findResources(new ClasspathResourcesUtil.Filter() {
183 
184                 @Override
185                 public boolean accept(String name) {
186                     return name.startsWith(startsWith) && name.endsWith(endsWith);
187                 }
188             });
189         }
190         multipleFilePathsCache.put(filePath, paths);
191 
192         if (paths.length == 0) {
193             response.sendError(HttpServletResponse.SC_NOT_FOUND);
194             return;
195         }
196 
197         streamMultipleFile(response, paths);
198     }
199 
200     private void streamMultipleFile(HttpServletResponse response, String[] paths) throws IOException {
201         ServletOutputStream out = response.getOutputStream();
202         InputStream in = null;
203 
204         for (String path : paths) {
205             try {
206                 if (!path.startsWith(resourcesRoot)) {
207                     path = resourcesRoot + path;
208                 }
209                 in = ClasspathResourcesUtil.getStream(path);
210                 if (in != null) {
211                     IOUtils.copy(in, out);
212                 }
213             }
214             finally {
215                 IOUtils.closeQuietly(in);
216             }
217         }
218 
219         out.flush();
220         IOUtils.closeQuietly(out);
221     }
222 
223     private void streamSingleFile(HttpServletResponse response, String filePath) throws IOException {
224         InputStream in = null;
225         // this method caches content if possible and checks the magnolia.develop property to avoid
226         // caching during the development process
227         try {
228             in = ClasspathResourcesUtil.getStream(resourcesRoot + filePath);
229         }
230         catch (IOException e) {
231             IOUtils.closeQuietly(in);
232         }
233 
234         if (in == null) {
235             if (!response.isCommitted()) {
236                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
237             }
238             return;
239         }
240 
241         ServletOutputStream out = null;
242         try {
243             out = response.getOutputStream();
244             IOUtils.copy(in, out);
245             out.flush();
246         }
247         catch (IOException e) {
248             // only log at debug level
249             // tomcat usually throws a ClientAbortException anytime the user stop loading the page
250             log.debug("Unable to spool resource due to a {} exception", e.getClass().getName());
251             if (!response.isCommitted()) {
252                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
253             }
254         }
255         finally {
256             IOUtils.closeQuietly(out);
257             IOUtils.closeQuietly(in);
258         }
259     }
260 
261 }