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.stream.Collectors.toList;
40
41 import info.magnolia.cms.util.FilteredEventListener;
42 import info.magnolia.context.SystemContext;
43 import info.magnolia.jcr.util.NodeTypes;
44 import info.magnolia.jcr.util.NodeUtil;
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.regex.Matcher;
64 import java.util.regex.Pattern;
65 import java.util.stream.StreamSupport;
66
67 import javax.inject.Provider;
68 import javax.jcr.Node;
69 import javax.jcr.Property;
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 import com.machinezoo.noexception.Exceptions;
85
86
87
88
89 @AutoFactory(implementing = ResourceOriginFactory.class)
90 public class JcrResourceOrigin extends AbstractResourceOrigin<JcrResource> {
91 private final static Logger log = LoggerFactory.getLogger(JcrResourceOrigin.class);
92
93 public static final String RESOURCES_WORKSPACE = "resources";
94
95 public static final String BINARY_NODE_NAME = "binary";
96
97 public static final String TEXT_PROPERTY = "text";
98 public static final String BYPASS_PROPERTY = "bypass";
99 private static final String SLASH = "/";
100
101 private final Provider<SystemContext> systemContextProvider;
102
103 JcrResourceOrigin(@Provided Provider<SystemContext> systemContextProvider, String name) {
104 super(name);
105 this.systemContextProvider = systemContextProvider;
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, SLASH, 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 public JcrResource getRoot() {
120 return getJcrSession()
121 .map(Exceptions.wrap().function(Session::getRootNode))
122 .map(this::newResource)
123 .orElseThrow(() -> new ResourceNotFoundException(this, SLASH));
124 }
125
126 @Override
127 public JcrResource getByPath(String path) {
128 return getNode(path)
129 .filter(node -> !isBypassed(node))
130 .map(this::newResource)
131 .orElseThrow(() -> new ResourceNotFoundException(this, path));
132 }
133
134 @Override
135 public boolean hasPath(String path) {
136 return getNode(path)
137 .filter(node -> !isBypassed(node))
138 .isPresent();
139 }
140
141 @Override
142 protected boolean isFile(JcrResource resource) {
143
144 return !isDirectory(resource);
145 }
146
147 @Override
148 protected boolean isDirectory(JcrResource resource) {
149
150 return getNode(resource)
151 .map(this::isDirectory)
152 .orElse(false);
153 }
154
155 @Override
156 protected boolean isEditable(JcrResource resource) {
157
158 return true;
159 }
160
161 @Override
162 protected String getPath(JcrResource resource) {
163 return getNode(resource)
164 .map(Exceptions.wrap().function(Node::getPath))
165 .orElse(null);
166 }
167
168 @Override
169 protected String getName(JcrResource resource) {
170 return getNode(resource)
171 .map(Exceptions.wrap().function(Node::getName))
172 .orElse(null);
173 }
174
175 @Override
176 protected long getLastModified(JcrResource resource) {
177 return getNode(resource)
178 .map(Exceptions.wrap().function(NodeTypes.LastModified::getLastModified))
179 .map(Calendar::getTimeInMillis)
180 .orElseThrow(() -> new RuntimeException("No lastModified or created date property on " + resource.getPath()));
181 }
182
183 @Override
184 protected List<JcrResource> listChildren(JcrResource resource) {
185 if (!resource.isDirectory()) {
186 throw new IllegalStateException(resource.getPath() + " is not a directory.");
187 }
188 return getNode(resource)
189 .map(Exceptions.wrap().function(node -> StreamSupport.stream(NodeUtil.getNodes(node, child -> nodeFilter((Node) child)).spliterator(), false)
190 .map(this::newResource)
191 .collect(toList())
192 ))
193 .orElse(new ArrayList<>());
194 }
195
196 @Override
197 protected JcrResource/../../info/magnolia/resourceloader/jcr/JcrResource.html#JcrResource">JcrResource getParent(JcrResource resource) {
198 return getNode(resource)
199 .filter(Exceptions.wrap().predicate(node -> !node.getPath().equals(SLASH)))
200 .map(Exceptions.wrap().function(Node::getParent))
201 .map(this::newResource)
202 .orElse(null);
203 }
204
205 @Override
206 protected Reader openReader(JcrResource resource) throws IOException {
207 Optional<Node> node = getNode(resource);
208 if (node.map(this::isTextResource).orElse(false)) {
209 return new StringReader(PropertyUtil.getString(node.get(), TEXT_PROPERTY));
210 }
211 return super.openReader(resource);
212 }
213
214 @Override
215 protected InputStream doOpenStream(JcrResource resource) {
216 return getNode(resource)
217 .map(this::getStream)
218 .orElseGet(() -> new NullInputStream(0));
219 }
220
221 @Override
222 protected Charset getCharsetFor(JcrResource resource) {
223 return getNode(resource)
224 .filter(Exceptions.wrap().predicate(node -> node.hasNode(BINARY_NODE_NAME)))
225 .map(Exceptions.wrap().function(node -> node.getNode(BINARY_NODE_NAME)))
226 .map(node -> PropertyUtil.getString(node, JcrConstants.JCR_ENCODING))
227 .map(Charset::forName)
228 .orElse(StandardCharsets.UTF_8);
229 }
230
231 protected JcrResource newResource(Node node) {
232 return new JcrResource(this, node);
233 }
234
235 private Optional<Node> getNode(String resource) {
236 return getJcrSession()
237 .filter(Exceptions.wrap().predicate(session -> session.nodeExists(resource)))
238 .map(Exceptions.wrap().function(session -> session.getNode(resource)));
239 }
240
241 private Optional<Node> getNode(JcrResource resource) {
242 return getNode(resource.getPath());
243 }
244
245 protected Optional<Session> getJcrSession() {
246 return Optional.of(Exceptions.wrap().get(() -> systemContextProvider.get().getJCRSession(RESOURCES_WORKSPACE)));
247 }
248
249 protected InputStream getStream(Node resourceNode) {
250 return Exceptions.wrap().get(() -> {
251 Property property = null;
252 if (isTextResource(resourceNode) && resourceNode.hasProperty(TEXT_PROPERTY)) {
253 property = resourceNode.getProperty(TEXT_PROPERTY);
254 } else if (resourceNode.hasNode(BINARY_NODE_NAME)) {
255 final Node binaryNode = resourceNode.getNode(BINARY_NODE_NAME);
256 if (binaryNode.hasProperty(JcrConstants.JCR_DATA)) {
257 property = binaryNode.getProperty(JcrConstants.JCR_DATA);
258 }
259 }
260 return property == null ? null : property.getBinary().getStream();
261 });
262 }
263
264 protected boolean isTextResource(Node resourceNode) {
265 return Optional.ofNullable(resourceNode)
266 .map(Exceptions.wrap().function(node -> node.hasProperty(TEXT_PROPERTY)))
267 .orElse(false);
268 }
269
270 protected boolean isBinaryResource(Node resourceNode) {
271 return Optional.ofNullable(resourceNode)
272 .map(Exceptions.wrap().function(node -> node.hasNode(BINARY_NODE_NAME)))
273 .orElse(false);
274 }
275
276 protected boolean isFile(Node node) {
277
278 return Exceptions.wrap().get(() -> NodeUtil.isNodeType(node, NodeTypes.Content.NAME));
279 }
280
281 protected boolean isDirectory(Node node) {
282 return Exceptions.wrap().get(() -> node != null && (node.isNodeType(NodeTypes.Folder.NAME) || node.getDepth() == 0));
283 }
284
285
286 protected boolean isBypassed(Node node) {
287 return PropertyUtil.getBoolean(node, BYPASS_PROPERTY, false);
288 }
289
290 private boolean nodeFilter(Node node) {
291 return node != null && !isBypassed(node) && (isDirectory(node) || isFile(node));
292 }
293
294
295
296
297
298 private class ResourcesObservationListener implements EventListener {
299
300 @Override
301 public void onEvent(EventIterator events) {
302
303 final Map<String, ResourceOriginChange> changes = new LinkedHashMap<>();
304
305 final ResourceOriginChange.Builder builder = resourceChange().inOrigin(JcrResourceOrigin.this);
306 while (events.hasNext()) {
307 final Event event = events.nextEvent();
308 int eventType = event.getType();
309
310 try {
311
312 String path = event.getPath();
313 builder.at(path);
314 if (eventType == Event.NODE_ADDED) {
315 builder.ofType(ResourceOriginChange.Type.ADDED);
316 changes.put(path, builder.build());
317 } else if (eventType == Event.NODE_REMOVED) {
318 builder.ofType(REMOVED);
319 changes.put(path, builder.build());
320 }
321
322
323 final Matcher itemPathMatcher = Pattern.compile("^(?<relatedNodePath>.+)?/(?<name>.+)?$").matcher(event.getPath());
324
325 if (itemPathMatcher.matches()) {
326 final String relatedNode = StringUtils.defaultIfBlank(itemPathMatcher.group("relatedNodePath"), SLASH);
327 final String name = StringUtils.defaultIfBlank(itemPathMatcher.group("name"), "");
328
329
330
331
332
333
334
335
336
337 if (eventType == Event.PROPERTY_ADDED || eventType == Event.PROPERTY_CHANGED || eventType == Event.PROPERTY_REMOVED) {
338 Optional<ResourceOriginChange> bypassStateChange = tryDetectBypassPropertyChange(relatedNode, name, eventType);
339 if (bypassStateChange.isPresent()) {
340 changes.put(bypassStateChange.get().getRelatedResourcePath(), bypassStateChange.get());
341 continue;
342 }
343 }
344
345
346 changes.put(relatedNode, builder.at(relatedNode).ofType(MODIFIED).build());
347 }
348 } catch (RepositoryException e) {
349 log.error("Failed to process event [{}] due to: {}", event, e.getMessage(), e);
350 }
351 }
352
353 for (final ResourceOriginChange change : changes.values()) {
354 if (change.getType() == MODIFIED || change.getType() == ADDED) {
355 if (getNode(change.getRelatedResourcePath()).map(JcrResourceOrigin.this::isBypassed).orElse(false)) {
356 continue;
357 }
358 }
359
360 dispatchResourceChange(change);
361 }
362 systemContextProvider.get().release();
363 }
364
365 private Optional<ResourceOriginChange> tryDetectBypassPropertyChange(String relatedNodePath, String name, int eventType) {
366 return getNode(relatedNodePath)
367 .filter(any -> BYPASS_PROPERTY.equals(name))
368 .map(resourceNode -> PropertyUtil.getBoolean(resourceNode, BYPASS_PROPERTY, false))
369 .map(bypassed -> {
370 if (bypassed) {
371 return resourceChange().at(relatedNodePath).inOrigin(JcrResourceOrigin.this).ofType(REMOVED).build();
372 } else if (eventType != Event.PROPERTY_ADDED) {
373 return resourceChange().at(relatedNodePath).inOrigin(JcrResourceOrigin.this).ofType(ADDED).build();
374 }
375 return null;
376 });
377 }
378 }
379
380
381
382
383
384 private final static org.apache.jackrabbit.commons.predicate.Predicate EVENT_LISTENER_FILTER = eventObject -> {
385 if (!JCR_SYSTEM_EXCLUDING_PREDICATE.evaluate(eventObject)) {
386 return false;
387 }
388
389 final Event event = (Event) eventObject;
390 try {
391 final String relatedPath = event.getPath();
392 return !relatedPath.endsWith(SLASH + NodeTypes.LastModified.LAST_MODIFIED) && !relatedPath.endsWith(SLASH + NodeTypes.LastModified.LAST_MODIFIED_BY);
393 } catch (RepositoryException e) {
394 return false;
395 }
396 };
397 }