View Javadoc
1   /**
2    * This file Copyright (c) 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.ui.contentapp.preview;
35  
36  import info.magnolia.cms.beans.runtime.FileProperties;
37  import info.magnolia.jcr.util.NodeTypes.LastModified;
38  import info.magnolia.jcr.util.NodeUtil;
39  import info.magnolia.link.LinkUtil;
40  import info.magnolia.ui.contentapp.browser.preview.PreviewProvider;
41  import info.magnolia.ui.datasource.jcr.JcrDatasourceDefinition;
42  
43  import java.io.InputStream;
44  import java.util.Calendar;
45  import java.util.Optional;
46  import java.util.Set;
47  
48  import javax.inject.Provider;
49  import javax.jcr.Item;
50  import javax.jcr.Node;
51  import javax.jcr.RepositoryException;
52  import javax.servlet.http.HttpServletRequest;
53  
54  import org.apache.jackrabbit.JcrConstants;
55  import org.apache.jackrabbit.util.Text;
56  
57  import com.google.common.collect.ImmutableSet;
58  import com.google.common.net.MediaType;
59  import com.vaadin.server.ExternalResource;
60  import com.vaadin.server.Resource;
61  import com.vaadin.server.StreamResource;
62  import com.vaadin.server.StreamResource.StreamSource;
63  
64  import lombok.Getter;
65  import lombok.Setter;
66  import lombok.SneakyThrows;
67  
68  /**
69   * This implementation of {@link info.magnolia.ui.contentapp.browser.preview.PreviewProvider} provides portrait or thumbnail images for JCR-based content.
70   * <p>
71   * It expects a given Node to have a binary sub-node named according to {@link info.magnolia.ui.contentapp.preview.JcrPreviewDefinition#getNodeName()},
72   * and will only resolve images when the binary is of mime-type image/*.
73   * It also relies on imaging module to generate and store the preview images.
74   */
75  @Setter
76  @Getter
77  public class JcrPreviewProvider implements PreviewProvider<Item> {
78  
79      private static final Set<String> UNSUPPORTED_IMAGE_TYPES = ImmutableSet.of(
80              "image/x-icon",
81              "image/x-portable-bitmap",
82              "image/x-xpixmap",
83              "image/x-portable-graymap",
84              "image/x-cmu-raster",
85              "image/x-portable-pixmap",
86              "image/x-xbitmap",
87              "image/x-rgb",
88              "image/x-xwindowdump",
89              "image/x-cmx",
90              "image/cis-cod",
91              "image/ief"
92      );
93  
94      private static final Set<String> SUPPORTED_AUDIO_TYPES = ImmutableSet.of(
95              MediaType.MPEG_AUDIO.toString(),
96              MediaType.WEBM_AUDIO.toString(),
97              MediaType.OGG_AUDIO.toString(),
98              MediaType.MP4_AUDIO.toString(),
99              MediaType.AAC_AUDIO.toString(),
100             "audio/aacp",
101             "audio/flac",
102             "audio/wav",
103             "audio/x-wav",
104             "audio/mp3" //chrome and chromium "Incorrect MIME type for mp3 files (audio/mp3)" https://bugs.chromium.org/p/chromium/issues/detail?id=227004
105     );
106 
107     private static final Set<String> SUPPORTED_VIDEO_TYPES = ImmutableSet.of(
108             MediaType.MPEG_VIDEO.toString(),
109             MediaType.WEBM_VIDEO.toString(),
110             MediaType.MP4_VIDEO.toString(),
111             MediaType.OGG_VIDEO.toString()
112     );
113 
114     private final Provider<HttpServletRequest> requestProvider;
115     private final JcrPreviewDefinition previewDefinition;
116 
117     public JcrPreviewProvider(JcrDatasourceDefinition datasourceDefinition, Provider<HttpServletRequest> requestProvider) {
118         this.requestProvider = requestProvider;
119         this.previewDefinition = datasourceDefinition.getPreviewDefinition();
120     }
121 
122     @Override
123     public Optional<Resource> getResource(Item item) {
124         return getPreviewNode(item).map(node -> {
125             String mimeType = getMimeType(node);
126             if (mimeType != null) {
127                 if (mimeType.matches("image.*")) {
128                     return imageResource(mimeType, (Node) item, node);
129                 } else if (SUPPORTED_AUDIO_TYPES.contains(mimeType)) {
130                     return new ExternalResource(LinkUtil.createAbsoluteLink(node), mimeType);
131                 } else if (SUPPORTED_VIDEO_TYPES.contains(mimeType)) {
132                     return new ExternalResource(LinkUtil.createAbsoluteLink(node), mimeType);
133                 }
134             }
135             return null;
136         });
137     }
138 
139     @SneakyThrows(RepositoryException.class)
140     private Optional<Node> getPreviewNode(Item item) {
141         if (item.isNode() && ((Node) item).hasNode(previewDefinition.getNodeName())) {
142             return Optional.of(((Node) item).getNode(previewDefinition.getNodeName()));
143         }
144         return Optional.empty();
145     }
146 
147     @SneakyThrows(RepositoryException.class)
148     private Resource imageResource(String mimeType, Node item, Node imageNode) {
149         if (MediaType.SVG_UTF_8.is(MediaType.parse(mimeType)) || MediaType.ICO.is(MediaType.parse(mimeType))) {
150             ImageStreamSource iss = new ImageStreamSource(imageNode.getIdentifier(), imageNode.getSession().getWorkspace().getName());
151             // By default a StreamResource is cached for one year - filename contains the last modified date so that image is cached by the browser until changed.
152             String filename = imageNode.getIdentifier() + LastModified.getLastModified(imageNode).getTimeInMillis();
153             StreamResource streamResource = new StreamResource(iss, filename);
154             streamResource.setMIMEType(mimeType);
155             return streamResource;
156         } else if (!UNSUPPORTED_IMAGE_TYPES.contains(mimeType)) {
157             return resolveImagePath(item, imageNode, previewDefinition.getGenerator()).map(path -> new ExternalResource(path, MediaType.PNG.toString())).orElse(null);
158         }
159         return null;
160     }
161 
162     private Optional<String> resolveImagePath(Node node, Node imageNode, String generator) throws RepositoryException {
163         String imageName;
164         if (imageNode.hasProperty(FileProperties.PROPERTY_FILENAME)) {
165             imageName = imageNode.getProperty(FileProperties.PROPERTY_FILENAME).getString();
166         } else {
167             imageName = node.getName();
168         }
169         // Crafting link manually;
170         // ImagingSupport should eventually create the link but it only supports the original variation so far
171         //  manual:  /.imaging/thumbnail/dam/374f8dbe-a8d7-4829-b9e7-3386a5ecb58f/blindtiger.png.jpg
172         // imaging:  /.imaging/default/dam/blindtiger.png/jcr:content.png
173         String imagePath = Text.escapePath(requestProvider.get().getContextPath() + "/" + previewDefinition.getServletPath() + "/" + generator + "/" + node.getSession().getWorkspace().getName() + "/" + imageNode.getIdentifier() + "/" + imageName + "." + previewDefinition.getExtension());
174         // Add cache fingerprint so that browser caches asset only until asset is modified.
175         Calendar lastModified = LastModified.getLastModified(node);
176         imagePath = LinkUtil.addFingerprintToLink(imagePath, lastModified);
177         return Optional.ofNullable(imagePath);
178     }
179 
180     @SneakyThrows(RepositoryException.class)
181     private String getMimeType(Node imageNode) {
182         return imageNode.getProperty(JcrConstants.JCR_MIMETYPE).getString();
183     }
184 
185     private static class ImageStreamSource implements StreamSource {
186         private final String id;
187         private final String workspace;
188 
189         private ImageStreamSource(String id, String workspace) {
190             this.id = id;
191             this.workspace = workspace;
192         }
193 
194         @SneakyThrows(RepositoryException.class)
195         @Override
196         public InputStream getStream() {
197             Node node = NodeUtil.getNodeByIdentifier(workspace, id);
198             if (node != null && node.hasProperty(JcrConstants.JCR_DATA)) {
199                 return node.getProperty(JcrConstants.JCR_DATA).getBinary().getStream();
200             }
201             return null;
202         }
203     }
204 }