View Javadoc
1   /**
2    * This file Copyright (c) 2015 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.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   * A {@link info.magnolia.resourceloader.ResourceOrigin} which loads resources from JCR.
81   * TODO impl or use 2 origins:
82   * 1) "new" with good node types and properties
83   * 2) legacy with warnings and funky handling of shit
84   * TODO avoid !bypass double negatives --> isEnabled
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         // Make sure resource is always either file or directory
137         return !isDirectory(resource.getNode());
138     }
139 
140     @Override
141     @SneakyThrows(RepositoryException.class)
142     protected boolean isDirectory(JcrResource resource) {
143         // TODO seems incomplete/unsafe ? what about other node types ?
144         return isDirectory(resource.getNode());
145     }
146 
147     @Override
148     protected boolean isEditable(JcrResource resource) {
149         // For now we naively consider that any resource in the JcrResourceOrigin will be editable
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         // TODO Compare: return resource.getNode().getProperty(JcrConstants.JCR_LASTMODIFIED)
169         final Calendar lastModified = NodeTypes.LastModified.getLastModified(resource.getNode());
170         if (lastModified == null) {
171             // TODO: shouldn't this happen in info.magnolia.jcr.util.NodeTypes.LastModified.getLastModified() ?
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         // Node exists, but has no content yet (i.e empty file)
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         // assume UTF-8
236         return Charset.forName("UTF-8");
237     }
238 
239     protected JcrResource newResource(Node node) {
240         return new JcrResource(this, node);
241     }
242 
243     /**
244      * Only returns the node if it exists.
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         // Resource node is neither binary nor text when it's freshly created, only #isDirectory is deterministic
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                 // Collect changed paths from EventIterator
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                         // If a node is added or removed, signal the event for the parent
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                 // And visit ...
320                 for (final String path : changedPaths) {
321                     try {
322                         final Node node = getNode(path);
323                         // We don't check isBypassed() here because we want to apply the callback for nodes which are _now_ bypassed
324                         // TODO this is bogus... getNode() returns null if node is deleted, so we should know about this too
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                 // for JcrOriginTest#callbackIsCalledAfterResourceBecomesBypassed
339                 // TODO check whether this can be replaced by something in tests only
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 }