# HG changeset patch # User Jaroslav Tulach # Date 1251362052 -7200 # Node ID 23c5b76b43e3c02a869f4437456b1ce29efa5728 # Parent 32b3d5254468b2314004d97d9c2e253c693aaef3 #170862: VS2 and JG02: Implementing FileUtil.addRecursiveListener(...) diff -r 32b3d5254468 -r 23c5b76b43e3 masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java --- a/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java Thu Aug 27 10:32:28 2009 +0200 +++ b/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java Thu Aug 27 10:34:12 2009 +0200 @@ -239,6 +239,142 @@ 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.", 3, fcl.check(EventType.FOLDER_CREATED)); + assertEquals("Event not fired when file was created.", 3, 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.", 2, 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.", 3, fcl.check(EventType.FOLDER_CREATED)); + dirFO.delete(); + assertEquals("Event not fired when parent dir deleted.", 2, 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.", 2, 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.", 2, fcl.check(EventType.CHANGED)); + assertEquals("Attribute change event not fired (see #129178).", 2, 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(fileF, "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, 2, fcl.check(EventType.DATA_CREATED)); + fileX.delete(); + fileX.getParentFile().delete(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file deleted; count=" + cntr, 2, 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 @@ -315,6 +451,7 @@ }; private static class TestFileChangeListener implements FileChangeListener { + boolean disabled; private final Map> type2Event = new HashMap>(); @@ -356,26 +493,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); } } diff -r 32b3d5254468 -r 23c5b76b43e3 openide.filesystems/src/org/openide/filesystems/DeepListener.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openide.filesystems/src/org/openide/filesystems/DeepListener.java Thu Aug 27 10:34:12 2009 +0200 @@ -0,0 +1,166 @@ +/* + * 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 org.openide.util.Utilities; + +/** + * + * @author Jaroslav Tulach + */ +final class DeepListener extends WeakReference +implements FileChangeListener, Runnable { + private final File path; + private FileObject watching; + 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); + } + 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) { + relisten(); + FileChangeListener listener = get(); + if (listener == null) { + return; + } + listener.fileRenamed(fe); + } + + public void fileFolderCreated(FileEvent fe) { + relisten(); + FileChangeListener listener = get(); + if (listener == null) { + return; + } + listener.fileFolderCreated(fe); + } + + public void fileDeleted(FileEvent fe) { + relisten(); + FileChangeListener listener = get(); + if (listener == null) { + return; + } + listener.fileDeleted(fe); + } + + public void fileDataCreated(FileEvent fe) { + relisten(); + FileChangeListener listener = get(); + if (listener == null) { + return; + } + listener.fileDataCreated(fe); + } + + public void fileChanged(FileEvent fe) { + FileChangeListener listener = get(); + if (listener == null) { + return; + } + listener.fileChanged(fe); + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + FileChangeListener listener = get(); + 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; + } + + +} diff -r 32b3d5254468 -r 23c5b76b43e3 openide.filesystems/src/org/openide/filesystems/FileUtil.java --- a/openide.filesystems/src/org/openide/filesystems/FileUtil.java Thu Aug 27 10:32:28 2009 +0200 +++ b/openide.filesystems/src/org/openide/filesystems/FileUtil.java Thu Aug 27 10:34:12 2009 +0200 @@ -255,6 +255,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); @@ -265,9 +269,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.24 + */ + 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#removeFileChangeListener + * @since org.openide.filesystems 7.24 + */ + 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.