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 java.util.concurrent.TimeUnit;
19  import org.modeshape.common.i18n.I18n;
20  import org.modeshape.common.util.FileUtil;
21  import org.modeshape.common.util.IoUtil;
22  import org.modeshape.common.util.Logger;
23  import org.modeshape.graph.ExecutionContext;
24  import org.modeshape.graph.JcrLexicon;
25  import org.modeshape.graph.JcrNtLexicon;
26  import org.modeshape.graph.Location;
27  import org.modeshape.graph.ModeShapeLexicon;
28  import org.modeshape.graph.connector.RepositorySourceException;
29  import org.modeshape.graph.connector.base.PathNode;
30  import org.modeshape.graph.connector.base.PathWorkspace;
31  import org.modeshape.graph.mimetype.MimeTypeDetector;
32  import org.modeshape.graph.property.Binary;
33  import org.modeshape.graph.property.BinaryFactory;
34  import org.modeshape.graph.property.DateTimeFactory;
35  import org.modeshape.graph.property.Name;
36  import org.modeshape.graph.property.NameFactory;
37  import org.modeshape.graph.property.NamespaceRegistry;
38  import org.modeshape.graph.property.Path;
39  import org.modeshape.graph.property.PathFactory;
40  import org.modeshape.graph.property.PathNotFoundException;
41  import org.modeshape.graph.property.Property;
42  import org.modeshape.graph.property.PropertyFactory;
43  import org.modeshape.graph.property.ValueFactory;
44  import org.modeshape.graph.property.Path.Segment;
45  import org.modeshape.graph.property.basic.FileSystemBinary;
46  import org.modeshape.graph.request.Request;
47  
48  /**
49   * Workspace implementation for the file system connector.
50   */
51  class FileSystemWorkspace extends PathWorkspace<PathNode> {
52      private static final Map<Name, Property> NO_PROPERTIES = Collections.emptyMap();
53      private static final String DEFAULT_MIME_TYPE = "application/octet";
54      private static final Set<Name> VALID_PRIMARY_TYPES = new HashSet<Name>(Arrays.asList(new Name[] {JcrNtLexicon.FOLDER,
55          JcrNtLexicon.FILE, JcrNtLexicon.RESOURCE, ModeShapeLexicon.RESOURCE}));
56  
57      private final FileSystemSource source;
58      private final FileSystemRepository repository;
59      private final ExecutionContext context;
60      private final File workspaceRoot;
61      private final boolean eagerLoading;
62      private final boolean contentUsedToDetermineMimeType;
63      private final Logger logger;
64      private final ValueFactory<String> stringFactory;
65      private final NameFactory nameFactory;
66  
67      public FileSystemWorkspace( String name,
68                                  FileSystemWorkspace originalToClone,
69                                  File workspaceRoot ) {
70          super(name, originalToClone.getRootNodeUuid());
71  
72          this.source = originalToClone.source;
73          this.context = originalToClone.context;
74          this.workspaceRoot = workspaceRoot;
75          this.repository = originalToClone.repository;
76          this.eagerLoading = this.source.isEagerFileLoading();
77          this.contentUsedToDetermineMimeType = this.source.isContentUsedToDetermineMimeType();
78          this.logger = Logger.getLogger(getClass());
79          this.stringFactory = context.getValueFactories().getStringFactory();
80          this.nameFactory = context.getValueFactories().getNameFactory();
81  
82          cloneWorkspace(originalToClone);
83      }
84  
85      public FileSystemWorkspace( FileSystemRepository repository,
86                                  String name ) {
87          super(name, repository.getRootNodeUuid());
88          this.workspaceRoot = repository.getWorkspaceDirectory(name);
89          this.repository = repository;
90          this.context = repository.getContext();
91          this.source = repository.source;
92          this.eagerLoading = this.source.isEagerFileLoading();
93          this.contentUsedToDetermineMimeType = this.source.isContentUsedToDetermineMimeType();
94          this.logger = Logger.getLogger(getClass());
95          this.stringFactory = context.getValueFactories().getStringFactory();
96          this.nameFactory = context.getValueFactories().getNameFactory();
97      }
98  
99      private void cloneWorkspace( FileSystemWorkspace original ) {
100         File originalRoot = repository.getWorkspaceDirectory(original.getName());
101         File newRoot = repository.getWorkspaceDirectory(this.getName());
102 
103         try {
104             FileUtil.copy(originalRoot, newRoot, source.filenameFilter(false));
105         } catch (IOException ioe) {
106             throw new IllegalStateException(ioe);
107         }
108     }
109 
110     private void moveFile( File originalFileOrDirectory,
111                            File newFileOrDirectory ) {
112         if (originalFileOrDirectory.renameTo(newFileOrDirectory)) return;
113 
114         /*
115          * This could fail if the originalFile and newFile are on different file systems.  See
116          * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4073756.  Try to do a copy and delete to
117          * work around this potential issue. 
118          */
119         try {
120             FileUtil.copy(originalFileOrDirectory, newFileOrDirectory);
121             FileUtil.delete(originalFileOrDirectory);
122         } catch (IOException ioe) {
123             throw new RepositorySourceException(FileSystemI18n.couldNotCopyData.text(source.getName(),
124                                                                                  originalFileOrDirectory.getAbsolutePath(),
125                                                                                  newFileOrDirectory.getAbsolutePath()), ioe);
126         }
127 
128     }
129 
130     @Override
131     public PathNode moveNode( PathNode node,
132                               PathNode newNode ) {
133         PathFactory pathFactory = context.getValueFactories().getPathFactory();
134         Path newPath = pathFactory.create(newNode.getParent(), newNode.getName());
135         Path oldPath = pathFactory.create(node.getParent(), node.getName());
136         File originalFile = fileFor(oldPath);
137         File newFile = fileFor(newPath, false);
138 
139         if (newFile.exists()) {
140             newFile.delete();
141         }
142 
143         // Read the custom properties ...
144         CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
145         Collection<Property> existingProps = null;
146         Collection<Property> existingResourceProps = null;
147         String sourceName = source.getName();
148         Location originalLocation = Location.create(oldPath);
149         if (originalFile.isDirectory()) {
150             existingProps = customPropertiesFactory.getDirectoryProperties(context, originalLocation, originalFile);
151             customPropertiesFactory.recordDirectoryProperties(context, sourceName, originalLocation, originalFile, NO_PROPERTIES);
152         } else {
153             Path resourcePath = pathFactory.create(oldPath, JcrLexicon.CONTENT);
154             Location originalResourceLocation = Location.create(resourcePath);
155             existingProps = customPropertiesFactory.getFileProperties(context, originalLocation, originalFile);
156             existingResourceProps = customPropertiesFactory.getResourceProperties(context,
157                                                                                   originalResourceLocation,
158                                                                                   originalFile,
159                                                                                   null);
160             customPropertiesFactory.recordFileProperties(context, sourceName, originalLocation, originalFile, NO_PROPERTIES);
161             customPropertiesFactory.recordResourceProperties(context,
162                                                              sourceName,
163                                                              originalResourceLocation,
164                                                              originalFile,
165                                                              NO_PROPERTIES);
166         }
167 
168         moveFile(originalFile, newFile);
169 
170         // Set the custom properties on the new location ...
171         Location newLocation = Location.create(newPath);
172         if (originalFile.isDirectory()) {
173             customPropertiesFactory.recordDirectoryProperties(context,
174                                                               sourceName,
175                                                               newLocation,
176                                                               newFile,
177                                                               extraFolder(mapOf(existingProps)));
178         } else {
179             Path resourcePath = pathFactory.create(newPath, JcrLexicon.CONTENT);
180             Location resourceLocation = Location.create(resourcePath);
181             customPropertiesFactory.recordFileProperties(context,
182                                                          sourceName,
183                                                          newLocation,
184                                                          newFile,
185                                                          extraFile(mapOf(existingProps)));
186             customPropertiesFactory.recordResourceProperties(context,
187                                                              sourceName,
188                                                              resourceLocation,
189                                                              newFile,
190                                                              extraResource(mapOf(existingResourceProps)));
191         }
192 
193         return getNode(newPath);
194     }
195 
196     protected Map<Name, Property> mapOf( Collection<Property> properties ) {
197         if (properties == null || properties.isEmpty()) return Collections.emptyMap();
198         Map<Name, Property> result = new HashMap<Name, Property>();
199         for (Property property : properties) {
200             result.put(property.getName(), property);
201         }
202         return result;
203     }
204 
205     @Override
206     public PathNode putNode( PathNode node ) {
207         PathFactory pathFactory = context.getValueFactories().getPathFactory();
208         NamespaceRegistry registry = context.getNamespaceRegistry();
209         CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
210 
211         Map<Name, Property> properties = node.getProperties();
212 
213         if (node.getParent() == null) {
214             // Root node
215             Path rootPath = pathFactory.createRootPath();
216             Location rootLocation = Location.create(rootPath, repository.getRootNodeUuid());
217             customPropertiesFactory.recordDirectoryProperties(context,
218                                                               source.getName(),
219                                                               rootLocation,
220                                                               workspaceRoot,
221                                                               extraFolder(node.getProperties()));
222             return getNode(rootPath);
223         }
224 
225         /*
226          * Get references to java.io.Files
227          */
228         Path parentPath = node.getParent();
229         boolean isRoot = parentPath == null;
230         File parentFile = fileFor(parentPath);
231 
232         Path newPath = isRoot ? pathFactory.createRootPath() : pathFactory.create(parentPath, node.getName());
233         Name name = node.getName().getName();
234         String newName = name.getString(registry);
235         File newFile = new File(parentFile, newName);
236 
237         /*
238          * Determine the node primary type
239          */
240         Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE);
241 
242         // Default primary type to nt:folder
243         Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue());
244 
245         if (JcrNtLexicon.FILE.equals(primaryType)) {
246 
247             // The FILE node is represented by the existence of the file
248             if (!parentFile.canWrite()) {
249                 I18n msg = FileSystemI18n.parentIsReadOnly;
250                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, this.getName(), source.getName()));
251             }
252 
253             try {
254                 ensureValidPathLength(newFile);
255 
256                 // Don't try to write if the node conflict behavior is DO_NOT_REPLACE
257                 if (!newFile.exists() && !newFile.createNewFile()) {
258                     I18n msg = FileSystemI18n.fileAlreadyExists;
259                     throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
260                 }
261             } catch (IOException ioe) {
262                 I18n msg = FileSystemI18n.couldNotCreateFile;
263                 throw new RepositorySourceException(source.getName(), msg.text(parentPath,
264                                                                                getName(),
265                                                                                source.getName(),
266                                                                                ioe.getMessage()), ioe);
267             }
268 
269             customPropertiesFactory.recordFileProperties(context,
270                                                          source.getName(),
271                                                          Location.create(newPath),
272                                                          newFile,
273                                                          extraFile(properties));
274         } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) {
275             assert parentFile != null;
276 
277             if (!JcrLexicon.CONTENT.equals(name)) {
278                 I18n msg = FileSystemI18n.invalidNameForResource;
279                 String nodeName = name.getString();
280                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName(), nodeName));
281             }
282 
283             if (!parentFile.isFile()) {
284                 I18n msg = FileSystemI18n.invalidPathForResource;
285                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
286             }
287 
288             if (!parentFile.canWrite()) {
289                 I18n msg = FileSystemI18n.parentIsReadOnly;
290                 throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
291             }
292 
293             // Copy over data into a temp file, then move it to the correct location
294             FileOutputStream fos = null;
295             try {
296                 File temp = File.createTempFile("modeshape", null);
297                 fos = new FileOutputStream(temp);
298 
299                 Property dataProp = properties.get(JcrLexicon.DATA);
300                 BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
301                 Binary binary = null;
302                 if (dataProp == null) {
303                     // There is no content, so make empty content ...
304                     binary = binaryFactory.create(new byte[] {});
305                     dataProp = context.getPropertyFactory().create(JcrLexicon.DATA, new Object[] {binary});
306                 } else {
307                     // Must read the value ...
308                     binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue());
309                 }
310 
311                 IoUtil.write(binary.getStream(), fos);
312 
313                 if (!FileUtil.delete(parentFile)) {
314                     I18n msg = FileSystemI18n.deleteFailed;
315                     throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
316                 }
317 
318                 moveFile(temp, parentFile);
319             } catch (IOException ioe) {
320                 I18n msg = FileSystemI18n.couldNotWriteData;
321                 throw new RepositorySourceException(source.getName(), msg.text(parentPath,
322                                                                                getName(),
323                                                                                source.getName(),
324                                                                                ioe.getMessage()), ioe);
325 
326             } finally {
327                 try {
328                     if (fos != null) fos.close();
329                 } catch (Exception ex) {
330                 }
331             }
332             customPropertiesFactory.recordResourceProperties(context,
333                                                              source.getName(),
334                                                              Location.create(parentPath),
335                                                              parentFile,
336                                                              extraResource(properties));
337 
338         } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) {
339             ensureValidPathLength(newFile);
340 
341             if (!newFile.exists() && !newFile.mkdir()) {
342                 I18n msg = FileSystemI18n.couldNotCreateFile;
343                 throw new RepositorySourceException(source.getName(),
344                                                     msg.text(parentPath,
345                                                              getName(),
346                                                              source.getName(),
347                                                              primaryType == null ? "null" : primaryType.getString(registry)));
348             }
349             customPropertiesFactory.recordDirectoryProperties(context,
350                                                               source.getName(),
351                                                               Location.create(newPath),
352                                                               newFile,
353                                                               extraFolder(properties));
354 
355         } else {
356             // Set error and return
357             I18n msg = FileSystemI18n.unsupportedPrimaryType;
358             throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry),
359                                                                            parentPath,
360                                                                            getName(),
361                                                                            source.getName()));
362         }
363 
364         node = getNode(newPath);
365 
366         return node;
367     }
368 
369     @Override
370     public PathNode removeNode( Path nodePath ) {
371         File nodeFile;
372 
373         CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
374 
375         if (!nodePath.isRoot() && JcrLexicon.CONTENT.equals(nodePath.getLastSegment().getName())) {
376             nodeFile = fileFor(nodePath.getParent());
377 
378             // Have the custom property factory remote all properties ...
379             customPropertiesFactory.recordResourceProperties(context,
380                                                              source.getName(),
381                                                              Location.create(nodePath),
382                                                              nodeFile,
383                                                              NO_PROPERTIES);
384             if (!nodeFile.exists()) return null;
385 
386             FileOutputStream fos = null;
387             try {
388                 fos = new FileOutputStream(nodeFile);
389                 IoUtil.write("", fos);
390             } catch (IOException ioe) {
391                 throw new RepositorySourceException(source.getName(), FileSystemI18n.deleteFailed.text(nodePath,
392                                                                                                        getName(),
393                                                                                                        source.getName()));
394             } finally {
395                 if (fos != null) try {
396                     fos.close();
397                 } catch (IOException ioe) {
398                 }
399             }
400         } else {
401             nodeFile = fileFor(nodePath);
402             // Have the custom property factory remote all properties ...
403             customPropertiesFactory.recordResourceProperties(context,
404                                                              source.getName(),
405                                                              Location.create(nodePath),
406                                                              nodeFile,
407                                                              NO_PROPERTIES);
408             if (!nodeFile.exists()) return null;
409 
410             FileUtil.delete(nodeFile);
411         }
412 
413         return null;
414     }
415 
416     @Override
417     public PathNode getRootNode() {
418         return getNode(context.getValueFactories().getPathFactory().createRootPath());
419     }
420 
421     @Override
422     public PathNode getNode( Path path ) {
423         Map<Name, Property> properties = new HashMap<Name, Property>();
424 
425         long startTime = System.nanoTime();
426         Name nodeType = null;
427         try {
428 
429             PropertyFactory factory = context.getPropertyFactory();
430             PathFactory pathFactory = context.getValueFactories().getPathFactory();
431             DateTimeFactory dateFactory = context.getValueFactories().getDateFactory();
432             MimeTypeDetector mimeTypeDetector = context.getMimeTypeDetector();
433             CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
434             NamespaceRegistry registry = context.getNamespaceRegistry();
435             Location location = Location.create(path);
436 
437             if (!path.isRoot() && JcrLexicon.CONTENT.equals(path.getLastSegment().getName())) {
438                 File file = fileFor(path.getParent());
439                 if (file == null) return null;
440 
441                 // First add any custom properties ...
442                 Collection<Property> customProps = customPropertiesFactory.getResourceProperties(context, location, file, null);
443                 for (Property customProp : customProps) {
444                     properties.put(customProp.getName(), customProp);
445                 }
446 
447                 if (!properties.containsKey(JcrLexicon.MIMETYPE)) {
448                     // Discover the mime type ...
449                     String mimeType = null;
450                     InputStream contents = null;
451                     try {
452                         // First try the file name (so we don't have to create an input stream,
453                         // which may have too much latency if a remote network file) ...
454                         mimeType = mimeTypeDetector.mimeTypeOf(file.getName(), null);
455                         if (mimeType == null && contentUsedToDetermineMimeType) {
456                             // Try to find the mime type using the content ...
457                             contents = new BufferedInputStream(new FileInputStream(file));
458                             mimeType = mimeTypeDetector.mimeTypeOf(null, contents);
459                         }
460                         if (mimeType == null) mimeType = DEFAULT_MIME_TYPE;
461                         properties.put(JcrLexicon.MIMETYPE, factory.create(JcrLexicon.MIMETYPE, mimeType));
462                     } catch (IOException e) {
463                         I18n msg = FileSystemI18n.couldNotReadData;
464                         throw new RepositorySourceException(source.getName(), msg.text(source.getName(),
465                                                                                        getName(),
466                                                                                        path.getString(registry)));
467                     } finally {
468                         if (contents != null) {
469                             try {
470                                 contents.close();
471                             } catch (IOException e) {
472                             }
473                         }
474                     }
475                 }
476 
477                 // The request is to get properties of the "jcr:content" child node ...
478                 // ... use the dna:resource node type. This is the same as nt:resource, but is not referenceable
479                 // since we cannot assume that we control all access to this file and can track its movements
480                 Property primaryType = properties.get(JcrLexicon.PRIMARY_TYPE);
481                 if (primaryType == null) {
482                     nodeType = JcrNtLexicon.RESOURCE;
483                     properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.RESOURCE));
484                 } else {
485                     nodeType = nameValueFor(primaryType);
486                 }
487                 properties.put(JcrLexicon.LAST_MODIFIED, factory.create(JcrLexicon.LAST_MODIFIED,
488                                                                         dateFactory.create(file.lastModified())));
489 
490                 // Now put the file's content into the "jcr:data" property ...
491                 Binary binary = binaryForContent(file);
492                 properties.put(JcrLexicon.DATA, factory.create(JcrLexicon.DATA, binary));
493 
494                 // Don't really know the encoding, either ...
495                 // properties.put(JcrLexicon.ENCODED, factory.create(JcrLexicon.ENCODED, "UTF-8"));
496 
497                 // return new PathNode(path, null, properties, Collections.<Segment>emptyList());
498                 return new PathNode(null, path.getParent(), path.getLastSegment(), properties, Collections.<Segment>emptyList());
499             }
500 
501             File file = fileFor(path);
502             if (file == null) return null;
503 
504             if (file.isDirectory()) {
505                 String[] childNames = file.list(source.filenameFilter(true));
506                 Arrays.sort(childNames);
507 
508                 List<Segment> childSegments = new ArrayList<Segment>(childNames.length);
509                 for (String childName : childNames) {
510                     childSegments.add(pathFactory.createSegment(childName));
511                 }
512 
513                 Collection<Property> customProps = customPropertiesFactory.getDirectoryProperties(context, location, file);
514                 for (Property customProp : customProps) {
515                     properties.put(customProp.getName(), customProp);
516                 }
517 
518                 if (path.isRoot()) {
519                     nodeType = ModeShapeLexicon.ROOT;
520                     properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.ROOT));
521                     // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments);
522                     return new PathNode(source.getRootNodeUuidObject(), path.getParent(), path.getLastSegment(), properties,
523                                         childSegments);
524 
525                 }
526                 Property primaryType = properties.get(JcrLexicon.PRIMARY_TYPE);
527                 if (primaryType == null) {
528                     nodeType = JcrNtLexicon.FOLDER;
529                     properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER));
530                 } else {
531                     nodeType = nameValueFor(primaryType);
532                 }
533                 // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments);
534                 return new PathNode(null, path.getParent(), path.getLastSegment(), properties, childSegments);
535 
536             }
537 
538             Collection<Property> customProps = customPropertiesFactory.getFileProperties(context, location, file);
539             for (Property customProp : customProps) {
540                 properties.put(customProp.getName(), customProp);
541             }
542 
543             Property primaryType = properties.get(JcrLexicon.PRIMARY_TYPE);
544             if (primaryType == null) {
545                 nodeType = JcrNtLexicon.FILE;
546                 properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE));
547             } else {
548                 nodeType = nameValueFor(primaryType);
549             }
550             if (!properties.containsKey(JcrLexicon.CREATED)) {
551                 properties.put(JcrLexicon.CREATED, factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified())));
552             }
553 
554             // node = new DefaultPathNode(path, null, properties,
555             // Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT)));
556 
557             return new PathNode(null, path.getParent(), path.getLastSegment(), properties,
558                                 Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT)));
559         } finally {
560             if (nodeType != null && logger.isTraceEnabled()) {
561                 long stopTime = System.nanoTime();
562                 long ms = TimeUnit.MICROSECONDS.convert(stopTime - startTime, TimeUnit.NANOSECONDS);
563                 String pathStr = stringFactory.create(path);
564                 String typeStr = stringFactory.create(nodeType);
565                 logger.trace("Loaded '{0}' node '{1}' in {2}microsec", typeStr, pathStr, ms);
566             }
567         }
568     }
569 
570     /**
571      * {@inheritDoc}
572      * 
573      * @see org.modeshape.graph.connector.base.PathWorkspace#verifyNodeExists(org.modeshape.graph.property.Path)
574      */
575     @Override
576     public Location verifyNodeExists( Path path ) {
577         File file = fileFor(path, true);
578         return file != null ? Location.create(path) : null;
579     }
580 
581     /**
582      * Create the Binary object used as the value for the "jcr:data" property where the file's content is stored on a
583      * "nt:resource" node.
584      * 
585      * @param file the file
586      * @return the binary representation
587      */
588     protected Binary binaryForContent( File file ) {
589         if (file == null) return null;
590         if (eagerLoading) {
591             BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
592             return binaryFactory.create(file);
593         }
594         // Not eager, so use the non-eager binary value implementation ...
595         return new FileSystemBinary(file);
596     }
597 
598     /**
599      * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
600      * valid.
601      * <p>
602      * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
603      * <code>jcr:primaryType</code>" of "<code>nt:file</code>"; and a child node with the name "<code>jcr:content</code> " and a "
604      * <code>jcr:primaryType</code>" of "<code>nt:resource</code>". The parent "<code>nt:file</code>" node and its properties
605      * represents the file itself, whereas the child "<code>nt:resource</code>" node and its properties represent the content of
606      * the file.
607      * </p>
608      * <p>
609      * As such, this method will return the File object for paths representing both the parent "<code>nt:file</code> " and child "
610      * <code>nt:resource</code>" node.
611      * </p>
612      * 
613      * @param path
614      * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
615      *         {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
616      */
617     protected File fileFor( Path path ) {
618         return fileFor(path, true);
619     }
620 
621     /**
622      * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
623      * valid.
624      * <p>
625      * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
626      * <code>jcr:primaryType</code>" of "<code>nt:file</code>"; and a child node with the name "<code>jcr:content</code> " and a "
627      * <code>jcr:primaryType</code>" of "<code>nt:resource</code>". The parent "<code>nt:file</code>" node and its properties
628      * represents the file itself, whereas the child "<code>nt:resource</code>" node and its properties represent the content of
629      * the file.
630      * </p>
631      * <p>
632      * As such, this method will return the File object for paths representing both the parent "<code>nt:file</code> " and child "
633      * <code>nt:resource</code>" node.
634      * </p>
635      * 
636      * @param path
637      * @param existingFilesOnly
638      * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
639      *         {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
640      */
641     protected File fileFor( Path path,
642                             boolean existingFilesOnly ) {
643         if (path == null || path.isRoot()) {
644             return workspaceRoot;
645         }
646         // See if the path is a "jcr:content" node ...
647         if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
648             // We only want to use the parent path to find the actual file ...
649             path = path.getParent();
650         }
651         File file = workspaceRoot;
652         for (Path.Segment segment : path) {
653             String localName = segment.getName().getLocalName();
654             // Verify the segment is valid ...
655             if (segment.getIndex() > 1) {
656                 I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed;
657                 throw new RepositorySourceException(source.getName(), msg.text(source.getName()));
658             }
659 
660             String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri();
661             if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) {
662                 I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed;
663                 throw new RepositorySourceException(source.getName(), msg.text(source.getName()));
664             }
665 
666             // The segment should exist as a child of the file ...
667             file = new File(file, localName);
668 
669             if (existingFilesOnly && (!file.canRead() || !file.exists())) {
670                 return null;
671             }
672         }
673         assert file != null;
674         return file;
675     }
676 
677     protected void validate( PathNode node ) {
678         // Don't validate the root node
679         if (node.getParent() == null) return;
680 
681         Map<Name, Property> properties = node.getProperties();
682         Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE);
683         Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue());
684 
685         if (!VALID_PRIMARY_TYPES.contains(primaryType)) {
686             // Set error and return
687             I18n msg = FileSystemI18n.unsupportedPrimaryType;
688             NamespaceRegistry registry = context.getNamespaceRegistry();
689             Path parentPath = node.getParent();
690             throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry),
691                                                                            parentPath,
692                                                                            getName(),
693                                                                            source.getName()));
694 
695         }
696 
697         Path nodePath = context.getValueFactories().getPathFactory().create(node.getParent(), node.getName());
698         ensureValidPathLength(fileFor(nodePath, false));
699     }
700 
701     protected void ensureValidPathLength( File file ) {
702         ensureValidPathLength(file, 0);
703     }
704 
705     /**
706      * Recursively checks if any of the files in the tree rooted at {@code root} would exceed the
707      * {@link FileSystemSource#getMaxPathLength() maximum path length for the processor} if their paths were {@code delta}
708      * characters longer. If any files would exceed this length, a {@link RepositorySourceException} is thrown.
709      * 
710      * @param root the root of the tree to check; may be a file or directory but may not be null
711      * @param delta the change in the length of the path to check. Used to preemptively check whether moving a file or directory
712      *        to a new path would violate path length rules
713      * @throws RepositorySourceException if any files in the tree rooted at {@code root} would exceed this
714      *         {@link FileSystemSource#getMaxPathLength() the maximum path length for this processor}
715      */
716     protected void ensureValidPathLength( File root,
717                                           int delta ) {
718         try {
719             int len = root.getCanonicalPath().length();
720             if (len > source.getMaxPathLength() - delta) {
721                 String msg = FileSystemI18n.maxPathLengthExceeded.text(source.getMaxPathLength(),
722                                                                        source.getName(),
723                                                                        root.getCanonicalPath(),
724                                                                        delta);
725                 throw new RepositorySourceException(source.getName(), msg);
726             }
727 
728             if (root.isDirectory()) {
729                 for (File child : root.listFiles(source.filenameFilter(false))) {
730                     ensureValidPathLength(child, delta);
731                 }
732 
733             }
734         } catch (IOException ioe) {
735             throw new RepositorySourceException(source.getName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe);
736         }
737     }
738 
739     /**
740      * Determine the 'extra' properties for a folder that should be stored by the CustomPropertiesFactory.
741      * 
742      * @param properties
743      * @return the extra properties, or null if the supplied properties reference is null
744      */
745     protected Map<Name, Property> extraFolder( Map<Name, Property> properties ) {
746         if (properties == null) return null;
747         if (properties.isEmpty()) return properties;
748         Map<Name, Property> extra = new HashMap<Name, Property>();
749         for (Property property : properties.values()) {
750             Name name = property.getName();
751             if (name.equals(JcrLexicon.PRIMARY_TYPE) && primaryTypeIs(property, JcrNtLexicon.FOLDER)) continue;
752             extra.put(name, property);
753         }
754         return extra;
755     }
756 
757     /**
758      * Determine the 'extra' properties for a file node that should be stored by the CustomPropertiesFactory.
759      * 
760      * @param properties
761      * @return the extra properties, or null if the supplied properties reference is null
762      */
763     protected Map<Name, Property> extraFile( Map<Name, Property> properties ) {
764         if (properties == null) return null;
765         if (properties.isEmpty()) return properties;
766         Map<Name, Property> extra = new HashMap<Name, Property>();
767         for (Property property : properties.values()) {
768             Name name = property.getName();
769             if (name.equals(JcrLexicon.PRIMARY_TYPE) && primaryTypeIs(property, JcrNtLexicon.FILE)) continue;
770             extra.put(name, property);
771         }
772         return extra;
773     }
774 
775     /**
776      * Determine the 'extra' properties for a resource node that should be stored by the CustomPropertiesFactory.
777      * 
778      * @param properties
779      * @return the extra properties, or null if the supplied properties reference is null
780      */
781     protected Map<Name, Property> extraResource( Map<Name, Property> properties ) {
782         if (properties == null) return null;
783         if (properties.isEmpty()) return properties;
784         Map<Name, Property> extra = new HashMap<Name, Property>();
785         for (Property property : properties.values()) {
786             Name name = property.getName();
787             if (name.equals(JcrLexicon.PRIMARY_TYPE) && primaryTypeIs(property, JcrNtLexicon.RESOURCE)) continue;
788             else if (name.equals(JcrLexicon.DATA)) continue;
789             extra.put(name, property);
790         }
791         return extra;
792     }
793 
794     protected boolean primaryTypeIs( Property property,
795                                      Name primaryType ) {
796         Name actualPrimaryType = nameValueFor(property);
797         return actualPrimaryType.equals(primaryType);
798     }
799 
800     protected Name nameValueFor( Property property ) {
801         return nameFactory.create(property.getFirstValue());
802     }
803 }