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