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.resourceloader.jcr;
35
36 import static org.apache.commons.lang3.StringUtils.substringBeforeLast;
37
38 import info.magnolia.cms.util.ObservationUtil;
39 import info.magnolia.context.SystemContext;
40 import info.magnolia.jcr.util.NodeTypes;
41 import info.magnolia.jcr.util.PropertyUtil;
42 import info.magnolia.resourceloader.AbstractResourceOrigin;
43 import info.magnolia.resourceloader.ResourceOriginFactory;
44 import info.magnolia.resourceloader.ResourceVisitor;
45
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.Reader;
49 import java.io.StringReader;
50 import java.nio.charset.Charset;
51 import java.util.Calendar;
52 import java.util.Iterator;
53 import java.util.List;
54 import java.util.Set;
55
56 import javax.jcr.Binary;
57 import javax.jcr.Node;
58 import javax.jcr.RepositoryException;
59 import javax.jcr.Session;
60 import javax.jcr.observation.Event;
61 import javax.jcr.observation.EventIterator;
62 import javax.jcr.observation.EventListener;
63
64 import org.apache.commons.io.input.NullInputStream;
65 import org.apache.jackrabbit.JcrConstants;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 import com.google.auto.factory.AutoFactory;
70 import com.google.auto.factory.Provided;
71 import com.google.common.base.Function;
72 import com.google.common.base.Predicate;
73 import com.google.common.collect.Iterators;
74 import com.google.common.collect.Lists;
75 import com.google.common.collect.Sets;
76
77 import lombok.SneakyThrows;
78
79
80
81
82
83
84
85
86 @AutoFactory(implementing = ResourceOriginFactory.class)
87 public class JcrResourceOrigin extends AbstractResourceOrigin<JcrResource> {
88 private final static Logger log = LoggerFactory.getLogger(JcrResourceOrigin.class);
89
90 public static final String RESOURCES_WORKSPACE = "resources";
91
92 public static final String BINARY_NODE_NAME = "binary";
93
94 public static final String TEXT_PROPERTY = "text";
95 public static final String BYPASS_PROPERTY = "bypass";
96
97 private final SystemContext systemContext;
98
99 JcrResourceOrigin(@Provided SystemContext systemContext, String name) {
100 super(name);
101 this.systemContext = systemContext;
102 }
103
104 @Override
105 public void watchForChanges(ResourceVisitor visitor) {
106 startObservation(visitor);
107 }
108
109 @Override
110 @SneakyThrows(RepositoryException.class)
111 public JcrResource getRoot() {
112 final Node rootNode = getJcrSession().getRootNode();
113 return newResource(rootNode);
114 }
115
116 @Override
117 @SneakyThrows(RepositoryException.class)
118 public JcrResource getByPath(String path) {
119 final Node node = getNode(path);
120 if (node == null || isBypassed(node)) {
121 throw new ResourceNotFoundException(this, path);
122 }
123 return newResource(node);
124 }
125
126 @Override
127 @SneakyThrows(RepositoryException.class)
128 public boolean hasPath(String path) {
129 final Node node = getNode(path);
130 return node != null && !isBypassed(node);
131 }
132
133 @Override
134 @SneakyThrows(RepositoryException.class)
135 protected boolean isFile(JcrResource resource) {
136
137 return !isDirectory(resource.getNode());
138 }
139
140 @Override
141 @SneakyThrows(RepositoryException.class)
142 protected boolean isDirectory(JcrResource resource) {
143
144 return isDirectory(resource.getNode());
145 }
146
147 @Override
148 protected boolean isEditable(JcrResource resource) {
149
150 return true;
151 }
152
153 @Override
154 @SneakyThrows(RepositoryException.class)
155 protected String getPath(JcrResource resource) {
156 return resource.getNode().getPath();
157 }
158
159 @Override
160 @SneakyThrows(RepositoryException.class)
161 protected String getName(JcrResource resource) {
162 return resource.getNode().getName();
163 }
164
165 @Override
166 @SneakyThrows(RepositoryException.class)
167 protected long getLastModified(JcrResource resource) {
168
169 final Calendar lastModified = NodeTypes.LastModified.getLastModified(resource.getNode());
170 if (lastModified == null) {
171
172 throw new RepositoryException("No lastModified or created date property on " + resource.getNode());
173 }
174 return lastModified.getTimeInMillis();
175 }
176
177 @Override
178 @SneakyThrows(RepositoryException.class)
179 protected List<JcrResource> listChildren(JcrResource resource) {
180 if (!resource.isDirectory()) {
181 throw new IllegalStateException(resource.getPath() + " is not a directory.");
182 }
183
184 final Iterator<Node> nodeIterator = resource.getNode().getNodes();
185 final Iterator<Node> filtered = Iterators.filter(nodeIterator, new NodeFilter());
186 final Iterator<JcrResource> resources = Iterators.transform(filtered, new NodeToResource());
187 return Lists.newArrayList(resources);
188 }
189
190 @Override
191 @SneakyThrows(RepositoryException.class)
192 protected JcrResource getParent(JcrResource resource) {
193 Node node = resource.getNode();
194 String childPath = node.getPath();
195
196 if (childPath.equals("/")) {
197 return null;
198 }
199
200 return newResource(node.getParent());
201 }
202
203 @Override
204 @SneakyThrows(RepositoryException.class)
205 protected Reader openReader(JcrResource resource) throws IOException {
206 Node node = resource.getNode();
207 if (isTextResource(node)) {
208 return new StringReader(node.getProperty(TEXT_PROPERTY).getString());
209 }
210 return super.openReader(resource);
211 }
212
213 @Override
214 @SneakyThrows(RepositoryException.class)
215 protected InputStream doOpenStream(JcrResource resource) {
216 Binary binary = getBinary(resource.getNode());
217 if (binary != null) {
218 return binary.getStream();
219 }
220
221 log.debug("JCR resource {} has no content", resource);
222 return new NullInputStream(0);
223 }
224
225 @Override
226 @SneakyThrows(RepositoryException.class)
227 protected Charset getCharsetFor(JcrResource resource) {
228 Node node = resource.getNode();
229 if (isBinaryResource(node)) {
230 Node binary = node.getNode(BINARY_NODE_NAME);
231 if (binary.hasProperty(JcrConstants.JCR_ENCODING)) {
232 return Charset.forName(node.getProperty(JcrConstants.JCR_ENCODING).getString());
233 }
234 }
235
236 return Charset.forName("UTF-8");
237 }
238
239 protected JcrResource newResource(Node node) {
240 return new JcrResource(this, node);
241 }
242
243
244
245
246 private Node getNode(String resource) throws RepositoryException {
247 final Session jcrSession = getJcrSession();
248 final boolean exists = jcrSession.nodeExists(resource);
249
250 if (!exists) {
251 return null;
252 }
253
254 return jcrSession.getNode(resource);
255 }
256
257 protected Session getJcrSession() throws RepositoryException {
258 return systemContext.getJCRSession(RESOURCES_WORKSPACE);
259 }
260
261 protected boolean isResource(Node node) throws RepositoryException {
262 return isBinaryResource(node) || isTextResource(node);
263 }
264
265 protected Binary getBinary(Node resourceNode) throws RepositoryException {
266 if (isTextResource(resourceNode)) {
267 return resourceNode.getProperty(TEXT_PROPERTY).getBinary();
268 } else if (isBinaryResource(resourceNode)) {
269 final Node binary = resourceNode.getNode(BINARY_NODE_NAME);
270 return binary.getProperty(JcrConstants.JCR_DATA).getBinary();
271 }
272 return null;
273 }
274
275 protected boolean isTextResource(Node resourceNode) throws RepositoryException {
276 return resourceNode.hasProperty(TEXT_PROPERTY);
277 }
278
279 protected boolean isBinaryResource(Node node) throws RepositoryException {
280 return node.hasNode(BINARY_NODE_NAME);
281 }
282
283 protected boolean isFile(Node node) throws RepositoryException {
284
285 return !isDirectory(node);
286 }
287
288 protected boolean isDirectory(Node node) throws RepositoryException {
289 return node.isNodeType(NodeTypes.Folder.NAME) || node.getDepth() == 0;
290 }
291
292 protected boolean isBypassed(Node node) {
293 return PropertyUtil.getBoolean(node, BYPASS_PROPERTY, false);
294 }
295
296 final Object observationEventNotification = new Object();
297
298 private void startObservation(final ResourceVisitor visitor) {
299 ObservationUtil.registerDeferredChangeListener(RESOURCES_WORKSPACE, "/", new EventListener() {
300 @Override
301 public void onEvent(EventIterator events) {
302
303 final Set<String> changedPaths = Sets.newHashSet();
304 while (events.hasNext()) {
305 final Event event = events.nextEvent();
306 try {
307 String path = event.getPath();
308
309 if (event.getType() != Event.NODE_ADDED && event.getType() != Event.NODE_REMOVED) {
310 path = getParentPath(path);
311 }
312
313 changedPaths.add(path);
314 } catch (RepositoryException e) {
315 log.error("Failed to process event [{}] due to: {}", event, e.getMessage(), e);
316 }
317 }
318
319
320 for (final String path : changedPaths) {
321 try {
322 final Node node = getNode(path);
323
324
325 if (node != null) {
326 final JcrResource resource = newResource(node);
327 if (resource.isDirectory()) {
328 visitor.visitDirectory(resource);
329 } else {
330 visitor.visitFile(resource);
331 }
332 }
333 } catch (RepositoryException e) {
334 log.error("Failed to get node for JcrResourceOrigin callback on {}.", path, e);
335 }
336 }
337
338
339
340 synchronized (observationEventNotification) {
341 observationEventNotification.notifyAll();
342 }
343 }
344
345 private String getParentPath(String path) {
346 path = substringBeforeLast(path, "/");
347 if (path.isEmpty()) {
348 path = "/";
349 }
350 return path;
351 }
352
353 }, 1000, 1000);
354 }
355
356 private class NodeFilter implements Predicate<Node> {
357 @Override
358 @SneakyThrows(RepositoryException.class)
359 public boolean apply(Node node) {
360 return node != null && !isBypassed(node) && (isResource(node) || isDirectory(node));
361 }
362 }
363
364 private class NodeToResource implements Function<Node, JcrResource> {
365 @Override
366 public JcrResource apply(Node node) {
367 return newResource(node);
368 }
369 }
370 }