View Javadoc

1   package org.modeshape.connector.svn;
2   
3   import java.io.ByteArrayOutputStream;
4   import java.util.Arrays;
5   import java.util.Collection;
6   import java.util.Collections;
7   import java.util.HashSet;
8   import java.util.LinkedList;
9   import java.util.List;
10  import java.util.Set;
11  import java.util.UUID;
12  import org.modeshape.common.i18n.I18n;
13  import org.modeshape.connector.scm.ScmAction;
14  import org.modeshape.connector.svn.mgnt.AddDirectory;
15  import org.modeshape.connector.svn.mgnt.AddFile;
16  import org.modeshape.connector.svn.mgnt.DeleteEntry;
17  import org.modeshape.connector.svn.mgnt.UpdateFile;
18  import org.modeshape.graph.ExecutionContext;
19  import org.modeshape.graph.JcrLexicon;
20  import org.modeshape.graph.JcrNtLexicon;
21  import org.modeshape.graph.ModeShapeIntLexicon;
22  import org.modeshape.graph.ModeShapeLexicon;
23  import org.modeshape.graph.connector.RepositorySourceException;
24  import org.modeshape.graph.connector.base.PathNode;
25  import org.modeshape.graph.connector.base.PathWorkspace;
26  import org.modeshape.graph.property.Binary;
27  import org.modeshape.graph.property.BinaryFactory;
28  import org.modeshape.graph.property.DateTimeFactory;
29  import org.modeshape.graph.property.Name;
30  import org.modeshape.graph.property.NameFactory;
31  import org.modeshape.graph.property.NamespaceRegistry;
32  import org.modeshape.graph.property.Path;
33  import org.modeshape.graph.property.PathFactory;
34  import org.modeshape.graph.property.Property;
35  import org.modeshape.graph.property.PropertyFactory;
36  import org.modeshape.graph.property.Path.Segment;
37  import org.tmatesoft.svn.core.SVNCommitInfo;
38  import org.tmatesoft.svn.core.SVNDirEntry;
39  import org.tmatesoft.svn.core.SVNException;
40  import org.tmatesoft.svn.core.SVNNodeKind;
41  import org.tmatesoft.svn.core.SVNProperties;
42  import org.tmatesoft.svn.core.SVNProperty;
43  import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
44  import org.tmatesoft.svn.core.io.ISVNEditor;
45  import org.tmatesoft.svn.core.io.SVNRepository;
46  import org.tmatesoft.svn.core.wc.SVNWCUtil;
47  
48  /**
49   * Workspace implementation for SVN repository connector
50   */
51  public class SvnWorkspace extends PathWorkspace<PathNode> {
52      private static final String DEFAULT_MIME_TYPE = "application/octet-stream";
53      protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
54  
55      private final Set<Name> ALLOWABLE_PRIMARY_TYPES = Collections.unmodifiableSet(new HashSet<Name>(Arrays.asList(new Name[] {
56          JcrNtLexicon.FOLDER, JcrNtLexicon.FILE, JcrNtLexicon.RESOURCE, ModeShapeLexicon.RESOURCE, null})));
57  
58      /**
59       * Only certain properties are tolerated when writing content (dna:resource or jcr:resource) nodes. These properties are
60       * implicitly stored (primary type, data) or silently ignored (encoded, mimetype, last modified). The silently ignored
61       * properties must be accepted to stay compatible with the JCR specification.
62       */
63      private final Set<Name> ALLOWABLE_PROPERTIES_FOR_CONTENT = Collections.unmodifiableSet(new HashSet<Name>(
64                                                                                                               Arrays.asList(new Name[] {
65                                                                                                                   JcrLexicon.PRIMARY_TYPE,
66                                                                                                                   JcrLexicon.DATA,
67                                                                                                                   JcrLexicon.ENCODED,
68                                                                                                                   JcrLexicon.MIMETYPE,
69                                                                                                                   JcrLexicon.LAST_MODIFIED,
70                                                                                                                   JcrLexicon.LAST_MODIFIED_BY,
71                                                                                                                   JcrLexicon.UUID,
72                                                                                                                   ModeShapeIntLexicon.NODE_DEFINITON})));
73      /**
74       * Only certain properties are tolerated when writing files (nt:file) or folders (nt:folder) nodes. These properties are
75       * implicitly stored in the file or folder (primary type, created).
76       */
77      private final Set<Name> ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER = Collections.unmodifiableSet(new HashSet<Name>(
78                                                                                                                      Arrays.asList(new Name[] {
79                                                                                                                          JcrLexicon.PRIMARY_TYPE,
80                                                                                                                          JcrLexicon.CREATED,
81                                                                                                                          JcrLexicon.CREATED_BY,
82                                                                                                                          JcrLexicon.UUID,
83                                                                                                                          ModeShapeIntLexicon.NODE_DEFINITON})));
84  
85      // The SvnRepository is a reference to the ModeShape repository class
86      private final SvnRepository repository;
87  
88      // The SVNRepository is a reference to the tmatesoft SVN repository class
89      private final SVNRepository workspaceRoot;
90  
91      public SvnWorkspace( SvnRepository repository,
92                           SVNRepository workspaceRoot,
93                           String name,
94                           UUID rootNodeUuid ) {
95          super(name, rootNodeUuid);
96  
97          this.repository = repository;
98          this.workspaceRoot = workspaceRoot;
99  
100         ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(repository.source().getUsername(),
101                                                                                              repository.source().getPassword());
102         workspaceRoot.setAuthenticationManager(authManager);
103     }
104 
105     public SvnWorkspace( String name,
106                          SvnWorkspace originalToClone,
107                          SVNRepository workspaceRoot ) {
108         super(name, originalToClone.getRootNodeUuid());
109 
110         this.repository = originalToClone.repository;
111         this.workspaceRoot = workspaceRoot;
112 
113         cloneWorkspace(originalToClone);
114     }
115 
116     private void cloneWorkspace( SvnWorkspace original ) {
117         I18n msg = SvnRepositoryConnectorI18n.sourceDoesNotSupportCloningWorkspaces;
118         throw new UnsupportedOperationException(msg.text(original.source().getName()));
119     }
120 
121     private final SvnRepositorySource source() {
122         return repository.source();
123     }
124 
125     private final String getSourceName() {
126         return source().getName();
127     }
128 
129     private final ExecutionContext context() {
130         return source().getRepositoryContext().getExecutionContext();
131     }
132 
133     private final NameFactory nameFactory() {
134         return context().getValueFactories().getNameFactory();
135     }
136 
137     private final PathFactory pathFactory() {
138         return context().getValueFactories().getPathFactory();
139     }
140 
141     private final Path pathTo( PathNode node ) {
142         if (node.getParent() == null) {
143             return pathFactory().createRootPath();
144         }
145         return pathFactory().create(node.getParent(), node.getName());
146     }
147 
148     @Override
149     public PathNode getRootNode() {
150         return getNode(context().getValueFactories().getPathFactory().createRootPath());
151     }
152 
153     @Override
154     public PathNode getNode( Path path ) {
155         PathNode node;
156 
157         ExecutionContext context = source().getRepositoryContext().getExecutionContext();
158         List<Property> properties = new LinkedList<Property>();
159         List<Segment> children = new LinkedList<Segment>();
160 
161         try {
162             boolean result = readNode(context, this.getName(), path, properties, children);
163             if (!result) return null;
164         } catch (SVNException ex) {
165             return null;
166         }
167 
168         UUID uuid = path.isRoot() ? source().getRootNodeUuidObject() : null;
169         Path parent = path.isRoot() ? null : path.getParent();
170         Segment name = path.isRoot() ? null : path.getLastSegment();
171 
172         node = new PathNode(uuid, parent, name, properties, children);
173 
174         return node;
175     }
176 
177     protected boolean readNode( ExecutionContext context,
178                                 String workspaceName,
179                                 Path requestedPath,
180                                 List<Property> properties,
181                                 List<Segment> children ) throws SVNException {
182         PathFactory pathFactory = context.getValueFactories().getPathFactory();
183         NamespaceRegistry registry = context.getNamespaceRegistry();
184 
185         if (requestedPath.isRoot()) {
186             // workspace root must be a directory
187             if (children != null) {
188                 final Collection<SVNDirEntry> entries = SvnRepositoryUtil.getDir(workspaceRoot, "");
189                 for (SVNDirEntry entry : entries) {
190                     // All of the children of a directory will be another directory or a file, but never a "jcr:content" node
191                     // ...
192                     children.add(pathFactory.createSegment(entry.getName()));
193                 }
194             }
195             // There are no properties on the root ...
196         } else {
197             // Generate the properties for this File object ...
198             PropertyFactory factory = context.getPropertyFactory();
199             DateTimeFactory dateFactory = context.getValueFactories().getDateFactory();
200 
201             // Figure out the kind of node this represents ...
202             SVNNodeKind kind = getNodeKind(context, requestedPath, source().getRepositoryRootUrl());
203             if (kind == SVNNodeKind.NONE) {
204                 // The node doesn't exist
205                 return false;
206             }
207             if (kind == SVNNodeKind.DIR) {
208                 String directoryPath = requestedPath.getString(registry);
209                 if (!source().getRepositoryRootUrl().equals(workspaceName)) {
210                     directoryPath = directoryPath.substring(1);
211                 }
212                 if (children != null) {
213                     // Decide how to represent the children ...
214                     Collection<SVNDirEntry> dirEntries = SvnRepositoryUtil.getDir(workspaceRoot, directoryPath);
215                     for (SVNDirEntry entry : dirEntries) {
216                         // All of the children of a directory will be another directory or a file,
217                         // but never a "jcr:content" node ...
218                         children.add(pathFactory.createSegment(entry.getName()));
219                     }
220                 }
221                 if (properties != null) {
222                     // Load the properties for this directory ......
223                     properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER));
224                     // SVNDirEntry entry = getEntryInfo(workspaceRoot, directoryPath);
225                     SVNDirEntry entry = workspaceRoot.info(directoryPath, -1);
226                     if (entry != null) {
227                         properties.add(factory.create(JcrLexicon.CREATED, dateFactory.create(entry.getDate())));
228                     }
229                 }
230             } else {
231                 // It's not a directory, so must be a file; the only child of an nt:file is the "jcr:content" node
232                 // ...
233                 if (requestedPath.endsWith(JcrLexicon.CONTENT)) {
234                     // There are never any children of these nodes, just properties ...
235                     if (properties != null) {
236                         String contentPath = requestedPath.getParent().getString(registry);
237                         if (!source().getRepositoryRootUrl().equals(workspaceName)) {
238                             contentPath = contentPath.substring(1);
239                         }
240                         SVNDirEntry entry = workspaceRoot.info(contentPath, -1);
241                         if (entry != null) {
242                             // The request is to get properties of the "jcr:content" child node ...
243                             // Do NOT use "nt:resource", since it extends "mix:referenceable". The JCR spec
244                             // does not require that "jcr:content" is of type "nt:resource", but rather just
245                             // suggests it. Therefore, we can use "dna:resource", which is identical to
246                             // "nt:resource" except it does not extend "mix:referenceable"
247                             properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE));
248                             properties.add(factory.create(JcrLexicon.LAST_MODIFIED, dateFactory.create(entry.getDate())));
249                         }
250 
251                         ByteArrayOutputStream os = new ByteArrayOutputStream();
252                         SVNProperties fileProperties = new SVNProperties();
253                         workspaceRoot.getFile(contentPath, -1, fileProperties, os);
254                         String mimeType = fileProperties.getStringValue(SVNProperty.MIME_TYPE);
255                         if (mimeType == null) mimeType = DEFAULT_MIME_TYPE;
256                         properties.add(factory.create(JcrLexicon.MIMETYPE, mimeType));
257 
258                         if (os.toByteArray().length > 0) {
259                             // Now put the file's content into the "jcr:data" property ...
260                             BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
261                             properties.add(factory.create(JcrLexicon.DATA, binaryFactory.create(os.toByteArray())));
262                         }
263                     }
264                 } else {
265                     // Determine the corresponding file path for this object ...
266                     String filePath = requestedPath.getString(registry);
267                     if (!source().getRepositoryRootUrl().equals(workspaceName)) {
268                         filePath = filePath.substring(1);
269                     }
270                     if (children != null) {
271                         // Not a "jcr:content" child node but rather an nt:file node, so add the child ...
272                         children.add(pathFactory.createSegment(JcrLexicon.CONTENT));
273                     }
274                     if (properties != null) {
275                         // Now add the properties to "nt:file" ...
276                         properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE));
277                         ByteArrayOutputStream os = new ByteArrayOutputStream();
278                         SVNProperties fileProperties = new SVNProperties();
279                         workspaceRoot.getFile(filePath, -1, fileProperties, os);
280                         String created = fileProperties.getStringValue(SVNProperty.COMMITTED_DATE);
281                         properties.add(factory.create(JcrLexicon.CREATED, dateFactory.create(created)));
282                     }
283                 }
284             }
285         }
286         return true;
287     }
288 
289     protected SVNNodeKind getNodeKind( ExecutionContext context,
290                                        Path path,
291                                        String repositoryRootUrl ) throws SVNException {
292         assert path != null;
293         assert repositoryRootUrl != null;
294 
295         // See if the path is a "jcr:content" node ...
296         if (path.endsWith(JcrLexicon.CONTENT)) {
297             // We only want to use the parent path to find the actual file ...
298             path = path.getParent();
299         }
300         String pathAsString = path.getString(context.getNamespaceRegistry());
301         if (!repositoryRootUrl.equals(getName())) {
302             pathAsString = pathAsString.substring(1);
303         }
304 
305         String absolutePath = pathAsString;
306         SVNNodeKind kind = workspaceRoot.checkPath(absolutePath, -1);
307         if (kind == SVNNodeKind.UNKNOWN) {
308             // node is unknown
309             throw new RepositorySourceException(getSourceName(),
310                                                 SvnRepositoryConnectorI18n.nodeIsActuallyUnknow.text(pathAsString));
311         }
312         return kind;
313     }
314 
315     private Name primaryTypeFor( PathNode node ) {
316         Property primaryTypeProp = node.getProperty(JcrLexicon.PRIMARY_TYPE);
317         Name primaryType = primaryTypeProp == null ? null : nameFactory().create(primaryTypeProp.getFirstValue());
318 
319         return primaryType;
320     }
321 
322     protected void validate( PathNode node ) {
323         Name primaryType = primaryTypeFor(node);
324 
325         if (!ALLOWABLE_PRIMARY_TYPES.contains(primaryType)) {
326             I18n msg = SvnRepositoryConnectorI18n.unsupportedPrimaryType;
327             NamespaceRegistry registry = context().getNamespaceRegistry();
328             String path = pathTo(node).getString(registry);
329             String primaryTypeName = primaryType.getString(registry);
330             throw new RepositorySourceException(getSourceName(), msg.text(path, getName(), getSourceName(), primaryTypeName));
331         }
332 
333         Set<Name> invalidPropertyNames = new HashSet<Name>(node.getProperties().keySet());
334         if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) {
335             invalidPropertyNames.removeAll(ALLOWABLE_PROPERTIES_FOR_CONTENT);
336         } else {
337             invalidPropertyNames.removeAll(ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER);
338         }
339 
340         if (!invalidPropertyNames.isEmpty()) {
341             I18n msg = SvnRepositoryConnectorI18n.invalidPropertyNames;
342             throw new RepositorySourceException(getSourceName(), msg.text(invalidPropertyNames));
343 
344         }
345 
346     }
347 
348     @Override
349     public ChangeCommand<PathNode> createMoveCommand( PathNode source,
350                                                       PathNode target ) {
351         // Manually create all of the commands needed to delete the source and recreate it in the target
352         List<SvnCommand> commands = new LinkedList<SvnCommand>();
353         LinkedList<Path> pathsToCopy = new LinkedList<Path>();
354 
355         Path sourceRoot = pathTo(source);
356         Path targetRoot = pathTo(target);
357 
358         pathsToCopy.add(sourceRoot);
359 
360         while (!pathsToCopy.isEmpty()) {
361             Path path = pathsToCopy.removeFirst();
362             PathNode node = getNode(path);
363 
364             assert node != null : path;
365 
366             Path oldParent = node.getParent();
367             Path newParent = oldParent.relativeTo(sourceRoot).resolveAgainst(targetRoot);
368 
369             PathNode newNode = node.clone().withParent(newParent);
370             if (path.equals(sourceRoot)) {
371                 newNode = newNode.withName(target.getName());
372             }
373             commands.add(createPutCommand(null, newNode));
374 
375             for (Segment child : node.getChildren()) {
376                 pathsToCopy.add(pathFactory().create(path, child));
377             }
378 
379         }
380 
381         commands.add(createRemoveCommand(pathTo(source)));
382         return new SvnCompositeCommand(commands);
383     }
384 
385     @Override
386     public SvnCommand createPutCommand( PathNode previousNode,
387                                         PathNode node ) {
388         Name primaryType = primaryTypeFor(node);
389 
390         // Can't modify the root node
391         if (node.getParent() == null) {
392             return null;
393         }
394 
395         NamespaceRegistry registry = context().getNamespaceRegistry();
396         String parentPath = node.getParent().getString(registry);
397         String name = node.getName().getString(registry);
398 
399         if (primaryType == null || JcrNtLexicon.FOLDER.equals(primaryType)) {
400             if (previousNode != null) {
401                 return null;
402             }
403             return new SvnPutFolderCommand(parentPath, name);
404         }
405 
406         if (JcrNtLexicon.FILE.equals(primaryType)) {
407             if (previousNode != null) {
408                 return null;
409             }
410             return new SvnPutFileCommand(parentPath, name, EMPTY_BYTE_ARRAY);
411         }
412 
413         byte[] oldContent;
414 
415         if (previousNode != null) {
416             Property oldContentProp = previousNode.getProperty(JcrLexicon.DATA);
417             Binary oldContentBin = oldContentProp == null ? null : context().getValueFactories()
418                                                                             .getBinaryFactory()
419                                                                             .create(oldContentProp.getFirstValue());
420             oldContent = oldContentBin == null ? EMPTY_BYTE_ARRAY : oldContentBin.getBytes();
421         } else {
422             oldContent = EMPTY_BYTE_ARRAY;
423         }
424 
425         Property contentProp = node.getProperty(JcrLexicon.DATA);
426         Binary contentBin = contentProp == null ? null : context().getValueFactories()
427                                                                   .getBinaryFactory()
428                                                                   .create(contentProp.getFirstValue());
429         byte[] newContent = contentBin == null ? EMPTY_BYTE_ARRAY : contentBin.getBytes();
430 
431         // The path for a content node ends with the /jcr:content. Need to go up one level to get the file name.
432         Path filePath = node.getParent();
433         String fileDir = filePath.isRoot() ? "/" : filePath.getParent().getString(registry);
434         String fileName = filePath.getLastSegment().getString(registry);
435 
436         return new SvnPutContentCommand(fileDir, fileName, oldContent, newContent);
437     }
438 
439     @Override
440     public SvnCommand createRemoveCommand( Path path ) {
441         String svnPath = path.getString(context().getNamespaceRegistry());
442         return new SvnRemoveCommand(svnPath);
443     }
444 
445     @Override
446     public void commit( List<ChangeCommand<PathNode>> commands ) {
447         ISVNEditor editor = null;
448         boolean commit = true;
449 
450         try {
451             editor = workspaceRoot.getCommitEditor("ModeShape commit", null);
452             editor.openRoot(-1);
453 
454             for (ChangeCommand<PathNode> command : commands) {
455                 if (command == null) continue;
456                 SvnCommand svnCommand = (SvnCommand)command;
457                 svnCommand.setEditor(editor);
458                 svnCommand.apply();
459             }
460         } catch (SVNException ex) {
461             commit = false;
462             throw new IllegalStateException(ex);
463         } finally {
464             if (editor != null) {
465                 try {
466                     editor.closeDir();
467                 } catch (SVNException ignore) {
468 
469                 }
470             }
471         }
472         assert editor != null;
473         if (commit) {
474             try {
475                 SVNCommitInfo info = editor.closeEdit();
476                 if (info.getErrorMessage() != null) {
477                     throw new IllegalStateException(info.getErrorMessage().getFullMessage());
478                 }
479             } catch (SVNException ex) {
480                 throw new IllegalStateException(ex);
481             }
482         }
483     }
484 
485     protected class SvnCommand implements ChangeCommand<PathNode> {
486         protected ISVNEditor editor;
487         private final ScmAction action;
488 
489         protected SvnCommand( ScmAction action ) {
490             this.action = action;
491         }
492 
493         public void setEditor( ISVNEditor editor ) {
494             this.editor = editor;
495         }
496 
497         @Override
498         public void apply() {
499             assert editor != null;
500             try {
501                 action.applyAction(editor);
502             } catch (Exception ex) {
503                 throw new IllegalStateException(ex);
504             }
505         }
506 
507         @Override
508         public String toString() {
509             return getClass().getSimpleName() + " for " + action.toString();
510         }
511 
512     }
513 
514     protected class SvnPutFileCommand extends SvnCommand {
515         public SvnPutFileCommand( String parentPath,
516                                   String fileName,
517                                   byte[] content ) {
518             super(new AddFile(parentPath, fileName, content));
519         }
520     }
521 
522     protected class SvnPutContentCommand extends SvnCommand {
523         public SvnPutContentCommand( String parentPath,
524                                      String fileName,
525                                      byte[] oldcontent,
526                                      byte[] content ) {
527             super(new UpdateFile(parentPath, fileName, oldcontent, content));
528         }
529     }
530 
531     protected class SvnPutFolderCommand extends SvnCommand {
532         public SvnPutFolderCommand( String parentPath,
533                                     String childPath ) {
534             super(new AddDirectory(parentPath, childPath));
535         }
536     }
537 
538     protected class SvnRemoveCommand extends SvnCommand {
539         public SvnRemoveCommand( String path ) {
540             super(new DeleteEntry(path));
541         }
542     }
543 
544     protected class SvnCompositeCommand extends SvnCommand {
545         List<SvnCommand> commands;
546 
547         protected SvnCompositeCommand( List<SvnCommand> commands ) {
548             super(null);
549 
550             this.commands = commands;
551         }
552 
553         @Override
554         public void apply() {
555             for (SvnCommand command : commands) {
556                 command.setEditor(editor);
557                 command.apply();
558             }
559         }
560 
561         @Override
562         public String toString() {
563             return commands.toString();
564         }
565     }
566 }