View Javadoc

1   package org.modeshape.connector.filesystem;
2   
3   import java.io.BufferedInputStream;
4   import java.io.File;
5   import java.io.FileInputStream;
6   import java.io.FileOutputStream;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.util.ArrayList;
10  import java.util.Arrays;
11  import java.util.Collection;
12  import java.util.Collections;
13  import java.util.HashMap;
14  import java.util.HashSet;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.Set;
18  import org.modeshape.common.i18n.I18n;
19  import org.modeshape.common.util.FileUtil;
20  import org.modeshape.common.util.IoUtil;
21  import org.modeshape.graph.ExecutionContext;
22  import org.modeshape.graph.JcrLexicon;
23  import org.modeshape.graph.JcrNtLexicon;
24  import org.modeshape.graph.Location;
25  import org.modeshape.graph.ModeShapeLexicon;
26  import org.modeshape.graph.connector.RepositorySourceException;
27  import org.modeshape.graph.connector.base.PathNode;
28  import org.modeshape.graph.connector.base.PathWorkspace;
29  import org.modeshape.graph.mimetype.MimeTypeDetector;
30  import org.modeshape.graph.property.Binary;
31  import org.modeshape.graph.property.BinaryFactory;
32  import org.modeshape.graph.property.DateTimeFactory;
33  import org.modeshape.graph.property.Name;
34  import org.modeshape.graph.property.NameFactory;
35  import org.modeshape.graph.property.NamespaceRegistry;
36  import org.modeshape.graph.property.Path;
37  import org.modeshape.graph.property.PathFactory;
38  import org.modeshape.graph.property.PathNotFoundException;
39  import org.modeshape.graph.property.Property;
40  import org.modeshape.graph.property.PropertyFactory;
41  import org.modeshape.graph.property.Path.Segment;
42  import org.modeshape.graph.request.Request;
43  
44  /**
45   * Workspace implementation for the file system connector.
46   */
47  class FileSystemWorkspace extends PathWorkspace<PathNode> {
48      private static final String DEFAULT_MIME_TYPE = "application/octet";
49      private static final Set<Name> VALID_PRIMARY_TYPES = new HashSet<Name>(Arrays.asList(new Name[] {JcrNtLexicon.FOLDER,
50          JcrNtLexicon.FILE, JcrNtLexicon.RESOURCE, ModeShapeLexicon.RESOURCE}));
51  
52      private final FileSystemSource source;
53      private final FileSystemRepository repository;
54      private final ExecutionContext context;
55      private final File workspaceRoot;
56  
57      public FileSystemWorkspace( String name,
58                                  FileSystemWorkspace originalToClone,
59                                  File workspaceRoot ) {
60          super(name, originalToClone.getRootNodeUuid());
61  
62          this.source = originalToClone.source;
63          this.context = originalToClone.context;
64          this.workspaceRoot = workspaceRoot;
65          this.repository = originalToClone.repository;
66  
67          cloneWorkspace(originalToClone);
68      }
69  
70      public FileSystemWorkspace( FileSystemRepository repository,
71                                  String name ) {
72          super(name, repository.getRootNodeUuid());
73          this.workspaceRoot = repository.getWorkspaceDirectory(name);
74          this.repository = repository;
75          this.context = repository.getContext();
76          this.source = repository.source;
77      }
78  
79      private void cloneWorkspace( FileSystemWorkspace original ) {
80          File originalRoot = repository.getWorkspaceDirectory(original.getName());
81          File newRoot = repository.getWorkspaceDirectory(this.getName());
82  
83          try {
84              FileUtil.copy(originalRoot, newRoot, source.filenameFilter());
85          } catch (IOException ioe) {
86              throw new IllegalStateException(ioe);
87          }
88      }
89  
90      @Override
91      public PathNode moveNode( PathNode node,
92                                PathNode newNode ) {
93          PathFactory pathFactory = context.getValueFactories().getPathFactory();
94          Path newPath = pathFactory.create(newNode.getParent(), newNode.getName());
95  
96          File originalFile = fileFor(pathFactory.create(node.getParent(), node.getName()));
97          File newFile = fileFor(newPath, false);
98  
99          if (newFile.exists()) {
100             newFile.delete();
101         }
102 
103         originalFile.renameTo(newFile);
104 
105         return getNode(newPath);
106     }
107 
108     @Override
109     public PathNode putNode( PathNode node ) {
110         NameFactory nameFactory = context.getValueFactories().getNameFactory();
111         PathFactory pathFactory = context.getValueFactories().getPathFactory();
112         NamespaceRegistry registry = context.getNamespaceRegistry();
113         CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
114 
115         Map<Name, Property> properties = node.getProperties();
116 
117         if (node.getParent() == null) {
118             // Root node
119             Path rootPath = pathFactory.createRootPath();
120             Location rootLocation = Location.create(rootPath, repository.getRootNodeUuid());
121             customPropertiesFactory.recordDirectoryProperties(context,
122                                                               source.getName(),
123                                                               rootLocation,
124                                                               workspaceRoot,
125                                                               node.getProperties());
126             return getNode(rootPath);
127         }
128 
129         /*
130          * Get references to java.io.Files
131          */
132         Path parentPath = node.getParent();
133         boolean isRoot = parentPath == null;
134         File parentFile = fileFor(parentPath);
135 
136         Path newPath = isRoot ? pathFactory.createRootPath() : pathFactory.create(parentPath, node.getName());
137         Name name = node.getName().getName();
138         String newName = name.getString(registry);
139         File newFile = new File(parentFile, newName);
140 
141         /*
142          * Determine the node primary type
143          */
144         Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE);
145 
146         // Default primary type to nt:folder
147         Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue());
148 
149         if (JcrNtLexicon.FILE.equals(primaryType)) {
150 
151             // The FILE node is represented by the existence of the file
152             if (!parentFile.canWrite()) {
153                 I18n msg = FileSystemI18n.parentIsReadOnly;
154                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, this.getName(), source.getName()));
155             }
156 
157             try {
158                 ensureValidPathLength(newFile);
159 
160                 // Don't try to write if the node conflict behavior is DO_NOT_REPLACE
161                 if (!newFile.exists() && !newFile.createNewFile()) {
162                     I18n msg = FileSystemI18n.fileAlreadyExists;
163                     throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
164                 }
165             } catch (IOException ioe) {
166                 I18n msg = FileSystemI18n.couldNotCreateFile;
167                 throw new RepositorySourceException(source.getName(), msg.text(parentPath,
168                                                                                getName(),
169                                                                                source.getName(),
170                                                                                ioe.getMessage()), ioe);
171             }
172 
173             customPropertiesFactory.recordFileProperties(context, source.getName(), Location.create(newPath), newFile, properties);
174         } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) {
175             assert parentFile != null;
176 
177             if (!JcrLexicon.CONTENT.equals(name)) {
178                 I18n msg = FileSystemI18n.invalidNameForResource;
179                 String nodeName = name.getString();
180                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName(), nodeName));
181             }
182 
183             if (!parentFile.isFile()) {
184                 I18n msg = FileSystemI18n.invalidPathForResource;
185                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
186             }
187 
188             if (!parentFile.canWrite()) {
189                 I18n msg = FileSystemI18n.parentIsReadOnly;
190                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
191             }
192 
193             // Copy over data into a temp file, then move it to the correct location
194             FileOutputStream fos = null;
195             try {
196                 File temp = File.createTempFile("dna", null);
197                 fos = new FileOutputStream(temp);
198 
199                 Property dataProp = properties.get(JcrLexicon.DATA);
200                 if (dataProp == null) {
201                     I18n msg = FileSystemI18n.missingRequiredProperty;
202                     String dataPropName = JcrLexicon.DATA.getString();
203                     throw new RepositorySourceException(source.getName(), msg.text(parentPath,
204                                                                                    getName(),
205                                                                                    source.getName(),
206                                                                                    dataPropName));
207                 }
208 
209                 BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
210                 Binary binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue());
211 
212                 IoUtil.write(binary.getStream(), fos);
213 
214                 if (!FileUtil.delete(parentFile)) {
215                     I18n msg = FileSystemI18n.deleteFailed;
216                     throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
217                 }
218 
219                 if (!temp.renameTo(parentFile)) {
220                     I18n msg = FileSystemI18n.couldNotUpdateData;
221                     throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
222                 }
223             } catch (IOException ioe) {
224                 I18n msg = FileSystemI18n.couldNotWriteData;
225                 throw new RepositorySourceException(source.getName(), msg.text(parentPath,
226                                                                                getName(),
227                                                                                source.getName(),
228                                                                                ioe.getMessage()), ioe);
229 
230             } finally {
231                 try {
232                     if (fos != null) fos.close();
233                 } catch (Exception ex) {
234                 }
235             }
236             customPropertiesFactory.recordResourceProperties(context,
237                                                              source.getName(),
238                                                              Location.create(parentPath),
239                                                              newFile,
240                                                              properties);
241 
242         } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) {
243             ensureValidPathLength(newFile);
244 
245             if (!newFile.exists() && !newFile.mkdir()) {
246                 I18n msg = FileSystemI18n.couldNotCreateFile;
247                 throw new RepositorySourceException(source.getName(),
248                                                     msg.text(parentPath,
249                                                              getName(),
250                                                              source.getName(),
251                                                              primaryType == null ? "null" : primaryType.getString(registry)));
252             }
253             customPropertiesFactory.recordDirectoryProperties(context,
254                                                               source.getName(),
255                                                               Location.create(newPath),
256                                                               newFile,
257                                                               properties);
258 
259         } else {
260             // Set error and return
261             I18n msg = FileSystemI18n.unsupportedPrimaryType;
262             throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry),
263                                                                            parentPath,
264                                                                            getName(),
265                                                                            source.getName()));
266         }
267 
268         node = getNode(newPath);
269 
270         return node;
271     }
272 
273     @Override
274     public PathNode removeNode( Path nodePath ) {
275         File nodeFile;
276 
277         if (!nodePath.isRoot() && JcrLexicon.CONTENT.equals(nodePath.getLastSegment().getName())) {
278             nodeFile = fileFor(nodePath.getParent());
279             if (!nodeFile.exists()) return null;
280 
281             FileOutputStream fos = null;
282             try {
283                 fos = new FileOutputStream(nodeFile);
284                 IoUtil.write("", fos);
285             } catch (IOException ioe) {
286                 throw new RepositorySourceException(source.getName(), FileSystemI18n.deleteFailed.text(nodePath,
287                                                                                                        getName(),
288                                                                                                        source.getName()));
289             } finally {
290                 if (fos != null) try {
291                     fos.close();
292                 } catch (IOException ioe) {
293                 }
294             }
295         } else {
296             nodeFile = fileFor(nodePath);
297             if (!nodeFile.exists()) return null;
298 
299             FileUtil.delete(nodeFile);
300         }
301 
302         return null;
303     }
304 
305     @Override
306     public PathNode getRootNode() {
307         return getNode(context.getValueFactories().getPathFactory().createRootPath());
308     }
309 
310     @Override
311     public PathNode getNode( Path path ) {
312         Map<Name, Property> properties = new HashMap<Name, Property>();
313 
314         PropertyFactory factory = context.getPropertyFactory();
315         PathFactory pathFactory = context.getValueFactories().getPathFactory();
316         DateTimeFactory dateFactory = context.getValueFactories().getDateFactory();
317         MimeTypeDetector mimeTypeDetector = context.getMimeTypeDetector();
318         CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
319         NamespaceRegistry registry = context.getNamespaceRegistry();
320         Location location = Location.create(path);
321 
322         if (!path.isRoot() && JcrLexicon.CONTENT.equals(path.getLastSegment().getName())) {
323             File file = fileFor(path.getParent());
324             if (file == null) return null;
325             // Discover the mime type ...
326             String mimeType = null;
327             InputStream contents = null;
328             try {
329                 contents = new BufferedInputStream(new FileInputStream(file));
330                 mimeType = mimeTypeDetector.mimeTypeOf(file.getName(), contents);
331                 if (mimeType == null) mimeType = DEFAULT_MIME_TYPE;
332                 properties.put(JcrLexicon.MIMETYPE, factory.create(JcrLexicon.MIMETYPE, mimeType));
333             } catch (IOException e) {
334                 I18n msg = FileSystemI18n.couldNotReadData;
335                 throw new RepositorySourceException(source.getName(), msg.text(source.getName(),
336                                                                                getName(),
337                                                                                path.getString(registry)));
338             } finally {
339                 if (contents != null) {
340                     try {
341                         contents.close();
342                     } catch (IOException e) {
343                     }
344                 }
345             }
346 
347             // First add any custom properties ...
348             Collection<Property> customProps = customPropertiesFactory.getResourceProperties(context, location, file, mimeType);
349             for (Property customProp : customProps) {
350                 properties.put(customProp.getName(), customProp);
351             }
352 
353             // The request is to get properties of the "jcr:content" child node ...
354             // ... use the dna:resource node type. This is the same as nt:resource, but is not referenceable
355             // since we cannot assume that we control all access to this file and can track its movements
356             properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE));
357             properties.put(JcrLexicon.LAST_MODIFIED, factory.create(JcrLexicon.LAST_MODIFIED,
358                                                                     dateFactory.create(file.lastModified())));
359             // Don't really know the encoding, either ...
360             // request.addProperty(factory.create(JcrLexicon.ENCODED, stringFactory.create("UTF-8")));
361 
362             // Now put the file's content into the "jcr:data" property ...
363             BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
364             properties.put(JcrLexicon.DATA, factory.create(JcrLexicon.DATA, binaryFactory.create(file)));
365             // return new PathNode(path, null, properties, Collections.<Segment>emptyList());
366             return new PathNode(null, path.getParent(), path.getLastSegment(), properties, Collections.<Segment>emptyList());
367         }
368 
369         File file = fileFor(path);
370         if (file == null) return null;
371 
372         if (file.isDirectory()) {
373             String[] childNames = file.list(source.filenameFilter());
374             Arrays.sort(childNames);
375 
376             List<Segment> childSegments = new ArrayList<Segment>(childNames.length);
377             for (String childName : childNames) {
378                 childSegments.add(pathFactory.createSegment(childName));
379             }
380 
381             Collection<Property> customProps = customPropertiesFactory.getDirectoryProperties(context, location, file);
382             for (Property customProp : customProps) {
383                 properties.put(customProp.getName(), customProp);
384             }
385 
386             if (path.isRoot()) {
387                 properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.ROOT));
388                 // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments);
389                 return new PathNode(source.getRootNodeUuidObject(), path.getParent(), path.getLastSegment(), properties,
390                                     childSegments);
391 
392             }
393             properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER));
394             // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments);
395             return new PathNode(null, path.getParent(), path.getLastSegment(), properties, childSegments);
396 
397         }
398 
399         Collection<Property> customProps = customPropertiesFactory.getFileProperties(context, location, file);
400         for (Property customProp : customProps) {
401             properties.put(customProp.getName(), customProp);
402         }
403         properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE));
404         properties.put(JcrLexicon.CREATED, factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified())));
405         // node = new DefaultPathNode(path, null, properties,
406         // Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT)));
407         return new PathNode(null, path.getParent(), path.getLastSegment(), properties,
408                             Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT)));
409     }
410     /**
411      * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
412      * valid.
413      * <p>
414      * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
415      * <code>jcr:primaryType</code>" of "<code>nt:file</code>"; and a child node with the name "<code>jcr:content</code> " and a "
416      * <code>jcr:primaryType</code>" of "<code>nt:resource</code>". The parent "<code>nt:file</code>" node and its properties
417      * represents the file itself, whereas the child "<code>nt:resource</code>" node and its properties represent the content of
418      * the file.
419      * </p>
420      * <p>
421      * As such, this method will return the File object for paths representing both the parent "<code>nt:file</code> " and child "
422      * <code>nt:resource</code>" node.
423      * </p>
424      * 
425      * @param path
426      * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
427      *         {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
428      */
429     protected File fileFor( Path path ) {
430         return fileFor(path, true);
431     }
432 
433     /**
434      * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
435      * valid.
436      * <p>
437      * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
438      * <code>jcr:primaryType</code>" of "<code>nt:file</code>"; and a child node with the name "<code>jcr:content</code> " and a "
439      * <code>jcr:primaryType</code>" of "<code>nt:resource</code>". The parent "<code>nt:file</code>" node and its properties
440      * represents the file itself, whereas the child "<code>nt:resource</code>" node and its properties represent the content of
441      * the file.
442      * </p>
443      * <p>
444      * As such, this method will return the File object for paths representing both the parent "<code>nt:file</code> " and child "
445      * <code>nt:resource</code>" node.
446      * </p>
447      * 
448      * @param path
449      * @param existingFilesOnly
450      * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
451      *         {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
452      */
453     protected File fileFor( Path path,
454                             boolean existingFilesOnly ) {
455         if (path == null || path.isRoot()) {
456             return workspaceRoot;
457         }
458         // See if the path is a "jcr:content" node ...
459         if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
460             // We only want to use the parent path to find the actual file ...
461             path = path.getParent();
462         }
463         File file = workspaceRoot;
464         for (Path.Segment segment : path) {
465             String localName = segment.getName().getLocalName();
466             // Verify the segment is valid ...
467             if (segment.getIndex() > 1) {
468                 I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed;
469                 throw new RepositorySourceException(source.getName(), msg.text(source.getName()));
470             }
471 
472             String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri();
473             if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) {
474                 I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed;
475                 throw new RepositorySourceException(source.getName(), msg.text(source.getName()));
476             }
477 
478             // The segment should exist as a child of the file ...
479             file = new File(file, localName);
480 
481             if (existingFilesOnly && (!file.canRead() || !file.exists())) {
482                 return null;
483             }
484         }
485         assert file != null;
486         return file;
487     }
488 
489     protected void validate( PathNode node ) {
490         // Don't validate the root node
491         if (node.getParent() == null) return;
492 
493         NameFactory nameFactory = context.getValueFactories().getNameFactory();
494         Map<Name, Property> properties = node.getProperties();
495         Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE);
496         Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue());
497 
498         if (!VALID_PRIMARY_TYPES.contains(primaryType)) {
499             // Set error and return
500             I18n msg = FileSystemI18n.unsupportedPrimaryType;
501             NamespaceRegistry registry = context.getNamespaceRegistry();
502             Path parentPath = node.getParent();
503             throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry),
504                                                                            parentPath,
505                                                                            getName(),
506                                                                            source.getName()));
507 
508         }
509 
510         Path nodePath = context.getValueFactories().getPathFactory().create(node.getParent(), node.getName());
511         ensureValidPathLength(fileFor(nodePath, false));
512     }
513 
514     protected void ensureValidPathLength( File file ) {
515         ensureValidPathLength(file, 0);
516     }
517 
518     /**
519      * Recursively checks if any of the files in the tree rooted at {@code root} would exceed the
520      * {@link FileSystemSource#getMaxPathLength() maximum path length for the processor} if their paths were {@code delta}
521      * characters longer. If any files would exceed this length, a {@link RepositorySourceException} is thrown.
522      * 
523      * @param root the root of the tree to check; may be a file or directory but may not be null
524      * @param delta the change in the length of the path to check. Used to preemptively check whether moving a file or directory
525      *        to a new path would violate path length rules
526      * @throws RepositorySourceException if any files in the tree rooted at {@code root} would exceed this
527      *         {@link FileSystemSource#getMaxPathLength() the maximum path length for this processor}
528      */
529     protected void ensureValidPathLength( File root,
530                                           int delta ) {
531         try {
532             int len = root.getCanonicalPath().length();
533             if (len > source.getMaxPathLength() - delta) {
534                 String msg = FileSystemI18n.maxPathLengthExceeded.text(source.getMaxPathLength(),
535                                                                        source.getName(),
536                                                                        root.getCanonicalPath(),
537                                                                        delta);
538                 throw new RepositorySourceException(source.getName(), msg);
539             }
540 
541             if (root.isDirectory()) {
542                 for (File child : root.listFiles(source.filenameFilter())) {
543                     ensureValidPathLength(child, delta);
544                 }
545 
546             }
547         } catch (IOException ioe) {
548             throw new RepositorySourceException(source.getName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe);
549         }
550     }
551 
552 }