1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.imaging.caching;
35
36 import info.magnolia.cms.beans.config.MIMEMapping;
37 import info.magnolia.cms.beans.runtime.FileProperties;
38 import info.magnolia.cms.core.Content;
39 import info.magnolia.cms.core.HierarchyManager;
40 import info.magnolia.cms.core.NodeData;
41 import info.magnolia.cms.util.ContentUtil;
42 import info.magnolia.cms.util.NodeDataUtil;
43 import info.magnolia.context.MgnlContext;
44 import info.magnolia.imaging.ImageGenerator;
45 import info.magnolia.imaging.ImageStreamer;
46 import info.magnolia.imaging.ImagingException;
47 import info.magnolia.imaging.ParameterProvider;
48
49 import java.io.ByteArrayInputStream;
50 import java.io.ByteArrayOutputStream;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.io.OutputStream;
54 import java.util.Calendar;
55 import java.util.concurrent.ExecutionException;
56 import java.util.concurrent.TimeUnit;
57 import java.util.concurrent.locks.ReentrantLock;
58
59 import javax.jcr.PropertyType;
60 import javax.jcr.RepositoryException;
61
62 import org.apache.commons.io.IOUtils;
63
64 import com.google.common.cache.CacheBuilder;
65 import com.google.common.cache.CacheLoader;
66 import com.google.common.cache.LoadingCache;
67
68
69
70
71
72
73
74 public class CachingImageStreamer<P> implements ImageStreamer<P> {
75 private static final String GENERATED_IMAGE_PROPERTY = "generated-image";
76
77 private final HierarchyManager hm;
78 private final CachingStrategy<P> cachingStrategy;
79 private final ImageStreamer<P> delegate;
80
81
82
83
84
85
86
87
88
89
90
91
92 private final LoadingCache<ImageGenerationJob<P>, NodeData> currentJobs;
93
94
95
96
97
98
99
100
101
102
103 private static final ReentrantLock lock = new ReentrantLock();
104
105 public CachingImageStreamer(HierarchyManager hm, CachingStrategy<P> cachingStrategy, ImageStreamer<P> delegate) {
106 this.hm = hm;
107 this.cachingStrategy = cachingStrategy;
108 this.delegate = delegate;
109
110 CacheBuilder<Object, Object> cb = CacheBuilder.newBuilder();
111 this.currentJobs = cb
112
113
114 .expireAfterWrite(500, TimeUnit.MILLISECONDS)
115
116
117 .build(new CacheLoader<ImageGenerationJob<P>, NodeData>() {
118
119 @Override
120 public NodeData load(ImageGenerationJob<P> job) throws Exception {
121 try {
122 return generateAndStore(job.getGenerator(), job.getParams());
123 } catch (IOException e) {
124
125 throw new RuntimeException(e);
126 } catch (ImagingException e) {
127
128 throw new RuntimeException(e);
129 }
130 }
131
132 });
133 }
134
135 @Override
136 public void serveImage(ImageGenerator<ParameterProvider<P>> generator, ParameterProvider<P> params, OutputStream out) throws IOException, ImagingException {
137 NodeData imgProp = fetchFromCache(generator, params);
138 if (imgProp == null) {
139
140 try {
141 imgProp = currentJobs.get(new ImageGenerationJob<P>(generator, params));
142 } catch (ExecutionException e) {
143
144 unwrapRuntimeException(e);
145 }
146 }
147 serve(imgProp, out);
148 }
149
150
151
152
153
154 protected NodeData fetchFromCache(ImageGenerator<ParameterProvider<P>> generator, ParameterProvider<P> parameterProvider) {
155 final String cachePath = cachingStrategy.getCachePath(generator, parameterProvider);
156 if (cachePath == null) {
157
158 return null;
159 }
160 try {
161 if (!hm.isExist(cachePath)) {
162 return null;
163 }
164 final Content imageNode = hm.getContent(cachePath);
165 final NodeData nodeData = imageNode.getNodeData(GENERATED_IMAGE_PROPERTY);
166 if (!nodeData.isExist()) {
167 return null;
168 }
169 InputStream in = null;
170 try {
171 in = nodeData.getStream();
172 } catch (Exception e) {
173
174
175 return null;
176 }
177 IOUtils.closeQuietly(in);
178
179 if (cachingStrategy.shouldRegenerate(nodeData, parameterProvider)) {
180 return null;
181 }
182 return nodeData;
183 } catch (RepositoryException e) {
184 throw new RuntimeException(e);
185 }
186 }
187
188
189 protected void serve(NodeData binary, OutputStream out) throws IOException {
190 final InputStream in = binary.getStream();
191 if (in == null) {
192 throw new IllegalStateException("Can't get InputStream from " + binary.getHandle());
193 }
194 IOUtils.copy(in, out);
195 IOUtils.closeQuietly(in);
196 IOUtils.closeQuietly(out);
197 }
198
199 protected NodeData generateAndStore(final ImageGenerator<ParameterProvider<P>> generator, final ParameterProvider<P> parameterProvider) throws IOException, ImagingException {
200
201 final ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
202 delegate.serveImage(generator, parameterProvider, tempOut);
203
204
205 lock.lock();
206 try {
207 return MgnlContext.doInSystemContext(new MgnlContext.Op<NodeData, RepositoryException>() {
208 @Override
209 public NodeData exec() throws RepositoryException {
210 HierarchyManager systemHM = MgnlContext.getHierarchyManager(hm.getName());
211
212 final String cachePath = cachingStrategy.getCachePath(generator, parameterProvider);
213 final Content cacheNode = ContentUtil.createPath(systemHM, cachePath, false);
214 final NodeData imageData = NodeDataUtil.getOrCreate(cacheNode, GENERATED_IMAGE_PROPERTY, PropertyType.BINARY);
215
216
217 final ByteArrayInputStream tempIn = new ByteArrayInputStream(tempOut.toByteArray());
218 imageData.setValue(tempIn);
219
220 final String formatName = generator.getOutputFormat(parameterProvider).getFormatName();
221 final String mimeType = MIMEMapping.getMIMEType(formatName);
222 imageData.setAttribute(FileProperties.PROPERTY_CONTENTTYPE, mimeType);
223 imageData.setAttribute(FileProperties.PROPERTY_LASTMODIFIED, Calendar.getInstance());
224
225
226
227 cacheNode.getMetaData().setModificationDate();
228
229
230 systemHM.save();
231 return imageData;
232 }
233 });
234 } catch (RepositoryException e) {
235 throw new ImagingException("Can't store rendered image: " + e.getMessage(), e);
236 } finally {
237 lock.unlock();
238 }
239 }
240
241
242
243
244
245
246
247 private void unwrapRuntimeException(Exception e) throws ImagingException, IOException {
248 final Throwable cause = e.getCause();
249 if (cause instanceof ImagingException) {
250 throw (ImagingException) cause;
251 } else if (cause instanceof IOException) {
252 throw (IOException) cause;
253 } else if (cause instanceof RuntimeException) {
254 unwrapRuntimeException((RuntimeException) cause);
255 } else if (cause == null) {
256
257 throw new IllegalStateException("Unexpected and unhandled exception: " + (e.getMessage() != null ? e.getMessage() : ""), e);
258 } else {
259
260 throw new ImagingException(e.getMessage(), cause);
261 }
262 }
263 }