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