View Javadoc
1   /**
2    * This file Copyright (c) 2013-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.dam.core.download;
35  
36  import info.magnolia.cms.filters.SelfMappingServlet;
37  import info.magnolia.cms.util.LinkUtil;
38  import info.magnolia.dam.api.Asset;
39  import info.magnolia.dam.api.AssetProvider;
40  import info.magnolia.dam.api.AssetProviderRegistry;
41  import info.magnolia.dam.api.AssetProviderRegistry.NoSuchAssetProviderException;
42  import info.magnolia.dam.api.ItemKey;
43  import info.magnolia.dam.api.PathAwareAssetProvider;
44  import info.magnolia.dam.api.PathAwareAssetProvider.PathNotFoundException;
45  import info.magnolia.dam.core.config.DamCoreConfiguration;
46  
47  import java.io.IOException;
48  import java.io.InputStream;
49  import java.net.SocketException;
50  import java.util.ArrayList;
51  import java.util.Enumeration;
52  import java.util.List;
53  
54  import javax.servlet.ServletException;
55  import javax.servlet.ServletOutputStream;
56  import javax.servlet.http.HttpServlet;
57  import javax.servlet.http.HttpServletRequest;
58  import javax.servlet.http.HttpServletResponse;
59  
60  import org.apache.commons.io.IOUtils;
61  import org.apache.commons.lang3.StringUtils;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  import com.google.inject.Inject;
66  
67  /**
68   * Servlet to handle dam document downloads.<br>
69   * This servlet support the following request: <br>
70   * support the following link patterns:<br>
71   * /dam/jcr:7ecd4045-45a0-4c81-b2b6-f4c4b0cd24ad/<whatever, this is ignored anyway><br>
72   * /dam/static:7ecd4045-45a0-4c81-b2b6-f4c4b0cd24ad (compatible with dms)<br>
73   * /dam/foo/bar/lol.pdf (compatible with dam 1.x)<br>
74   * /dam/jcr/foo/bar/lol.pdf (so /dam/<provider-id>/<path>.ext: we ignore .ext and ensure that the corresponding provider is a PathAwareProvider<br>
75   */
76  public class DamDownloadServlet extends HttpServlet implements SelfMappingServlet {
77      private static final Logger log = LoggerFactory.getLogger(DamDownloadServlet.class);
78  
79      static final String CONTENT_DISPOSITION = "Content-Disposition";
80      static final String LAST_MODIFIED = "Last-Modified";
81  
82      /**
83       * Name of the default provider.
84       */
85      public static final String FALL_BACK_PROVIDER_ID = "jcr";
86  
87      private final AssetProviderRegistry assetProviderRegistry;
88      private final DamCoreConfiguration configuration;
89  
90      @Inject
91      public DamDownloadServlet(final DamCoreConfiguration configuration, final AssetProviderRegistry assetProviderRegistry) {
92          this.configuration = configuration;
93          this.assetProviderRegistry = assetProviderRegistry;
94      }
95  
96      @Override
97      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
98          doGet(request, response);
99      }
100 
101     @Override
102     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
103         try {
104             process(request, response);
105         } catch (Exception e) {
106             log.error("error during download", e);
107         }
108     }
109 
110     /**
111      * The main task is to retrieve an Asset Object based on the <br>
112      * current Uri. Basically, if no Asset could be found based on the current
113      * Uri, send an error to the response. <br>
114      * If an Asset is found, set the response header and put the Asset Binary in
115      * the response.
116      */
117     protected void process(HttpServletRequest request, HttpServletResponse response) throws Exception {
118         // Get Asset
119         Asset asset = getAsset(request);
120 
121         if (asset == null) {
122             response.sendError(HttpServletResponse.SC_NOT_FOUND);
123             return;
124         }
125         handleResourceRequest(request, response, asset);
126     }
127 
128     @Override
129     public String getSelfMappingPath() {
130         return configuration.getDownloadPath() + "/*";
131     }
132 
133     /**
134      * Retrieve the {@link Asset} related to the information contained into the pathInfo.
135      * 
136      * @return found {@link Asset}, null otherwise.
137      */
138     protected Asset getAsset(HttpServletRequest request) {
139         Asset asset = null;
140         String pathInfo = request.getPathInfo();
141         try {
142             // Try to handle Item keys based on UUID
143             asset = getAssetBasedOnIdentifier(pathInfo, request.getServletPath());
144             // Try to handle Item based on path
145             if (asset == null) {
146                 asset = getAssetBasedOnPath(pathInfo);
147             }
148         } catch (Exception e) {
149             log.warn("Could not retrieve an asset based on the following pathInfo {}", pathInfo, e);
150         }
151         return asset;
152     }
153 
154     void handleResourceRequest(HttpServletRequest req, HttpServletResponse res, Asset asset) throws Exception {
155         InputStream is = asset.getContentStream();
156         if (is == null) {
157             res.sendError(HttpServletResponse.SC_NOT_FOUND);
158             return;
159         }
160 
161         // Set the mime type to enable voting by contentType (ResponseContentTypeVoter).
162         res.setContentType(asset.getMimeType());
163 
164         // set content disposition
165         if (configuration.getContentDisposition() == null || configuration.getContentDisposition().vote(res) > 0) {
166             res.setHeader(CONTENT_DISPOSITION, "attachment; filename=\"" + asset.getFileName() + "\"");
167         }
168 
169         // unset the Mime Type
170         if (!configuration.isEnforceDocumentMimeType()) {
171             res.setContentType(null);
172         }
173 
174         // set length
175         res.setContentLength((int) asset.getFileSize());
176         // set last update
177         if (asset.getLastModified() != null) {
178             res.setDateHeader(LAST_MODIFIED, asset.getLastModified().getTimeInMillis());
179         }
180         // TODO ehechinger: always send as is, find better way to discover if resource could be compressed
181         try {
182             sendUnCompressed(is, res);
183         } catch (IOException e) {
184             if (log.isDebugEnabled()) {
185                 log.error(
186                         "Download of document [" + asset.getName() + "] with headers " + getHeader(req) + " was interrupted due: " + (e.getMessage() == null ? e.getClass().getName() : e.getMessage())
187                                 + ":" + (e.getCause() == null ? "" : e.getCause().toString()) + ".", e);
188             } else {
189                 // Tomcat tends to wrap the error in ClientAbortException with
190                 // null message ... other servers don't
191                 log.info("Download of document [{}]was interrupted due: {}:{}. To see more details change logging level to debug.", asset.getName(),
192                         e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e.getCause() == null ? "" : e.getCause().toString());
193             }
194         } finally {
195             IOUtils.closeQuietly(is);
196         }
197     }
198 
199     private List<String> getHeader(HttpServletRequest req) {
200         List<String> out = new ArrayList<String>();
201         @SuppressWarnings("unchecked")
202         Enumeration<String> names = req.getHeaderNames();
203         while (names.hasMoreElements()) {
204             String name = names.nextElement();
205             out.add(name + "=" + req.getHeader(name));
206         }
207         return out;
208     }
209 
210     private void sendUnCompressed(java.io.InputStream is, HttpServletResponse res) throws IOException {
211         ServletOutputStream os = res.getOutputStream();
212         try {
213             IOUtils.copyLarge(is, os);
214         } catch (SocketException e) {
215             if (log.isDebugEnabled()) {
216                 log.error(e.getMessage(), e);
217             } else {
218                 log.info("Detected client abort of download. For more details increase logging level to debug.");
219             }
220         } finally {
221             IOUtils.closeQuietly(os);
222         }
223     }
224 
225     /**
226      * Handle pathInfo containing identifier. The following cases are handled:<br>
227      * - /dam/jcr:7ecd4045-45a0-4c81-b2b6-f4c4b0cd24ad/<whatever...<br>
228      * - /dam/static:7ecd4045-45a0-4c81-b2b6-f4c4b0cd24ad (compatible with dms)<br>
229      * 
230      * @return the found {@link Asset}, null otherwise.
231      */
232     private Asset getAssetBasedOnIdentifier(String pathInfo, String mapping) {
233         // extract all between the first and the second '/'
234         String keyStr = pathInfo.split("/")[1];
235         // handling of: /dam/static:7ecd4045-45a0-4c81-b2b6-f4c4b0cd24ad (compatible with dms)
236         if (mapping.contains("static") && !ItemKey.isValid(keyStr)) {
237             keyStr = FALL_BACK_PROVIDER_ID + ":" + keyStr;
238         }
239         // handling of /dam/jcr:7ecd4045-45a0-4c81-b2b6-f4c4b0cd24ad/<whatever...
240         if (ItemKey.isValid(keyStr)) {
241             final ItemKey itemKey = ItemKey.from(keyStr);
242             return assetProviderRegistry.getProviderFor(itemKey).getAsset(itemKey);
243         }
244         return null;
245     }
246 
247     /**
248      * Handle pathInfo containing absolute path to dam Assets. The following cases are handled:<br>
249      * - /dam/foo/bar/lol.pdf (compatible with dam 1.x).<br>
250      * - /dam/jcr/foo/bar/lol.pdf (so /dam/<provider-id>/<path><br>
251      * 
252      * @return the found {@link Asset}, null otherwise.
253      */
254     private Asset getAssetBasedOnPath(String pathInfo) {
255         AssetProvider assetProvider = null;
256         // extract all between the first and the second '/'
257         String keyStr = pathInfo.split("/")[1];
258         String path = pathInfo;
259         // Check if the keyStr refers to a provider ID
260         try {
261             // handling of: /dam/jcr/foo/bar/lol.pdf (so /dam/<provider-id>/<path>.ext
262             assetProvider = assetProviderRegistry.getProviderById(keyStr);
263             path = path.replaceFirst("/" + assetProvider.getIdentifier() + "/", "/");
264         } catch (NoSuchAssetProviderException nsape) {
265             // handling of: /dam/foo/bar/lol.pdf (compatible with dam 1.x)
266             assetProvider = assetProviderRegistry.getProviderById(FALL_BACK_PROVIDER_ID);
267         }
268         // ensure that the provider is an instance of PathAwareAssetProvider
269         if (assetProvider instanceof PathAwareAssetProvider) {
270             // remove the extension (.pdf) and cache fingerPrint if present
271             // (print-logo.2012-11-20-12-15-20.pdf --> print-logo)
272             String extension = StringUtils.substringAfterLast(path, ".");
273             String assetPath = LinkUtil.removeFingerprintAndExtensionFromLink(path);
274             // try to retrieve without extension
275             try {
276                 return ((PathAwareAssetProvider) assetProvider).getAsset(assetPath);
277             } catch (PathNotFoundException e) {
278                 // Do nothing, try to search with extension
279             }
280             // try with the extension
281             try {
282                 return ((PathAwareAssetProvider) assetProvider).getAsset(assetPath + "." + extension);
283             } catch (PathNotFoundException e) {
284                 log.warn("No asset could be found for the following path {}", assetPath);
285             }
286         }
287         log.warn("Provider {} can not support assets search based on path", assetProvider.getIdentifier());
288         return null;
289     }
290 }