View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.web.jcr.webdav;
25  
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.security.Principal;
29  import java.util.Arrays;
30  import java.util.Calendar;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.Date;
34  import java.util.HashSet;
35  import java.util.LinkedList;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.WeakHashMap;
40  import javax.jcr.Binary;
41  import javax.jcr.Item;
42  import javax.jcr.Node;
43  import javax.jcr.NodeIterator;
44  import javax.jcr.PathNotFoundException;
45  import javax.jcr.Property;
46  import javax.jcr.RepositoryException;
47  import javax.jcr.Session;
48  import javax.servlet.http.HttpServletRequest;
49  import net.sf.webdav.ITransaction;
50  import net.sf.webdav.IWebdavStore;
51  import net.sf.webdav.StoredObject;
52  import net.sf.webdav.exceptions.WebdavException;
53  import org.modeshape.common.util.CheckArg;
54  import org.modeshape.common.util.IoUtil;
55  import org.modeshape.graph.mimetype.MimeTypeDetector;
56  import org.modeshape.mimetype.aperture.ApertureMimeTypeDetector;
57  import org.modeshape.web.jcr.RepositoryFactory;
58  
59  /**
60   * Implementation of the {@code IWebdavStore} interface that uses a JCR repository as a backing store.
61   * <p>
62   * This implementation takes several OSX-specific WebDAV workarounds from the WebDAVImpl class in Drools Guvnor.
63   * </p>
64   */
65  public class ModeShapeWebdavStore implements IWebdavStore {
66  
67      private static final String[] EMPTY_STRING_ARRAY = new String[0];
68  
69      private static final ThreadLocal<HttpServletRequest> THREAD_LOCAL_REQUEST = new ThreadLocal<HttpServletRequest>();
70  
71      /** OSX workaround */
72      private static final Map<String, byte[]> OSX_DOUBLE_DATA = Collections.synchronizedMap(new WeakHashMap<String, byte[]>());
73  
74      private static final String CONTENT_NODE_NAME = "jcr:content";
75      private static final String DATA_PROP_NAME = "jcr:data";
76      private static final String CREATED_PROP_NAME = "jcr:created";
77      private static final String MODIFIED_PROP_NAME = "jcr:lastModified";
78      private static final String ENCODING_PROP_NAME = "jcr:encoding";
79      private static final String MIME_TYPE_PROP_NAME = "jcr:mimeType";
80  
81      private static final String DEFAULT_CONTENT_PRIMARY_TYPES = "nt:resource, mode:resource";
82      private static final String DEFAULT_RESOURCE_PRIMARY_TYPES = "nt:file";
83      private static final String DEFAULT_NEW_FOLDER_PRIMARY_TYPE = "nt:folder";
84      private static final String DEFAULT_NEW_RESOURCE_PRIMARY_TYPE = "nt:file";
85      private static final String DEFAULT_NEW_CONTENT_PRIMARY_TYPE = "mode:resource";
86  
87      private final Collection<String> contentPrimaryTypes;
88      private final Collection<String> filePrimaryTypes;
89      private final String newFolderPrimaryType;
90      private final String newResourcePrimaryType;
91      private final String newContentPrimaryType;
92      private final MimeTypeDetector mimeTypeDetector = new ApertureMimeTypeDetector();
93      private final RequestResolver uriResolver;
94  
95      public ModeShapeWebdavStore( RequestResolver uriResolver ) {
96          this(null, null, null, null, null, uriResolver);
97      }
98  
99      public ModeShapeWebdavStore( String contentPrimaryTypes,
100                                  String filePrimaryTypes,
101                                  String newFolderPrimaryType,
102                                  String newResourcePrimaryType,
103                                  String newContentPrimaryType,
104                                  RequestResolver uriResolver ) {
105         super();
106         this.contentPrimaryTypes = split(contentPrimaryTypes != null ? contentPrimaryTypes : DEFAULT_CONTENT_PRIMARY_TYPES);
107         this.filePrimaryTypes = split(filePrimaryTypes != null ? filePrimaryTypes : DEFAULT_RESOURCE_PRIMARY_TYPES);
108         this.newFolderPrimaryType = newFolderPrimaryType != null ? newFolderPrimaryType : DEFAULT_NEW_FOLDER_PRIMARY_TYPE;
109         this.newResourcePrimaryType = newResourcePrimaryType != null ? newResourcePrimaryType : DEFAULT_NEW_RESOURCE_PRIMARY_TYPE;
110         this.newContentPrimaryType = newContentPrimaryType != null ? newContentPrimaryType : DEFAULT_NEW_CONTENT_PRIMARY_TYPE;
111 
112         this.uriResolver = uriResolver;
113     }
114 
115     /**
116      * Returns an unmodifiable set containing the elements passed in to this method
117      * 
118      * @param elements a set of elements; may not be null
119      * @return an unmodifiable set containing all of the elements in {@code elements}; never null
120      */
121     private static final Set<String> setFor( String... elements ) {
122         Set<String> set = new HashSet<String>(elements.length);
123         set.addAll(Arrays.asList(elements));
124 
125         return set;
126     }
127 
128     /**
129      * Splits a comma-delimited string into an unmodifiable set containing the substrings between the commas in the source string.
130      * The elements in the set will be {@link String#trim() trimmed}.
131      * 
132      * @param commaDelimitedString input string; may not be null, but need not contain any commas
133      * @return an unmodifiable set whose elements are the trimmed substrings of the source string; never null
134      */
135     private static final Set<String> split( String commaDelimitedString ) {
136         return setFor(commaDelimitedString.split("\\s*,\\s*"));
137     }
138 
139     /**
140      * Updates thread-local storage for the current thread to reference the given request.
141      * 
142      * @param request the request to store in thread-local storage; null to clear the storage
143      */
144     static final void setRequest( HttpServletRequest request ) {
145         THREAD_LOCAL_REQUEST.set(request);
146     }
147 
148     /**
149      * {@inheritDoc}
150      */
151     @Override
152     public ITransaction begin( Principal principal ) {
153         return new JcrSessionTransaction(principal);
154     }
155 
156     /**
157      * {@inheritDoc}
158      */
159     @Override
160     public void commit( ITransaction transaction ) {
161         CheckArg.isNotNull(transaction, "transaction");
162 
163         assert transaction instanceof JcrSessionTransaction;
164         ((JcrSessionTransaction)transaction).commit();
165     }
166 
167     /**
168      * {@inheritDoc}
169      */
170     @Override
171     public void rollback( ITransaction transaction ) {
172         // No op. By not saving the session, we will let the session expire without committing any changes
173 
174     }
175 
176     /**
177      * {@inheritDoc}
178      */
179     @Override
180     public void checkAuthentication( ITransaction transaction ) {
181         // No op.
182     }
183 
184     /**
185      * {@inheritDoc}
186      */
187     @Override
188     public void createFolder( ITransaction transaction,
189                               String folderUri ) {
190         int ind = folderUri.lastIndexOf('/');
191         String parentUri = folderUri.substring(0, ind + 1);
192         String resourceName = folderUri.substring(ind + 1);
193 
194         try {
195             Node parentNode = nodeFor(transaction, parentUri);
196             parentNode.addNode(resourceName, newFolderPrimaryType);
197 
198         } catch (RepositoryException re) {
199             throw new WebdavException(re);
200         }
201     }
202 
203     /**
204      * {@inheritDoc}
205      */
206     @Override
207     public void createResource( ITransaction transaction,
208                                 String resourceUri ) {
209         // Mac OS X workaround from Drools Guvnor
210         if (resourceUri.endsWith(".DS_Store")) return;
211 
212         int ind = resourceUri.lastIndexOf('/');
213         String parentUri = resourceUri.substring(0, ind + 1);
214         String resourceName = resourceUri.substring(ind + 1);
215 
216         // Mac OS X workaround from Drools Guvnor
217         if (resourceName.startsWith("._")) {
218             OSX_DOUBLE_DATA.put(resourceUri, null);
219             return;
220         }
221 
222         try {
223             Node parentNode = nodeFor(transaction, parentUri);
224             Node resourceNode = parentNode.addNode(resourceName, newResourcePrimaryType);
225 
226             Node contentNode = resourceNode.addNode(CONTENT_NODE_NAME, newContentPrimaryType);
227             contentNode.setProperty(DATA_PROP_NAME, "");
228             contentNode.setProperty(MODIFIED_PROP_NAME, Calendar.getInstance());
229             contentNode.setProperty(ENCODING_PROP_NAME, "UTF-8");
230             contentNode.setProperty(MIME_TYPE_PROP_NAME, "text/plain");
231 
232         } catch (RepositoryException re) {
233             throw new WebdavException(re);
234         }
235 
236     }
237 
238     /**
239      * {@inheritDoc}
240      */
241     @Override
242     public String[] getChildrenNames( ITransaction transaction,
243                                       String folderUri ) {
244         try {
245             Node node = nodeFor(transaction, folderUri);
246 
247             if (isFile(node) || isContent(node)) {
248                 return null;
249             }
250 
251             List<String> children = new LinkedList<String>();
252             for (NodeIterator iter = node.getNodes(); iter.hasNext();) {
253                 children.add(iter.nextNode().getName());
254             }
255 
256             return children.toArray(EMPTY_STRING_ARRAY);
257         } catch (RepositoryException re) {
258             throw new WebdavException(re);
259         }
260     }
261 
262     /**
263      * {@inheritDoc}
264      */
265     @Override
266     public InputStream getResourceContent( ITransaction transaction,
267                                            String resourceUri ) {
268         try {
269             Node node = nodeFor(transaction, resourceUri);
270 
271             if (!isFile(node)) {
272                 return null;
273             }
274 
275             if (!node.hasNode(CONTENT_NODE_NAME)) {
276                 return null;
277             }
278 
279             return node.getProperty(CONTENT_NODE_NAME + "/" + DATA_PROP_NAME).getBinary().getStream();
280 
281         } catch (RepositoryException re) {
282             throw new WebdavException(re);
283         }
284     }
285 
286     /**
287      * {@inheritDoc}
288      */
289     @Override
290     public long getResourceLength( ITransaction transaction,
291                                    String path ) {
292         try {
293             Node node = nodeFor(transaction, path);
294 
295             if (!isFile(node)) {
296                 return -1;
297             }
298 
299             if (!node.hasNode(CONTENT_NODE_NAME)) {
300                 return -1;
301             }
302 
303             Property contentProp = node.getProperty(CONTENT_NODE_NAME + "/" + DATA_PROP_NAME);
304             long length = contentProp.getLength();
305             if (length != -1) return length;
306 
307             String data = contentProp.getString();
308             return data.length();
309 
310         } catch (RepositoryException re) {
311             throw new WebdavException(re);
312         }
313     }
314 
315     /**
316      * {@inheritDoc}
317      */
318     @Override
319     public StoredObject getStoredObject( ITransaction transaction,
320                                          String uri ) {
321         if (uri.length() == 0) uri = "/";
322 
323         StoredObject ob = new StoredObject();
324         try {
325             Node node = nodeFor(transaction, uri);
326 
327             if (isContent(node)) {
328                 return null;
329             }
330 
331             if (!isFile(node)) {
332                 ob.setFolder(true);
333                 ob.setCreationDate(new Date());
334                 ob.setLastModified(new Date());
335                 ob.setResourceLength(0);
336 
337             } else if (node.hasNode(CONTENT_NODE_NAME)) {
338                 Node content = node.getNode(CONTENT_NODE_NAME);
339 
340                 ob.setFolder(false);
341                 Date createDate;
342                 if (node.hasProperty(CREATED_PROP_NAME)) {
343                     createDate = node.getProperty(CREATED_PROP_NAME).getDate().getTime();
344                 } else {
345                     createDate = new Date();
346                 }
347                 ob.setCreationDate(createDate);
348                 ob.setLastModified(content.getProperty(MODIFIED_PROP_NAME).getDate().getTime());
349                 ob.setResourceLength(content.getProperty(DATA_PROP_NAME).getLength());
350             } else {
351                 ob.setNullResource(true);
352             }
353 
354         } catch (PathNotFoundException pnfe) {
355             return null;
356         } catch (RepositoryException re) {
357             throw new WebdavException(re);
358         }
359         return ob;
360     }
361 
362     /**
363      * {@inheritDoc}
364      */
365     @Override
366     public void removeObject( ITransaction transaction,
367                               String uri ) {
368         int ind = uri.lastIndexOf('/');
369         String resourceName = uri.substring(ind + 1);
370 
371         // Mac OS X workaround from Drools Guvnor
372         if (resourceName.startsWith("._")) {
373             OSX_DOUBLE_DATA.put(uri, null);
374             return;
375         }
376 
377         try {
378             nodeFor(transaction, uri).remove();
379         } catch (PathNotFoundException pnfe) {
380             // Return silently
381         } catch (RepositoryException re) {
382             throw new WebdavException(re);
383         }
384     }
385 
386     /**
387      * {@inheritDoc}
388      */
389     @Override
390     public long setResourceContent( ITransaction transaction,
391                                     String resourceUri,
392                                     InputStream content,
393                                     String contentType,
394                                     String characterEncoding ) {
395         // Mac OS X workaround from Drools Guvnor
396         if (resourceUri.endsWith(".DS_Store")) return 0;
397 
398         int ind = resourceUri.lastIndexOf('/');
399         String resourceName = resourceUri.substring(ind + 1);
400 
401         // Mac OS X workaround from Drools Guvnor
402         if (resourceName.startsWith("._")) {
403             try {
404                 OSX_DOUBLE_DATA.put(resourceUri, IoUtil.readBytes(content));
405             } catch (IOException e) {
406                 throw new RuntimeException(e);
407             }
408             return 0;
409         }
410 
411         try {
412             Node node = nodeFor(transaction, resourceUri);
413             if (!isFile(node)) {
414                 return -1;
415             }
416 
417             Node contentNode;
418             if (node.hasNode(CONTENT_NODE_NAME)) {
419                 contentNode = node.getNode(CONTENT_NODE_NAME);
420             } else {
421                 contentNode = node.addNode(CONTENT_NODE_NAME, newContentPrimaryType);
422             }
423 
424             // contentNode.setProperty(MIME_TYPE_PROP_NAME, contentType != null ? contentType : "application/octet-stream");
425             contentNode.setProperty(ENCODING_PROP_NAME, characterEncoding != null ? characterEncoding : "UTF-8");
426             Binary binary = node.getSession().getValueFactory().createBinary(content);
427             contentNode.setProperty(DATA_PROP_NAME, binary);
428             contentNode.setProperty(MODIFIED_PROP_NAME, Calendar.getInstance());
429 
430             // Copy the content to the property, THEN re-read the content from the Binary value to avoid discaring the first
431             // bytes of the stream
432             if (contentType == null) {
433                 contentType = mimeTypeDetector.mimeTypeOf(resourceName, binary.getStream());
434             }
435 
436             return contentNode.getProperty(DATA_PROP_NAME).getLength();
437         } catch (RepositoryException re) {
438             throw new WebdavException(re);
439         } catch (IOException ioe) {
440             throw new WebdavException(ioe);
441         } catch (RuntimeException t) {
442             throw t;
443         }
444     }
445 
446     /**
447      * @param node the node to check
448      * @return true if {@code node}'s primary type is one of the types in {@link #contentPrimaryTypes}; may not be null
449      * @throws RepositoryException if an error occurs checking the node's primary type
450      */
451     private boolean isContent( Node node ) throws RepositoryException {
452         for (String nodeType : contentPrimaryTypes) {
453             if (node.isNodeType(nodeType)) return true;
454         }
455 
456         return false;
457     }
458 
459     /**
460      * @param node the node to check
461      * @return true if {@code node}'s primary type is one of the types in {@link #filePrimaryTypes}; may not be null
462      * @throws RepositoryException if an error occurs checking the node's primary type
463      */
464     private boolean isFile( Node node ) throws RepositoryException {
465         for (String nodeType : filePrimaryTypes) {
466             if (node.isNodeType(nodeType)) return true;
467         }
468 
469         return false;
470     }
471 
472     /**
473      * @param transaction the active transaction; may not be null
474      * @param uri the uri from which the node should be read; never null
475      * @return the node at the given uri; never null
476      * @throws WebdavException if the uri references a property instead of a node
477      * @throws RepositoryException if an error occurs accessing the repository or no {@link Item} exists at the given uri
478      */
479     private final Node nodeFor( ITransaction transaction,
480                                 String uri ) throws RepositoryException {
481         return ((JcrSessionTransaction)transaction).nodeFor(uri);
482     }
483 
484     protected final RequestResolver uriResolver() {
485         return uriResolver;
486     }
487 
488     /**
489      * Implementation of the {@link ITransaction} interface that uses a {@link Session JCR session} to load and store webdav
490      * content. The session also provides support for transactional access to the underlying store.
491      */
492     class JcrSessionTransaction implements ITransaction {
493 
494         private final Principal principal;
495         private final Session session;
496         private final UriResolver uriResolver;
497 
498         @SuppressWarnings( "synthetic-access" )
499         JcrSessionTransaction( Principal principal ) {
500             super();
501             this.principal = principal;
502 
503             HttpServletRequest request = THREAD_LOCAL_REQUEST.get();
504 
505             if (request == null) {
506                 throw new WebdavException(WebdavI18n.noStoredRequest.text());
507             }
508 
509             try {
510                 ResolvedRequest resolvedRequest = uriResolver().resolve(request);
511 
512                 this.session = RepositoryFactory.getSession(request,
513                                                             resolvedRequest.getRepositoryName(),
514                                                             resolvedRequest.getWorkspaceName());
515                 this.uriResolver = resolvedRequest.getUriResolver();
516                 assert session != null;
517             } catch (RepositoryException re) {
518                 throw new WebdavException(re);
519             }
520         }
521 
522         /**
523          * @return the session associated with this transaction; never null
524          */
525         Session session() {
526             return this.session;
527         }
528 
529         Node nodeFor( String uri ) throws RepositoryException {
530             String resolvedUri = uriResolver.resolve(uri);
531             Item item = session.getItem(resolvedUri);
532             if (item instanceof Property) {
533                 throw new WebdavException();
534             }
535 
536             return (Node)item;
537 
538         }
539 
540         /**
541          * {@inheritDoc}
542          */
543         @Override
544         public Principal getPrincipal() {
545             return principal;
546         }
547 
548         /**
549          * Commits any pending changes to the underlying store.
550          */
551         void commit() {
552             try {
553                 this.session.save();
554             } catch (RepositoryException re) {
555                 throw new WebdavException(re);
556             }
557         }
558     }
559 }