--- a/masterfs/manifest.mf Wed Sep 16 14:48:03 2009 +0200 +++ a/masterfs/manifest.mf Wed Sep 16 16:32:49 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 Wed Sep 16 14:48:03 2009 +0200 +++ a/masterfs/nbproject/project.xml Wed Sep 16 16:32:49 2009 +0200 @@ -60,7 +60,7 @@ - 6.2 + 7.28 --- a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java Wed Sep 16 14:48:03 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java Wed Sep 16 16:32:49 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 { @@ -376,6 +382,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 Wed Sep 16 14:48:03 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FileObj.java Wed Sep 16 16:32:49 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 +++ 70eda1b69061 Wed Sep 16 16:32:49 2009 +0200 @@ -0,0 +1,249 @@ +/* + * 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.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +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 Collection 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 CopyOnWriteArraySet(); + } + if (listeners.isEmpty()) { + 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) { + final BaseFileObj prevFO = factory.getCachedOnly(f); + if (prevFO == null) { + BaseFileObj who = factory.getValidFileObject(f, Caller.Others); + LOG.log(Level.FINE, "External change detected {0}", who); + who.fireFileChangedEvent(expected); + } else { + prevFO.refresh(expected, true); + } + } + } + } + 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); + + root.removeFileChangeListener(this); + Set k = kept; + if (k != null) { + for (FileObject fo : k) { + fo.removeFileChangeListener(this); + } + kept = null; + } + } + + public void fileFolderCreated(FileEvent fe) { + Collection arr = listeners; + if (arr == null) { + return; + } + final FileObject f = fe.getFile(); + if (f instanceof FolderObj) { + synchronized (this) { + kept.add(f); + f.addFileChangeListener(this); + Enumeration en = f.getChildren(true); + while (en.hasMoreElements()) { + FileObject fo = en.nextElement(); + if (fo instanceof FolderObj) { + fo.addFileChangeListener(this); + } + } + } + } + for (FileChangeListener l : arr) { + l.fileFolderCreated(fe); + } + } + + public void fileDataCreated(FileEvent fe) { + Collection arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileDataCreated(fe); + } + } + + public void fileChanged(FileEvent fe) { + Collection arr = listeners; + if (arr == null) { + return; + } + for (FileChangeListener l : arr) { + l.fileChanged(fe); + } + } + + public void fileDeleted(FileEvent fe) { + Collection arr = listeners; + if (arr == null) { + return; + } + final FileObject f = fe.getFile(); + if (f.isFolder() && fe.getSource() == f && f != root) { + // there will be another event for parent folder + return; + } + + for (FileChangeListener l : arr) { + l.fileDeleted(fe); + } + if (f instanceof FolderObj) { + synchronized (this) { + if (kept != null) { + kept.remove(f); + } + f.removeFileChangeListener(this); + } + } + } + + public void fileRenamed(FileRenameEvent fe) { + Collection arr = listeners; + if (arr == null) { + return; + } + final FileObject f = fe.getFile(); + if (f.isFolder() && fe.getSource() == f && f != root) { + // there will be another event for parent folder + return; + } + for (FileChangeListener l : arr) { + l.fileRenamed(fe); + } + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + Collection 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 Wed Sep 16 14:48:03 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FolderObj.java Wed Sep 16 16:32:49 2009 +0200 @@ -48,7 +48,6 @@ import java.io.OutputStream; import java.io.SyncFailedException; import java.util.ArrayList; -import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -69,6 +68,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 +82,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 +334,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 +414,11 @@ fireFileDeletedEvent(expected); } } + + if (previous != -1) { + assert keeper != null; + keeper.init(previous, factory, expected); + } } @Override @@ -487,10 +494,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 +511,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 Wed Sep 16 14:48:03 2009 +0200 +++ a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/ReplaceForSerialization.java Wed Sep 16 16:32:49 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. * --- a/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java Wed Sep 16 14:48:03 2009 +0200 +++ a/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java Wed Sep 16 16:32:49 2009 +0200 @@ -53,6 +53,7 @@ import java.util.List; import java.util.Map; import java.util.Random; +import junit.framework.Test; import org.netbeans.junit.NbTestCase; import org.netbeans.junit.RandomlyFails; import org.openide.filesystems.FileAttributeEvent; @@ -239,6 +240,141 @@ assertGC("FileChangeListener not collected.", ref); } + public void testAddRecursiveListener() throws IOException, InterruptedException { + clearWorkDir(); + File rootF = getWorkDir(); + File dirF = new File(rootF, "dir"); + File fileF = new File(dirF, "subdir"); + + // adding listeners + TestFileChangeListener fcl = new TestFileChangeListener(); + FileUtil.addRecursiveListener(fcl, fileF); + try { + FileUtil.addRecursiveListener(fcl, fileF); + fail("Should not be possible to add listener for the same path."); + } catch (IllegalArgumentException iae) { + // ok + } + TestFileChangeListener fcl2 = new TestFileChangeListener(); + try { + FileUtil.removeRecursiveListener(fcl2, fileF); + fail("Should not be possible to remove listener which is not registered."); + } catch (IllegalArgumentException iae) { + // ok + } + FileUtil.addRecursiveListener(fcl2, fileF); + + // creation + final FileObject rootFO = FileUtil.toFileObject(rootF); + FileObject dirFO = rootFO.createFolder("dir"); + assertEquals("Event fired when just parent dir created.", 0, fcl.checkAll()); + FileObject fileFO = FileUtil.createData(dirFO, "subdir/subsubdir/file"); + assertEquals("Event not fired when file was created.", 2, fcl.check(EventType.FOLDER_CREATED)); + assertEquals("Event not fired when file was created.", 2, fcl2.check(EventType.FOLDER_CREATED)); + FileObject fileFO2 = FileUtil.createData(dirFO, "subdir/subsubdir/file2"); + assertEquals("Event not fired when file was created.", 2, fcl.check(EventType.DATA_CREATED)); + assertEquals("Event not fired when file was created.", 2, fcl2.check(EventType.DATA_CREATED)); + FileObject fileAFO = FileUtil.createData(dirFO, "fileA"); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // remove listener + FileUtil.removeRecursiveListener(fcl2, fileF); + fcl2.disabled = true; + assertEquals("No other events should be fired.", 0, fcl2.checkAll()); + + // modification + fileFO.getOutputStream().close(); + fileFO.getOutputStream().close(); + assertEquals("Event not fired when file was modified.", 2, fcl.check(EventType.CHANGED)); + // no event fired when other file modified + fileAFO.getOutputStream().close(); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // deletion + fileFO.delete(); + assertEquals("Event not fired when file deleted.", 1, fcl.check(EventType.DELETED)); + dirFO.delete(); + assertEquals("Event not fired when parent dir deleted.", 1, fcl.checkAll()); + dirFO = rootFO.createFolder("dir"); + fileFO = FileUtil.createData(dirFO, "subdir/subsubdir/file"); + assertEquals("Event not fired when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + assertEquals("Event not fired when dirs created.", 2, fcl.check(EventType.FOLDER_CREATED)); + dirFO.delete(); + assertEquals("Event not fired when parent dir deleted.", 1, fcl.check(EventType.DELETED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // atomic action + FileUtil.runAtomicAction(new Runnable() { + + public void run() { + FileObject dirFO; + try { + dirFO = rootFO.createFolder("dir"); + rootFO.createFolder("fakedir"); + rootFO.setAttribute("fake", "fake"); + rootFO.createData("fakefile"); + FileUtil.createData(dirFO, "subdir/subsubdir/file"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + } + }); + assertEquals("Notifying the folder creation only.", 1, fcl.check(EventType.FOLDER_CREATED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // rename + dirFO = FileUtil.toFileObject(dirF); + fileFO = FileUtil.toFileObject(fileF); + FileLock lock = dirFO.lock(); + dirFO.rename(lock, "dirRenamed", null); + lock.releaseLock(); + assertEquals("Event fired when parent dir renamed.", 0, fcl.checkAll()); + lock = fileFO.lock(); + fileFO.rename(lock, "fileRenamed", null); + lock.releaseLock(); + assertEquals("Renamed event not fired.", 1, fcl.check(EventType.RENAMED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // disk changes + dirF.mkdir(); + final File subdir = new File(fileF, "subdir"); + subdir.mkdirs(); + final File newfile = new File(subdir, "newfile"); + assertTrue(newfile.createNewFile()); + FileUtil.refreshAll(); + assertEquals("Event not fired when file was created.", 1, fcl.check(EventType.FOLDER_CREATED)); + Thread.sleep(1000); // make sure timestamp changes + new FileOutputStream(newfile).close(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file was modified.", 1, fcl.check(EventType.CHANGED)); + assertEquals("Attribute change event not fired (see #129178).", 3, fcl.check(EventType.ATTRIBUTE_CHANGED)); + newfile.delete(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file deleted.", 1, fcl.check(EventType.DELETED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // disk changes #66444 + File fileX = new File(subdir, "oscilating.file"); + for (int cntr = 0; cntr < 50; cntr++) { + fileX.getParentFile().mkdirs(); + new FileOutputStream(fileX).close(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file was created; count=" + cntr, 1, fcl.check(EventType.DATA_CREATED)); + fileX.delete(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file deleted; count=" + cntr, 1, fcl.check(EventType.DELETED)); + } + + // removed listener + assertEquals("No other events should be fired in removed listener.", 0, fcl2.checkAll()); + + // weakness + WeakReference ref = new WeakReference(fcl); + fcl = null; + assertGC("FileChangeListener not collected.", ref); + } + /** Tests FileChangeListener on folder. As declared in * {@link FileUtil#addFileChangeListener(org.openide.filesystems.FileChangeListener, java.io.File) } * - fileFolderCreated event is fired when the folder is created or a child folder created @@ -309,12 +445,327 @@ assertEquals("No other events should be fired.", 0, fcl.checkAll()); } + /** Tests FileObject.addRecursiveListener on folder as declared in + * {@link FileObject#addRecursiveListener(org.openide.filesystems.FileChangeListener) }. + * It is expected that all events from sub folders are delivered just once. + */ + public void testAddRecursiveListenerToFileObjectFolder() throws Exception { + checkFolderRecursiveListener(false); + } + + /** Tests FileUtil.addRecursiveListener on folder as declared in + * {@link FileUtil#addRecursiveListener(org.openide.filesystems.FileChangeListener, java.io.File) }. + * It is expected that all events from sub folders are delivered just once. + */ + public void testAddRecursiveListenerToFileFolder() throws Exception { + checkFolderRecursiveListener(true); + } + + /** Tests addRecursiveListener on folder either added to FileObject or File. + * @param isOnFile true to add listener to java.io.File, false to FileObject + */ + private void checkFolderRecursiveListener(boolean isOnFile) throws Exception { + clearWorkDir(); + // test files: dir/file1, dir/subdir/subfile, dir/subdir/subsubdir/subsubfile + final File rootF = getWorkDir(); + final File dirF = new File(rootF, "dir"); + File fileF = new File(dirF, "file1"); + File subdirF = new File(dirF, "subdir"); + File subfileF = new File(subdirF, "subfile"); + File subsubdirF = new File(subdirF, "subsubdir"); + File subsubfileF = new File(subsubdirF, "subsubfile"); + + TestFileChangeListener fcl = new TestFileChangeListener(); + FileObject dirFO; + if (isOnFile) { + FileUtil.addRecursiveListener(fcl, dirF); + dirFO = FileUtil.createFolder(dirF); + assertEquals("Wrong number of events fired when folder created.", 1, fcl.check(EventType.FOLDER_CREATED)); + } else { + dirFO = FileUtil.createFolder(dirF); + dirFO.addRecursiveListener(fcl); + } + + // create dir + FileObject subdirFO = dirFO.createFolder("subdir"); + assertEquals("Wrong number of events fired when sub folder created.", 1, fcl.check(EventType.FOLDER_CREATED)); + FileObject subsubdirFO = subdirFO.createFolder("subsubdir"); + assertEquals("Wrong number of events when sub sub folder created.", 1, fcl.check(EventType.FOLDER_CREATED)); + + // create file + FileObject file1FO = dirFO.createData("file1"); + assertEquals("Wrong number of events when data created.", 1, fcl.check(EventType.DATA_CREATED)); + FileObject subfileFO = subdirFO.createData("subfile"); + assertEquals("Wrong number of events when data in sub folder created.", 1, fcl.check(EventType.DATA_CREATED)); + FileObject subsubfileFO = subsubdirFO.createData("subsubfile"); + assertEquals("Wrong number of events when data in sub sub folder created.", 1, fcl.check(EventType.DATA_CREATED)); + + // modify + file1FO.getOutputStream().close(); + assertEquals("Wrong number of events when file folder modified.", 1, fcl.check(EventType.CHANGED)); + subfileFO.getOutputStream().close(); + assertEquals("Wrong number of events when file in sub folder modified.", 1, fcl.check(EventType.CHANGED)); + subsubfileFO.getOutputStream().close(); + assertEquals("Wrong number of events when file in sub sub folder modified.", 1, fcl.check(EventType.CHANGED)); + + // delete + file1FO.delete(); + assertEquals("Wrong number of events when child file deleted.", 1, fcl.check(EventType.DELETED)); + subsubfileFO.delete(); + assertEquals("Wrong number of events when child file in sub sub folder deleted.", 1, fcl.check(EventType.DELETED)); + subsubdirFO.delete(); + assertEquals("Wrong number of events when sub sub folder deleted.", 1, fcl.check(EventType.DELETED)); + subfileFO.delete(); + assertEquals("Wrong number of events when child file in sub folder deleted.", 1, fcl.check(EventType.DELETED)); + subdirFO.delete(); + assertEquals("Wrong number of events when sub folder deleted.", 1, fcl.check(EventType.DELETED)); + + // atomic action + FileUtil.runAtomicAction(new Runnable() { + + public void run() { + try { + FileObject rootFO = FileUtil.toFileObject(rootF); + rootFO.createFolder("fakedir"); // no events + rootFO.setAttribute("fake", "fake"); // no events + rootFO.createData("fakefile"); // no events + FileObject dirFO = FileUtil.toFileObject(dirF); + dirFO.createData("file1"); + FileObject subdirFO = dirFO.createFolder("subdir"); + subdirFO.createData("subfile"); + FileObject subsubdirFO = subdirFO.createFolder("subsubdir"); + subsubdirFO.createData("subsubfile"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + } + }); + // TODO - should be 3 + assertEquals("Wrong number of events fired when file was created in atomic action.", 1, fcl.check(EventType.DATA_CREATED)); + // TODO - should be 2 + assertEquals("Wrong number of events fired when file was created in atomic action.", 1, fcl.check(EventType.FOLDER_CREATED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // rename + file1FO = dirFO.getFileObject("file1"); + subdirFO = dirFO.getFileObject("subdir"); + subfileFO = subdirFO.getFileObject("subfile"); + subsubdirFO = subdirFO.getFileObject("subsubdir"); + subsubfileFO = subsubdirFO.getFileObject("subsubfile"); + fcl.clearAll(); + FileLock lock = file1FO.lock(); + file1FO.rename(lock, "file1Renamed", null); + lock.releaseLock(); + assertEquals("Wrong number of events when child file renamed.", 1, fcl.check(EventType.RENAMED)); + lock = subfileFO.lock(); + subfileFO.rename(lock, "subfileRenamed", null); + lock.releaseLock(); + assertEquals("Wrong number of events when child file in sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + lock = subsubfileFO.lock(); + subsubfileFO.rename(lock, "subsubfileRenamed", null); + lock.releaseLock(); + assertEquals("Wrong number of events when child file in sub sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + lock = subsubdirFO.lock(); + subsubdirFO.rename(lock, "subsubdirRenamed", null); + lock.releaseLock(); + assertEquals("Wrong number of events when sub sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + lock = subdirFO.lock(); + subdirFO.rename(lock, "subdirRenamed", null); + lock.releaseLock(); + assertEquals("Wrong number of events when sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + lock = dirFO.lock(); + dirFO.rename(lock, "dirRenamed", null); + lock.releaseLock(); + assertEquals("Wrong number of events when sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + lock = dirFO.lock(); + dirFO.rename(lock, "dir", null); + lock.releaseLock(); + /* According to jskrivanek in http://www.netbeans.org/nonav/issues/showattachment.cgi/86910/X.diff, the rename back does not need to + * fire an event. Instead the support delivers FOLDER_CREATED event: + assertEquals("Wrong number of events when sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + assertEquals("Wrong number of events when sub folder renamed.", 1, fcl.check(EventType.FOLDER_CREATED)); + fcl.printAll(); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + */ + // cleanup after rename + dirFO.getFileObject("file1Renamed").delete(); + dirFO.getFileObject("subdirRenamed").delete(); + fcl.clearAll(); + + // disk changes + Thread.sleep(1000); // give OS same time + assertTrue(subsubdirF.mkdirs()); + assertTrue(fileF.createNewFile()); + assertTrue(subfileF.createNewFile()); + assertTrue(subsubfileF.createNewFile()); + FileUtil.refreshAll(); + // TODO - should be 3 + assertEquals("Wrong number of events when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + // TODO - should be 2 + assertEquals("Wrong number of events when folder created.", 1, fcl.check(EventType.FOLDER_CREATED)); + // TODO - should be 0 + assertEquals("Wrong number of Attribute change events (see #129178).", 1, fcl.check(EventType.ATTRIBUTE_CHANGED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + Thread.sleep(1000); // make sure timestamp changes + new FileOutputStream(subsubfileF).close(); + new FileOutputStream(subfileF).close(); + new FileOutputStream(fileF).close(); + FileUtil.refreshAll(); + assertEquals("Wrong number of events when file was modified.", 3, fcl.check(EventType.CHANGED)); + assertEquals("Wrong number of Attribute change events (see #129178).", 7, fcl.check(EventType.ATTRIBUTE_CHANGED)); + + assertTrue(subsubfileF.delete()); + assertTrue(subsubdirF.delete()); + assertTrue(subfileF.delete()); + assertTrue(subdirF.delete()); + assertTrue(fileF.delete()); + FileUtil.refreshAll(); + assertEquals("Wrong number of events when file deleted.", 5, fcl.check(EventType.DELETED)); + + // delete folder itself + dirFO.delete(); + assertEquals("Wrong number of events when folder deleted.", 1, fcl.check(EventType.DELETED)); + } + + /** Tests recursive FileChangeListener on File. + * @see FileUtil#addRecursiveListener(org.openide.filesystems.FileChangeListener, java.io.File) + */ + public void testAddRecursiveListenerToFile() throws IOException, InterruptedException { + clearWorkDir(); + File rootF = getWorkDir(); + File dirF = new File(rootF, "dir"); + File fileF = new File(dirF, "file"); + + // adding listeners + TestFileChangeListener fcl = new TestFileChangeListener(); + FileUtil.addRecursiveListener(fcl, fileF); + try { + FileUtil.addRecursiveListener(fcl, fileF); + fail("Should not be possible to add listener for the same path."); + } catch (IllegalArgumentException iae) { + // ok + } + TestFileChangeListener fcl2 = new TestFileChangeListener(); + try { + FileUtil.removeRecursiveListener(fcl2, fileF); + fail("Should not be possible to remove listener which is not registered."); + } catch (IllegalArgumentException iae) { + // ok + } + FileUtil.addRecursiveListener(fcl2, fileF); + + // creation + final FileObject rootFO = FileUtil.toFileObject(rootF); + FileObject dirFO = rootFO.createFolder("dir"); + assertEquals("Event fired when just parent dir created.", 0, fcl.checkAll()); + FileObject fileFO = dirFO.createData("file"); + assertEquals("Wrong number of events when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + assertEquals("Wrong number of events when file was created.", 1, fcl2.check(EventType.DATA_CREATED)); + FileObject fileAFO = dirFO.createData("fileA"); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // remove listener + FileUtil.removeRecursiveListener(fcl2, fileF); + + // modification + fileFO.getOutputStream().close(); + fileFO.getOutputStream().close(); + assertEquals("Wrong number of events when file was modified.", 2, fcl.check(EventType.CHANGED)); + // no event fired when other file modified + fileAFO.getOutputStream().close(); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // deletion + fileFO.delete(); + assertEquals("Wrong number of events when file deleted.", 1, fcl.check(EventType.DELETED)); + dirFO.delete(); + assertEquals("Event fired when parent dir deleted and file already deleted.", 0, fcl.checkAll()); + dirFO = rootFO.createFolder("dir"); + fileFO = dirFO.createData("file"); + assertEquals("Wrong number of events when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + dirFO.delete(); + assertEquals("Wrong number of events when parent dir deleted.", 1, fcl.check(EventType.DELETED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // atomic action + FileUtil.runAtomicAction(new Runnable() { + + public void run() { + FileObject dirFO; + try { + dirFO = rootFO.createFolder("dir"); + rootFO.createFolder("fakedir"); + rootFO.setAttribute("fake", "fake"); + rootFO.createData("fakefile"); + dirFO.createData("file"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + } + }); + assertEquals("Wrong number of events fired when file was created in atomic action.", 1, fcl.check(EventType.DATA_CREATED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // rename + dirFO = FileUtil.toFileObject(dirF); + fileFO = FileUtil.toFileObject(fileF); + FileLock lock = dirFO.lock(); + dirFO.rename(lock, "dirRenamed", null); + lock.releaseLock(); + assertEquals("Event fired when parent dir renamed.", 0, fcl.checkAll()); + lock = fileFO.lock(); + fileFO.rename(lock, "fileRenamed", null); + lock.releaseLock(); + assertEquals("Renamed event not fired.", 1, fcl.check(EventType.RENAMED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // disk changes + dirF.mkdir(); + assertTrue(fileF.createNewFile()); + FileUtil.refreshAll(); + assertEquals("Wrong number of events when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + Thread.sleep(1000); // make sure timestamp changes + new FileOutputStream(fileF).close(); + FileUtil.refreshAll(); + assertEquals("Wrong number of events when file was modified.", 1, fcl.check(EventType.CHANGED)); + assertEquals("Attribute change event not fired (see #129178).", 2, fcl.check(EventType.ATTRIBUTE_CHANGED)); + fileF.delete(); + dirF.delete(); + FileUtil.refreshAll(); + assertEquals("Wrong number of events when file deleted.", 1, fcl.check(EventType.DELETED)); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + + // disk changes #66444 + for (int cntr = 0; cntr < 50; cntr++) { + dirF.mkdir(); + new FileOutputStream(fileF).close(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file was created; count=" + cntr, 1, fcl.check(EventType.DATA_CREATED)); + fileF.delete(); + dirF.delete(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file deleted; count=" + cntr, 1, fcl.check(EventType.DELETED)); + } + + // removed listener + assertEquals("No other events should be fired in removed listener.", 0, fcl2.checkAll()); + + // weakness + WeakReference ref = new WeakReference(fcl); + fcl = null; + assertGC("FileChangeListener not collected.", ref); + } + private static enum EventType { DATA_CREATED, FOLDER_CREATED, DELETED, CHANGED, RENAMED, ATTRIBUTE_CHANGED }; private static class TestFileChangeListener implements FileChangeListener { + boolean disabled; private final Map> type2Event = new HashMap>(); @@ -356,26 +807,32 @@ } public void fileFolderCreated(FileEvent fe) { + assertFalse("No changes expected", disabled); type2Event.get(EventType.FOLDER_CREATED).add(fe); } public void fileDataCreated(FileEvent fe) { + assertFalse("No changes expected", disabled); type2Event.get(EventType.DATA_CREATED).add(fe); } public void fileChanged(FileEvent fe) { + assertFalse("No changes expected", disabled); type2Event.get(EventType.CHANGED).add(fe); } public void fileDeleted(FileEvent fe) { + assertFalse("No changes expected", disabled); type2Event.get(EventType.DELETED).add(fe); } public void fileRenamed(FileRenameEvent fe) { + assertFalse("No changes expected", disabled); type2Event.get(EventType.RENAMED).add(fe); } public void fileAttributeChanged(FileAttributeEvent fe) { + assertFalse("No changes expected", disabled); type2Event.get(EventType.ATTRIBUTE_CHANGED).add(fe); } } --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ 70eda1b69061 Wed Sep 16 16:32:49 2009 +0200 @@ -0,0 +1,340 @@ +/* + * 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); + + + 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); + + Thread.sleep(1000); + + 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 sub; + File fobj; + File fsub; + { + FileObject obj = FileUtil.createData(testFolder, "my/sub/children/children.java"); + fobj = FileUtil.toFile(obj); + assertNotNull("File found", fobj); + sub = obj.getParent().getParent(); + fsub = FileUtil.toFile(sub); + + WeakReference ref = new WeakReference(obj); + obj = null; + assertGC("File object can disappear", ref); + } + + 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(fobj.getParentFile(), "sibling.java"); + fo.createNewFile(); + LOG.info("sibling created, now refresh"); + FileUtil.refreshAll(); + LOG.info("sibling refresh finished"); + + recursive.assertMessages("Creation", "DataCreated"); + 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", ""); + recursive.assertMessages("written", "Changed"); + + fo.delete(); + FileUtil.refreshAll(); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("gone", "Deleted"); + + new File(fsub, "testFolder").mkdirs(); + FileUtil.refreshAll(); + + flat.assertMessages("Direct Folder notified", "FolderCreated"); + recursive.assertMessages("Direct Folder notified", "FolderCreated"); + + new File(fsub.getParentFile(), "unimportant.txt").createNewFile(); + FileUtil.refreshAll(); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("No messages in recursive mode", ""); + + File deepest = new File(new File(new File(fsub, "deep"), "deeper"), "deepest"); + deepest.mkdirs(); + FileUtil.refreshAll(); + + flat.assertMessages("Folder in flat mode", "FolderCreated"); + recursive.assertMessages("Folder detected", "FolderCreated"); + + File hidden = new File(deepest, "hide.me"); + hidden.createNewFile(); + FileUtil.refreshAll(); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("Folder detected", "DataCreated"); + + + sub.removeRecursiveListener(recursive); + + new File(fsub, "test.data").createNewFile(); + FileUtil.refreshAll(); + + flat.assertMessages("Direct file notified", "DataCreated"); + recursive.assertMessages("No longer active", ""); + + WeakReference ref = new WeakReference(recursive); + recursive = null; + assertGC("Listener can be GCed", ref); + } + +} --- a/openide.filesystems/apichanges.xml Wed Sep 16 14:48:03 2009 +0200 +++ a/openide.filesystems/apichanges.xml Wed Sep 16 16:32:49 2009 +0200 @@ -46,6 +46,24 @@ Filesystems API + + + Support for recursive listeners + + + + + +

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

+
+ + + +
Non-initializing LayerBuilder.instanceFile added --- a/openide.filesystems/nbproject/project.properties Wed Sep 16 14:48:03 2009 +0200 +++ a/openide.filesystems/nbproject/project.properties Wed Sep 16 16:32:49 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.27.0 +spec.version.base=7.28.0 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ 70eda1b69061 Wed Sep 16 16:32:49 2009 +0200 @@ -0,0 +1,201 @@ +/* + * 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.io.File; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.openide.util.Utilities; +import org.openide.util.WeakSet; + +/** + * + * @author Jaroslav Tulach + */ +final class DeepListener extends WeakReference +implements FileChangeListener, Runnable { + private final File path; + private FileObject watching; + private boolean removed; + private static List keep = new ArrayList(); + + public DeepListener(FileChangeListener listener, File path) { + super(listener, Utilities.activeReferenceQueue()); + this.path = path; + relisten(); + keep.add(this); + } + + public void run() { + FileObject fo = FileUtil.toFileObject(path); + if (fo != null) { + fo.removeRecursiveListener(this); + } + removed = true; + keep.remove(this); + } + + private synchronized void relisten() { + FileObject fo = FileUtil.toFileObject(path); + if (fo == watching) { + return; + } + if (watching != null) { + watching.removeRecursiveListener(this); + watching = null; + } + if (fo != null) { + watching = fo; + fo.addRecursiveListener(this); + } + } + + public void fileRenamed(FileRenameEvent fe) { + fileRenamed(fe, false); + } + public void fileRenamed(FileRenameEvent fe, boolean fromHolder) { + relisten(); + FileChangeListener listener = get(fe, fromHolder); + if (listener == null) { + return; + } + listener.fileRenamed(fe); + } + + public void fileFolderCreated(FileEvent fe) { + relisten(); + fileFolderCreated(fe, false); + } + public void fileFolderCreated(FileEvent fe, boolean fromHolder) { + relisten(); + FileChangeListener listener = get(fe, fromHolder); + if (listener == null) { + return; + } + listener.fileFolderCreated(fe); + } + + public void fileDeleted(FileEvent fe) { + fileDeleted(fe, false); + } + public void fileDeleted(FileEvent fe, boolean fromHolder) { + relisten(); + FileChangeListener listener = get(fe, fromHolder); + if (listener == null) { + return; + } + listener.fileDeleted(fe); + } + + public void fileDataCreated(FileEvent fe) { + fileDataCreated(fe, false); + } + public void fileDataCreated(FileEvent fe, boolean fromHolder) { + relisten(); + FileChangeListener listener = get(fe, fromHolder); + if (listener == null) { + return; + } + listener.fileDataCreated(fe); + } + + public void fileChanged(FileEvent fe) { + fileChanged(fe, false); + } + public void fileChanged(FileEvent fe, boolean fromHolder) { + FileChangeListener listener = get(fe, fromHolder); + if (listener == null) { + return; + } + listener.fileChanged(fe); + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + FileChangeListener listener = get(fe, false); + if (listener == null) { + return; + } + listener.fileAttributeChanged(fe); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DeepListener other = (DeepListener) obj; + FileChangeListener thisListener = get(); + FileChangeListener otherListener = other.get(); + if (thisListener != otherListener && (thisListener == null || !thisListener.equals(otherListener))) { + return false; + } + return true; + } + + @Override + public int hashCode() { + FileChangeListener thisListener = get(); + int hash = 7; + hash = 11 * hash + (thisListener != null ? thisListener.hashCode() : 0); + return hash; + } + + private Set delivered = new WeakSet(); + private FileChangeListener get(FileEvent fe, boolean fromHolder) { + if (removed) { + return null; + } + if (fromHolder) { + if (fe.getFile() != fe.getSource()) { + return null; + } + } + if (!delivered.add(fe)) { + return null; + } + return get(); + } + +} --- a/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java Wed Sep 16 14:48:03 2009 +0200 +++ a/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java Wed Sep 16 16:32:49 2009 +0200 @@ -90,7 +90,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 Wed Sep 16 14:48:03 2009 +0200 +++ a/openide.filesystems/src/org/openide/filesystems/FileObject.java Wed Sep 16 16:32:49 2009 +0200 @@ -58,6 +58,7 @@ 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.NbBundle; import org.openide.util.UserQuestionException; @@ -413,6 +414,62 @@ */ 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.28 + */ + public void addRecursiveListener(FileChangeListener fcl) { + if (!isFolder()) { + addFileChangeListener(fcl); + return; + } + try { + boolean allowsExternalChanges = getFileSystem() instanceof LocalFileSystem; + getFileSystem().addFileChangeListener(new RecursiveListener(this, fcl, allowsExternalChanges)); + } 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.28 + */ + public void removeRecursiveListener(FileChangeListener fcl) { + if (!isFolder()) { + removeFileChangeListener(fcl); + return; + } + try { + getFileSystem().removeFileChangeListener(new RecursiveListener(this, fcl, false)); + } 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 --- a/openide.filesystems/src/org/openide/filesystems/FileUtil.java Wed Sep 16 14:48:03 2009 +0200 +++ a/openide.filesystems/src/org/openide/filesystems/FileUtil.java Wed Sep 16 16:32:49 2009 +0200 @@ -257,6 +257,10 @@ * @since org.openide.filesystems 7.20 */ public static void removeFileChangeListener(FileChangeListener listener, File path) { + removeFileChangeListenerImpl(listener, path); + } + + private static FileChangeListener removeFileChangeListenerImpl(FileChangeListener listener, File path) { assert path.equals(FileUtil.normalizeFile(path)) : "Need to normalize " + path + "!"; //NOI18N synchronized (holders) { Map f2H = holders.get(listener); @@ -267,9 +271,58 @@ throw new IllegalArgumentException(listener + " was not listening to " + path + "; only to " + f2H.keySet()); // NOI18N } // remove Holder instance from map and call run to unregister its current listener - f2H.remove(path).run(); + Holder h = f2H.remove(path); + h.run(); + return h.get(); } } + /** + * Adds a listener to changes under given path. It permits you to listen to a file + * which does not yet exist, or continue listening to it after it is deleted and recreated, etc. + *
+ * When given path represents a file ({@code path.isDirectory() == false}), this + * code behaves exectly like {@link #addFileChangeListener(org.openide.filesystems.FileChangeListener, java.io.File)}. + * Usually the path shall represent a folder ({@code path.isDirectory() == true}) + *
    + *
  • fileFolderCreated event is fired when the folder is created or a child folder created
  • + *
  • fileDataCreated event is fired when a child file is created
  • + *
  • fileDeleted event is fired when the folder is deleted or a child file/folder removed
  • + *
  • fileChanged event is fired when a child file is modified
  • + *
  • fileRenamed event is fired when the folder is renamed or a child file/folder is renamed
  • + *
  • fileAttributeChanged is fired when FileObject's attribute is changed
  • + *
+ * The above events are delivered for changes in all subdirectories (recursively). + * It is guaranteed that with each change at least one event is generated. + * For example adding a folder does not notify about content of the folder, + * hence one event is delivered. + * + * Can only add a given [listener, path] pair once. However a listener can + * listen to any number of paths. Note that listeners are always held weakly + * - if the listener is collected, it is quietly removed. + * + * @param listener FileChangeListener to listen to changes in path + * @param path File path to listen to (even not existing) + * + * @see FileObject#addRecursiveListener + * @since org.openide.filesystems 7.28 + */ + public static void addRecursiveListener(FileChangeListener listener, File path) { + addFileChangeListener(new DeepListener(listener, path), path); + } + + /** + * Removes a listener to changes under given path. + * @param listener FileChangeListener to be removed + * @param path File path in which listener was listening + * @throws IllegalArgumentException if listener was not listening to given path + * + * @see FileObject#removeRecursiveListener + * @since org.openide.filesystems 7.28 + */ + public static void removeRecursiveListener(FileChangeListener listener, File path) { + DeepListener dl = (DeepListener)removeFileChangeListenerImpl(new DeepListener(listener, path), path); + dl.run(); + } /** Holds FileChangeListener and File pair and handle movement of auxiliary * FileChangeListener to the first existing upper folder and firing appropriate events. @@ -346,7 +399,9 @@ if (fe.getSource() == current) { if (isOnTarget) { FileChangeListener listener = get(); - if (listener != null) { + if (listener instanceof DeepListener) { + ((DeepListener)listener).fileChanged(fe, true); + } else if (listener != null) { listener.fileChanged(fe); } } else { @@ -359,7 +414,9 @@ if (fe.getSource() == current) { if (isOnTarget) { FileChangeListener listener = get(); - if (listener != null) { + if (listener instanceof DeepListener) { + ((DeepListener)listener).fileDeleted(fe, true); + } else if (listener != null) { listener.fileDeleted(fe); } } @@ -371,7 +428,9 @@ if (fe.getSource() == current) { if (isOnTarget) { FileChangeListener listener = get(); - if (listener != null) { + if (listener instanceof DeepListener) { + ((DeepListener)listener).fileDataCreated(fe, true); + } else if (listener != null) { listener.fileDataCreated(fe); } } else { @@ -384,7 +443,9 @@ if (fe.getSource() == current) { if (isOnTarget) { FileChangeListener listener = get(); - if (listener != null) { + if (listener instanceof DeepListener) { + ((DeepListener)listener).fileFolderCreated(fe, true); + } else if (listener != null) { listener.fileFolderCreated(fe); } } else { @@ -397,7 +458,9 @@ if (fe.getSource() == current) { if (isOnTarget) { FileChangeListener listener = get(); - if (listener != null) { + if (listener instanceof DeepListener) { + ((DeepListener)listener).fileRenamed(fe, true); + } else if (listener != null) { listener.fileRenamed(fe); } } --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ 70eda1b69061 Wed Sep 16 16:32:49 2009 +0200 @@ -0,0 +1,154 @@ +/* + * 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; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +/** + * + * @author Jaroslav Tulach + */ +final class RecursiveListener extends WeakReference +implements FileChangeListener { + private final FileChangeListener fcl; + private final Set kept; + + public RecursiveListener(FileObject source, FileChangeListener fcl, boolean keep) { + super(source); + this.fcl = fcl; + this.kept = keep ? new HashSet() : null; + addAll(source); + } + + private void addAll(FileObject folder) { + if (kept != null) { + kept.add(folder); + Enumeration en = folder.getChildren(true); + while (en.hasMoreElements()) { + FileObject fo = en.nextElement(); + kept.add(fo); + } + } + } + + 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(); + final FileObject file = fe.getFile(); + if (thisFo != null && FileUtil.isParentOf(thisFo, file)) { + fcl.fileFolderCreated(fe); + addAll(file); + } + } + + public void fileDeleted(FileEvent fe) { + FileObject thisFo = this.get(); + final FileObject file = fe.getFile(); + if (thisFo != null && FileUtil.isParentOf(thisFo, file)) { + fcl.fileDeleted(fe); + if (kept != null) { + kept.remove(file); + } + } + } + + public void fileDataCreated(FileEvent fe) { + FileObject thisFo = this.get(); + final FileObject file = fe.getFile(); + if (thisFo != null && FileUtil.isParentOf(thisFo, file)) { + fcl.fileDataCreated(fe); + if (kept != null) { + kept.add(file); + } + } + } + + 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); + } + } + + @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; + } +} --- a/openide.filesystems/test/unit/src/org/openide/filesystems/FileObjectTestHid.java Wed Sep 16 14:48:03 2009 +0200 +++ a/openide.filesystems/test/unit/src/org/openide/filesystems/FileObjectTestHid.java Wed Sep 16 16:32:49 2009 +0200 @@ -1614,6 +1614,109 @@ 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"); + + FileObject subdir = sub.createFolder("testFolder"); + + flat.assertMessages("Direct Folder notified", "FolderCreated"); + recursive.assertMessages("Direct Folder notified", "FolderCreated"); + + subdir.createData("subchild.txt"); + + recursive.assertMessages("SubFolder's change notified", "DataCreated"); + flat.assertMessages("SubFolder's change not important", ""); + + 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 { --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ 70eda1b69061 Wed Sep 16 16:32:49 2009 +0200 @@ -0,0 +1,338 @@ +/* + * 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.openide.filesystems; + +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; + +public class LocalFileSystemExternalTouchTest extends NbTestCase { + private Logger LOG; + private FileObject testFolder; + private LocalFileSystem lfs; + + public LocalFileSystemExternalTouchTest(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(); + + lfs = new LocalFileSystem(); + lfs.setRootDirectory(dir); + + testFolder = lfs.getRoot(); + 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); + + + 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); + + Thread.sleep(1000); + + 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, 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 sub; + File fobj; + File fsub; + { + FileObject obj = FileUtil.createData(testFolder, "my/sub/children/children.java"); + fobj = FileUtil.toFile(obj); + assertNotNull("File found", fobj); + sub = obj.getParent().getParent(); + fsub = FileUtil.toFile(sub); + + WeakReference ref = new WeakReference(obj); + obj = null; + assertGC("File object can disappear", ref); + } + + 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(fobj.getParentFile(), "sibling.java"); + fo.createNewFile(); + LOG.info("sibling created, now refresh"); + lfs.refresh(true); + LOG.info("sibling refresh finished"); + + recursive.assertMessages("Creation", "DataCreated"); + 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"); + lfs.refresh(true); + LOG.info("After refresh"); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("written", "Changed"); + + fo.delete(); + lfs.refresh(true); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("gone", "Deleted"); + + new File(fsub, "testFolder").mkdirs(); + lfs.refresh(true); + + flat.assertMessages("Direct Folder notified", "FolderCreated"); + recursive.assertMessages("Direct Folder notified", "FolderCreated"); + + new File(fsub.getParentFile(), "unimportant.txt").createNewFile(); + lfs.refresh(true); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("No messages in recursive mode", ""); + + File deepest = new File(new File(new File(fsub, "deep"), "deeper"), "deepest"); + deepest.mkdirs(); + lfs.refresh(true); + + flat.assertMessages("Folder in flat mode", "FolderCreated"); + recursive.assertMessages("Folder detected", "FolderCreated"); + + File hidden = new File(deepest, "hide.me"); + hidden.createNewFile(); + lfs.refresh(true); + + flat.assertMessages("No messages in flat mode", ""); + recursive.assertMessages("Folder detected", "DataCreated"); + + + sub.removeRecursiveListener(recursive); + + new File(fsub, "test.data").createNewFile(); + lfs.refresh(true); + + flat.assertMessages("Direct file notified", "DataCreated"); + recursive.assertMessages("No longer active", ""); + + WeakReference ref = new WeakReference(recursive); + recursive = null; + assertGC("Listener can be GCed", ref); + } + +}