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 }