diff --git a/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java b/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java --- a/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java +++ b/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/FileUtilTest.java @@ -44,23 +44,44 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +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.NbTestSuite; import org.netbeans.junit.RandomlyFails; +import org.openide.filesystems.FileAttributeEvent; +import org.openide.filesystems.FileChangeListener; +import org.openide.filesystems.FileEvent; +import org.openide.filesystems.FileLock; import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileRenameEvent; import org.openide.filesystems.FileUtil; /** * @author Jiri Skrivanek */ -public class FileUtilTest extends NbTestCase { - +public class FileUtilTest extends NbTestCase { + public FileUtilTest(String name) { super(name); } - + + public static Test suite() { + NbTestSuite suite = new NbTestSuite(); + suite.addTest(new FileUtilTest("testCopy136308")); + suite.addTest(new FileUtilTest("testAddFileChangeListener")); + suite.addTest(new FileUtilTest("testAddFileChangeListenerFolder")); + return suite; + } + /** Test performance of FileUtil.copy(FileObject.getInputStream(), FileObject.getOutputStream()) * against FileUtil.copy(FileInputStream, FileOutputStream). It should be the same. */ @@ -95,6 +116,257 @@ is.close(); os.close(); long timeFileStreams = end - start + 1; - assertTrue("Time of FileUtil.copy(FileObject.getInputStream(), FileObject.getOutputStream()) "+timeDefault+" should not be extremly bigger than time of FileUtil.copy(FileInputStream, FileOutputStream) "+timeFileStreams+".", timeFileStreams*100 > timeDefault); + assertTrue("Time of FileUtil.copy(FileObject.getInputStream(), FileObject.getOutputStream()) " + timeDefault + " should not be extremly bigger than time of FileUtil.copy(FileInputStream, FileOutputStream) " + timeFileStreams + ".", timeFileStreams * 100 > timeDefault); + } + + /** Tests FileChangeListener on File. + * @see FileUtil#addFileChangeListener(org.openide.filesystems.FileChangeListener, java.io.File) + */ + public void testAddFileChangeListener() 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.addFileChangeListener(fcl, fileF); + try { + FileUtil.addFileChangeListener(fcl, fileF); + fail("Should not be possible to add listener for the same path."); + } catch (IllegalArgumentException iae) { + // ok + } + TestFileChangeListener fcl2 = new TestFileChangeListener(); + try { + FileUtil.removeFileChangeListener(fcl2, fileF); + fail("Should not be possible to remove listener which is not registered."); + } catch (IllegalArgumentException iae) { + // ok + } + FileUtil.addFileChangeListener(fcl2, fileF); + + // creation + 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("Event not fired when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + assertEquals("Event not fired 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.removeFileChangeListener(fcl2, fileF); + + // 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 fired when parent dir deleted and file already deleted.", 0, fcl.checkAll()); + dirFO = rootFO.createFolder("dir"); + fileFO = dirFO.createData("file"); + assertEquals("Event not fired when file was created.", 1, fcl.check(EventType.DATA_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()); + + // rename + dirFO = rootFO.createFolder("dir"); + fileFO = dirFO.createData("file"); + assertEquals("Event not fired when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + 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("Event not fired when file was created.", 1, fcl.check(EventType.DATA_CREATED)); + Thread.sleep(1000); // make sure timestamp changes + new FileOutputStream(fileF).close(); + FileUtil.refreshAll(); + assertEquals("Event not fired when file was modified.", 1, fcl.check(EventType.CHANGED)); + fileF.delete(); + dirF.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 + 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); + } + + /** 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 + * - 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 + */ + public void testAddFileChangeListenerFolder() throws IOException { + clearWorkDir(); + // test files: dir/file1, dir/subdir/subfile + File rootF = getWorkDir(); + File dirF = new File(rootF, "dir"); + TestFileChangeListener fcl = new TestFileChangeListener(); + FileUtil.addFileChangeListener(fcl, dirF); + // create dir + FileObject dirFO = FileUtil.createFolder(dirF); + assertEquals("Event not fired when folder created.", 1, fcl.check(EventType.FOLDER_CREATED)); + FileObject subdirFO = dirFO.createFolder("subdir"); + assertEquals("Event not fired when sub folder created.", 1, fcl.check(EventType.FOLDER_CREATED)); + + // create file + FileObject file1FO = dirFO.createData("file1"); + assertEquals("Event not fired when data created.", 1, fcl.check(EventType.DATA_CREATED)); + FileObject subfileFO = subdirFO.createData("subfile"); + assertEquals("Event fired when data in sub folder created.", 0, fcl.checkAll()); + + // modify + file1FO.getOutputStream().close(); + assertEquals("fileChanged event not fired.", 1, fcl.check(EventType.CHANGED)); + subfileFO.getOutputStream().close(); + assertEquals("Event fired when file sub folder modified.", 0, fcl.checkAll()); + + // delete + file1FO.delete(); + assertEquals("Event not fired when child file deleted.", 1, fcl.check(EventType.DELETED)); + subfileFO.delete(); + assertEquals("Event fired when child file in sub folder deleted.", 0, fcl.checkAll()); + subdirFO.delete(); + assertEquals("Event not fired when sub folder deleted.", 1, fcl.check(EventType.DELETED)); + dirFO.delete(); + assertEquals("Event not fired when folder deleted.", 1, fcl.check(EventType.DELETED)); + + // rename + dirFO = FileUtil.createFolder(dirF); + file1FO = dirFO.createData("file1"); + subdirFO = dirFO.createFolder("subdir"); + subfileFO = subdirFO.createData("subfile"); + fcl.checkAll(); + FileLock lock = file1FO.lock(); + file1FO.rename(lock, "file1Renamed", null); + lock.releaseLock(); + assertEquals("Event not fired when child file renamed.", 1, fcl.check(EventType.RENAMED)); + lock = subfileFO.lock(); + subfileFO.rename(lock, "subfileRenamed", null); + lock.releaseLock(); + assertEquals("Event fired when child file in sub folder renamed.", 0, fcl.check(EventType.RENAMED)); + lock = subdirFO.lock(); + subdirFO.rename(lock, "subdirRenamed", null); + lock.releaseLock(); + assertEquals("Event not fired when sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + lock = dirFO.lock(); + dirFO.rename(lock, "dirRenamed", null); + lock.releaseLock(); + assertEquals("Event not fired when sub folder renamed.", 1, fcl.check(EventType.RENAMED)); + fcl.printAll(); + assertEquals("No other events should be fired.", 0, fcl.checkAll()); + } + + private static enum EventType { + + DATA_CREATED, FOLDER_CREATED, DELETED, CHANGED, RENAMED, ATTRIBUTE_CHANGED + }; + + private static class TestFileChangeListener implements FileChangeListener { + + private final Map> type2Event = new HashMap>(); + + public TestFileChangeListener() { + super(); + for (EventType eventType : EventType.values()) { + type2Event.put(eventType, new ArrayList()); + } + } + + public void clearAll() { + for (EventType type : EventType.values()) { + type2Event.get(type).clear(); + } + } + + /** Returns number of events and clears counter. */ + public int check(EventType type) { + int size = type2Event.get(type).size(); + type2Event.get(type).clear(); + return size; + } + + public int checkAll() { + int sum = 0; + for (EventType type : EventType.values()) { + sum += type2Event.get(type).size(); + type2Event.get(type).clear(); + } + return sum; + } + + public void printAll() { + for (EventType type : EventType.values()) { + for (FileEvent fe : type2Event.get(type)) { + System.out.println(type + "=" + fe); + } + } + } + + public void fileFolderCreated(FileEvent fe) { + type2Event.get(EventType.FOLDER_CREATED).add(fe); + } + + public void fileDataCreated(FileEvent fe) { + type2Event.get(EventType.DATA_CREATED).add(fe); + } + + public void fileChanged(FileEvent fe) { + type2Event.get(EventType.CHANGED).add(fe); + } + + public void fileDeleted(FileEvent fe) { + type2Event.get(EventType.DELETED).add(fe); + } + + public void fileRenamed(FileRenameEvent fe) { + type2Event.get(EventType.RENAMED).add(fe); + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + type2Event.get(EventType.ATTRIBUTE_CHANGED).add(fe); + } } } diff --git a/openide.filesystems/apichanges.xml b/openide.filesystems/apichanges.xml --- a/openide.filesystems/apichanges.xml +++ b/openide.filesystems/apichanges.xml @@ -46,6 +46,26 @@ Filesystems API + + + Possibility to add FileChangeListeners on File (even not existing) + + + + + +

+ Added + FileUtil.addFileChangeListener(FileChangeListener listener, File path) + and + FileUtil.addFileChangeListener(FileChangeListener listener, File 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. +

+
+ + +
Persisted registration of file extension to MIME type diff --git a/openide.filesystems/nbproject/project.properties b/openide.filesystems/nbproject/project.properties --- a/openide.filesystems/nbproject/project.properties +++ b/openide.filesystems/nbproject/project.properties @@ -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.18.0 +spec.version.base=7.19.0 diff --git a/openide.filesystems/src/org/openide/filesystems/FileUtil.java b/openide.filesystems/src/org/openide/filesystems/FileUtil.java --- a/openide.filesystems/src/org/openide/filesystems/FileUtil.java +++ b/openide.filesystems/src/org/openide/filesystems/FileUtil.java @@ -48,6 +48,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.SyncFailedException; +import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; @@ -86,6 +87,9 @@ */ public final class FileUtil extends Object { + /** Contains mapping of FileChangeListener to File. */ + private static final Map> holders = new WeakHashMap>(); + private static final Logger LOG = Logger.getLogger(FileUtil.class.getName()); /** Normal header for ZIP files. */ @@ -190,8 +194,222 @@ fs.removeFileChangeListener(fcl); } } - - + + /** + * Adds a listener to changes in a 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}) + *
    + *
  • fileDataCreated event is fired when the file is created
  • + *
  • fileDeleted event is fired when the file is deleted
  • + *
  • fileChanged event is fired when the file is modified
  • + *
  • fileRenamed event is fired when the file is renamed
  • + *
  • fileAttributeChanged is fired when FileObject's attribute is changed
  • + *
+ * When given path represents 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
  • + *
+ * 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#addFileChangeListener + * @since org.openide.filesystems 7.19 + */ + public static void addFileChangeListener(FileChangeListener listener, File path) { + assert path.equals(FileUtil.normalizeFile(path)) : "Need to normalize " + path + "!"; //NOI18N + synchronized (holders) { + Map f2H = holders.get(listener); + if (f2H == null) { + f2H = new HashMap(); + holders.put(listener, f2H); + } + if (f2H.containsKey(path)) { + throw new IllegalArgumentException("Already listening to " + path); // NOI18N + } + f2H.put(path, new Holder(listener, path)); + } + } + + /** + * Removes a listener to changes in a 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.19 + */ + public static void removeFileChangeListener(FileChangeListener listener, File path) { + assert path.equals(FileUtil.normalizeFile(path)) : "Need to normalize " + path + "!"; //NOI18N + synchronized (holders) { + Map f2H = holders.get(listener); + if (f2H == null) { + throw new IllegalArgumentException("Was not listening to " + path); // NOI18N + } + if (!f2H.containsKey(path)) { + 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(); + } + } + + /** Holds FileChangeListener and File pair and handle movement of auxiliary + * FileChangeListener to the first existing upper folder and firing appropriate events. + */ + private static final class Holder extends WeakReference implements FileChangeListener, Runnable { + + private final File path; + private FileObject current; + private File currentF; + /** Whether listener is seeded on target path. */ + private boolean isOnTarget = false; + + public Holder(FileChangeListener listener, File path) { + super(listener, Utilities.activeReferenceQueue()); + assert path != null; + this.path = path; + locateCurrent(); + } + + private void locateCurrent() { + FileObject oldCurrent = current; + currentF = path; + while (true) { + try { + current = FileUtil.toFileObject(currentF); + } catch (IllegalArgumentException x) { + // #73526: was originally normalized, but now is not. E.g. file changed case. + currentF = FileUtil.normalizeFile(currentF); + current = FileUtil.toFileObject(currentF); + } + if (current != null) { + isOnTarget = path.equals(currentF); + break; + } + currentF = currentF.getParentFile(); + if (currentF == null) { + // #47320: can happen on Windows in case the drive does not exist. + // (Inside constructor for Holder.) In that case skip it. + return; + } + } + assert current != null; + if (current != oldCurrent) { + if (oldCurrent != null) { + oldCurrent.removeFileChangeListener(this); + } + current.addFileChangeListener(this); + current.getChildren();//to get events about children + } + } + + private void someChange() { + FileChangeListener listener; + boolean wasOnTarget; + synchronized (this) { + if (current == null) { + return; + } + listener = get(); + if (listener == null) { + return; + } + wasOnTarget = isOnTarget; + locateCurrent(); + } + if (isOnTarget && !wasOnTarget) { + // fire events about itself creation (it is difference from FCL + // on FileOject - it cannot be fired because we attach FCL on already existing FileOject + if (current.isFolder()) { + listener.fileFolderCreated(new FileEvent(current)); + } else { + listener.fileDataCreated(new FileEvent(current)); + } + } + } + + public void fileChanged(FileEvent fe) { + if (isOnTarget) { + FileChangeListener listener = get(); + if (listener != null) { + listener.fileChanged(fe); + } + } else { + someChange(); + } + } + + public void fileDeleted(FileEvent fe) { + if (isOnTarget) { + FileChangeListener listener = get(); + if (listener != null) { + listener.fileDeleted(fe); + } + } + someChange(); + } + + public void fileDataCreated(FileEvent fe) { + if (isOnTarget) { + FileChangeListener listener = get(); + if (listener != null) { + listener.fileDataCreated(fe); + } + } else { + someChange(); + } + } + + public void fileFolderCreated(FileEvent fe) { + if (isOnTarget) { + FileChangeListener listener = get(); + if (listener != null) { + listener.fileFolderCreated(fe); + } + } else { + someChange(); + } + } + + public void fileRenamed(FileRenameEvent fe) { + if (isOnTarget) { + FileChangeListener listener = get(); + if (listener != null) { + listener.fileRenamed(fe); + } + } + someChange(); + } + + public void fileAttributeChanged(FileAttributeEvent fe) { + if (isOnTarget) { + FileChangeListener listener = get(); + if (listener != null) { + listener.fileAttributeChanged(fe); + } + } + } + + public synchronized void run() { + if (current != null) { + current.removeFileChangeListener(this); + current = null; + } + } + } + /** * Executes atomic action. For more info see {@link FileSystem#runAtomicAction}. *