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, null);
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 updatesAllowed(request);
395 }
396
397 /**
398 * {@inheritDoc}
399 *
400 * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest)
401 */
402 @Override
403 public void process( CreateWorkspaceRequest request ) {
404 final String workspaceName = request.desiredNameOfNewWorkspace();
405 if (!creatingWorkspacesAllowed) {
406 String msg = FileSystemI18n.unableToCreateWorkspaces.text(getSourceName(), workspaceName);
407 request.setError(new InvalidRequestException(msg));
408 return;
409 }
410 // This doesn't create the directory representing the workspace (it must already exist), but it will add
411 // the workspace name to the available names ...
412 File directory = new File(workspaceName);
413 if (directory.exists() && directory.isDirectory() && directory.canRead()) {
414 request.setActualWorkspaceName(getCanonicalWorkspaceName(directory));
415 request.setActualRootLocation(Location.create(pathFactory().createRootPath()));
416 availableWorkspaceNames.add(workspaceName);
417 recordChange(request);
418 } else {
419 request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(workspaceName)));
420 }
421 }
422
423 /**
424 * {@inheritDoc}
425 *
426 * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DestroyWorkspaceRequest)
427 */
428 @Override
429 public void process( DestroyWorkspaceRequest request ) {
430 final String workspaceName = request.workspaceName();
431 if (!creatingWorkspacesAllowed) {
432 String msg = FileSystemI18n.unableToCreateWorkspaces.text(getSourceName(), workspaceName);
433 request.setError(new InvalidRequestException(msg));
434 }
435 // This doesn't delete the file/directory; rather, it just remove the workspace from the available set ...
436 if (!this.availableWorkspaceNames.remove(workspaceName)) {
437 request.setError(new InvalidWorkspaceException(FileSystemI18n.workspaceDoesNotExist.text(workspaceName)));
438 } else {
439 request.setActualRootLocation(Location.create(pathFactory().createRootPath()));
440 recordChange(request);
441 }
442 }
443
444 protected boolean updatesAllowed( Request request ) {
445 if (!updatesAllowed) {
446 request.setError(new InvalidRequestException(FileSystemI18n.sourceIsReadOnly.text(getSourceName())));
447 }
448 return !request.hasError();
449 }
450
451 protected NameFactory nameFactory() {
452 return getExecutionContext().getValueFactories().getNameFactory();
453 }
454
455 protected PathFactory pathFactory() {
456 return getExecutionContext().getValueFactories().getPathFactory();
457 }
458
459 protected Path getPathFor( Location location,
460 Request request ) {
461 Path path = location.getPath();
462 if (path == null) {
463 I18n msg = FileSystemI18n.locationInRequestMustHavePath;
464 throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request));
465 }
466 return path;
467 }
468
469 protected File getWorkspaceDirectory( String workspaceName ) {
470 File workspace = defaultWorkspace;
471 if (workspaceName != null) {
472 File directory = new File(workspaceName);
473 if (directory.exists() && directory.isDirectory() && directory.canRead()) workspace = directory;
474 else return null;
475 }
476 return workspace;
477 }
478
479 /**
480 * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
481 * valid.
482 * <p>
483 * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
484 * <code>jcr:primaryType</code>" of "<code>nt:file</code>"; and a child node with the name "<code>jcr:content</code>" and a "
485 * <code>jcr:primaryType</code>" of "<code>nt:resource</code>". The parent "<code>nt:file</code>" node and its properties
486 * represents the file itself, whereas the child "<code>nt:resource</code>" node and its properties represent the content of
487 * the file.
488 * </p>
489 * <p>
490 * As such, this method will return the File object for paths representing both the parent "<code>nt:file</code>" and child "
491 * <code>nt:resource</code>" node.
492 * </p>
493 *
494 * @param workspaceRoot
495 * @param path
496 * @param location the location containing the path; may not be null
497 * @param request the request containing the path (and the location); may not be null
498 * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
499 * {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
500 */
501 protected File getExistingFileFor( File workspaceRoot,
502 Path path,
503 Location location,
504 Request request ) {
505 assert path != null;
506 assert location != null;
507 assert request != null;
508 if (path.isRoot()) {
509 return workspaceRoot;
510 }
511 // See if the path is a "jcr:content" node ...
512 if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
513 // We only want to use the parent path to find the actual file ...
514 path = path.getParent();
515 }
516 File file = workspaceRoot;
517 for (Path.Segment segment : path) {
518 String localName = segment.getName().getLocalName();
519 // Verify the segment is valid ...
520 if (segment.getIndex() > 1) {
521 I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed;
522 throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request));
523 }
524 if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) {
525 I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed;
526 throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request));
527 }
528 // The segment should exist as a child of the file ...
529 file = new File(file, localName);
530 if (!file.exists() || !file.canRead()) {
531 // Unable to complete the path, so prepare the exception by determining the lowest path that exists ...
532 Path lowest = path;
533 while (lowest.getLastSegment() != segment) {
534 lowest = lowest.getParent();
535 }
536 lowest = lowest.getParent();
537 request.setError(new PathNotFoundException(location, lowest));
538 return null;
539 }
540 }
541 assert file != null;
542 return file;
543 }
544 }