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
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
60
61
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
75
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
86 private final SvnRepository repository;
87
88
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
187 if (children != null) {
188 final Collection<SVNDirEntry> entries = SvnRepositoryUtil.getDir(workspaceRoot, "");
189 for (SVNDirEntry entry : entries) {
190
191
192 children.add(pathFactory.createSegment(entry.getName()));
193 }
194 }
195
196 } else {
197
198 PropertyFactory factory = context.getPropertyFactory();
199 DateTimeFactory dateFactory = context.getValueFactories().getDateFactory();
200
201
202 SVNNodeKind kind = getNodeKind(context, requestedPath, source().getRepositoryRootUrl());
203 if (kind == SVNNodeKind.NONE) {
204
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
214 Collection<SVNDirEntry> dirEntries = SvnRepositoryUtil.getDir(workspaceRoot, directoryPath);
215 for (SVNDirEntry entry : dirEntries) {
216
217
218 children.add(pathFactory.createSegment(entry.getName()));
219 }
220 }
221 if (properties != null) {
222
223 properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER));
224
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
232
233 if (requestedPath.endsWith(JcrLexicon.CONTENT)) {
234
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
243
244
245
246
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
260 BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
261 properties.add(factory.create(JcrLexicon.DATA, binaryFactory.create(os.toByteArray())));
262 }
263 }
264 } else {
265
266 String filePath = requestedPath.getString(registry);
267 if (!source().getRepositoryRootUrl().equals(workspaceName)) {
268 filePath = filePath.substring(1);
269 }
270 if (children != null) {
271
272 children.add(pathFactory.createSegment(JcrLexicon.CONTENT));
273 }
274 if (properties != null) {
275
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
296 if (path.endsWith(JcrLexicon.CONTENT)) {
297
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
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
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
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
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 }