001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.connector.filesystem;
025    
026    import java.io.BufferedInputStream;
027    import java.io.File;
028    import java.io.FileInputStream;
029    import java.io.FilenameFilter;
030    import java.io.IOException;
031    import java.io.InputStream;
032    import java.util.Collections;
033    import java.util.HashSet;
034    import java.util.Set;
035    import org.jboss.dna.common.i18n.I18n;
036    import org.jboss.dna.graph.ExecutionContext;
037    import org.jboss.dna.graph.JcrLexicon;
038    import org.jboss.dna.graph.JcrNtLexicon;
039    import org.jboss.dna.graph.Location;
040    import org.jboss.dna.graph.connector.RepositorySourceException;
041    import org.jboss.dna.graph.mimetype.MimeTypeDetector;
042    import org.jboss.dna.graph.property.BinaryFactory;
043    import org.jboss.dna.graph.property.DateTimeFactory;
044    import org.jboss.dna.graph.property.Name;
045    import org.jboss.dna.graph.property.NameFactory;
046    import org.jboss.dna.graph.property.Path;
047    import org.jboss.dna.graph.property.PathFactory;
048    import org.jboss.dna.graph.property.PathNotFoundException;
049    import org.jboss.dna.graph.property.PropertyFactory;
050    import org.jboss.dna.graph.request.CloneWorkspaceRequest;
051    import org.jboss.dna.graph.request.CopyBranchRequest;
052    import org.jboss.dna.graph.request.CreateNodeRequest;
053    import org.jboss.dna.graph.request.CreateWorkspaceRequest;
054    import org.jboss.dna.graph.request.DeleteBranchRequest;
055    import org.jboss.dna.graph.request.DestroyWorkspaceRequest;
056    import org.jboss.dna.graph.request.GetWorkspacesRequest;
057    import org.jboss.dna.graph.request.InvalidRequestException;
058    import org.jboss.dna.graph.request.InvalidWorkspaceException;
059    import org.jboss.dna.graph.request.MoveBranchRequest;
060    import org.jboss.dna.graph.request.ReadAllChildrenRequest;
061    import org.jboss.dna.graph.request.ReadAllPropertiesRequest;
062    import org.jboss.dna.graph.request.RenameNodeRequest;
063    import org.jboss.dna.graph.request.Request;
064    import org.jboss.dna.graph.request.UpdatePropertiesRequest;
065    import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
066    import org.jboss.dna.graph.request.processor.RequestProcessor;
067    
068    /**
069     * The {@link RequestProcessor} implementation for the file systme connector. This is the class that does the bulk of the work in
070     * the file system connector, since it processes all requests.
071     * 
072     * @author Randall Hauch
073     */
074    public class FileSystemRequestProcessor extends RequestProcessor {
075    
076        private static final String DEFAULT_MIME_TYPE = "application/octet";
077    
078        private final String defaultNamespaceUri;
079        private final Set<String> availableWorkspaceNames;
080        private final boolean creatingWorkspacesAllowed;
081        private final File defaultWorkspace;
082        private final FilenameFilter filenameFilter;
083        private final boolean updatesAllowed;
084        private final MimeTypeDetector mimeTypeDetector;
085    
086        /**
087         * @param sourceName
088         * @param defaultWorkspace
089         * @param availableWorkspaceNames
090         * @param creatingWorkspacesAllowed
091         * @param context
092         * @param filenameFilter the filename filter to use to restrict the allowable nodes, or null if all files/directories are to
093         *        be exposed by this connector
094         * @param updatesAllowed true if this connector supports updating the file system, or false if the connector is readonly
095         */
096        protected FileSystemRequestProcessor( String sourceName,
097                                              File defaultWorkspace,
098                                              Set<String> availableWorkspaceNames,
099                                              boolean creatingWorkspacesAllowed,
100                                              ExecutionContext context,
101                                              FilenameFilter filenameFilter,
102                                              boolean updatesAllowed ) {
103            super(sourceName, context);
104            assert defaultWorkspace != null;
105            assert defaultWorkspace.exists();
106            assert defaultWorkspace.canRead();
107            assert defaultWorkspace.isDirectory();
108            assert availableWorkspaceNames != null;
109            this.availableWorkspaceNames = availableWorkspaceNames;
110            this.creatingWorkspacesAllowed = creatingWorkspacesAllowed;
111            this.defaultNamespaceUri = getExecutionContext().getNamespaceRegistry().getDefaultNamespaceUri();
112            this.filenameFilter = filenameFilter;
113            this.defaultWorkspace = defaultWorkspace;
114            this.updatesAllowed = updatesAllowed;
115            this.mimeTypeDetector = context.getMimeTypeDetector();
116        }
117    
118        /**
119         * {@inheritDoc}
120         * 
121         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllChildrenRequest)
122         */
123        @Override
124        public void process( ReadAllChildrenRequest request ) {
125    
126            // Get the java.io.File object that represents the workspace ...
127            File workspaceRoot = getWorkspaceDirectory(request.inWorkspace());
128            if (workspaceRoot == null) {
129                request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(request.inWorkspace())));
130                return;
131            }
132    
133            // Find the existing file for the parent ...
134            Location location = request.of();
135            Path parentPath = getPathFor(location, request);
136            File parent = getExistingFileFor(workspaceRoot, parentPath, location, request);
137            if (parent == null) {
138                // An error was set on the request
139                assert request.hasError();
140                return;
141            }
142            // Decide how to represent the children ...
143            if (parent.isDirectory()) {
144                // Create a Location for each file and directory contained by the parent directory ...
145                PathFactory pathFactory = pathFactory();
146                NameFactory nameFactory = nameFactory();
147                for (String localName : parent.list(filenameFilter)) {
148                    Name childName = nameFactory.create(defaultNamespaceUri, localName);
149                    Path childPath = pathFactory.create(parentPath, childName);
150                    request.addChild(Location.create(childPath));
151                }
152            } else {
153                // The parent is a java.io.File, and the path may refer to the node that is either the "nt:file" parent
154                // node, or the child "jcr:content" node...
155                if (!parentPath.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
156                    // This node represents the "nt:file" parent node, so the only child is the "jcr:content" node ...
157                    Path contentPath = pathFactory().create(parentPath, JcrLexicon.CONTENT);
158                    Location content = Location.create(contentPath);
159                    request.addChild(content);
160                }
161                // otherwise, the path ends in "jcr:content", and there are no children
162            }
163            request.setActualLocationOfNode(location);
164            setCacheableInfo(request);
165        }
166    
167        /**
168         * {@inheritDoc}
169         * 
170         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllPropertiesRequest)
171         */
172        @Override
173        public void process( ReadAllPropertiesRequest request ) {
174    
175            // Get the java.io.File object that represents the workspace ...
176            File workspaceRoot = getWorkspaceDirectory(request.inWorkspace());
177            if (workspaceRoot == null) {
178                request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(request.inWorkspace())));
179                return;
180            }
181    
182            // Find the existing file for the parent ...
183            Location location = request.at();
184            Path path = getPathFor(location, request);
185            if (path.isRoot()) {
186                // There are no properties on the root ...
187                request.setActualLocationOfNode(location);
188                setCacheableInfo(request);
189                return;
190            }
191    
192            File file = getExistingFileFor(workspaceRoot, path, location, request);
193            if (file == null) {
194                // An error was set on the request
195                assert request.hasError();
196                return;
197            }
198            // Generate the properties for this File object ...
199            PropertyFactory factory = getExecutionContext().getPropertyFactory();
200            DateTimeFactory dateFactory = getExecutionContext().getValueFactories().getDateFactory();
201            // Note that we don't have 'created' timestamps, just last modified, so we'll have to use them
202            if (file.isDirectory()) {
203                // Add properties for the directory ...
204                request.addProperty(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER));
205                request.addProperty(factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified())));
206    
207            } else {
208                // It is a file, but ...
209                if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
210                    // The request is to get properties of the "jcr:content" child node ...
211                    request.addProperty(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.RESOURCE));
212                    request.addProperty(factory.create(JcrLexicon.LAST_MODIFIED, dateFactory.create(file.lastModified())));
213                    // Don't really know the encoding, either ...
214                    // request.addProperty(factory.create(JcrLexicon.ENCODED, stringFactory.create("UTF-8")));
215    
216                    // Discover the mime type ...
217                    String mimeType = null;
218                    InputStream contents = null;
219                    boolean mimeTypeError = false;
220                    try {
221                        contents = new BufferedInputStream(new FileInputStream(file));
222                        mimeType = mimeTypeDetector.mimeTypeOf(file.getName(), contents);
223                        if (mimeType == null) mimeType = DEFAULT_MIME_TYPE;
224                        request.addProperty(factory.create(JcrLexicon.MIMETYPE, mimeType));
225                    } catch (IOException e) {
226                        mimeTypeError = true;
227                        request.setError(e);
228                    } finally {
229                        if (contents != null) {
230                            try {
231                                contents.close();
232                            } catch (IOException e) {
233                                if (!mimeTypeError) request.setError(e);
234                            }
235                        }
236                    }
237    
238                    // Now put the file's content into the "jcr:data" property ...
239                    BinaryFactory binaryFactory = getExecutionContext().getValueFactories().getBinaryFactory();
240                    request.addProperty(factory.create(JcrLexicon.DATA, binaryFactory.create(file)));
241    
242                } else {
243                    // The request is to get properties for the node representing the file
244                    request.addProperty(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE));
245                    request.addProperty(factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified())));
246                }
247    
248            }
249            request.setActualLocationOfNode(location);
250            setCacheableInfo(request);
251        }
252    
253        /**
254         * {@inheritDoc}
255         * 
256         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateNodeRequest)
257         */
258        @Override
259        public void process( CreateNodeRequest request ) {
260            updatesAllowed(request);
261        }
262    
263        /**
264         * {@inheritDoc}
265         * 
266         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.UpdatePropertiesRequest)
267         */
268        @Override
269        public void process( UpdatePropertiesRequest request ) {
270            updatesAllowed(request);
271        }
272    
273        /**
274         * {@inheritDoc}
275         * 
276         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CopyBranchRequest)
277         */
278        @Override
279        public void process( CopyBranchRequest request ) {
280            updatesAllowed(request);
281        }
282    
283        /**
284         * {@inheritDoc}
285         * 
286         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DeleteBranchRequest)
287         */
288        @Override
289        public void process( DeleteBranchRequest request ) {
290            updatesAllowed(request);
291        }
292    
293        /**
294         * {@inheritDoc}
295         * 
296         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.MoveBranchRequest)
297         */
298        @Override
299        public void process( MoveBranchRequest request ) {
300            updatesAllowed(request);
301        }
302    
303        /**
304         * {@inheritDoc}
305         * 
306         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.RenameNodeRequest)
307         */
308        @Override
309        public void process( RenameNodeRequest request ) {
310            if (updatesAllowed(request)) super.process(request);
311        }
312    
313        /**
314         * {@inheritDoc}
315         * 
316         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyWorkspaceRequest)
317         */
318        @Override
319        public void process( VerifyWorkspaceRequest request ) {
320            // If the request contains a null name, then we use the default ...
321            String workspaceName = request.workspaceName();
322            if (workspaceName == null) workspaceName = getCanonicalWorkspaceName(defaultWorkspace);
323    
324            if (!this.creatingWorkspacesAllowed) {
325                // Then the workspace name must be one of the available names ...
326                boolean found = false;
327                for (String available : this.availableWorkspaceNames) {
328                    if (workspaceName.equals(available)) {
329                        found = true;
330                        break;
331                    }
332                    File directory = new File(available);
333                    if (directory.exists() && directory.isDirectory() && directory.canRead()
334                        && getCanonicalWorkspaceName(directory).equals(workspaceName)) {
335                        found = true;
336                        break;
337                    }
338                }
339                if (!found) {
340                    request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(workspaceName)));
341                    return;
342                }
343                // We know it is an available workspace, so just continue ...
344            }
345            // Verify that there is a directory at the path given by the workspace name ...
346            File directory = new File(workspaceName);
347            if (directory.exists() && directory.isDirectory() && directory.canRead()) {
348                request.setActualWorkspaceName(getCanonicalWorkspaceName(directory));
349                request.setActualRootLocation(Location.create(pathFactory().createRootPath()));
350            } else {
351                request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(workspaceName)));
352            }
353        }
354    
355        /**
356         * {@inheritDoc}
357         * 
358         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.GetWorkspacesRequest)
359         */
360        @Override
361        public void process( GetWorkspacesRequest request ) {
362            // Return the set of available workspace names, even if new workspaces can be created ...
363            Set<String> names = new HashSet<String>();
364            for (String name : this.availableWorkspaceNames) {
365                File directory = new File(name);
366                if (directory.exists() && directory.isDirectory() && directory.canRead()) {
367                    names.add(getCanonicalWorkspaceName(directory));
368                }
369            }
370            request.setAvailableWorkspaceNames(Collections.unmodifiableSet(names));
371        }
372    
373        /**
374         * Utility method to return the canonical path (without "." and ".." segments) for a file.
375         * 
376         * @param directory the directory; may not be null
377         * @return the canonical path, or if there is an error the absolute path
378         */
379        protected String getCanonicalWorkspaceName( File directory ) {
380            try {
381                return directory.getCanonicalPath();
382            } catch (IOException e) {
383                return directory.getAbsolutePath();
384            }
385        }
386    
387        /**
388         * {@inheritDoc}
389         * 
390         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest)
391         */
392        @Override
393        public void process( CloneWorkspaceRequest request ) {
394            if (!updatesAllowed) {
395                request.setError(new InvalidRequestException(FileSystemI18n.sourceIsReadOnly.text(getSourceName())));
396            }
397        }
398    
399        /**
400         * {@inheritDoc}
401         * 
402         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest)
403         */
404        @Override
405        public void process( CreateWorkspaceRequest request ) {
406            final String workspaceName = request.desiredNameOfNewWorkspace();
407            if (!creatingWorkspacesAllowed) {
408                String msg = FileSystemI18n.unableToCreateWorkspaces.text(getSourceName(), workspaceName);
409                request.setError(new InvalidRequestException(msg));
410                return;
411            }
412            // This doesn't create the directory representing the workspace (it must already exist), but it will add
413            // the workspace name to the available names ...
414            File directory = new File(workspaceName);
415            if (directory.exists() && directory.isDirectory() && directory.canRead()) {
416                request.setActualWorkspaceName(getCanonicalWorkspaceName(directory));
417                request.setActualRootLocation(Location.create(pathFactory().createRootPath()));
418                availableWorkspaceNames.add(workspaceName);
419            } else {
420                request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(workspaceName)));
421            }
422        }
423    
424        /**
425         * {@inheritDoc}
426         * 
427         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DestroyWorkspaceRequest)
428         */
429        @Override
430        public void process( DestroyWorkspaceRequest request ) {
431            final String workspaceName = request.workspaceName();
432            if (!creatingWorkspacesAllowed) {
433                String msg = FileSystemI18n.unableToCreateWorkspaces.text(getSourceName(), workspaceName);
434                request.setError(new InvalidRequestException(msg));
435            }
436            // This doesn't delete the file/directory; rather, it just remove the workspace from the available set ...
437            if (!this.availableWorkspaceNames.remove(workspaceName)) {
438                request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(workspaceName)));
439            }
440        }
441    
442        protected boolean updatesAllowed( Request request ) {
443            if (!updatesAllowed) {
444                request.setError(new InvalidRequestException(FileSystemI18n.sourceIsReadOnly.text(getSourceName())));
445            }
446            return !request.hasError();
447        }
448    
449        protected NameFactory nameFactory() {
450            return getExecutionContext().getValueFactories().getNameFactory();
451        }
452    
453        protected PathFactory pathFactory() {
454            return getExecutionContext().getValueFactories().getPathFactory();
455        }
456    
457        protected Path getPathFor( Location location,
458                                   Request request ) {
459            Path path = location.getPath();
460            if (path == null) {
461                I18n msg = FileSystemI18n.locationInRequestMustHavePath;
462                throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request));
463            }
464            return path;
465        }
466    
467        protected File getWorkspaceDirectory( String workspaceName ) {
468            File workspace = defaultWorkspace;
469            if (workspaceName != null) {
470                File directory = new File(workspaceName);
471                if (directory.exists() && directory.isDirectory() && directory.canRead()) workspace = directory;
472                else return null;
473            }
474            return workspace;
475        }
476    
477        /**
478         * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
479         * valid.
480         * <p>
481         * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
482         * <code>jcr:primaryType</code>" of "<code>nt:file</code>"; and a child node with the name "<code>jcr:content</code>" and a "
483         * <code>jcr:primaryType</code>" of "<code>nt:resource</code>". The parent "<code>nt:file</code>" node and its properties
484         * represents the file itself, whereas the child "<code>nt:resource</code>" node and its properties represent the content of
485         * the file.
486         * </p>
487         * <p>
488         * As such, this method will return the File object for paths representing both the parent "<code>nt:file</code>" and child "
489         * <code>nt:resource</code>" node.
490         * </p>
491         * 
492         * @param workspaceRoot
493         * @param path
494         * @param location the location containing the path; may not be null
495         * @param request the request containing the path (and the location); may not be null
496         * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
497         *         {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
498         */
499        protected File getExistingFileFor( File workspaceRoot,
500                                           Path path,
501                                           Location location,
502                                           Request request ) {
503            assert path != null;
504            assert location != null;
505            assert request != null;
506            if (path.isRoot()) {
507                return workspaceRoot;
508            }
509            // See if the path is a "jcr:content" node ...
510            if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
511                // We only want to use the parent path to find the actual file ...
512                path = path.getParent();
513            }
514            File file = workspaceRoot;
515            for (Path.Segment segment : path) {
516                String localName = segment.getName().getLocalName();
517                // Verify the segment is valid ...
518                if (segment.getIndex() > 1) {
519                    I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed;
520                    throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request));
521                }
522                if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) {
523                    I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed;
524                    throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request));
525                }
526                // The segment should exist as a child of the file ...
527                file = new File(file, localName);
528                if (!file.exists() || !file.canRead()) {
529                    // Unable to complete the path, so prepare the exception by determining the lowest path that exists ...
530                    Path lowest = path;
531                    while (lowest.getLastSegment() != segment) {
532                        lowest = lowest.getParent();
533                    }
534                    lowest = lowest.getParent();
535                    request.setError(new PathNotFoundException(location, lowest));
536                    return null;
537                }
538            }
539            assert file != null;
540            return file;
541        }
542    }