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 info.magnolia.cms.util.FilteredEventListener.JCR_SYSTEM_EXCLUDING_PREDICATE;
37 import static info.magnolia.resourceloader.ResourceOriginChange.Type.*;
38 import static info.magnolia.resourceloader.ResourceOriginChange.resourceChange;
39 import static java.util.Spliterators.spliteratorUnknownSize;
40 import static java.util.stream.Collectors.toList;
41
42 import info.magnolia.cms.util.FilteredEventListener;
43 import info.magnolia.context.SystemContext;
44 import info.magnolia.jcr.util.NodeTypes;
45 import info.magnolia.jcr.util.PropertyUtil;
46 import info.magnolia.observation.WorkspaceEventListenerRegistration;
47 import info.magnolia.resourceloader.AbstractResourceOrigin;
48 import info.magnolia.resourceloader.ResourceOriginChange;
49 import info.magnolia.resourceloader.ResourceOriginFactory;
50
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.io.Reader;
54 import java.io.StringReader;
55 import java.nio.charset.Charset;
56 import java.nio.charset.StandardCharsets;
57 import java.util.ArrayList;
58 import java.util.Calendar;
59 import java.util.LinkedHashMap;
60 import java.util.List;
61 import java.util.Map;
62 import java.util.Optional;
63 import java.util.Spliterator;
64 import java.util.regex.Matcher;
65 import java.util.regex.Pattern;
66 import java.util.stream.StreamSupport;
67
68 import javax.jcr.Binary;
69 import javax.jcr.Node;
70 import javax.jcr.RepositoryException;
71 import javax.jcr.Session;
72 import javax.jcr.observation.Event;
73 import javax.jcr.observation.EventIterator;
74 import javax.jcr.observation.EventListener;
75
76 import org.apache.commons.io.input.NullInputStream;
77 import org.apache.commons.lang3.StringUtils;
78 import org.apache.jackrabbit.JcrConstants;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
81
82 import com.google.auto.factory.AutoFactory;
83 import com.google.auto.factory.Provided;
84
85 import lombok.SneakyThrows;
86
87
88
89
90 @AutoFactory(implementing = ResourceOriginFactory.class)
91 public class JcrResourceOrigin extends AbstractResourceOrigin<JcrResource> {
92 private final static Logger log = LoggerFactory.getLogger(JcrResourceOrigin.class);
93
94 public static final String RESOURCES_WORKSPACE = "resources";
95
96 public static final String BINARY_NODE_NAME = "binary";
97
98 public static final String TEXT_PROPERTY = "text";
99 public static final String BYPASS_PROPERTY = "bypass";
100
101 private final SystemContext systemContext;
102
103 JcrResourceOrigin(@Provided SystemContext systemContext, String name) {
104 super(name);
105 this.systemContext = systemContext;
106 }
107
108 @Override
109 protected void initializeResourceChangeMonitoring() {
110 try {
111 EventListener listener = new FilteredEventListener(new ResourcesObservationListener(), EVENT_LISTENER_FILTER);
112 WorkspaceEventListenerRegistration.observe(RESOURCES_WORKSPACE, "/", listener).withDelay(1000L, 1000L).register();
113 } catch (RepositoryException e) {
114 log.error("Failed to initialize JCR resource change monitoring: {}", e.getMessage(), e);
115 }
116 }
117
118 @Override
119 @SneakyThrows(RepositoryException.class)
120 public JcrResource getRoot() {
121 final Node rootNode = getJcrSession().getRootNode();
122 return newResource(rootNode);
123 }
124
125 @Override
126 @SneakyThrows(RepositoryException.class)
127 public JcrResource getByPath(String path) {
128 final Node node = getNode(path);
129 if (node == null || isBypassed(node)) {
130 throw new ResourceNotFoundException(this, path);
131 }
132 return newResource(node);
133 }
134
135 @Override
136 @SneakyThrows(RepositoryException.class)
137 public boolean hasPath(String path) {
138 final Node node = getNode(path);
139 return node != null && !isBypassed(node);
140 }
141
142 @Override
143 @SneakyThrows(RepositoryException.class)
144 protected boolean isFile(JcrResource resource) {
145
146 return !isDirectory(getNode(resource));
147 }
148
149 @Override
150 @SneakyThrows(RepositoryException.class)
151 protected boolean isDirectory(JcrResource resource) {
152
153 return isDirectory(getNode(resource));
154 }
155
156 @Override
157 protected boolean isEditable(JcrResource resource) {
158
159 return true;
160 }
161
162 @Override
163 @SneakyThrows(RepositoryException.class)
164 protected String getPath(JcrResource resource) {
165 Node node = getNode(resource);
166 return node == null ? null : node.getPath();
167 }
168
169 @Override
170 @SneakyThrows(RepositoryException.class)
171 protected String getName(JcrResource resource) {
172 Node node = getNode(resource);
173 return node == null ? null : node.getName();
174 }
175
176 @Override
177 @SneakyThrows(RepositoryException.class)
178 protected long getLastModified(JcrResource resource) {
179 Node node = getNode(resource);
180
181 final Calendar lastModified = node == null ? null : NodeTypes.LastModified.getLastModified(node);
182 if (lastModified == null) {
183
184 throw new RepositoryException("No lastModified or created date property on " + node);
185 }
186 return lastModified.getTimeInMillis();
187 }
188
189 @Override
190 @SneakyThrows(RepositoryException.class)
191 protected List<JcrResource> listChildren(JcrResource resource) {
192 if (!resource.isDirectory()) {
193 throw new IllegalStateException(resource.getPath() + " is not a directory.");
194 }
195 Node node = getNode(resource);
196 if (node == null) {
197 return new ArrayList<>();
198 }
199 Spliterator<Node> nodeIterator = spliteratorUnknownSize(node.getNodes(), Spliterator.ORDERED);
200 return StreamSupport.stream(nodeIterator, false)
201 .filter(this::nodeFilter)
202 .map(this::newResource)
203 .collect(toList());
204 }
205
206 @Override
207 @SneakyThrows(RepositoryException.class)
208 protected JcrResource getParent(JcrResource resource) {
209 Node node = getNode(resource);
210 String childPath = node == null ? null : node.getPath();
211
212 if (childPath == null || childPath.equals("/")) {
213 return null;
214 }
215
216 return newResource(node.getParent());
217 }
218
219 @Override
220 @SneakyThrows(RepositoryException.class)
221 protected Reader openReader(JcrResource resource) throws IOException {
222 Node node = getNode(resource);
223 if (isTextResource(node)) {
224 return new StringReader(node.getProperty(TEXT_PROPERTY).getString());
225 }
226 return super.openReader(resource);
227 }
228
229 @Override
230 @SneakyThrows(RepositoryException.class)
231 protected InputStream doOpenStream(JcrResource resource) {
232 Binary binary = getBinary(getNode(resource));
233 if (binary != null) {
234 return binary.getStream();
235 }
236
237 log.debug("JCR resource {} has no content", resource);
238 return new NullInputStream(0);
239 }
240
241 @Override
242 @SneakyThrows(RepositoryException.class)
243 protected Charset getCharsetFor(JcrResource resource) {
244 Node node = getNode(resource);
245 if (isBinaryResource(node)) {
246 Node binary = node.getNode(BINARY_NODE_NAME);
247 if (binary.hasProperty(JcrConstants.JCR_ENCODING)) {
248 return Charset.forName(node.getProperty(JcrConstants.JCR_ENCODING).getString());
249 }
250 }
251
252 return StandardCharsets.UTF_8;
253 }
254
255 protected JcrResource newResource(Node node) {
256 return new JcrResource(this, node);
257 }
258
259
260
261
262 private Node getNode(String resource) throws RepositoryException {
263 final Session jcrSession = getJcrSession();
264 final boolean exists = jcrSession.nodeExists(resource);
265
266 if (!exists) {
267 return null;
268 }
269
270 return jcrSession.getNode(resource);
271 }
272
273 private Node getNode(JcrResource resource) throws RepositoryException {
274 return getNode(resource.getPath());
275 }
276
277 protected Session getJcrSession() throws RepositoryException {
278 return systemContext.getJCRSession(RESOURCES_WORKSPACE);
279 }
280
281 protected boolean isResource(Node node) throws RepositoryException {
282 return isBinaryResource(node) || isTextResource(node);
283 }
284
285 protected Binary getBinary(Node resourceNode) throws RepositoryException {
286 if (isTextResource(resourceNode)) {
287 return resourceNode.getProperty(TEXT_PROPERTY).getBinary();
288 } else if (isBinaryResource(resourceNode)) {
289 final Node binary = resourceNode.getNode(BINARY_NODE_NAME);
290 return binary.getProperty(JcrConstants.JCR_DATA).getBinary();
291 }
292 return null;
293 }
294
295 protected boolean isTextResource(Node resourceNode) throws RepositoryException {
296 return resourceNode != null && resourceNode.hasProperty(TEXT_PROPERTY);
297 }
298
299 protected boolean isBinaryResource(Node node) throws RepositoryException {
300 return node != null && node.hasNode(BINARY_NODE_NAME);
301 }
302
303 protected boolean isFile(Node node) throws RepositoryException {
304
305 return node.isNodeType(NodeTypes.Content.NAME);
306 }
307
308 protected boolean isDirectory(Node node) throws RepositoryException {
309 return node != null && (node.isNodeType(NodeTypes.Folder.NAME) || node.getDepth() == 0);
310 }
311
312
313 protected boolean isBypassed(Node node) {
314 return PropertyUtil.getBoolean(node, BYPASS_PROPERTY, false);
315 }
316
317 @SneakyThrows(RepositoryException.class)
318 private boolean nodeFilter(Node node) {
319 return node != null && !isBypassed(node) && (isDirectory(node) || isFile(node));
320 }
321
322
323
324
325
326 private class ResourcesObservationListener implements EventListener {
327
328 @Override
329 public void onEvent(EventIterator events) {
330
331 final Map<String, ResourceOriginChange> changes = new LinkedHashMap<>();
332
333 final ResourceOriginChange.Builder builder = resourceChange().inOrigin(JcrResourceOrigin.this);
334 while (events.hasNext()) {
335 final Event event = events.nextEvent();
336 int eventType = event.getType();
337
338 try {
339
340 String path = event.getPath();
341 builder.at(path);
342 if (eventType == Event.NODE_ADDED) {
343 builder.ofType(ResourceOriginChange.Type.ADDED);
344 changes.put(path, builder.build());
345 } else if (eventType == Event.NODE_REMOVED) {
346 builder.ofType(REMOVED);
347 changes.put(path, builder.build());
348 }
349
350
351 final Matcher itemPathMatcher = Pattern.compile("^(?<relatedNodePath>.+)?/(?<name>.+)?$").matcher(event.getPath());
352
353 if (itemPathMatcher.matches()) {
354 final String relatedNode = StringUtils.defaultIfBlank(itemPathMatcher.group("relatedNodePath"), "/");
355 final String name = StringUtils.defaultIfBlank(itemPathMatcher.group("name"), "");
356
357
358
359
360
361
362
363
364
365 if (eventType == Event.PROPERTY_ADDED || eventType == Event.PROPERTY_CHANGED || eventType == Event.PROPERTY_REMOVED) {
366 Optional<ResourceOriginChange> bypassStateChange = tryDetectBypassPropertyChange(relatedNode, name, eventType);
367 if (bypassStateChange.isPresent()) {
368 changes.put(bypassStateChange.get().getRelatedResourcePath(), bypassStateChange.get());
369 continue;
370 }
371 }
372
373
374 changes.put(relatedNode, builder.at(relatedNode).ofType(MODIFIED).build());
375 }
376 } catch (RepositoryException e) {
377 log.error("Failed to process event [{}] due to: {}", event, e.getMessage(), e);
378 }
379 }
380
381 for (final ResourceOriginChange change : changes.values()) {
382 if (change.getType() == MODIFIED || change.getType() == ADDED) {
383 try {
384 final Node relatedNode = getNode(change.getRelatedResourcePath());
385 if (isBypassed(relatedNode)) {
386 continue;
387 }
388 } catch (RepositoryException e) {
389 log.warn("Failed to retrieve supposedly present node [{}] during resource change dispatching: {}", change.getRelatedResourcePath(), e.getMessage());
390 continue;
391 }
392 }
393
394 dispatchResourceChange(change);
395 }
396 }
397
398 private Optional<ResourceOriginChange> tryDetectBypassPropertyChange(String relatedNodePath, String name, int eventType) throws RepositoryException {
399 if (BYPASS_PROPERTY.equals(name)) {
400 final Node resourceNode = getNode(relatedNodePath);
401 boolean isBypassed = PropertyUtil.getBoolean(resourceNode, BYPASS_PROPERTY, false);
402
403 if (isBypassed) {
404 return Optional.of(resourceChange().at(relatedNodePath).inOrigin(JcrResourceOrigin.this).ofType(REMOVED).build());
405 } else if (eventType != Event.PROPERTY_ADDED) {
406 return Optional.of(resourceChange().at(relatedNodePath).inOrigin(JcrResourceOrigin.this).ofType(ADDED).build());
407 }
408 }
409 return Optional.empty();
410 }
411 }
412
413
414
415
416
417 private final static org.apache.jackrabbit.commons.predicate.Predicate EVENT_LISTENER_FILTER = new org.apache.jackrabbit.commons.predicate.Predicate() {
418 @Override
419 public boolean evaluate(Object eventObject) {
420 if (!JCR_SYSTEM_EXCLUDING_PREDICATE.evaluate(eventObject)) {
421 return false;
422 }
423
424 final Event event = (Event) eventObject;
425 try {
426 final String relatedPath = event.getPath();
427 return !relatedPath.endsWith("/" + NodeTypes.LastModified.LAST_MODIFIED) && !relatedPath.endsWith("/" + NodeTypes.LastModified.LAST_MODIFIED_BY);
428 } catch (RepositoryException e) {
429 return false;
430 }
431 }
432 };
433 }