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-2007 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: package org.netbeans.modules.versioning;
042:
043: import org.openide.filesystems.*;
044: import org.netbeans.modules.masterfs.providers.ProvidedExtensions;
045: import org.netbeans.modules.versioning.spi.VCSInterceptor;
046: import org.netbeans.modules.versioning.spi.VersioningSystem;
047: import java.io.File;
048: import java.io.IOException;
049: import java.util.*;
050:
051: /**
052: * Plugs into IDE filesystem and delegates file operations to registered versioning systems.
053: *
054: * @author Maros Sandor
055: */
056: class FilesystemInterceptor extends ProvidedExtensions implements
057: FileChangeListener {
058:
059: private VersioningManager master;
060:
061: // === LIFECYCLE =======================================================================================
062:
063: /**
064: * Initializes the interceptor by registering it into master filesystem.
065: * Registers listeners to all disk filesystems.
066: * @param versioningManager
067: */
068: void init(VersioningManager versioningManager) {
069: assert master == null;
070: master = versioningManager;
071: Set filesystems = getRootFilesystems();
072: for (Iterator i = filesystems.iterator(); i.hasNext();) {
073: FileSystem fileSystem = (FileSystem) i.next();
074: fileSystem.addFileChangeListener(this );
075: }
076: }
077:
078: /**
079: * Unregisters listeners from all disk filesystems.
080: */
081: void shutdown() {
082: Set filesystems = getRootFilesystems();
083: for (Iterator i = filesystems.iterator(); i.hasNext();) {
084: FileSystem fileSystem = (FileSystem) i.next();
085: fileSystem.removeFileChangeListener(this );
086: }
087: }
088:
089: /**
090: * Retrieves all filesystems.
091: *
092: * @return Set<FileSystem> set of filesystems
093: */
094: private Set<FileSystem> getRootFilesystems() {
095: Set<FileSystem> filesystems = new HashSet<FileSystem>();
096: File[] roots = File.listRoots();
097: for (int i = 0; i < roots.length; i++) {
098: File root = roots[i];
099: FileObject fo = FileUtil.toFileObject(FileUtil
100: .normalizeFile(root));
101: if (fo == null)
102: continue;
103: try {
104: filesystems.add(fo.getFileSystem());
105: } catch (FileStateInvalidException e) {
106: // ignore invalid filesystems
107: }
108: }
109: return filesystems;
110: }
111:
112: // ==================================================================================================
113: // CHANGE
114: // ==================================================================================================
115:
116: public void fileChanged(FileEvent fe) {
117: removeFromDeletedFiles(fe.getFile());
118: getInterceptor(fe).afterChange();
119: }
120:
121: public void beforeChange(FileObject fo) {
122: getInterceptor(FileUtil.toFile(fo), fo.isFolder())
123: .beforeChange();
124: }
125:
126: // ==================================================================================================
127: // DELETE
128: // ==================================================================================================
129:
130: private void removeFromDeletedFiles(File file) {
131: synchronized (deletedFiles) {
132: deletedFiles.remove(file);
133: }
134: }
135:
136: private void removeFromDeletedFiles(FileObject fo) {
137: synchronized (deletedFiles) {
138: if (deletedFiles.size() > 0) {
139: deletedFiles.remove(FileUtil.toFile(fo));
140: }
141: }
142: }
143:
144: public DeleteHandler getDeleteHandler(File file) {
145: removeFromDeletedFiles(file);
146: DelegatingInterceptor dic = getInterceptor(file, false);
147: return dic.beforeDelete() ? dic : null;
148: }
149:
150: public void fileDeleted(FileEvent fe) {
151: removeFromDeletedFiles(fe.getFile());
152: getInterceptor(fe).afterDelete();
153: }
154:
155: // ==================================================================================================
156: // CREATE
157: // ==================================================================================================
158:
159: /**
160: * Stores files that are being created inside the IDE and the owner interceptor wants to handle the creation. Entries
161: * are added in beforeCreate() and removed in fileDataCreated() or createFailure().
162: */
163: private final Map<FileEx, DelegatingInterceptor> filesBeingCreated = new HashMap<FileEx, DelegatingInterceptor>(
164: 10);
165:
166: public void beforeCreate(FileObject parent, String name,
167: boolean isFolder) {
168: File file = FileUtil.toFile(parent);
169: if (file == null)
170: return;
171: file = new File(file, name);
172: DelegatingInterceptor dic = getInterceptor(file, isFolder);
173: if (dic.beforeCreate()) {
174: filesBeingCreated.put(new FileEx(parent, name, isFolder),
175: dic);
176: }
177: }
178:
179: public void createFailure(FileObject parent, String name,
180: boolean isFolder) {
181: filesBeingCreated.remove(new FileEx(parent, name, isFolder));
182: }
183:
184: public void fileFolderCreated(FileEvent fe) {
185: fileDataCreated(fe);
186: }
187:
188: public void fileDataCreated(FileEvent fe) {
189: FileObject fo = fe.getFile();
190: FileEx fileEx = new FileEx(fo.getParent(), fo.getNameExt(), fo
191: .isFolder());
192: DelegatingInterceptor interceptor = filesBeingCreated
193: .remove(fileEx);
194: if (interceptor != null) {
195: try {
196: interceptor.doCreate();
197: } catch (Exception e) {
198: // ignore errors, the file is already created anyway
199: }
200: }
201: removeFromDeletedFiles(fe.getFile());
202: // special handling of create events => all interceptors are notified. This is to work around the "implicit logic"
203: // bug that assumes that files missing from the cache are uptodate
204: getAllInterceptors(fe).afterCreate();
205: }
206:
207: // ==================================================================================================
208: // MOVE
209: // ==================================================================================================
210:
211: public IOHandler getMoveHandler(File from, File to) {
212: DelegatingInterceptor dic = getInterceptor(from, to);
213: return dic.beforeMove() ? dic : null;
214: }
215:
216: public IOHandler getRenameHandler(File from, String newName) {
217: File to = new File(from.getParentFile(), newName);
218: return getMoveHandler(from, to);
219: }
220:
221: public void fileRenamed(FileRenameEvent fe) {
222: removeFromDeletedFiles(fe.getFile());
223: getInterceptor(fe).afterMove();
224: }
225:
226: public void fileAttributeChanged(FileAttributeEvent fe) {
227: // not interested
228: }
229:
230: /**
231: * There is a contract that says that when a file is locked, it is expected to be changed. This is what openide/text
232: * does when it creates a Document. A versioning system is expected to make the file r/w.
233: *
234: * @param fo a FileObject
235: */
236: public void fileLocked(FileObject fo) {
237: getInterceptor(new FileEvent(fo)).beforeEdit();
238: }
239:
240: private DelegatingInterceptor getInterceptor(FileEvent fe) {
241: FileObject fo = fe.getFile();
242: if (fo == null)
243: return nullDelegatingInterceptor;
244: File file = FileUtil.toFile(fo);
245: if (file == null)
246: return nullDelegatingInterceptor;
247:
248: VersioningSystem lh = master.getLocalHistory(file);
249: VersioningSystem vs = master.getOwner(file);
250:
251: VCSInterceptor vsInterceptor = vs != null ? vs
252: .getVCSInterceptor() : null;
253: VCSInterceptor lhInterceptor = lh != null ? lh
254: .getVCSInterceptor() : null;
255:
256: if (vsInterceptor == null && lhInterceptor == null)
257: return nullDelegatingInterceptor;
258:
259: if (fe instanceof FileRenameEvent) {
260: FileRenameEvent fre = (FileRenameEvent) fe;
261: File parent = file.getParentFile();
262: if (parent != null) {
263: String name = fre.getName();
264: String ext = fre.getExt();
265: if (ext != null && ext.length() > 0) { // NOI18N
266: name += "." + ext; // NOI18N
267: }
268: File from = new File(parent, name);
269: return new DelegatingInterceptor(vsInterceptor,
270: lhInterceptor, from, file, false);
271: }
272: return nullDelegatingInterceptor;
273: } else {
274: return new DelegatingInterceptor(vsInterceptor,
275: lhInterceptor, file, null, false);
276: }
277: }
278:
279: private DelegatingInterceptor getAllInterceptors(FileEvent fe) {
280: FileObject fo = fe.getFile();
281: if (fo == null)
282: return nullDelegatingInterceptor;
283: File file = FileUtil.toFile(fo);
284: if (file == null)
285: return nullDelegatingInterceptor;
286:
287: VersioningSystem[] systems = master.getVersioningSystems();
288: VersioningSystem lh = master.getLocalHistory(file);
289:
290: List<VCSInterceptor> interceptors = new ArrayList<VCSInterceptor>(
291: systems.length);
292: for (VersioningSystem system : systems) {
293: VCSInterceptor interceptor = system.getVCSInterceptor();
294: if (system != lh && interceptor != null) {
295: interceptors.add(interceptor);
296: }
297:
298: }
299: VCSInterceptor lhInterceptor = lh != null ? lh
300: .getVCSInterceptor() : null;
301:
302: if (interceptors.size() == 0 && lhInterceptor == null)
303: return nullDelegatingInterceptor;
304:
305: return new DelegatingInterceptor(interceptors, lhInterceptor,
306: file, null, false);
307: }
308:
309: private DelegatingInterceptor getInterceptor(File file,
310: boolean isDirectory) {
311: if (file == null || master == null)
312: return nullDelegatingInterceptor;
313:
314: VersioningSystem vs = master.getOwner(file);
315: VCSInterceptor vsInterceptor = vs != null ? vs
316: .getVCSInterceptor() : nullVCSInterceptor;
317:
318: VersioningSystem lhvs = master.getLocalHistory(file);
319: VCSInterceptor localHistoryInterceptor = lhvs != null ? lhvs
320: .getVCSInterceptor() : nullVCSInterceptor;
321:
322: return new DelegatingInterceptor(vsInterceptor,
323: localHistoryInterceptor, file, null, isDirectory);
324: }
325:
326: private DelegatingInterceptor getInterceptor(File from, File to) {
327: if (from == null || to == null)
328: return nullDelegatingInterceptor;
329:
330: VersioningSystem vs = master.getOwner(from);
331: VCSInterceptor vsInterceptor = vs != null ? vs
332: .getVCSInterceptor() : nullVCSInterceptor;
333:
334: VersioningSystem lhvs = master.getLocalHistory(from);
335: VCSInterceptor localHistoryInterceptor = lhvs != null ? lhvs
336: .getVCSInterceptor() : nullVCSInterceptor;
337:
338: return new DelegatingInterceptor(vsInterceptor,
339: localHistoryInterceptor, from, to, false);
340: }
341:
342: private final DelegatingInterceptor nullDelegatingInterceptor = new DelegatingInterceptor() {
343: public boolean beforeDelete() {
344: return false;
345: }
346:
347: public void doDelete() throws IOException {
348: }
349:
350: public void afterDelete() {
351: }
352:
353: public boolean beforeMove() {
354: return false;
355: }
356:
357: public void doMove() throws IOException {
358: }
359:
360: public boolean beforeCreate() {
361: return false;
362: }
363:
364: public void doCreate() throws IOException {
365: }
366:
367: public void afterCreate() {
368: }
369:
370: public void beforeChange() {
371: }
372:
373: public void beforeEdit() {
374: }
375:
376: public void afterChange() {
377: }
378:
379: public void afterMove() {
380: }
381:
382: public void handle() throws IOException {
383: }
384:
385: public boolean delete(File file) {
386: throw new UnsupportedOperationException();
387: }
388: };
389:
390: private final VCSInterceptor nullVCSInterceptor = new VCSInterceptor() {
391: };
392:
393: /**
394: * Delete interceptor: holds files and folders that we do not want to delete but must pretend that they were deleted.
395: */
396: private final Set<File> deletedFiles = new HashSet<File>(5);
397:
398: private class DelegatingInterceptor implements IOHandler,
399: DeleteHandler {
400:
401: final Collection<VCSInterceptor> interceptors;
402: final VCSInterceptor interceptor;
403: final VCSInterceptor lhInterceptor;
404: final File file;
405: final File to;
406: private final boolean isDirectory;
407:
408: private DelegatingInterceptor() {
409: this ((VCSInterceptor) null, null, null, null, false);
410: }
411:
412: public DelegatingInterceptor(VCSInterceptor interceptor,
413: VCSInterceptor lhInterceptor, File file, File to,
414: boolean isDirectory) {
415: this .interceptor = interceptor != null ? interceptor
416: : nullVCSInterceptor;
417: this .interceptors = Collections.singleton(this .interceptor);
418: this .lhInterceptor = lhInterceptor != null ? lhInterceptor
419: : nullVCSInterceptor;
420: this .file = file;
421: this .to = to;
422: this .isDirectory = isDirectory;
423: }
424:
425: // TODO: special hotfix for #95243
426: public DelegatingInterceptor(
427: Collection<VCSInterceptor> interceptors,
428: VCSInterceptor lhInterceptor, File file, File to,
429: boolean isDirectory) {
430: this .interceptors = interceptors != null
431: && interceptors.size() > 0 ? interceptors
432: : Collections.singleton(nullVCSInterceptor);
433: this .interceptor = this .interceptors.iterator().next();
434: this .lhInterceptor = lhInterceptor != null ? lhInterceptor
435: : nullVCSInterceptor;
436: this .file = file;
437: this .to = to;
438: this .isDirectory = isDirectory;
439: }
440:
441: public boolean beforeDelete() {
442: lhInterceptor.beforeDelete(file);
443: return interceptor.beforeDelete(file);
444: }
445:
446: public void doDelete() throws IOException {
447: lhInterceptor.doDelete(file);
448: interceptor.doDelete(file);
449: }
450:
451: public void afterDelete() {
452: lhInterceptor.afterDelete(file);
453: interceptor.afterDelete(file);
454: }
455:
456: public boolean beforeMove() {
457: lhInterceptor.beforeMove(file, to);
458: return interceptor.beforeMove(file, to);
459: }
460:
461: public void doMove() throws IOException {
462: lhInterceptor.doMove(file, to);
463: interceptor.doMove(file, to);
464: }
465:
466: public void afterMove() {
467: lhInterceptor.afterMove(file, to);
468: interceptor.afterMove(file, to);
469: }
470:
471: public boolean beforeCreate() {
472: lhInterceptor.beforeCreate(file, isDirectory);
473: return interceptor.beforeCreate(file, isDirectory);
474: }
475:
476: public void doCreate() throws IOException {
477: lhInterceptor.doCreate(file, isDirectory);
478: interceptor.doCreate(file, isDirectory);
479: }
480:
481: public void afterCreate() {
482: lhInterceptor.afterCreate(file);
483: // TODO: special hotfix for #95243
484: for (VCSInterceptor vcsInterceptor : interceptors) {
485: vcsInterceptor.afterCreate(file);
486: }
487: }
488:
489: public void afterChange() {
490: lhInterceptor.afterChange(file);
491: interceptor.afterChange(file);
492: }
493:
494: public void beforeChange() {
495: lhInterceptor.beforeChange(file);
496: interceptor.beforeChange(file);
497: }
498:
499: public void beforeEdit() {
500: lhInterceptor.beforeEdit(file);
501: interceptor.beforeEdit(file);
502: }
503:
504: /**
505: * We are doing MOVE here, inspite of the generic name of the method.
506: *
507: * @throws IOException
508: */
509: public void handle() throws IOException {
510: lhInterceptor.doMove(file, to);
511: interceptor.doMove(file, to);
512: lhInterceptor.afterMove(file, to);
513: interceptor.afterMove(file, to);
514: }
515:
516: /**
517: * This must act EXACTLY like java.io.File.delete(). This means:
518:
519: * 1.1 if the file is a file and was deleted, return true
520: * 1.2 if the file is a file and was NOT deleted because we want to keep it (is part of versioning metadata), also return true
521: * this is done this way to enable bottom-up recursive file deletion
522: * 1.3 if the file is a file that should be deleted but the operation failed (the file is locked, for example), return false
523: *
524: * 2.1 if the file is an empty directory that was deleted, return true
525: * 2.2 if the file is a NON-empty directory that was NOT deleted because it contains files that were NOT deleted in step 1.2, return true
526: * 2.3 if the file is a NON-empty directory that was NOT deleted because it contains some files that were not previously deleted, return false
527: *
528: * @param file file or folder to delete
529: * @return true if the file was successfully deleted (event virtually deleted), false otherwise
530: */
531: public boolean delete(File file) {
532: File[] children = file.listFiles();
533: if (children != null) {
534: synchronized (deletedFiles) {
535: for (File child : children) {
536: if (!deletedFiles.contains(child))
537: return false;
538: }
539: }
540: }
541: try {
542: lhInterceptor.doDelete(file);
543: interceptor.doDelete(file);
544: synchronized (deletedFiles) {
545: if (file.isDirectory()) {
546: // the directory was virtually deleted, we can forget about its children
547: for (Iterator<File> i = deletedFiles.iterator(); i
548: .hasNext();) {
549: File fakedFile = i.next();
550: if (file.equals(fakedFile.getParentFile())) {
551: i.remove();
552: }
553: }
554: }
555: if (file.exists()) {
556: deletedFiles.add(file);
557: } else {
558: deletedFiles.remove(file);
559: }
560: }
561: return true;
562: } catch (IOException e) {
563: // the interceptor failed to delete the file
564: return false;
565: }
566: }
567:
568: // VCSInterceptor getInterceptor() {
569: // return interceptor;
570: // }
571: }
572:
573: private class FileEx {
574: final FileObject parent;
575: final String name;
576: final boolean isFolder;
577:
578: public FileEx(FileObject parent, String name, boolean folder) {
579: this .parent = parent;
580: this .name = name;
581: isFolder = folder;
582: }
583:
584: public boolean equals(Object o) {
585: if (this == o)
586: return true;
587: if (o == null || !(o instanceof FileEx))
588: return false;
589: FileEx fileEx = (FileEx) o;
590: return isFolder == fileEx.isFolder
591: && name.equals(fileEx.name)
592: && parent.equals(fileEx.parent);
593: }
594:
595: public int hashCode() {
596: int result = parent.hashCode();
597: result = 17 * result + name.hashCode();
598: result = 17 * result + (isFolder ? 1 : 0);
599: return result;
600: }
601: }
602: }
|