001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.subversion;
043:
044: import javax.swing.SwingUtilities;
045: import org.netbeans.modules.subversion.util.FileUtils;
046: import org.netbeans.modules.subversion.util.SvnUtils;
047: import org.netbeans.modules.subversion.client.SvnClient;
048: import java.io.File;
049: import java.io.IOException;
050: import java.util.*;
051: import java.util.logging.Level;
052: import org.netbeans.modules.subversion.client.SvnClientExceptionHandler;
053: import org.netbeans.modules.versioning.spi.VCSInterceptor;
054: import org.netbeans.modules.versioning.util.Utils;
055: import org.tigris.subversion.svnclientadapter.*;
056:
057: /**
058: * Handles events fired from the filesystem such as file/folder create/delete/move.
059: *
060: * @author Maros Sandor
061: */
062: class FilesystemHandler extends VCSInterceptor {
063:
064: private final Subversion svn;
065: private final FileStatusCache cache;
066:
067: /**
068: * Stores .svn folders that should be deleted ASAP.
069: */
070: private final Set<File> invalidMetadata = new HashSet<File>(5);
071:
072: public FilesystemHandler(Subversion svn) {
073: this .svn = svn;
074: cache = svn.getStatusCache();
075: }
076:
077: public boolean beforeDelete(File file) {
078: Subversion.LOG.fine("beforeDelete " + file);
079: if (SvnUtils.isPartOfSubversionMetadata(file))
080: return true;
081: // calling cache results in SOE, we must check manually
082: return !file.isFile() && hasMetadata(file);
083: }
084:
085: /**
086: * This interceptor ensures that subversion metadata is NOT deleted.
087: *
088: * @param file file to delete
089: */
090: public void doDelete(File file) throws IOException {
091: Subversion.LOG.fine("doDelete " + file);
092: boolean isMetadata = SvnUtils.isPartOfSubversionMetadata(file);
093: if (!isMetadata) {
094: remove(file);
095: }
096: }
097:
098: public void afterDelete(final File file) {
099: Subversion.LOG.fine("afterDelete " + file);
100: Utils.post(new Runnable() {
101: public void run() {
102: // If a regular file is deleted then update its Entries as if it has been removed.
103: if (file == null)
104: return;
105: int status = cache.getStatus(file).getStatus();
106: if (status != FileInformation.STATUS_NOTVERSIONED_EXCLUDED
107: && status != FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY) {
108: try {
109: SvnClient client = Subversion.getInstance()
110: .getClient(false);
111: client.remove(new File[] { file }, true);
112:
113: } catch (SVNClientException e) {
114: // ignore; we do not know what to do here; does no harm, the file was probably Locally New
115: }
116: }
117: // fire event explicitly because the file is already gone
118: // so svnClientAdapter does not fire ISVNNotifyListener event
119: cache.refresh(file,
120: FileStatusCache.REPOSITORY_STATUS_UNKNOWN);
121: }
122: });
123: }
124:
125: public boolean beforeMove(File from, File to) {
126: Subversion.LOG.fine("beforeMove " + from + " -> " + to);
127: File destDir = to.getParentFile();
128: if (from != null && destDir != null) {
129: // a direct cache call could, because of the synchrone beforeMove handling,
130: // trigger an reentrant call on FS => we have to check manually
131: if (isVersioned(from)) {
132: return Subversion.getInstance().isManaged(to);
133: }
134: // else XXX handle file with saved administative
135: // right now they have old status in cache but is it guaranteed?
136: }
137:
138: return false;
139: }
140:
141: public void doMove(final File from, final File to)
142: throws IOException {
143: Subversion.LOG.fine("doMove " + from + " -> " + to);
144: if (SwingUtilities.isEventDispatchThread()) {
145:
146: Subversion.LOG.log(Level.INFO,
147: "Warning: launching external process in AWT",
148: new Exception().fillInStackTrace());
149: final Throwable innerT[] = new Throwable[1];
150: Runnable outOfAwt = new Runnable() {
151: public void run() {
152: try {
153: svnMoveImplementation(from, to);
154: } catch (Throwable t) {
155: innerT[0] = t;
156: }
157: }
158: };
159:
160: Subversion.getInstance().getRequestProcessor().post(
161: outOfAwt).waitFinished();
162: if (innerT[0] != null) {
163: if (innerT[0] instanceof IOException) {
164: throw (IOException) innerT[0];
165: } else if (innerT[0] instanceof RuntimeException) {
166: throw (RuntimeException) innerT[0];
167: } else if (innerT[0] instanceof Error) {
168: throw (Error) innerT[0];
169: } else {
170: throw new IllegalStateException(
171: "Unexpected exception class: " + innerT[0]); // NOI18N
172: }
173: }
174:
175: // end of hack
176:
177: } else {
178: svnMoveImplementation(from, to);
179: }
180: }
181:
182: public void afterMove(final File from, final File to) {
183: Subversion.LOG.fine("afterMove " + from + " -> " + to);
184: Utils.post(new Runnable() {
185: public void run() {
186: // there might have been no notification
187: // for the children files - refresh them all
188: SvnUtils.refreshRecursively(to);
189: cache.onNotify(to, null); // as if there were an event
190: File parent = to.getParentFile();
191: if (parent != null) {
192: if (from.equals(to)) {
193: Subversion.LOG
194: .warning("Wrong (identity) rename event for "
195: + from.getAbsolutePath());
196: }
197: cache.onNotify(from, null); // as if there were an event
198: }
199: }
200: });
201: }
202:
203: public boolean beforeCreate(File file, boolean isDirectory) {
204: Subversion.LOG.fine("beforeCreate " + file);
205: if (SvnUtils.isPartOfSubversionMetadata(file)) {
206: synchronized (invalidMetadata) {
207: File p = file;
208: while (!p.getName().equals(".svn")
209: && !p.getName().equals("_svn")) {
210: p = p.getParentFile();
211: assert p != null : "file " + file
212: + " doesn't have a .svn parent";
213: }
214: invalidMetadata.add(p);
215: }
216: return false;
217: } else {
218: if (!file.exists()) {
219: try {
220: SvnClient client = Subversion.getInstance()
221: .getClient(true);
222: // check if the file wasn't just deleted in this session
223: revertDeleted(client, file, true);
224: } catch (SVNClientException ex) {
225: SvnClientExceptionHandler.notifyException(ex,
226: false, false);
227: }
228: }
229: return false;
230: }
231: }
232:
233: /**
234: * Returns all direct parent folders from the given file which are scheduled for deletion
235: *
236: * @param file
237: * @param client
238: * @return a list of folders
239: * @throws org.tigris.subversion.svnclientadapter.SVNClientException
240: */
241: private static List<File> getDeletedParents(File file,
242: SvnClient client) throws SVNClientException {
243: List<File> ret = new ArrayList<File>();
244: for (File parent = file.getParentFile(); parent != null; parent = parent
245: .getParentFile()) {
246: ISVNStatus status = getStatus(client, parent);
247: if (status == null
248: || !status.getTextStatus().equals(
249: SVNStatusKind.DELETED)) {
250: return ret;
251: }
252: ret.add(parent);
253: }
254: return ret;
255: }
256:
257: public void doCreate(File file, boolean isDirectory)
258: throws IOException {
259: // do nothing
260: }
261:
262: public void afterCreate(final File file) {
263: Subversion.LOG.fine("afterCreate " + file);
264: Utils.post(new Runnable() {
265: public void run() {
266: if (file == null)
267: return;
268: int status = cache.refresh(file,
269: FileStatusCache.REPOSITORY_STATUS_UNKNOWN)
270: .getStatus();
271: if ((status & FileInformation.STATUS_MANAGED) == 0) {
272: return;
273: }
274: if (file.isDirectory())
275: cache.directoryContentChanged(file);
276: }
277: });
278: }
279:
280: public void afterChange(final File file) {
281: Subversion.LOG.fine("afterChange " + file);
282: Utils.post(new Runnable() {
283: public void run() {
284: if ((cache.getStatus(file).getStatus() & FileInformation.STATUS_MANAGED) != 0) {
285: cache.refreshCached(file,
286: FileStatusCache.REPOSITORY_STATUS_UNKNOWN);
287: }
288: }
289: });
290: }
291:
292: /**
293: * Removes invalid metadata from all known folders.
294: */
295: void removeInvalidMetadata() {
296: synchronized (invalidMetadata) {
297: for (File file : invalidMetadata) {
298: Utils.deleteRecursively(file);
299: }
300: invalidMetadata.clear();
301: }
302: }
303:
304: // private methods ---------------------------
305:
306: private boolean hasMetadata(File file) {
307: return new File(file, ".svn/entries").canRead()
308: || new File(file, "_svn/entries").canRead();
309: }
310:
311: private boolean isVersioned(File file) {
312: if (SvnUtils.isPartOfSubversionMetadata(file))
313: return false;
314: return (!file.isFile() && hasMetadata(file))
315: || (file.isFile() && hasMetadata(file.getParentFile()));
316: }
317:
318: private boolean remove(File file) {
319: try {
320: SvnClient client = Subversion.getInstance()
321: .getClient(false);
322: // funny thing is, the command will delete all files recursively
323: client.remove(new File[] { file }, true);
324: return true;
325: } catch (SVNClientException e) {
326: return false;
327: }
328: }
329:
330: private void revertDeleted(SvnClient client, final File file,
331: boolean checkParents) {
332:
333: try {
334: ISVNStatus status = getStatus(client, file);
335: if (status != null
336: && status.getTextStatus().equals(
337: SVNStatusKind.DELETED)) {
338: if (checkParents) {
339: // we have a file scheduled for deletion but it's giong to created again,
340: // so it's parent folder can't stay deleted either
341: List<File> deletedParents = getDeletedParents(file,
342: client);
343: client.revert(deletedParents
344: .toArray(new File[deletedParents.size()]),
345: false);
346: }
347:
348: // reverting the file will set the metadata uptodate
349: client.revert(file, false);
350: // our goal was ony to fix the metadata ->
351: // -> get rid of the reverted file
352: file.delete();
353: }
354: } catch (SVNClientException ex) {
355: SvnClientExceptionHandler.notifyException(ex, false, false);
356: }
357: }
358:
359: private void svnMoveImplementation(final File srcFile,
360: final File dstFile) throws IOException {
361: try {
362: boolean force = true; // file with local changes must be forced
363: SvnClient client = Subversion.getInstance()
364: .getClient(false);
365:
366: File tmpMetadata = null;
367: try {
368: // prepare destination, it must be under Subversion control
369: removeInvalidMetadata();
370:
371: File parent;
372: if (dstFile.isDirectory()) {
373: parent = dstFile;
374: } else {
375: parent = dstFile.getParentFile();
376: }
377:
378: if (parent != null) {
379: assert Subversion.getInstance().isManaged(parent); // see implsMove above
380: // a direct cache call could, because of the synchrone svnMoveImplementation handling,
381: // trigger an reentrant call on FS => we have to check manually
382: if (!hasMetadata(parent)) {
383: addDirectories(parent);
384: }
385: }
386:
387: // perform
388: int retryCounter = 6;
389: while (true) {
390: try {
391: // check if the file wasn't just deleted in this session
392: revertDeleted(client, dstFile, false);
393:
394: // check the status - if the file isn't in the repository yet ( ADDED | UNVERSIONED )
395: // then it also can't be moved via the svn client
396: ISVNStatus status = getStatus(client, srcFile);
397: if (status != null
398: && status.getTextStatus().equals(
399: SVNStatusKind.ADDED)) {
400: client.revert(srcFile, true);
401: renameFile(srcFile, dstFile);
402: } else if (status != null
403: && status.getTextStatus().equals(
404: SVNStatusKind.UNVERSIONED)) {
405: renameFile(srcFile, dstFile);
406: } else {
407: List<File> srcChildren = listAllChildren(srcFile);
408: client.move(srcFile, dstFile, force);
409:
410: // fire events explicitly for all children which are already gone
411: for (File f : srcChildren) {
412: cache.onNotify(f, null);
413: }
414: }
415:
416: break;
417: } catch (SVNClientException e) {
418: // svn: Working copy '/tmp/co/svn-prename-19/AnagramGame-pack-rename/src/com/toy/anagrams/ui2' locked
419: if (e.getMessage().endsWith("' locked")
420: && retryCounter > 0) { // NOI18N
421: // XXX HACK AWT- or FS Monitor Thread performs
422: // concurrent operation
423: try {
424: Thread.sleep(107);
425: } catch (InterruptedException ex) {
426: // ignore
427: }
428: retryCounter--;
429: continue;
430: }
431:
432: IOException ex = new IOException(
433: "Subversion failed to rename "
434: + srcFile.getAbsolutePath()
435: + " to: "
436: + dstFile.getAbsolutePath()); // NOI18N
437: ex.initCause(e);
438: throw ex;
439:
440: }
441: }
442: } finally {
443: if (tmpMetadata != null) {
444: FileUtils.deleteRecursively(tmpMetadata);
445: }
446: }
447: } catch (SVNClientException e) {
448: IOException ex = new IOException(
449: "Subversion failed to rename "
450: + srcFile.getAbsolutePath() + " to: "
451: + dstFile.getAbsolutePath()); // NOI18N
452: ex.initCause(e);
453: throw ex;
454: }
455: }
456:
457: private void renameFile(File srcFile, File dstFile) {
458: List<File> srcChildren = listAllChildren(srcFile);
459: srcFile.renameTo(dstFile);
460:
461: // notify the cache
462: cache.onNotify(srcFile, null);
463: for (File f : srcChildren) {
464: // fire events explicitly for
465: // all children which are already gone
466: cache.onNotify(f, null);
467: }
468: cache.onNotify(dstFile, null);
469: }
470:
471: private List<File> listAllChildren(File file) {
472: if (file.isFile())
473: return new ArrayList<File>(0);
474: List<File> ret = new ArrayList<File>();
475: File[] files = file.listFiles();
476: if (files != null) {
477: for (File f : files) {
478: ret.add(f);
479: ret.addAll(listAllChildren(f));
480: }
481: }
482: return ret;
483: }
484:
485: /**
486: * Seeks versioned root and then adds all folders
487: * under Subversion (so it contains metadata),
488: */
489: private void addDirectories(final File dir)
490: throws SVNClientException {
491: File parent = dir.getParentFile();
492: if (parent != null) {
493: if (Subversion.getInstance().isManaged(parent)
494: && !hasMetadata(parent)) {
495: addDirectories(parent); // RECURSION
496: }
497: SvnClient client = Subversion.getInstance()
498: .getClient(false);
499: client.addDirectory(dir, false);
500: Utils.post(new Runnable() {
501: public void run() {
502: cache.refresh(dir,
503: FileStatusCache.REPOSITORY_STATUS_UNKNOWN);
504: }
505: });
506: } else {
507: throw new SVNClientException(
508: "Reached FS root, but it's still not Subversion versioned!"); // NOI18N
509: }
510: }
511:
512: private static ISVNStatus getStatus(SvnClient client, File file)
513: throws SVNClientException {
514: // a direct cache call could, because of the synchrone beforeCreate handling,
515: // trigger an reentrant call on FS => we have to check manually
516: return client.getSingleStatus(file);
517: }
518:
519: }
|