Index: dna-graph/src/main/java/org/jboss/dna/graph/Graph.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/Graph.java (revision 1137) +++ dna-graph/src/main/java/org/jboss/dna/graph/Graph.java (working copy) @@ -4226,6 +4226,15 @@ * @return the interface for additional requests or actions */ Next as( Name newName ); + + /** + * Finish the request by specifying the name of the new child node. This method indicates that the child should be added + * as a new node with the given name at the end of the parents children + * + * @param newName the new name + * @return the interface for additional requests or actions + */ + Next as( String newName ); } /** @@ -6480,8 +6489,13 @@ return source.submit(workspaceName, from, intoWorkspaceName, into, name, null, removeExisting); } }; + } + public Into> as( final String name ) { + return as(context.getValueFactories().getNameFactory().create(name)); + } + public Into> as( final Segment segment ) { return new CloneTargetAction(afterConjunction(), source) { @Override Index: extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemConnection.java =================================================================== --- extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemConnection.java (revision 1126) +++ extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemConnection.java (working copy) @@ -51,6 +51,7 @@ private final boolean creatingWorkspacesAllowed; private final FilenameFilter filenameFilter; private final UUID rootNodeUuid; + private final int maxPathLength; private final String workspaceRootPath; private final boolean updatesAllowed; @@ -61,6 +62,7 @@ CachePolicy cachePolicy, UUID rootNodeUuid, String workspaceRootPath, + int maxPathLength, FilenameFilter filenameFilter, boolean updatesAllowed ) { assert sourceName != null; @@ -74,6 +76,7 @@ this.cachePolicy = cachePolicy; this.rootNodeUuid = rootNodeUuid; this.workspaceRootPath = workspaceRootPath; + this.maxPathLength = maxPathLength; this.filenameFilter = filenameFilter; this.updatesAllowed = updatesAllowed; } @@ -125,7 +128,7 @@ Request request ) throws RepositorySourceException { RequestProcessor proc = new FileSystemRequestProcessor(sourceName, defaultWorkspaceName, availableWorkspaces, creatingWorkspacesAllowed, rootNodeUuid, workspaceRootPath, - context, filenameFilter, updatesAllowed); + maxPathLength, context, filenameFilter, updatesAllowed); try { proc.process(request); } finally { Index: extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemI18n.java =================================================================== --- extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemI18n.java (revision 1126) +++ extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemI18n.java (working copy) @@ -46,9 +46,27 @@ public static I18n propertyIsRequired; public static I18n locationInRequestMustHavePath; public static I18n sameNameSiblingsAreNotAllowed; + public static I18n nodeOrderingNotSupported; public static I18n onlyTheDefaultNamespaceIsAllowed; public static I18n sourceIsReadOnly; + public static I18n pathIsReadOnly; public static I18n unableToCreateWorkspaces; + + // Writable messages + public static I18n parentIsReadOnly; + public static I18n fileAlreadyExists; + public static I18n couldNotCreateFile; + public static I18n unsupportedPrimaryType; + public static I18n invalidNameForResource; + public static I18n invalidPathForResource; + public static I18n invalidPropertyNames; + public static I18n couldNotWriteData; + public static I18n couldNotUpdateData; + public static I18n missingRequiredProperty; + public static I18n deleteFailed; + public static I18n copyFailed; + public static I18n getCanonicalPathFailed; + public static I18n maxPathLengthExceeded; static { try { Index: extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemRequestProcessor.java =================================================================== --- extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemRequestProcessor.java (revision 1126) +++ extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemRequestProcessor.java (working copy) @@ -26,30 +26,43 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.jboss.dna.common.i18n.I18n; +import org.jboss.dna.common.util.FileUtil; import org.jboss.dna.graph.DnaLexicon; import org.jboss.dna.graph.ExecutionContext; import org.jboss.dna.graph.JcrLexicon; import org.jboss.dna.graph.JcrNtLexicon; import org.jboss.dna.graph.Location; +import org.jboss.dna.graph.NodeConflictBehavior; import org.jboss.dna.graph.connector.RepositorySourceException; import org.jboss.dna.graph.mimetype.MimeTypeDetector; +import org.jboss.dna.graph.property.Binary; import org.jboss.dna.graph.property.BinaryFactory; import org.jboss.dna.graph.property.DateTimeFactory; import org.jboss.dna.graph.property.Name; import org.jboss.dna.graph.property.NameFactory; +import org.jboss.dna.graph.property.NamespaceRegistry; import org.jboss.dna.graph.property.Path; import org.jboss.dna.graph.property.PathFactory; import org.jboss.dna.graph.property.PathNotFoundException; +import org.jboss.dna.graph.property.Property; import org.jboss.dna.graph.property.PropertyFactory; +import org.jboss.dna.graph.property.UuidFactory; +import org.jboss.dna.graph.property.Path.Segment; import org.jboss.dna.graph.request.CloneBranchRequest; import org.jboss.dna.graph.request.CloneWorkspaceRequest; import org.jboss.dna.graph.request.CopyBranchRequest; @@ -79,11 +92,33 @@ private static final String DEFAULT_MIME_TYPE = "application/octet"; + /** + * Only certain properties are tolerated when writing content (dna:resource or jcr:resource) nodes. These properties are + * implicitly stored (primary type, data) or silently ignored (encoded, mimetype, last modified). The silently ignored + * properties must be accepted to stay compatible with the JCR specification. + */ + private static final Set ALLOWABLE_PROPERTIES_FOR_CONTENT = Collections.unmodifiableSet(new HashSet( + Arrays.asList(new Name[] { + JcrLexicon.PRIMARY_TYPE, + JcrLexicon.DATA, + JcrLexicon.ENCODED, + JcrLexicon.MIMETYPE, + JcrLexicon.LAST_MODIFIED}))); + /** + * Only certain properties are tolerated when writing files (nt:file) or folders (nt:folder) nodes. These properties are + * implicitly stored in the file or folder (primary type, created). + */ + private static final Set ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER = Collections.unmodifiableSet(new HashSet( + Arrays.asList(new Name[] { + JcrLexicon.PRIMARY_TYPE, + JcrLexicon.CREATED,}))); + private final String defaultNamespaceUri; private final Map availableWorkspaces; private final boolean creatingWorkspacesAllowed; private final String defaultWorkspaceName; private final File workspaceRootPath; + private final int maxPathLength; private final FilenameFilter filenameFilter; private final boolean updatesAllowed; private final MimeTypeDetector mimeTypeDetector; @@ -99,6 +134,7 @@ * generated each time that the repository is started. * @param workspaceRootPath the path to the workspace root directory; may be null. If specified, all workspace names will be * treated as relative paths from this directory. + * @param maxPathLength the maximum absolute path length supported by this processor * @param filenameFilter the filename filter to use to restrict the allowable nodes, or null if all files/directories are to * be exposed by this connector * @param updatesAllowed true if this connector supports updating the file system, or false if the connector is readonly @@ -109,6 +145,7 @@ boolean creatingWorkspacesAllowed, UUID rootNodeUuid, String workspaceRootPath, + int maxPathLength, ExecutionContext context, FilenameFilter filenameFilter, boolean updatesAllowed ) { @@ -120,6 +157,7 @@ this.creatingWorkspacesAllowed = creatingWorkspacesAllowed; this.defaultNamespaceUri = getExecutionContext().getNamespaceRegistry().getDefaultNamespaceUri(); this.rootNodeUuid = rootNodeUuid; + this.maxPathLength = maxPathLength; this.filenameFilter = filenameFilter; this.defaultWorkspaceName = defaultWorkspaceName; this.updatesAllowed = updatesAllowed; @@ -299,7 +337,213 @@ */ @Override public void process( CreateNodeRequest request ) { - updatesAllowed(request); + if (!updatesAllowed(request)) return; + + Path parentPath = getPathFor(request.under(), request); + if (parentPath == null) return; + + File workspace = getWorkspaceDirectory(request.inWorkspace()); + assert workspace != null; + + File parent = getExistingFileFor(workspace, parentPath, request.under(), request); + assert parent != null; + + NamespaceRegistry registry = getExecutionContext().getNamespaceRegistry(); + String newName = request.named().getString(registry); + File newFile = new File(parent, newName); + + Map properties = new HashMap(request.properties().size()); + for (Property property : request.properties()) { + properties.put(property.getName(), property); + } + + Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE); + Name primaryType = primaryTypeProp == null ? null : nameFactory().create(primaryTypeProp.getFirstValue()); + + if (JcrNtLexicon.FILE.equals(primaryType)) { + ensureValidProperties(request.properties(), ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER); + + // The FILE node is represented by the existence of the file + if (!parent.canWrite()) { + I18n msg = FileSystemI18n.parentIsReadOnly; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + } + + try { + ensureValidPathLength(newFile); + boolean skipWrite = false; + + if (newFile.exists()) { + if (request.conflictBehavior().equals(NodeConflictBehavior.APPEND)) { + I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed; + throw new InvalidRequestException(msg.text(this.getSourceName(), newName)); + } else if (request.conflictBehavior().equals(NodeConflictBehavior.DO_NOT_REPLACE)) { + skipWrite = true; + } + } + + // Don't try to write if the node conflict behavior is DO_NOT_REPLACE + if (!skipWrite) { + if (!newFile.createNewFile()) { + I18n msg = FileSystemI18n.fileAlreadyExists; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + } + } + } catch (IOException ioe) { + I18n msg = FileSystemI18n.couldNotCreateFile; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName(), + ioe.getMessage()), ioe)); + return; + } + } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || DnaLexicon.RESOURCE.equals(primaryType)) { + ensureValidProperties(request.properties(), ALLOWABLE_PROPERTIES_FOR_CONTENT); + if (!JcrLexicon.CONTENT.equals(request.named())) { + I18n msg = FileSystemI18n.invalidNameForResource; + String nodeName = request.named().getString(registry); + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName(), + nodeName))); + return; + } + + if (!parent.isFile()) { + I18n msg = FileSystemI18n.invalidPathForResource; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + } + + if (!parent.canWrite()) { + I18n msg = FileSystemI18n.parentIsReadOnly; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + } + + boolean skipWrite = false; + + if (parent.exists()) { + if (request.conflictBehavior().equals(NodeConflictBehavior.APPEND)) { + I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed; + throw new InvalidRequestException(msg.text(this.getSourceName(), newName)); + } else if (request.conflictBehavior().equals(NodeConflictBehavior.DO_NOT_REPLACE)) { + // The content node logically maps to the file contents. If there are file contents, don't replace them. + FileInputStream checkForContents = null; + try { + checkForContents = new FileInputStream(parent); + if (-1 != checkForContents.read()) skipWrite = true; + + } catch (IOException ignore) { + + } finally { + try { + checkForContents.close(); + } catch (Exception ignore) { + } + } + skipWrite = true; + } + } + + if (!skipWrite) { + // Copy over data into a temp file, then move it to the correct location + FileOutputStream fos = null; + try { + File temp = File.createTempFile("dna", null); + fos = new FileOutputStream(temp); + + Property dataProp = properties.get(JcrLexicon.DATA); + if (dataProp == null) { + I18n msg = FileSystemI18n.missingRequiredProperty; + String dataPropName = JcrLexicon.DATA.getString(registry); + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName(), + dataPropName))); + return; + } + + BinaryFactory binaryFactory = getExecutionContext().getValueFactories().getBinaryFactory(); + Binary binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue()); + InputStream is = binary.getStream(); + + final int BUFF_SIZE = 2 << 15; + byte[] buff = new byte[BUFF_SIZE]; + int len; + while (-1 != (len = is.read(buff, 0, BUFF_SIZE))) { + fos.write(buff, 0, len); + } + fos.flush(); + fos.close(); + is.close(); + + if (!FileUtil.delete(parent)) { + I18n msg = FileSystemI18n.deleteFailed; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + } + + if (!temp.renameTo(parent)) { + I18n msg = FileSystemI18n.couldNotUpdateData; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + + } + } catch (IOException ioe) { + I18n msg = FileSystemI18n.couldNotWriteData; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName(), + ioe.getMessage()), ioe)); + return; + + } finally { + try { + if (fos != null) fos.close(); + } catch (Exception ex) { + } + } + } + + } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) { + ensureValidProperties(request.properties(), ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER); + ensureValidPathLength(newFile); + + if (!newFile.mkdir()) { + I18n msg = FileSystemI18n.couldNotCreateFile; + request.setError(new RepositorySourceException(getSourceName(), msg.text(parent.getPath(), + request.inWorkspace(), + getSourceName(), + primaryType.getString(registry)))); + return; + } + } else { + // Set error and return + I18n msg = FileSystemI18n.unsupportedPrimaryType; + request.setError(new RepositorySourceException(getSourceName(), msg.text(primaryType.getString(registry), + parent.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + } + + Path newPath = pathFactory().create(parentPath, request.named()); + request.setActualLocationOfNode(Location.create(newPath)); } /** @@ -309,7 +553,24 @@ */ @Override public void process( UpdatePropertiesRequest request ) { - updatesAllowed(request); + if (!updatesAllowed(request)) return; + + File workspace = getWorkspaceDirectory(request.inWorkspace()); + File target = getExistingFileFor(workspace, request.on().getPath(), request.on(), request); + + if (!target.exists()) { + // getExistingFile fills in the PathNotFoundException for non-existent files + assert request.hasError(); + return; + } + + if (target.isFile()) { + ensureValidProperties(request.properties().values(), ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER); + } else { + ensureValidProperties(request.properties().values(), ALLOWABLE_PROPERTIES_FOR_CONTENT); + } + + request.setActualLocationOfNode(request.on()); } /** @@ -319,7 +580,96 @@ */ @Override public void process( CopyBranchRequest request ) { - updatesAllowed(request); + if (!updatesAllowed(request)) return; + + File fromWorkspace = getWorkspaceDirectory(request.fromWorkspace()); + File intoWorkspace = getWorkspaceDirectory(request.intoWorkspace()); + Path fromPath = getPathFor(request.from(), request); + if (fromPath == null) return; + File from = getExistingFileFor(fromWorkspace, fromPath, request.from(), request); + + Path intoPath = getPathFor(request.into(), request); + if (intoPath == null) return; + File into = getExistingFileFor(intoWorkspace, intoPath, request.into(), request); + + NamespaceRegistry registry = getExecutionContext().getNamespaceRegistry(); + Name desiredName = request.desiredName(); + String fileName = desiredName != null ? desiredName.getString(registry) : fromPath.getLastSegment().getString(registry); + File target = new File(into, fileName); + File tempInto = null; + + Location actualFrom = null; + Location actualTo = null; + try { + actualFrom = locationFor(fromWorkspace, from); + actualTo = locationFor(intoWorkspace, target); + } catch (IOException ioe) { + throw new RepositorySourceException(this.getSourceName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe); + } + + try { + int pathLenDelta = into.getCanonicalPath().length() - from.getCanonicalFile().getParent().length(); + if (pathLenDelta > 0) { + ensureValidPathLength(from, pathLenDelta); + } + } catch (IOException ioe) { + throw new RepositorySourceException(this.getSourceName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe); + } + + if (target.exists() && from.isFile()) { + try { + tempInto = File.createTempFile("dna", null, into); + } catch (IOException ioe) { + throw new RepositorySourceException(this.getSourceName(), + FileSystemI18n.couldNotCreateFile.text("temporary file", + request.intoWorkspace(), + getSourceName(), + ioe.getMessage()), ioe); + } + + try { + FileUtil.copy(from, tempInto); + } catch (IOException ioe) { + FileUtil.delete(tempInto); + throw new RepositorySourceException(this.getSourceName(), FileSystemI18n.copyFailed.text(from.getPath(), + request.fromWorkspace(), + tempInto.getPath(), + request.intoWorkspace(), + getSourceName()), ioe); + } + + // If everything worked, delete whatever was there and rename + if (target.exists()) { + if (!FileUtil.delete(target)) { + I18n msg = FileSystemI18n.deleteFailed; + request.setError(new RepositorySourceException(getSourceName(), msg.text(target.getPath(), + request.intoWorkspace(), + getSourceName()))); + FileUtil.delete(tempInto); + return; + } + } + + if (!tempInto.renameTo(target)) { + I18n msg = FileSystemI18n.couldNotUpdateData; + request.setError(new RepositorySourceException(getSourceName(), msg.text(target.getPath(), + request.intoWorkspace(), + getSourceName()))); + FileUtil.delete(tempInto); + return; + } + } else { + if (!from.renameTo(target)) { + I18n msg = FileSystemI18n.couldNotUpdateData; + request.setError(new RepositorySourceException(getSourceName(), msg.text(target.getPath(), + request.intoWorkspace(), + getSourceName()))); + return; + } + + } + request.setActualLocations(actualFrom, actualTo); + } /** @@ -329,7 +679,19 @@ */ @Override public void process( CloneBranchRequest request ) { - updatesAllowed(request); + if (!updatesAllowed(request)) return; + + CopyBranchRequest copy = new CopyBranchRequest(request.from(), request.fromWorkspace(), request.into(), + request.intoWorkspace(), request.desiredName()); + + process(copy); + + if (copy.hasError()) { + request.setError(copy.getError()); + return; + } + + request.setActualLocations(copy.getActualLocationBefore(), copy.getActualLocationAfter()); } /** @@ -339,7 +701,30 @@ */ @Override public void process( DeleteBranchRequest request ) { - updatesAllowed(request); + if (!updatesAllowed(request)) return; + + File workspace = getWorkspaceDirectory(request.inWorkspace()); + Path targetPath = getPathFor(request.at(), request); + if (targetPath == null) return; + + File target = getExistingFileFor(workspace, targetPath, request.at(), request); + Location actual = null; + + try { + actual = locationFor(workspace, target); + } catch (IOException ioe) { + throw new RepositorySourceException(this.getSourceName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe); + } + + if (!FileUtil.delete(target)) { + request.setError(new RepositorySourceException(this.getSourceName(), + FileSystemI18n.deleteFailed.text(target.getPath(), + request.inWorkspace(), + getSourceName()))); + return; + } + + request.setActualLocationOfNode(actual); } /** @@ -349,7 +734,56 @@ */ @Override public void process( MoveBranchRequest request ) { - updatesAllowed(request); + if (!updatesAllowed(request)) return; + + /* This connector does not support node ordering */ + if (request.before() != null) { + throw new InvalidRequestException(FileSystemI18n.nodeOrderingNotSupported.text(this.getSourceName())); + } + + File workspace = getWorkspaceDirectory(request.inWorkspace()); + Path fromPath = getPathFor(request.from(), request); + if (fromPath == null) return; + File from = getExistingFileFor(workspace, fromPath, request.from(), request); + + Path intoPath = getPathFor(request.into(), request); + if (intoPath == null) return; + File into = getExistingFileFor(workspace, intoPath, request.into(), request); + + NamespaceRegistry registry = getExecutionContext().getNamespaceRegistry(); + Name desiredName = request.desiredName(); + String fileName = desiredName != null ? desiredName.getString(registry) : fromPath.getLastSegment().getString(registry); + File target = new File(into, fileName); + + Location actualFrom = null; + Location actualTo = null; + try { + actualFrom = locationFor(workspace, from); + actualTo = locationFor(workspace, target); + } catch (IOException ioe) { + request.setError(new RepositorySourceException(this.getSourceName(), FileSystemI18n.getCanonicalPathFailed.text())); + return; + } + + try { + int pathLenDelta = into.getCanonicalPath().length() - from.getCanonicalFile().getParent().length(); + if (pathLenDelta > 0) { + ensureValidPathLength(from, pathLenDelta); + } + } catch (IOException ioe) { + request.setError(new RepositorySourceException(this.getSourceName(), FileSystemI18n.getCanonicalPathFailed.text())); + return; + } + + if (!from.renameTo(target)) { + I18n msg = FileSystemI18n.couldNotUpdateData; + request.setError(new RepositorySourceException(getSourceName(), + msg.text(target.getPath(), workspace, getSourceName()))); + return; + } + + request.setActualLocations(actualFrom, actualTo); + } /** @@ -359,12 +793,46 @@ */ @Override public void process( RenameNodeRequest request ) { - if (updatesAllowed(request)) super.process(request); + if (!updatesAllowed(request)) return; + + super.process(request); } /** * {@inheritDoc} * + * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest) + */ + @Override + public void process( CloneWorkspaceRequest request ) { + if (!updatesAllowed(request)) return; + + CreateWorkspaceRequest create = new CreateWorkspaceRequest(request.desiredNameOfTargetWorkspace(), + request.targetConflictBehavior()); + process(create); + + if (create.hasError()) { + request.setError(create.getError()); + return; + } + + File fromWorkspace = getWorkspaceDirectory(request.nameOfWorkspaceToBeCloned()); + assert fromWorkspace != null; + File toWorkspace = getWorkspaceDirectory(create.getActualWorkspaceName()); + assert toWorkspace != null; + + try { + FileUtil.copy(fromWorkspace, toWorkspace); + request.setActualWorkspaceName(create.getActualWorkspaceName()); + request.setActualRootLocation(Location.create(pathFactory().createRootPath(), this.rootNodeUuid)); + } catch (IOException ioe) { + throw new RepositorySourceException(this.getSourceName(), ioe.getMessage()); + } + } + + /** + * {@inheritDoc} + * * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyWorkspaceRequest) */ @Override @@ -442,16 +910,6 @@ /** * {@inheritDoc} * - * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest) - */ - @Override - public void process( CloneWorkspaceRequest request ) { - updatesAllowed(request); - } - - /** - * {@inheritDoc} - * * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest) */ @Override @@ -464,7 +922,7 @@ } // This doesn't create the directory representing the workspace (it must already exist), but it will add // the workspace name to the available names ... - File directory = new File(workspaceName); + File directory = getWorkspaceDirectory(workspaceName); if (directory.exists() && directory.isDirectory() && directory.canRead()) { request.setActualWorkspaceName(getCanonicalWorkspaceName(directory)); request.setActualRootLocation(Location.create(pathFactory().createRootPath())); @@ -503,6 +961,19 @@ return !request.hasError(); } + private UUID uuidFor( Location location ) { + if (location.getUuid() != null) return location.getUuid(); + if (!location.hasIdProperties()) return null; + + for (Property idProperty : location.getIdProperties()) { + if (JcrLexicon.UUID.equals(idProperty.getName())) { + return uuidFactory().create(idProperty.getFirstValue()); + } + } + + return null; + } + protected NameFactory nameFactory() { return getExecutionContext().getValueFactories().getNameFactory(); } @@ -511,19 +982,103 @@ return getExecutionContext().getValueFactories().getPathFactory(); } + protected UuidFactory uuidFactory() { + return getExecutionContext().getValueFactories().getUuidFactory(); + } + + /** + * Checks that the collection of {@code properties} only contains properties with allowable names. + * + * @param properties + * @param validPropertyNames + * @throws RepositorySourceException if {@code properties} contains a + * @see #ALLOWABLE_PROPERTIES_FOR_CONTENT + * @see #ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER + */ + protected void ensureValidProperties( Collection properties, + Set validPropertyNames ) { + List invalidNames = new LinkedList(); + NamespaceRegistry registry = getExecutionContext().getNamespaceRegistry(); + + for (Property property : properties) { + if (!validPropertyNames.contains(property.getName())) { + invalidNames.add(property.getName().getString(registry)); + } + } + + if (!invalidNames.isEmpty()) { + throw new RepositorySourceException(this.getSourceName(), + FileSystemI18n.invalidPropertyNames.text(invalidNames.toString())); + } + } + + protected void ensureValidPathLength( File root ) { + ensureValidPathLength(root, 0); + } + + /** + * Recursively checks if any of the files in the tree rooted at {@code root} would exceed the {@link #maxPathLength maximum + * path length for the processor} if their paths were {@code delta} characters longer. If any files would exceed this length, + * a {@link RepositorySourceException} is thrown. + * + * @param root the root of the tree to check; may be a file or directory but may not be null + * @param delta the change in the length of the path to check. Used to preemptively check whether moving a file or directory + * to a new path would violate path length rules + * @throws RepositorySourceException if any files in the tree rooted at {@code root} would exceed this {@link #maxPathLength + * the maximum path length for this processor} + */ + protected void ensureValidPathLength( File root, + int delta ) { + try { + int len = root.getCanonicalPath().length(); + if (len > maxPathLength - delta) { + String msg = FileSystemI18n.maxPathLengthExceeded.text(this.maxPathLength, + this.getSourceName(), + root.getCanonicalPath()); + throw new RepositorySourceException(this.getSourceName(), msg); + } + + if (root.isDirectory()) { + for (File child : root.listFiles()) { + ensureValidPathLength(child, delta); + } + + } + } catch (IOException ioe) { + throw new RepositorySourceException(this.getSourceName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe); + } + } + + protected Location locationFor( File workspaceRoot, + File path ) throws IOException { + assert path.getCanonicalPath().startsWith(workspaceRoot.getCanonicalPath()); + + String relativePath = path.getCanonicalPath().substring(workspaceRoot.getCanonicalPath().length()); + PathFactory pathFactory = pathFactory(); + List segments = new LinkedList(); + + String sepString = File.separator.equals("\\") ? "\\\\" : File.separator; + assert relativePath.charAt(0) == File.separatorChar; + for (String segment : relativePath.substring(1).split(sepString)) { + segments.add(pathFactory.createSegment(segment, 1)); + } + + return Location.create(pathFactory().createAbsolutePath(segments)); + } + protected Path getPathFor( Location location, Request request ) { - Path path = location.getPath(); - if (location.getUuid() != null && rootNodeUuid.equals(location.getUuid())) { + if (location.hasPath()) return location.getPath(); + + UUID uuid = uuidFor(location); + if (rootNodeUuid.equals(uuid)) { return pathFactory().createRootPath(); } - if (path == null) { - I18n msg = FileSystemI18n.locationInRequestMustHavePath; - throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request)); - } - return path; + I18n msg = FileSystemI18n.locationInRequestMustHavePath; + request.setError(new RepositorySourceException(getSourceName(), msg.text(getSourceName(), request))); + return null; } protected File getWorkspaceDirectory( String workspaceName ) { Index: extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemSource.java =================================================================== --- extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemSource.java (revision 1126) +++ extensions/dna-connector-filesystem/src/main/java/org/jboss/dna/connector/filesystem/FileSystemSource.java (working copy) @@ -78,6 +78,8 @@ protected static final String WORKSPACE_ROOT = "workspaceRootPath"; protected static final String PREDEFINED_WORKSPACE_NAMES = "predefinedWorkspaceNames"; protected static final String ALLOW_CREATING_WORKSPACES = "allowCreatingWorkspaces"; + protected static final String MAX_PATH_LENGTH = "maxPathLength"; + protected static final String ALLOW_UPDATES = "allowUpdates"; /** * This source supports events. @@ -103,6 +105,7 @@ public static final int DEFAULT_RETRY_LIMIT = 0; public static final int DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS = 60 * 5; // 5 minutes + public static final int DEFAULT_MAX_PATH_LENGTH = 60 * 5; // 255 for windows users private volatile String name; private volatile int retryLimit = DEFAULT_RETRY_LIMIT; @@ -111,6 +114,7 @@ private volatile String workspaceRootPath; private volatile String[] predefinedWorkspaces = new String[] {}; private volatile UUID rootNodeUuid = UUID.randomUUID(); + private volatile int maxPathLength = DEFAULT_MAX_PATH_LENGTH; private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities( SUPPORTS_SAME_NAME_SIBLINGS, DEFAULT_SUPPORTS_UPDATES, @@ -219,6 +223,31 @@ } /** + * Get the UUID that is used for the root node of each workspace + * + * @return the UUID that is used for the root node of each workspace + */ + public int getMaxPathLength() { + return maxPathLength; + } + + /** + * Set the maximum absolute path length supported by this source. + *

+ * The length of any path is calculated relative to the file system root, NOT the repository root. That is, if a workspace + * {@code foo} is mapped to the {@code /tmp/foo/bar} directory on the file system, then the path {@code /node1/node2} in the + * {@code foo} workspace has an effective length of 23 for the purposes of the {@code maxPathLength} calculation ({@code + * /tmp/foo/bar} has length 11, {@code /node1/node2} has length 12, 11 + 12 = 23). + *

+ * + * @param maxPathLength the maximum absolute path length supported by this source; must be non-negative + */ + public synchronized void setMaxPathLength( int maxPathLength ) { + CheckArg.isNonNegative(maxPathLength, "maxPathLength"); + this.maxPathLength = maxPathLength; + } + + /** * Get the name of the default workspace. * * @return the name of the workspace that should be used by default; never null @@ -292,6 +321,29 @@ } /** + * Get whether this source allows updates. + * + * @return true if this source allows updates by clients, or false if no updates are allowed + * @see #setUpdatesAllowed(boolean) + */ + public boolean areUpdatesAllowed() { + return capabilities.supportsUpdates(); + } + + /** + * Set whether this source allows updates to data within workspaces + * + * @param allowUpdates true if this source allows updates to data within workspaces clients, or false if updates are not + * allowed. + * @see #areUpdatesAllowed() + */ + public synchronized void setUpdatesAllowed( boolean allowUpdates ) { + capabilities = new RepositorySourceCapabilities(capabilities.supportsSameNameSiblings(), allowUpdates, + capabilities.supportsEvents(), capabilities.supportsCreatingWorkspaces(), + capabilities.supportsReferences()); + } + + /** * {@inheritDoc} * * @see org.jboss.dna.graph.connector.RepositorySource#getRetryLimit() @@ -467,7 +519,8 @@ } return new FileSystemConnection(name, defaultWorkspaceName, availableWorkspaces, isCreatingWorkspacesAllowed(), - cachePolicy, rootNodeUuid, workspaceRootPath, (FilenameFilter) null, getSupportsUpdates()); + cachePolicy, rootNodeUuid, workspaceRootPath, maxPathLength, (FilenameFilter)null, + getSupportsUpdates()); } @Immutable Index: extensions/dna-connector-filesystem/src/main/resources/org/jboss/dna/connector/filesystem/FileSystemI18n.properties =================================================================== --- extensions/dna-connector-filesystem/src/main/resources/org/jboss/dna/connector/filesystem/FileSystemI18n.properties (revision 1126) +++ extensions/dna-connector-filesystem/src/main/resources/org/jboss/dna/connector/filesystem/FileSystemI18n.properties (working copy) @@ -35,6 +35,24 @@ propertyIsRequired = The {0} property is required but has no value locationInRequestMustHavePath = {0} requires a path in the request: {1} sameNameSiblingsAreNotAllowed = {0} does not allow same name siblings on nodes: {1} +nodeOrderingNotSupported = {0} does not support node ordering onlyTheDefaultNamespaceIsAllowed = {0} requires node names use the default namespace: {1} -sourceIsReadOnly = {0} is a read-only source; no updates are allowed +sourceIsReadOnly = The source "{0}" does not allow updates +pathIsReadOnly = The path "{0}" in workspace "{1}" in {2} cannot be written to. See java.io.File\#canWrite(). + +# Writable tests +parentIsReadOnly = The parent node at path "{0}" in workspace "{1}" in {2} cannot be written to. See java.io.File\#canWrite(). unableToCreateWorkspaces = {0} does not allow creating new workspaces (request was to create "{1}") +fileAlreadyExists = The path "{0}" in workspace "{1}" in {2} already exists. +couldNotCreateFile = Error creating the path "{0}" in workspace "{1}" in {2}: {3} +unsupportedPrimaryType = Primary type "{3}" for path "{0}" in workspace "{1}" in {2} is not valid for the file system connector. Valid primary types are nt\:file, nt\:folder, nt\:resource, and dna\:resouce. +invalidNameForResource = Invalid node name "{3}" for node at path "{0}" in workspace "{1}" in {2}. The name of nodes with primary type nt:resource or dna:resource must be "jcr:content". +invalidPathForResource = Invalid parent type for node at path "{0}" in workspace "{1}" in {2}. The parent node for nodes with primary type nt:resource or dna:resource must be of type nt:file. +invalidPropertyNames = Attempt to set or update invalid property names: {0} +couldNotWriteData = Error writing data to path "{0}" in workspace "{1}" in {2}\: {3} +couldNotUpdateData = Error moving temporary data file to path "{0}" in workspace "{1}" in {2} +missingRequiredProperty = Missing required property "{3}" at path "{0}" in workspace "{1}" in {2} +deleteFailed = Could not delete file at path "{0}" in workspace "{1}" in {2} +copyFailed = Could not copy file at path "{0}" in workspace "{1}" to path "{2}" in workspace "{3}" in {4} +getCanonicalPathFailed = Could not determine canonical path +maxPathLengthExceeded = The maximum absolute path length ({0}) for source "{1}" was exceeded by the node at: {2} Index: extensions/dna-connector-filesystem/src/test/java/org/jboss/dna/connector/filesystem/FileSystemConnectorNotWritableTest.java =================================================================== --- extensions/dna-connector-filesystem/src/test/java/org/jboss/dna/connector/filesystem/FileSystemConnectorNotWritableTest.java (revision 1126) +++ extensions/dna-connector-filesystem/src/test/java/org/jboss/dna/connector/filesystem/FileSystemConnectorNotWritableTest.java (working copy) @@ -1,64 +0,0 @@ -/* - * JBoss DNA (http://www.jboss.org/dna) - * See the COPYRIGHT.txt file distributed with this work for information - * regarding copyright ownership. Some portions may be licensed - * to Red Hat, Inc. under one or more contributor license agreements. - * See the AUTHORS.txt file in the distribution for a full listing of - * individual contributors. - * - * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA - * is licensed to you under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * JBoss DNA is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.jboss.dna.connector.filesystem; - -import java.io.File; -import org.jboss.dna.graph.Graph; -import org.jboss.dna.graph.connector.RepositorySource; -import org.jboss.dna.graph.connector.test.NotWritableConnectorTest; - -/** - * @author Randall Hauch - */ -public class FileSystemConnectorNotWritableTest extends NotWritableConnectorTest { - - /** - * {@inheritDoc} - * - * @see org.jboss.dna.graph.connector.test.AbstractConnectorTest#setUpSource() - */ - @Override - protected RepositorySource setUpSource() { - // Set the connection properties to be use the content of "./src/test/resources/repositories" as a repository ... - String path = new File(".").getAbsolutePath() + "/src/test/resources/repositories/"; - String[] predefinedWorkspaceNames = new String[] {path + "airplanes", path + "cars"}; - FileSystemSource source = new FileSystemSource(); - source.setName("Test Repository"); - source.setPredefinedWorkspaceNames(predefinedWorkspaceNames); - source.setDefaultWorkspaceName(predefinedWorkspaceNames[0]); - source.setCreatingWorkspacesAllowed(false); - - return source; - } - - /** - * {@inheritDoc} - * - * @see org.jboss.dna.graph.connector.test.AbstractConnectorTest#initializeContent(org.jboss.dna.graph.Graph) - */ - @Override - protected void initializeContent( Graph graph ) { - // No need to initialize any content ... - } -} Index: extensions/dna-connector-filesystem/src/test/java/org/jboss/dna/connector/filesystem/FileSystemConnectorWritableTest.java =================================================================== --- extensions/dna-connector-filesystem/src/test/java/org/jboss/dna/connector/filesystem/FileSystemConnectorWritableTest.java (revision 1125) +++ extensions/dna-connector-filesystem/src/test/java/org/jboss/dna/connector/filesystem/FileSystemConnectorWritableTest.java (working copy) @@ -23,42 +23,452 @@ */ package org.jboss.dna.connector.filesystem; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import org.jboss.dna.common.util.FileUtil; +import org.jboss.dna.graph.DnaLexicon; import org.jboss.dna.graph.Graph; +import org.jboss.dna.graph.JcrLexicon; +import org.jboss.dna.graph.JcrMixLexicon; +import org.jboss.dna.graph.JcrNtLexicon; import org.jboss.dna.graph.connector.RepositorySource; -import org.jboss.dna.graph.connector.test.NotWritableConnectorTest; +import org.jboss.dna.graph.connector.RepositorySourceException; +import org.jboss.dna.graph.connector.test.AbstractConnectorTest; +import org.jboss.dna.graph.request.InvalidRequestException; +import org.junit.Test; -/** - * @author Randall Hauch - */ -public class FileSystemConnectorNotWritableTest extends NotWritableConnectorTest { +public class FileSystemConnectorWritableTest extends AbstractConnectorTest { - /** - * {@inheritDoc} - * - * @see org.jboss.dna.graph.connector.test.AbstractConnectorTest#setUpSource() - */ + public static final String ARBITRARY_PROPERTIES_NOT_SUPPORTED = "This connector does not support setting arbitrary properties"; + + private static final String REPO_PATH = "./src/test/resources/repositories/"; + private final String TEST_CONTENT = "Test content"; + + protected File testWorkspaceRoot; + protected File otherWorkspaceRoot; + protected File newWorkspaceRoot; + @Override protected RepositorySource setUpSource() { // Set the connection properties to be use the content of "./src/test/resources/repositories" as a repository ... - String path = new File(".").getAbsolutePath() + "/src/test/resources/repositories/"; - String[] predefinedWorkspaceNames = new String[] {path + "airplanes", path + "cars"}; + String[] predefinedWorkspaceNames = new String[] {"test", "otherWorkspace", "airplanes", "cars"}; FileSystemSource source = new FileSystemSource(); source.setName("Test Repository"); + source.setWorkspaceRootPath(REPO_PATH); source.setPredefinedWorkspaceNames(predefinedWorkspaceNames); source.setDefaultWorkspaceName(predefinedWorkspaceNames[0]); - source.setCreatingWorkspacesAllowed(false); + source.setCreatingWorkspacesAllowed(true); + source.setUpdatesAllowed(true); + testWorkspaceRoot = new File(REPO_PATH, "test"); + testWorkspaceRoot.mkdir(); + + otherWorkspaceRoot = new File(REPO_PATH, "otherWorkspace"); + otherWorkspaceRoot.mkdir(); + + newWorkspaceRoot = new File(REPO_PATH, "newWorkspace"); + newWorkspaceRoot.mkdir(); + return source; } - /** - * {@inheritDoc} - * - * @see org.jboss.dna.graph.connector.test.AbstractConnectorTest#initializeContent(org.jboss.dna.graph.Graph) - */ @Override protected void initializeContent( Graph graph ) { - // No need to initialize any content ... + // No setup required } + + @Override + public void afterEach() throws Exception { + FileUtil.delete(testWorkspaceRoot); + FileUtil.delete(otherWorkspaceRoot); + FileUtil.delete(newWorkspaceRoot); + super.afterEach(); + } + + @Test + public void shouldBeAbleToCreateFileWithContent() { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + } + + @Test + public void shouldRespectConflictBehaviorOnCreate() { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + "Should not overwrite".getBytes()).ifAbsent().and(); + + File newFile = new File(testWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToCreateFileWithNoContent() { + graph.create("/testEmptyFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testEmptyFile"); + assertThat(newFile.exists(), is(true)); + assertThat(newFile.isFile(), is(true)); + } + + @Test + public void shouldBeAbleToCreateFolder() { + graph.create("/testFolder").orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFolder"); + assertThat(newFile.exists(), is(true)); + assertThat(newFile.isDirectory(), is(true)); + } + + @Test + public void shouldBeAbleToAddChildrenToFolder() { + graph.create("/testFolder").orReplace().and(); + + File newFolder = new File(testWorkspaceRoot, "testFolder"); + assertThat(newFolder.exists(), is(true)); + assertThat(newFolder.isDirectory(), is(true)); + + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testfile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + + } + + @Test( expected = RepositorySourceException.class ) + public void shouldNotBeAbleToCreateInvalidTypeForRepository() { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.UNSTRUCTURED).orReplace().and(); + } + + @Test( expected = RepositorySourceException.class ) + public void shouldNotBeAbleToSetArbitraryProperties() { + graph.create("/testFile").with(JcrLexicon.MIXIN_TYPES, JcrMixLexicon.LOCKABLE).orReplace().and(); + } + + @Test + public void shouldBeAbleToCopyFile() { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.copy("/testFile").to("/copiedFile"); + File copiedFile = new File(testWorkspaceRoot, "copiedFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToCopyFolder() { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.copy("/testFolder").to("/copiedFolder"); + File copiedFolder = new File(testWorkspaceRoot, "copiedFolder"); + assertTrue(copiedFolder.exists()); + assertTrue(copiedFolder.isDirectory()); + + File copiedFile = new File(testWorkspaceRoot, "copiedFolder/testFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToMoveFile() { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.create("/newFolder").orReplace().and(); + + graph.move("/testFile").into("/newFolder"); + assertThat(newFile.exists(), is(false)); + + File copiedFile = new File(testWorkspaceRoot, "newFolder/testFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToMoveFolder() { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.create("/newFolder").orReplace().and(); + + graph.move("/testFolder").into("/newFolder"); + assertThat(newFile.exists(), is(false)); + + File copiedFolder = new File(testWorkspaceRoot, "newFolder/testFolder"); + assertTrue(copiedFolder.exists()); + assertTrue(copiedFolder.isDirectory()); + + File copiedFile = new File(testWorkspaceRoot, "newFolder/testFolder/testFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToDeleteFolderWithContents() { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFolder = new File(testWorkspaceRoot, "testFolder"); + assertTrue(newFolder.exists()); + assertTrue(newFolder.isDirectory()); + + File newFile = new File(testWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.delete("/testFolder"); + + assertThat(newFolder.exists(), is(false)); + } + + @Test + public void shouldBeAbleToDeleteFile() { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFolder = new File(testWorkspaceRoot, "testFolder"); + assertTrue(newFolder.exists()); + assertTrue(newFolder.isDirectory()); + + File newFile = new File(testWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.delete("/testFolder/testFile"); + + assertTrue(newFolder.exists()); + assertThat(newFile.exists(), is(false)); + } + + /** + * Since the FS connector does not support UUIDs (under the root node), all clones are just copies (clone for + * non-referenceable nodes is a copy to the corresponding path). + */ + @Test + public void shouldBeAbleToCloneFolder() { + graph.useWorkspace("otherWorkspace"); + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(otherWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.useWorkspace("test"); + graph.clone("/testFolder").fromWorkspace("otherWorkspace").as("clonedFolder").into("/").failingIfAnyUuidsMatch(); + File copiedFolder = new File(testWorkspaceRoot, "clonedFolder"); + assertTrue(copiedFolder.exists()); + assertTrue(copiedFolder.isDirectory()); + + File copiedFile = new File(testWorkspaceRoot, "clonedFolder/testFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + /** + * Since the FS connector does not support UUIDs (under the root node), all clones are just copies (clone for + * non-referenceable nodes is a copy to the corresponding path). + */ + @Test + public void shouldBeAbleToCloneFile() { + graph.useWorkspace("otherWorkspace"); + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(otherWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.useWorkspace("test"); + graph.clone("/testFile").fromWorkspace("otherWorkspace").as("clonedFile").into("/").failingIfAnyUuidsMatch(); + File copiedFile = new File(testWorkspaceRoot, "clonedFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + @Test( expected = InvalidRequestException.class ) + public void shouldNotBeAbleToReorderFolder() { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder2").orReplace().and(); + + File newFolder = new File(testWorkspaceRoot, "testFolder"); + assertTrue(newFolder.exists()); + assertTrue(newFolder.isDirectory()); + + File newFolder2 = new File(testWorkspaceRoot, "testFolder2"); + assertTrue(newFolder2.exists()); + assertTrue(newFolder2.isDirectory()); + + graph.move("/testFolder2").before("/testFolder"); + } + + @Test( expected = InvalidRequestException.class ) + public void shouldNotBeAbleToReorderFile() { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + graph.create("/testFolder/testFile2").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile2/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFolder = new File(testWorkspaceRoot, "testFolder"); + assertTrue(newFolder.exists()); + assertTrue(newFolder.isDirectory()); + + File newFile = new File(testWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + File newFile2 = new File(testWorkspaceRoot, "testFolder/testFile2"); + assertContents(newFile2, TEST_CONTENT); + + graph.move("/testFolder/testFile2").before("/testFolder/testFile"); + } + + @Test + public void shouldBeAbleToRenameFolder() { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFolder/testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.move("/testFolder").as("newFolder").into("/"); + assertThat(newFile.exists(), is(false)); + + File copiedFolder = new File(testWorkspaceRoot, "newFolder"); + assertTrue(copiedFolder.exists()); + assertTrue(copiedFolder.isDirectory()); + + File copiedFile = new File(testWorkspaceRoot, "newFolder/testFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToRenameFile() { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.move("/testFile").as("copiedFile").into("/"); + assertThat(newFile.exists(), is(false)); + + File copiedFile = new File(testWorkspaceRoot, "copiedFile"); + assertContents(copiedFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToCreateWorkspace() { + graph.createWorkspace().named("newWorkspace"); + + graph.useWorkspace("newWorkspace"); + + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(newWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + } + + @Test + public void shouldBeAbleToCloneWorkspace() { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, DnaLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + File newFile = new File(testWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + + graph.createWorkspace().clonedFrom("test").named("newWorkspace"); + + newFile = new File(newWorkspaceRoot, "testFile"); + assertContents(newFile, TEST_CONTENT); + + } + + @Test + public void shouldBeAbleToCreateDeepPath() { + String pathName = ""; + + for (int i = 0; i < 30; i++) { + pathName += "/test"; + graph.create(pathName).orReplace().and(); + } + } + + @Test( expected = RepositorySourceException.class ) + public void shouldNotBeAbleToCreateTooDeepPath() { + String pathName = ""; + + for (int i = 0; i < 100; i++) { + pathName += "/testFolder"; + graph.create(pathName).orReplace().and(); + } + } + + protected void assertContents( File file, + String contents ) { + assertTrue(file.exists()); + assertTrue(file.isFile()); + + StringBuilder buff = new StringBuilder(); + final int BUFF_SIZE = 8192; + byte[] bytes = new byte[BUFF_SIZE]; + int len; + FileInputStream fis = null; + + try { + fis = new FileInputStream(file); + + while (-1 != (len = fis.read(bytes, 0, BUFF_SIZE))) { + buff.append(new String(bytes, 0, len)); + } + + assertThat(buff.toString(), is(contents)); + } catch (IOException ioe) { + ioe.printStackTrace(); + fail(ioe.getMessage()); + return; + } finally { + try { + fis.close(); + } catch (Exception ignore) { + } + } + } }