--- a/masterfs/manifest.mf Thu Aug 20 17:55:11 2009 +0200 +++ a/masterfs/manifest.mf Wed Aug 26 11:49:55 2009 +0200 @@ -1,7 +1,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.masterfs/2 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/masterfs/resources/Bundle.properties -OpenIDE-Module-Specification-Version: 2.17 +OpenIDE-Module-Specification-Version: 2.18 AutoUpdate-Show-In-Client: false AutoUpdate-Essential-Module: true --- a/masterfs/nbproject/project.xml Thu Aug 20 17:55:11 2009 +0200 +++ a/masterfs/nbproject/project.xml Wed Aug 26 11:49:55 2009 +0200 @@ -60,7 +60,7 @@ - 6.2 + 7.24 --- a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java Thu Aug 20 17:55:11 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java Wed Aug 26 11:49:55 2009 +0200 @@ -203,6 +203,12 @@ public final boolean isRoot() { return false; } + + public final java.util.Date lastModified() { + final File f = getFileName().getFile(); + final long lastModified = f.lastModified(); + return new Date(lastModified); + } @Override public final FileObject move(FileLock lock, FileObject target, String name, String ext) throws IOException { @@ -379,6 +385,16 @@ getEventSupport().remove(FileChangeListener.class, fcl); } + @Override + public void addRecursiveListener(FileChangeListener fcl) { + addFileChangeListener(fcl); + } + + @Override + public void removeRecursiveListener(FileChangeListener fcl) { + removeFileChangeListener(fcl); + } + private Enumeration getListeners() { if (eventSupport == null) { return Enumerations.empty(); --- a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FileObj.java Thu Aug 20 17:55:11 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FileObj.java Wed Aug 26 11:49:55 2009 +0200 @@ -210,11 +210,6 @@ return super.canWrite(); } - public final Date lastModified() { - final File f = getFileName().getFile(); - return new Date(f.lastModified()); - } - final void setLastModified(long lastModified) { if (this.lastModified != 0) { // #130998 - don't set when already invalidated if (this.lastModified != -1 && !realLastModifiedCached) { --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ a9b41094a261 Wed Aug 26 11:49:55 2009 +0200 @@ -0,0 +1,217 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2009 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.masterfs.filebasedfs.fileobjects; + +import java.io.File; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.modules.masterfs.filebasedfs.fileobjects.FileObjectFactory.Caller; +import org.openide.filesystems.FileAttributeEvent; +import org.openide.filesystems.FileChangeListener; +import org.openide.filesystems.FileEvent; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileRenameEvent; + +/** Keeps list of fileobjects under given root. Adapted from Jan Lahoda's work + * in issue 168237 + */ +final class FileObjectKeeper implements FileChangeListener { + private static final Logger LOG = Logger.getLogger(FileObjectKeeper.class.getName()); + + private Set kept; + private List listeners; + private final FolderObj root; + private long timeStamp; + + public FileObjectKeeper(FolderObj root) { + this.root = root; + } + + public synchronized void addRecursiveListener(FileChangeListener fcl) { + if (listeners == null) { + listeners = new CopyOnWriteArrayList(); + listenToAll(); + } + listeners.add(fcl); + } + + public synchronized void removeRecursiveListener(FileChangeListener fcl) { + if (listeners == null) { + return; + } + listeners.remove(fcl); + if (listeners.isEmpty()) { + listenNoMore(); + } + } + + public void init(long previous, FileObjectFactory factory, boolean expected) { + File file = root.getFileName().getFile(); + File[] arr = file.listFiles(); + long ts = 0; + if (arr != null) { + for (File f : arr) { + if (f.isDirectory()) { + continue; + } + long lm = f.lastModified(); + LOG.log(Level.FINE, " check {0} for {1}", new Object[] { lm, f }); + if (lm > ts) { + ts = lm; + } + if (lm > previous && factory != null) { + BaseFileObj who = factory.getValidFileObject(f, Caller.Others); + LOG.log(Level.FINE, "External change detected {0}", who); + who.fireFileChangedEvent(expected); + } + } + } + timeStamp = ts; + LOG.log(Level.FINE, "Testing {0}, time {1}", new Object[] { file, timeStamp }); + } + + private final void listenToAll() { + assert Thread.holdsLock(this); + assert kept == null; + kept = new HashSet(); + root.addFileChangeListener(this); + Enumeration en = root.getChildren(true); + while (en.hasMoreElements()) { + FileObject fo = en.nextElement(); + if (fo instanceof FolderObj) { + FolderObj obj = (FolderObj)fo; + obj.addFileChangeListener(this); + kept.add(obj); + obj.getKeeper(); + } + } + } + + private final void listenNoMore() { + assert Thread.holdsLock(this); + assert kept != null; + + root.removeFileChangeListener(this); + for (FileObject fo : kept) { + fo.removeFileChangeListener(this); + } + kept = null; + } + + public void fileFolderCreated(FileEvent fe) { + List arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileFolderCreated(fe); + } + if (fe.getFile() instanceof FolderObj) { + synchronized (this) { + kept.add(fe.getFile()); + } + } + } + + public void fileDataCreated(FileEvent fe) { + List arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileDataCreated(fe); + } + } + + public void fileChanged(FileEvent fe) { + List arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileChanged(fe); + } + } + + public void fileDeleted(FileEvent fe) { + List arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileDeleted(fe); + } + if (fe.getFile() instanceof FolderObj) { + synchronized (this) { + kept.remove(fe.getFile()); + } + } + } + + public void fileRenamed(FileRenameEvent fe) { + List arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileRenamed(fe); + } + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + List arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileAttributeChanged(fe); + } + } + + long childrenLastModified() { + return timeStamp; + } + +} --- a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FolderObj.java Thu Aug 20 17:55:11 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FolderObj.java Wed Aug 26 11:49:55 2009 +0200 @@ -49,6 +49,7 @@ import java.io.SyncFailedException; import java.util.ArrayList; import java.util.Date; +import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -62,6 +63,7 @@ import org.netbeans.modules.masterfs.filebasedfs.FileBasedFileSystem.FSCallable; import org.netbeans.modules.masterfs.filebasedfs.children.ChildrenCache; import org.netbeans.modules.masterfs.filebasedfs.children.ChildrenSupport; +import org.netbeans.modules.masterfs.filebasedfs.fileobjects.FileObjectFactory.Caller; import org.netbeans.modules.masterfs.filebasedfs.naming.FileName; import org.netbeans.modules.masterfs.filebasedfs.naming.FileNaming; import org.netbeans.modules.masterfs.filebasedfs.naming.NamingFactory; @@ -69,6 +71,7 @@ import org.netbeans.modules.masterfs.filebasedfs.utils.FileChangedManager; import org.netbeans.modules.masterfs.filebasedfs.utils.FileInfo; import org.netbeans.modules.masterfs.providers.ProvidedExtensions; +import org.openide.filesystems.FileChangeListener; import org.openide.filesystems.FileLock; import org.openide.filesystems.FileObject; import org.openide.util.Mutex; @@ -82,7 +85,8 @@ private static final Mutex mutex = new Mutex(FolderObj.mp); private FolderChildrenCache folderChildren; - boolean valid = true; + boolean valid = true; + private FileObjectKeeper keeper; /** * Creates a new instance of FolderImpl @@ -333,6 +337,7 @@ public void refreshImpl(final boolean expected, boolean fire) { final ChildrenCache cache = getChildrenCache(); final Mutex.Privileged mutexPrivileged = cache.getMutexPrivileged(); + final long previous = keeper == null ? -1 : keeper.childrenLastModified(); Set oldChildren = null; Map refreshResult = null; @@ -412,6 +417,11 @@ fireFileDeletedEvent(expected); } } + + if (previous != -1) { + assert keeper != null; + keeper.init(previous, factory, expected); + } } @Override @@ -487,10 +497,6 @@ throw new IOException(getPath()); } - public final java.util.Date lastModified() { - final File f = getFileName().getFile(); - return new Date(f.lastModified()); - } public final FileLock lock() throws IOException { return new FileLock(); @@ -508,6 +514,25 @@ return folderChildren; } + synchronized FileObjectKeeper getKeeper() { + if (keeper == null) { + keeper = new FileObjectKeeper(this); + keeper.init(-1, null, false); + } + return keeper; + } + + @Override + public final void addRecursiveListener(FileChangeListener fcl) { + getKeeper().addRecursiveListener(fcl); + } + + @Override + public final void removeRecursiveListener(FileChangeListener fcl) { + getKeeper().removeRecursiveListener(fcl); + } + + public final class FolderChildrenCache implements ChildrenCache { public final ChildrenSupport ch = new ChildrenSupport(); --- a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/ReplaceForSerialization.java Thu Aug 20 17:55:11 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/ReplaceForSerialization.java Wed Aug 26 11:49:55 2009 +0200 @@ -91,10 +91,6 @@ return false; } - public Date lastModified() { - return new Date(0L); - } - /* Test whether the file is valid. The file can be invalid if it has been deserialized * and the file no longer exists on disk; or if the file has been deleted. * --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ a9b41094a261 Wed Aug 26 11:49:55 2009 +0200 @@ -0,0 +1,318 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2009 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package org.netbeans.modules.masterfs.filebasedfs.fileobjects; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.junit.NbTestCase; +import org.openide.filesystems.FileAttributeEvent; +import org.openide.filesystems.FileChangeAdapter; +import org.openide.filesystems.FileChangeListener; +import org.openide.filesystems.FileEvent; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileRenameEvent; +import org.openide.filesystems.FileUtil; + +public class ExternalTouchTest extends NbTestCase { + private Logger LOG; + private FileObject testFolder; + + public ExternalTouchTest(String testName) { + super(testName); + } + + @Override + protected Level logLevel() { + return Level.FINE; + } + + @Override + protected void setUp() throws Exception { + clearWorkDir(); + + LOG = Logger.getLogger("test." + getName()); + Logger.getLogger("org.openide.util.Mutex").setUseParentHandlers(false); + + File dir = new File(getWorkDir(), "test"); + dir.mkdirs(); + testFolder = FileUtil.toFileObject(dir); + assertNotNull("Test folder created", testFolder); + + } + + public void testChangeInChildrenNoticed() throws Exception { + long lm = System.currentTimeMillis(); + FileObject fileObject1 = testFolder.createData("fileObject1"); + assertNotNull("Just to initialize the stamp", lm); + FileObject[] arr = testFolder.getChildren(); + assertEquals("One child", 1, arr.length); + assertEquals("Right child", fileObject1, arr[0]); + + File file = FileUtil.toFile(fileObject1); + assertNotNull("File found", file); + Reference ref = new WeakReference(fileObject1); + arr = null; + fileObject1 = null; + assertGC("File Object can disappear", ref); + + Thread.sleep(1000); + + class L extends FileChangeAdapter { + int cnt; + FileEvent event; + + @Override + public void fileChanged(FileEvent fe) { + LOG.info("file change " + fe.getFile()); + cnt++; + event = fe; + } + } + L listener = new L(); + testFolder.addRecursiveListener(listener); + + FileOutputStream os = new FileOutputStream(file); + os.write(10); + os.close(); + + if (lm > file.lastModified() - 50) { + fail("New modification time shall be at last 50ms after the original one: " + (file.lastModified() - lm)); + } + + testFolder.refresh(); + + assertEquals("Change notified", 1, listener.cnt); + assertEquals("Right file", file, FileUtil.toFile(listener.event.getFile())); + assertEquals("Right source", file.getParentFile(), FileUtil.toFile((FileObject)listener.event.getSource())); + } + public void testNewChildNoticed() throws Exception { + FileObject fileObject1 = testFolder.createData("fileObject1"); + FileObject[] arr = testFolder.getChildren(); + assertEquals("One child", 1, arr.length); + assertEquals("Right child", fileObject1, arr[0]); + + File file = FileUtil.toFile(fileObject1); + assertNotNull("File found", file); + arr = null; + fileObject1 = null; + Reference ref = new WeakReference(fileObject1); + assertGC("File Object can disappear", ref); + + Thread.sleep(100); + + class L extends FileChangeAdapter { + int cnt; + FileEvent event; + + @Override + public void fileDataCreated(FileEvent fe) { + cnt++; + event = fe; + } + + } + L listener = new L(); + testFolder.addRecursiveListener(listener); + + File nfile = new File(file.getParentFile(), "new.txt"); + nfile.createNewFile(); + + testFolder.refresh(); + + assertEquals("Change notified", 1, listener.cnt); + assertEquals("Right file", nfile, FileUtil.toFile(listener.event.getFile())); + } + public void testDeleteOfAChildNoticed() throws Exception { + FileObject fileObject1 = testFolder.createData("fileObject1"); + FileObject[] arr = testFolder.getChildren(); + assertEquals("One child", 1, arr.length); + assertEquals("Right child", fileObject1, arr[0]); + + File file = FileUtil.toFile(fileObject1); + assertNotNull("File found", file); + arr = null; + fileObject1 = null; + Reference ref = new WeakReference(fileObject1); + assertGC("File Object can disappear", ref); + + Thread.sleep(100); + + class L extends FileChangeAdapter { + int cnt; + FileEvent event; + + @Override + public void fileDeleted(FileEvent fe) { + cnt++; + event = fe; + } + + } + L listener = new L(); + testFolder.addRecursiveListener(listener); + + file.delete(); + + testFolder.refresh(); + + assertEquals("Change notified", 1, listener.cnt); + assertEquals("Right file", file, FileUtil.toFile(listener.event.getFile())); + } + + public void testRecursiveListener() throws Exception { + FileObject obj = FileUtil.createData(testFolder, "my/sub/children/children.java"); + FileObject sub = obj.getParent().getParent(); + + class L implements FileChangeListener { + StringBuilder sb = new StringBuilder(); + + public void fileFolderCreated(FileEvent fe) { + LOG.info("FolderCreated: " + fe.getFile()); + sb.append("FolderCreated"); + } + + public void fileDataCreated(FileEvent fe) { + LOG.info("DataCreated: " + fe.getFile()); + sb.append("DataCreated"); + } + + public void fileChanged(FileEvent fe) { + LOG.info("Changed: " + fe.getFile()); + sb.append("Changed"); + } + + public void fileDeleted(FileEvent fe) { + LOG.info("Deleted: " + fe.getFile()); + sb.append("Deleted"); + } + + public void fileRenamed(FileRenameEvent fe) { + LOG.info("Renamed: " + fe.getFile()); + sb.append("Renamed"); + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + if (fe.getName().startsWith("DataEditorSupport.read-only.refresh")) { + return; + } + LOG.info("AttributeChanged: " + fe.getFile()); + sb.append("AttributeChanged"); + } + + public void assertMessages(String txt, String msg) { + assertEquals(txt, msg, sb.toString()); + sb.setLength(0); + } + } + L recursive = new L(); + L flat = new L(); + + sub.addFileChangeListener(flat); + LOG.info("Adding listener"); + sub.addRecursiveListener(recursive); + LOG.info("Adding listener finished"); + + Thread.sleep(1000); + + File fo = new File(FileUtil.toFile(obj.getParent()), "sibling.java"); + fo.createNewFile(); + LOG.info("sibling created, now refresh"); + FileUtil.refreshAll(); + LOG.info("sibling refresh finished"); + +// Right now it is DataCreatedChanged +// recursive.assertMessages("Creation", "DataCreated"); + recursive.assertMessages("Creation", "DataCreatedChanged"); + flat.assertMessages("No messages in flat mode", ""); + + Thread.sleep(1000); + + final OutputStream os = new FileOutputStream(fo); + os.write(10); + os.close(); + LOG.info("Before refresh"); + FileUtil.refreshAll(); + LOG.info("After refresh"); + + flat.assertMessages("No messages in flat mode", ""); +// Two changes detected now: +// recursive.assertMessages("written", "Changed"); + recursive.assertMessages("written", "ChangedChanged"); + + fo.delete(); + FileUtil.refreshAll(); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("gone", "Deleted"); + + new File(FileUtil.toFile(sub), "testFolder").mkdirs(); + FileUtil.refreshAll(); + + flat.assertMessages("Direct Folder notified", "FolderCreated"); + recursive.assertMessages("Direct Folder notified", "FolderCreated"); + + new File(FileUtil.toFile(sub.getParent()), "unimportant.txt").createNewFile(); + FileUtil.refreshAll(); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("No messages in recursive mode", ""); + + sub.removeRecursiveListener(recursive); + + new File(FileUtil.toFile(sub), "test.data").createNewFile(); + FileUtil.refreshAll(); + +// Double notification right now: +// flat.assertMessages("Direct file notified", "DataCreated"); + flat.assertMessages("Direct file notified", "DataCreatedChanged"); + recursive.assertMessages("No longer active", ""); + + WeakReference ref = new WeakReference(recursive); + recursive = null; + assertGC("Listener can be GCed", ref); + } + +} --- a/openide.filesystems/apichanges.xml Thu Aug 20 17:55:11 2009 +0200 +++ a/openide.filesystems/apichanges.xml Wed Aug 26 11:49:55 2009 +0200 @@ -46,6 +46,23 @@ Filesystems API + + + Support for recursive listeners + + + + + +

+ One can register a recursive listener on a file object by + calling + FileObject.addRecursiveListener(FileChangeListener). +

+
+ + +
Allow modules to dynamically add/remove layer content --- a/openide.filesystems/nbproject/project.properties Thu Aug 20 17:55:11 2009 +0200 +++ a/openide.filesystems/nbproject/project.properties Wed Aug 26 11:49:55 2009 +0200 @@ -44,4 +44,4 @@ javadoc.main.page=org/openide/filesystems/doc-files/api.html javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=7.23.0 +spec.version.base=7.24.0 --- a/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java Thu Aug 20 17:55:11 2009 +0200 +++ a/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java Wed Aug 26 11:49:55 2009 +0200 @@ -112,7 +112,7 @@ return orig; } - private static Logger LOG = Logger.getLogger("org.openide.filesystems"); // NOI18N + final static Logger LOG = Logger.getLogger("org.openide.filesystems"); // NOI18N /** Logs a text. */ public static void log(String msg) { --- a/openide.filesystems/src/org/openide/filesystems/FileObject.java Thu Aug 20 17:55:11 2009 +0200 +++ a/openide.filesystems/src/org/openide/filesystems/FileObject.java Wed Aug 26 11:49:55 2009 +0200 @@ -58,7 +58,9 @@ import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; +import java.util.logging.Level; import org.openide.util.Enumerations; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.util.UserQuestionException; @@ -413,6 +415,53 @@ */ public abstract void removeFileChangeListener(FileChangeListener fcl); + + /** Adds a listener to this {@link FileObject} and all its children and + * children or its children. + * It is guaranteed that whenever a change + * is made via the FileSystem API itself under this {@link FileObject} + * that it is notified to the fcl listener. Whether external + * changes (if they make sense) are detected and + * notified depends on actual implementation. As some implementations may + * need to perform non-trivial amount of work during initialization of + * listeners, this methods can take long time. Usage of this method may + * consume a lot of system resources and as such it shall be used with care. + * Traditional {@link #addFileChangeListener(org.openide.filesystems.FileChangeListener)} + * is definitely preferred variant. + *

+ * If you are running with the MasterFS module enabled, it guarantees + * that for files backed with real {@link File}, the system initializes + * itself to detect external changes on the whole subtree. + * This requires non-trivial amount of work and especially on slow + * disks (aka networks ones) may take a long time to add the listener + * and also refresh the system when {@link FileObject#refresh()} + * and especially {@link FileUtil#refreshAll()} is requested. + *

+ * + * @param fcl the listener to register + * @since 7.24 + */ + public void addRecursiveListener(FileChangeListener fcl) { + try { + getFileSystem().addFileChangeListener(new RecursiveListener(this, fcl)); + } catch (FileStateInvalidException ex) { + ExternalUtil.LOG.log(Level.FINE, "Cannot remove listener from " + this, ex); + } + } + + /** Removes listener previously added by {@link #addRecursiveListener(org.openide.filesystems.FileChangeListener)} + * + * @param fcl the listener to remove + * @since 7.24 + */ + public void removeRecursiveListener(FileChangeListener fcl) { + try { + getFileSystem().removeFileChangeListener(new RecursiveListener(this, fcl)); + } catch (FileStateInvalidException ex) { + ExternalUtil.LOG.log(Level.FINE, "Cannot remove listener from " + this, ex); + } + } + /** Fire data creation event. * @param en listeners that should receive the event * @param fe the event to fire in this object --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ a9b41094a261 Wed Aug 26 11:49:55 2009 +0200 @@ -0,0 +1,127 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2009 Sun Microsystems, Inc. + */ + +package org.openide.filesystems; + +import java.lang.ref.WeakReference; + +/** + * + * @author Jaroslav Tulach + */ +final class RecursiveListener extends WeakReference +implements FileChangeListener { + private final FileChangeListener fcl; + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final RecursiveListener other = (RecursiveListener) obj; + if (this.fcl != other.fcl && (this.fcl == null || !this.fcl.equals(other.fcl))) { + return false; + } + final FileObject otherFo = other.get(); + final FileObject thisFo = this.get(); + if (thisFo != otherFo && (thisFo == null || !thisFo.equals(otherFo))) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final FileObject thisFo = this.get(); + int hash = 3; + hash = 37 * hash + (this.fcl != null ? this.fcl.hashCode() : 0); + hash = 13 * hash + (thisFo != null ? thisFo.hashCode() : 0); + return hash; + } + + public RecursiveListener(FileObject source, FileChangeListener fcl) { + super(source); + this.fcl = fcl; + } + + public void fileRenamed(FileRenameEvent fe) { + FileObject thisFo = this.get(); + if (thisFo != null && FileUtil.isParentOf(thisFo, fe.getFile())) { + fcl.fileRenamed(fe); + } + } + + public void fileFolderCreated(FileEvent fe) { + FileObject thisFo = this.get(); + if (thisFo != null && FileUtil.isParentOf(thisFo, fe.getFile())) { + fcl.fileFolderCreated(fe); + } + } + + public void fileDeleted(FileEvent fe) { + FileObject thisFo = this.get(); + if (thisFo != null && FileUtil.isParentOf(thisFo, fe.getFile())) { + fcl.fileDeleted(fe); + } + } + + public void fileDataCreated(FileEvent fe) { + FileObject thisFo = this.get(); + if (thisFo != null && FileUtil.isParentOf(thisFo, fe.getFile())) { + fcl.fileDataCreated(fe); + } + } + + public void fileChanged(FileEvent fe) { + FileObject thisFo = this.get(); + if (thisFo != null && FileUtil.isParentOf(thisFo, fe.getFile())) { + fcl.fileChanged(fe); + } + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + FileObject thisFo = this.get(); + if (thisFo != null && FileUtil.isParentOf(thisFo, fe.getFile())) { + fcl.fileAttributeChanged(fe); + } + } +} --- a/openide.filesystems/test/unit/src/org/openide/filesystems/FileObjectTestHid.java Thu Aug 20 17:55:11 2009 +0200 +++ a/openide.filesystems/test/unit/src/org/openide/filesystems/FileObjectTestHid.java Wed Aug 26 11:49:55 2009 +0200 @@ -1569,6 +1569,104 @@ assertNotNull(child); } } + + public void testRecursiveListener() throws IOException { + checkSetUp(); + + FileObject folder1 = getTestFolder1 (root); + /** delete first time*/ + try { + FileObject obj = FileUtil.createData(folder1, "my/sub/children/children.java"); + FileObject sub = obj.getParent().getParent(); + + class L implements FileChangeListener { + StringBuilder sb = new StringBuilder(); + + public void fileFolderCreated(FileEvent fe) { + sb.append("FolderCreated"); + } + + public void fileDataCreated(FileEvent fe) { + sb.append("DataCreated"); + } + + public void fileChanged(FileEvent fe) { + sb.append("Changed"); + } + + public void fileDeleted(FileEvent fe) { + sb.append("Deleted"); + } + + public void fileRenamed(FileRenameEvent fe) { + sb.append("Renamed"); + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + sb.append("AttributeChanged"); + } + + public void assertMessages(String txt, String msg) { + assertEquals(txt, msg, sb.toString()); + sb.setLength(0); + } + } + L recursive = new L(); + L flat = new L(); + + sub.addFileChangeListener(flat); + sub.addRecursiveListener(recursive); + + FileObject fo = obj.getParent().createData("sibling.java"); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("Creation", "DataCreated"); + + fo.setAttribute("jarda", "hello"); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("attr", "AttributeChanged"); + + final OutputStream os = fo.getOutputStream(); + os.write(10); + os.close(); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("written", "Changed"); + + fo.delete(); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("gone", "Deleted"); + + sub.createFolder("testFolder"); + + flat.assertMessages("Direct Folder notified", "FolderCreated"); + recursive.assertMessages("Direct Folder notified", "FolderCreated"); + + sub.getParent().createData("unimportant.txt"); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("No messages in recursive mode", ""); + + sub.removeRecursiveListener(recursive); + + sub.createData("test.data"); + + flat.assertMessages("Direct file notified", "DataCreated"); + recursive.assertMessages("No longer active", ""); + + WeakReference ref = new WeakReference(recursive); + recursive = null; + assertGC("Listener can be GCed", ref); + + } catch (IOException iex) { + if (fs.isReadOnly() || root.isReadOnly()) return; + throw iex; + } finally { + } + } + /** Test of delete method, of class org.openide.filesystems.FileObject. */ public void testCreateDeleteFolderCreate () throws IOException {