001: /*
002: * ====================================================================
003: * Copyright (c) 2004-2008 TMate Software Ltd. All rights reserved.
004: *
005: * This software is licensed as described in the file COPYING, which
006: * you should have received as part of this distribution. The terms
007: * are also available at http://svnkit.com/license.html
008: * If newer versions of this license are posted there, you may use a
009: * newer version instead, at your option.
010: * ====================================================================
011: */
012: package org.tmatesoft.svn.core.replicator;
013:
014: import java.io.OutputStream;
015: import java.util.ArrayList;
016: import java.util.Arrays;
017: import java.util.Collection;
018: import java.util.HashMap;
019: import java.util.Iterator;
020: import java.util.List;
021: import java.util.Map;
022: import java.util.Stack;
023:
024: import org.tmatesoft.svn.core.ISVNDirEntryHandler;
025: import org.tmatesoft.svn.core.SVNCommitInfo;
026: import org.tmatesoft.svn.core.SVNErrorCode;
027: import org.tmatesoft.svn.core.SVNErrorMessage;
028: import org.tmatesoft.svn.core.SVNException;
029: import org.tmatesoft.svn.core.SVNLogEntry;
030: import org.tmatesoft.svn.core.SVNLogEntryPath;
031: import org.tmatesoft.svn.core.SVNNodeKind;
032: import org.tmatesoft.svn.core.SVNProperty;
033: import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
034: import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
035: import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
036: import org.tmatesoft.svn.core.io.ISVNEditor;
037: import org.tmatesoft.svn.core.io.SVNRepository;
038: import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
039: import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
040:
041: /**
042: * The <b>SVNReplicationEditor</b> is an editor implementation used by a
043: * repository replicator as a bridge between an update editor for the source
044: * repository and a commit editor of the target one. This editor is provided
045: * to an update method of a source <b>SVNRepository</b> driver to properly translate
046: * the calls of that driver to calls to a commit editor of the destination <b>SVNRepository</b>
047: * driver.
048: *
049: * @version 1.1.1
050: * @author TMate Software Ltd.
051: * @see org.tmatesoft.svn.core.io.SVNRepository
052: * @since 1.1.0
053: */
054: public class SVNReplicationEditor implements ISVNEditor {
055:
056: private static final int ACCEPT = 0;
057: private static final int IGNORE = 1;
058: private static final int DECIDE = 2;
059:
060: private ISVNEditor myCommitEditor;
061: private Map myCopiedPaths;
062: private Map myChangedPaths;
063: private SVNRepository myRepos;
064: private Map myPathsToFileBatons;
065: private Stack myDirsStack;
066: private long myPreviousRevision;
067: private long myTargetRevision;
068: private SVNCommitInfo myCommitInfo;
069: private SVNRepository mySourceRepository;
070:
071: /**
072: * Creates a new replication editor.
073: *
074: * <p>
075: * <code>repository</code> must be created for the root location of
076: * the source repository which is to be replicated.
077: *
078: * @param repository a source repository
079: * @param commitEditor a commit editor received from the destination
080: * repository driver (which also must be point to the
081: * root location of the destination repository)
082: * @param revision log information of the revision to be copied
083: */
084: public SVNReplicationEditor(SVNRepository repository,
085: ISVNEditor commitEditor, SVNLogEntry revision) {
086: myRepos = repository;
087: myCommitEditor = commitEditor;
088: myPathsToFileBatons = new HashMap();
089: myDirsStack = new Stack();
090: myCopiedPaths = new HashMap();
091: myChangedPaths = revision.getChangedPaths();
092:
093: for (Iterator paths = myChangedPaths.keySet().iterator(); paths
094: .hasNext();) {
095: String path = (String) paths.next();
096: SVNLogEntryPath pathChange = (SVNLogEntryPath) myChangedPaths
097: .get(path);
098: //make sure it's a copy
099: if ((pathChange.getType() == SVNLogEntryPath.TYPE_REPLACED || pathChange
100: .getType() == SVNLogEntryPath.TYPE_ADDED)
101: && pathChange.getCopyPath() != null
102: && pathChange.getCopyRevision() >= 0) {
103: myCopiedPaths.put(path, pathChange);
104: }
105: }
106: }
107:
108: public void targetRevision(long revision) throws SVNException {
109: myPreviousRevision = revision - 1;
110: myTargetRevision = revision;
111: }
112:
113: public void openRoot(long revision) throws SVNException {
114: //open root
115: myCommitEditor.openRoot(myPreviousRevision);
116: EntryBaton baton = new EntryBaton("/");
117: baton.myPropsAct = ACCEPT;
118: myDirsStack.push(baton);
119:
120: }
121:
122: public void deleteEntry(String path, long revision)
123: throws SVNException {
124: String absPath = getSourceRepository().getRepositoryPath(path);
125: SVNLogEntryPath deletedPath = (SVNLogEntryPath) myChangedPaths
126: .get(absPath);
127: if (deletedPath != null
128: && (deletedPath.getType() == SVNLogEntryPath.TYPE_DELETED || deletedPath
129: .getType() == SVNLogEntryPath.TYPE_REPLACED)) {
130: if (deletedPath.getType() == SVNLogEntryPath.TYPE_DELETED) {
131: myChangedPaths.remove(absPath);
132: }
133: } else {
134: SVNErrorMessage err = SVNErrorMessage
135: .create(
136: SVNErrorCode.UNKNOWN,
137: "Expected that path ''{0}'' is deleted in revision {1,number,integer}",
138: new Object[] { absPath,
139: new Long(myPreviousRevision) });
140: SVNErrorManager.error(err);
141: }
142: myCommitEditor.deleteEntry(path, myPreviousRevision);
143: }
144:
145: public void absentDir(String path) throws SVNException {
146: }
147:
148: public void absentFile(String path) throws SVNException {
149: }
150:
151: public void addDir(String path, String copyFromPath,
152: long copyFromRevision) throws SVNException {
153: String absPath = getSourceRepository().getRepositoryPath(path);
154: EntryBaton baton = new EntryBaton(absPath);
155: myDirsStack.push(baton);
156: SVNLogEntryPath changedPath = (SVNLogEntryPath) myChangedPaths
157: .get(absPath);
158: if (changedPath != null
159: && (changedPath.getType() == SVNLogEntryPath.TYPE_ADDED || changedPath
160: .getType() == SVNLogEntryPath.TYPE_REPLACED)
161: && changedPath.getCopyPath() != null
162: && changedPath.getCopyRevision() >= 0) {
163: baton.myPropsAct = DECIDE;
164: HashMap props = new HashMap();
165: getSourceRepository().getDir(changedPath.getCopyPath(),
166: changedPath.getCopyRevision(), props,
167: (ISVNDirEntryHandler) null);
168: baton.myProps = props;
169:
170: if (changedPath.getType() == SVNLogEntryPath.TYPE_REPLACED) {
171: myCommitEditor.deleteEntry(path, myPreviousRevision);
172: myChangedPaths.remove(absPath);
173: }
174: myCommitEditor.addDir(path, changedPath.getCopyPath(),
175: changedPath.getCopyRevision());
176: } else if (changedPath != null
177: && (changedPath.getType() == SVNLogEntryPath.TYPE_ADDED || changedPath
178: .getType() == SVNLogEntryPath.TYPE_REPLACED)) {
179: baton.myPropsAct = ACCEPT;
180: myCommitEditor.addDir(path, null, -1);
181: } else if (changedPath != null
182: && changedPath.getType() == SVNLogEntryPath.TYPE_MODIFIED) {
183: baton.myPropsAct = ACCEPT;
184: myCommitEditor.openDir(path, myPreviousRevision);
185: } else if (changedPath == null) {
186: baton.myPropsAct = IGNORE;
187: myCommitEditor.openDir(path, myPreviousRevision);
188: } else {
189: SVNErrorMessage err = SVNErrorMessage.create(
190: SVNErrorCode.UNKNOWN, "Unknown bug in addDir()");
191: SVNErrorManager.error(err);
192: }
193: }
194:
195: public void openDir(String path, long revision) throws SVNException {
196: EntryBaton baton = new EntryBaton(getSourceRepository()
197: .getRepositoryPath(path));
198: baton.myPropsAct = ACCEPT;
199: myDirsStack.push(baton);
200: myCommitEditor.openDir(path, myPreviousRevision);
201: }
202:
203: public void changeDirProperty(String name, String value)
204: throws SVNException {
205: if (!SVNProperty.isRegularProperty(name)) {
206: return;
207: }
208: EntryBaton baton = (EntryBaton) myDirsStack.peek();
209: if (baton.myPropsAct == ACCEPT) {
210: myCommitEditor.changeDirProperty(name, value);
211: } else if (baton.myPropsAct == DECIDE) {
212: String propVal = (String) baton.myProps.get(name);
213: if (propVal != null && propVal.equals(value)) {
214: /*
215: * The properties seem to be the same as of the copy origin,
216: * do not reset them again.
217: */
218: baton.myPropsAct = IGNORE;
219: return;
220: }
221: /*
222: * Properties do differ, accept them.
223: */
224: baton.myPropsAct = ACCEPT;
225: myCommitEditor.changeDirProperty(name, value);
226:
227: }
228: }
229:
230: public void closeDir() throws SVNException {
231: if (myDirsStack.size() > 1 && !myCopiedPaths.isEmpty()) {
232: EntryBaton currentDir = (EntryBaton) myDirsStack.peek();
233: completeDeletion(currentDir.myPath);
234: }
235: myDirsStack.pop();
236: myCommitEditor.closeDir();
237: }
238:
239: public void addFile(String path, String copyFromPath,
240: long copyFromRevision) throws SVNException {
241: String absPath = getSourceRepository().getRepositoryPath(path);
242: EntryBaton baton = new EntryBaton(absPath);
243: myPathsToFileBatons.put(path, baton);
244: SVNLogEntryPath changedPath = (SVNLogEntryPath) myChangedPaths
245: .get(absPath);
246:
247: if (changedPath != null
248: && (changedPath.getType() == SVNLogEntryPath.TYPE_ADDED || changedPath
249: .getType() == SVNLogEntryPath.TYPE_REPLACED)
250: && changedPath.getCopyPath() != null
251: && changedPath.getCopyRevision() >= 0) {
252: baton.myPropsAct = DECIDE;
253: baton.myTextAct = ACCEPT;
254: Map props = new HashMap();
255: if (areFileContentsEqual(absPath, myTargetRevision,
256: changedPath.getCopyPath(), changedPath
257: .getCopyRevision(), props)) {
258: baton.myTextAct = IGNORE;
259: }
260: baton.myProps = props;
261: if (changedPath.getType() == SVNLogEntryPath.TYPE_REPLACED) {
262: myCommitEditor.deleteEntry(path, myPreviousRevision);
263: myChangedPaths.remove(absPath);
264: }
265: myCommitEditor.addFile(path, changedPath.getCopyPath(),
266: changedPath.getCopyRevision());
267: } else if (changedPath != null
268: && (changedPath.getType() == SVNLogEntryPath.TYPE_ADDED || changedPath
269: .getType() == SVNLogEntryPath.TYPE_REPLACED)) {
270: baton.myPropsAct = ACCEPT;
271: baton.myTextAct = ACCEPT;
272: if (changedPath.getType() == SVNLogEntryPath.TYPE_REPLACED) {
273: myCommitEditor.deleteEntry(path, myPreviousRevision);
274: myChangedPaths.remove(absPath);
275: }
276: myCommitEditor.addFile(path, null, -1);
277: } else if (changedPath != null
278: && changedPath.getType() == SVNLogEntryPath.TYPE_MODIFIED) {
279: baton.myPropsAct = DECIDE;
280: baton.myTextAct = ACCEPT;
281: SVNLogEntryPath realPath = getFileCopyOrigin(absPath);
282: if (realPath == null) {
283: SVNErrorMessage err = SVNErrorMessage
284: .create(SVNErrorCode.UNKNOWN,
285: "Unknown error, can't get the copy origin of a file");
286: SVNErrorManager.error(err);
287: }
288: Map props = new HashMap();
289: if (areFileContentsEqual(absPath, myTargetRevision,
290: realPath.getCopyPath(), realPath.getCopyRevision(),
291: props)) {
292: baton.myTextAct = IGNORE;
293: }
294: baton.myProps = props;
295: myCommitEditor.openFile(path, myPreviousRevision);
296: } else if (changedPath == null) {
297: baton.myPropsAct = IGNORE;
298: baton.myTextAct = IGNORE;
299: } else {
300: SVNErrorMessage err = SVNErrorMessage.create(
301: SVNErrorCode.UNKNOWN, "Unknown bug in addFile()");
302: SVNErrorManager.error(err);
303: }
304: }
305:
306: private SVNLogEntryPath getFileCopyOrigin(String path)
307: throws SVNException {
308: Object[] paths = myCopiedPaths.keySet().toArray();
309: Arrays
310: .sort(paths, 0, paths.length,
311: SVNPathUtil.PATH_COMPARATOR);
312: SVNLogEntryPath realPath = null;
313: List candidates = new ArrayList();
314: for (int i = 0; i < paths.length; i++) {
315: String copiedPath = (String) paths[i];
316:
317: if (!path.startsWith(copiedPath + "/")) {
318: continue;
319: } else if (path.equals(copiedPath)) {
320: return (SVNLogEntryPath) myCopiedPaths.get(copiedPath);
321: }
322: candidates.add(copiedPath);
323: }
324: // check candidates from the end of the list
325: for (int i = candidates.size() - 1; i >= 0; i--) {
326: String candidateParent = (String) candidates.get(i);
327: if (getSourceRepository().checkPath(candidateParent,
328: myTargetRevision) != SVNNodeKind.DIR) {
329: continue;
330: }
331: SVNLogEntryPath changedPath = (SVNLogEntryPath) myCopiedPaths
332: .get(candidateParent);
333: String fileRelativePath = path.substring(candidateParent
334: .length() + 1);
335: fileRelativePath = SVNPathUtil.append(changedPath
336: .getCopyPath(), fileRelativePath);
337: return new SVNLogEntryPath(path, ' ', fileRelativePath,
338: changedPath.getCopyRevision());
339: }
340: return realPath;
341: }
342:
343: private boolean areFileContentsEqual(String path1, long rev1,
344: String path2, long rev2, Map props2) throws SVNException {
345: Map props1 = new HashMap();
346: props2 = props2 == null ? new HashMap() : props2;
347:
348: SVNRepository repos = getSourceRepository();
349: repos.getFile(path1, rev1, props1, null);
350: repos.getFile(path2, rev2, props2, null);
351: String crc1 = (String) props1.get(SVNProperty.CHECKSUM);
352: String crc2 = (String) props2.get(SVNProperty.CHECKSUM);
353: return crc1 != null && crc1.equals(crc2);
354: }
355:
356: public void openFile(String path, long revision)
357: throws SVNException {
358: EntryBaton baton = new EntryBaton(getSourceRepository()
359: .getRepositoryPath(path));
360: baton.myPropsAct = ACCEPT;
361: baton.myTextAct = ACCEPT;
362: myPathsToFileBatons.put(path, baton);
363: myCommitEditor.openFile(path, myPreviousRevision);
364: }
365:
366: public void applyTextDelta(String path, String baseChecksum)
367: throws SVNException {
368: EntryBaton baton = (EntryBaton) myPathsToFileBatons.get(path);
369: if (baton.myTextAct == ACCEPT) {
370: myCommitEditor.applyTextDelta(path, baseChecksum);
371: }
372: }
373:
374: public OutputStream textDeltaChunk(String path,
375: SVNDiffWindow diffWindow) throws SVNException {
376: EntryBaton baton = (EntryBaton) myPathsToFileBatons.get(path);
377: if (baton.myTextAct == ACCEPT) {
378: return myCommitEditor.textDeltaChunk(path, diffWindow);
379: }
380: return SVNFileUtil.DUMMY_OUT;
381: }
382:
383: public void textDeltaEnd(String path) throws SVNException {
384: EntryBaton baton = (EntryBaton) myPathsToFileBatons.get(path);
385: if (baton.myTextAct == ACCEPT) {
386: myCommitEditor.textDeltaEnd(path);
387: }
388: }
389:
390: public void changeFileProperty(String path, String name,
391: String value) throws SVNException {
392: if (!SVNProperty.isRegularProperty(name)) {
393: return;
394: }
395: EntryBaton baton = (EntryBaton) myPathsToFileBatons.get(path);
396: if (baton.myPropsAct == ACCEPT) {
397: myCommitEditor.changeFileProperty(path, name, value);
398: } else if (baton.myPropsAct == DECIDE) {
399: String propVal = (String) baton.myProps.get(name);
400: if (propVal != null && propVal.equals(value)) {
401: /*
402: * The properties seem to be the same as of the copy origin,
403: * do not reset them again.
404: */
405: baton.myPropsAct = IGNORE;
406: return;
407: }
408: /*
409: * Properties do differ, accept them.
410: */
411: baton.myPropsAct = ACCEPT;
412: myCommitEditor.changeFileProperty(path, name, value);
413: }
414: }
415:
416: public void closeFile(String path, String textChecksum)
417: throws SVNException {
418: EntryBaton baton = (EntryBaton) myPathsToFileBatons.get(path);
419: if (baton.myTextAct != IGNORE || baton.myTextAct != IGNORE) {
420: myCommitEditor.closeFile(path, textChecksum);
421: }
422: }
423:
424: public SVNCommitInfo closeEdit() throws SVNException {
425: myCommitInfo = myCommitEditor.closeEdit();
426: if (mySourceRepository != null) {
427: mySourceRepository.closeSession();
428: mySourceRepository = null;
429: }
430: return myCommitInfo;
431:
432: }
433:
434: public void abortEdit() throws SVNException {
435: if (mySourceRepository != null) {
436: mySourceRepository.closeSession();
437: mySourceRepository = null;
438: }
439: myCommitEditor.abortEdit();
440: }
441:
442: /**
443: * Returns commit information on the revision
444: * committed to the replication destination repository.
445: *
446: * @return commit info (revision, author, date)
447: */
448: public SVNCommitInfo getCommitInfo() {
449: return myCommitInfo;
450: }
451:
452: private SVNRepository getSourceRepository() throws SVNException {
453: if (mySourceRepository == null) {
454: mySourceRepository = SVNRepositoryFactory.create(myRepos
455: .getLocation());
456: mySourceRepository.setAuthenticationManager(myRepos
457: .getAuthenticationManager());
458: mySourceRepository.setDebugLog(myRepos.getDebugLog());
459: mySourceRepository.setTunnelProvider(myRepos
460: .getTunnelProvider());
461: mySourceRepository.setCanceller(myRepos.getCanceller());
462: }
463: return mySourceRepository;
464:
465: }
466:
467: private void completeDeletion(String dirPath) throws SVNException {
468: Collection pathsToDelete = new ArrayList();
469: for (Iterator paths = myChangedPaths.keySet().iterator(); paths
470: .hasNext();) {
471: String path = (String) paths.next();
472: if (!path.startsWith(dirPath + "/")) {
473: continue;
474: }
475: SVNLogEntryPath pathChange = (SVNLogEntryPath) myChangedPaths
476: .get(path);
477: if (pathChange.getType() == SVNLogEntryPath.TYPE_DELETED) {
478: String relativePath = path
479: .substring(dirPath.length() + 1);
480: pathsToDelete.add(relativePath);
481: }
482: }
483: String[] pathsArray = (String[]) pathsToDelete
484: .toArray(new String[pathsToDelete.size()]);
485: Arrays.sort(pathsArray, SVNPathUtil.PATH_COMPARATOR);
486: String currentOpened = "";
487: for (int i = 0; i < pathsArray.length; i++) {
488: String nextRelativePath = pathsArray[i];
489: while (!"".equals(currentOpened)
490: && nextRelativePath.indexOf(currentOpened) == -1) {
491: myCommitEditor.closeDir();
492: currentOpened = SVNPathUtil.removeTail(currentOpened);
493: }
494:
495: String nextRelativePathToDelete = null;
496: if (!"".equals(currentOpened)) {
497: nextRelativePathToDelete = nextRelativePath
498: .substring(currentOpened.length() + 1);
499: } else {
500: nextRelativePathToDelete = nextRelativePath;
501: }
502:
503: String[] entries = nextRelativePathToDelete.split("/");
504: int j = 0;
505: for (j = 0; j < entries.length - 1; j++) {
506: currentOpened = SVNPathUtil.append(currentOpened,
507: entries[j]);
508: myCommitEditor.openDir(SVNPathUtil.append(dirPath,
509: currentOpened), myPreviousRevision);
510: }
511: String pathToDelete = SVNPathUtil.append(currentOpened,
512: entries[j]);
513: String absPathToDelete = SVNPathUtil.append(dirPath,
514: pathToDelete);
515: myCommitEditor.deleteEntry(absPathToDelete,
516: myPreviousRevision);
517: myChangedPaths.remove(absPathToDelete);
518: }
519: while (!"".equals(currentOpened)) {
520: myCommitEditor.closeDir();
521: currentOpened = SVNPathUtil.removeTail(currentOpened);
522: }
523: }
524:
525: private static class EntryBaton {
526:
527: public EntryBaton(String path) {
528: myPath = path;
529: }
530:
531: private String myPath;
532: private int myPropsAct;
533: private int myTextAct;
534: private Map myProps;
535: }
536: }
|