/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.loaders; import java.util.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import javax.swing.event.ChangeListener; import org.openide.ErrorManager; import org.openide.filesystems.*; import org.openide.util.RequestProcessor; import java.lang.ref.*; import org.openide.util.WeakSet; import org.openide.util.Lookup; /** Registraction list of all data objects in the system. * Maps data objects to its handlers. * * @author Jaroslav Tulach */ final class DataObjectPool extends Object implements ChangeListener, RepositoryListener, PropertyChangeListener, Runnable { /** validator */ private static final Validator VALIDATOR = new Validator (); /** hashtable that maps FileObject to DataObjectPool.Item */ private HashMap map = new HashMap (); /** Set<FileSystem> covering all FileSystems we're listening on */ private WeakSet knownFileSystems = new WeakSet(); /** the pool for all objects. Use getPOOL method instead of direct referencing * this field. */ private static DataObjectPool POOL; /** Lock for creating POOL instance */ private static Object lockPOOL = new Object(); /** Get the instance of DataObjectPool - value of static field 'POOL'. * Initialize the field if necessary. * * @return The DataObjectPool. */ static DataObjectPool getPOOL() { synchronized (lockPOOL) { if (POOL != null) return POOL; POOL = new DataObjectPool (); } ((DataLoaderPool)Lookup.getDefault ().lookup (DataLoaderPool.class)).addChangeListener (POOL); Repository.getDefault().addRepositoryListener (POOL); return POOL; } /** Collection of all objects that has been created but their * creation has not been yet notified to OperationListener.postCreate * method. * * Set<Item> */ private HashSet toNotify = new HashSet(); /** A special hack to work around code like: * <pre> * MyDataObject (FileObject fo) { // constructor of a data object * super (fo); * * DataObject.find (fo); * </pre> * which is very common (MultiDataObject.secondaryEntries ()) and which * waits SAFE_NOTIFY_DELAY in waitNotified method. * * * <P> * This variable holds the reference to DataObject that was created by each * thread, so if the same thread calls back, it will not wait in waitNotified * method. * <P> * Contais object of value Item */ private ThreadLocal last = new ThreadLocal (); /** Time when the toNotify set has been modified. */ private long toNotifyModified; /** A delay to check the notify modified content. It is expected that * in 500ms each constructor can finish, so 500ms after the registration * of object in a toNotify map, it should be ready and initialized. */ private static final int SAFE_NOTIFY_DELAY = 500; private static final Integer ONE = new Integer(1); /** A task to check toNotify content and notify that objects were created. */ private RequestProcessor.Task task; /** Constructor. */ private DataObjectPool () { task = RequestProcessor.createRequest (this); task.setPriority (Thread.MIN_PRIORITY); } /** Checks whether there is a data object with primary file * passed thru the parameter. * * @param fo the file to check * @return data object with fo as primary file or null */ public DataObject find (FileObject fo) { synchronized (this) { Item doh = (Item)map.get (fo); if (doh == null) { return null; } // do not return DOs before their creation were notified to OperationListeners if (toNotify.contains (doh)) { // special test for data objects calling this method from // their own constructor, those are ok to be returned if // they exist if (last.get () != doh) { return null; } } return doh.getDataObjectOrNull (); } } /** mapping of files to registration count */ private final Map registrationCounts = new WeakHashMap(); // Map<FileObject,int> void countRegistration(FileObject fo) { Integer i = (Integer)registrationCounts.get(fo); Integer i2; if (i == null) { i2 = ONE; } else { i2 = new Integer(i.intValue() + 1); } registrationCounts.put(fo, i2); } /** For use from FolderChildren. @see "#20699" */ int registrationCount(FileObject fo) { Integer i = (Integer)registrationCounts.get(fo); if (i == null) { return 0; } else { return i.intValue(); } } /** Refresh of all folders. */ private void refreshAllFolders () { Set files; synchronized (this) { files = new HashSet (map.keySet ()); } Iterator it = files.iterator (); while (it.hasNext ()) { FileObject fo = (FileObject)it.next (); if (fo.isFolder ()) { DataObject obj = find (fo); if (obj instanceof DataFolder) { DataFolder df = (DataFolder)obj; FileObject file = df.getPrimaryFile (); synchronized (this) { if (toNotify.isEmpty() || !toNotify.contains((Item)map.get(file))) { FolderList.changedDataSystem (file); } } } } } } /** Rescans all fileobjects in given set. * @param s mutable set of FileObjects * @return set of DataObjects that refused to be revalidated */ public Set revalidate (Set s) { return VALIDATOR.revalidate (s); } /** Rescan all primary files of currently existing data * objects. * * @return set of DataObjects that refused to be revalidated */ public Set revalidate () { Set files; synchronized (this) { files = createSetOfAllFiles (map.values ()); } return revalidate (files); } /** Notifies that an object has been created. * @param obj the object that was created */ public void notifyCreation (DataObject obj) { synchronized (this) { if (toNotify.isEmpty()) { return; } if (!toNotify.remove (obj.item)) { return; } if (toNotify.isEmpty ()) { // ok, we do not need the task task.cancel (); } // if somebody is caught in waitNotified then wake him up notifyAll (); } DataLoaderPool pool = (DataLoaderPool)Lookup.getDefault().lookup(DataLoaderPool.class); pool.fireOperationEvent ( new OperationEvent (obj), OperationEvent.CREATE ); } /** Wait till the data object will be notified. But wait limited amount * of time so we will not deadlock * * @param obj data object to check */ public void waitNotified (DataObject obj) { try { synchronized (this) { if (toNotify.isEmpty()) { return; } if (obj.item == last.get ()) { return; } if (!toNotify.contains (obj.item)) { return; } wait (SAFE_NOTIFY_DELAY); } } catch (InterruptedException ex) { } } /** Invoked to periodicaly check whether some data objects are not notified * to be created. In such case it notifies about their creation. */ public void run () { Item arr []; synchronized (this) { if (toNotify.isEmpty()) { return; } if (System.currentTimeMillis () < toNotifyModified + SAFE_NOTIFY_DELAY) { task.schedule (SAFE_NOTIFY_DELAY); return; } arr = (Item [])toNotify.toArray (new Item [toNotify.size ()]); } // notify each created object for (int i = 0; i < arr.length; i++) { DataObject obj = arr[i].getDataObjectOrNull (); // notifyCreation removes object from toNotify queue, // if object was already invalidated then remove it as well if (obj != null) { notifyCreation (obj); } else { synchronized (this) { toNotify.remove (arr[i]); } } } } /** Add to list of created objects. */ private void notifyAdd (Item item) { if (toNotify.isEmpty()) { task.schedule (SAFE_NOTIFY_DELAY); } toNotify.add (item); last.set (item); toNotifyModified = System.currentTimeMillis (); } /** Listener used to distribute the File events to their DOs. * [pnejedly] A little bit about its internals/motivation: * Originally, every created DO have hooked its onw listener to the primary * FO's parent folder for listening on primary FO changes. The listener * was enhanced in MDO to also cover secondaries. * Now there is one FSListener per FileSystem which have to distribute * the events to the DOs using limited DOPool's knowledge about FO->DO * mapping. Because the mapping knowledge is limited to primary FOs only, * it have to resort to notifying all known DOs for given folder * if the changed file is not known. Although it is not as good as direct * notification used for known primaries, it is still no worse than * all DOs listening on their folder themselves as it spares at least * the zillions of WeakListener instances. */ private final class FSListener extends FileChangeAdapter { FSListener() {} /** * @return Iterator<Item> */ private Iterator getTargets(FileEvent fe) { FileObject fo = fe.getFile(); List toNotify = new LinkedList(); // The FileSystem notifying us about the changes should // not hold any lock so we're safe here synchronized (DataObjectPool.this) { Item itm = (Item)map.get (fo); if (itm != null) { // the file was someones' primary toNotify.add(itm); // so notify only owner } else { // unknown file or someone secondary FileObject parent = fo.getParent(); if (parent != null) { // the fo is not root FileObject[] siblings = parent.getChildren(); // notify all in folder for (int i=0; i<siblings.length; i++) { itm = (Item)map.get (siblings[i]); if (itm != null) toNotify.add(itm); } } } } return toNotify.iterator(); } public void fileRenamed (FileRenameEvent fe) { for( Iterator it = getTargets(fe); it.hasNext(); ) { DataObject dobj = ((Item)it.next()).getDataObjectOrNull(); if (dobj != null) dobj.notifyFileRenamed(fe); } } public void fileDeleted (FileEvent fe) { for( Iterator it = getTargets(fe); it.hasNext(); ) { DataObject dobj = ((Item)it.next()).getDataObjectOrNull(); if (dobj != null) dobj.notifyFileDeleted(fe); } } public void fileDataCreated (FileEvent fe) { for( Iterator it = getTargets(fe); it.hasNext(); ) { DataObject dobj = ((Item)it.next()).getDataObjectOrNull(); if (dobj != null) dobj.notifyFileDataCreated(fe); } } public void fileAttributeChanged (FileAttributeEvent fe) { for( Iterator it = getTargets(fe); it.hasNext(); ) { DataObject dobj = ((Item)it.next()).getDataObjectOrNull(); if (dobj != null) dobj.notifyAttributeChanged(fe); } } } /** Registers new DataObject instance. * @param fo primary file for obj * @param loader the loader of the object to be created * * @return object with common information for this <CODE>DataObject</CODE> * @exception DataObjectExistsException if the file object is already registered */ public Item register (FileObject fo, DataLoader loader) throws DataObjectExistsException { // here we're registering a listener on fo's FileSystem so we can deliver // fo changes to DO without lots of tiny listeners on folders // The new DS bound to a repository can simply place a single listener // on its repository instead of registering listeners on FileSystems. try { // to register a listener of fo's FileSystem FileSystem fs = fo.getFileSystem(); synchronized (knownFileSystems) { if (! knownFileSystems.contains(fs)) { fs.addFileChangeListener (new FSListener()); knownFileSystems.add(fs); } } } catch (FileStateInvalidException e ) { // no need to listen then } Item doh; DataObject obj; synchronized (this) { doh = (Item)map.get (fo); // if Item for this file has not been created yet if (doh == null) { doh = new Item (fo); map.put (fo, doh); countRegistration(fo); notifyAdd (doh); VALIDATOR.notifyRegistered (fo); return doh; } obj = doh.getDataObjectOrNull (); if (obj == null) { // the item is to be finalize => create new doh = new Item (fo); map.put (fo, doh); countRegistration(fo); notifyAdd (doh); return doh; } if (!VALIDATOR.reregister (obj, loader)) { throw new DataObjectExistsException (obj); } } try { obj.setValid (false); synchronized (this) { // check if there isn't any new data object registered // when this thread left synchronization block. Item doh2 = (Item)map.get (fo); if (doh2 == null) { doh = new Item (fo); map.put (fo, doh); countRegistration(fo); notifyAdd (doh); return doh; } } } catch (java.beans.PropertyVetoException ex) { VALIDATOR.refusingObjects.add (obj); } throw new DataObjectExistsException (obj); } /** Notifies all newly created objects to /** Deregister. * @param item the item with common information to deregister * @param refresh true if the parent folder should be refreshed */ private synchronized void deregister (Item item, boolean refresh) { FileObject fo = item.primaryFile; Item previous = (Item)map.remove (fo); if (previous != null && previous != item) { // ops, mistake, // return back the original map.put (fo, previous); countRegistration(fo); // Furthermore, item is probably in toNotify by mistake. // Observed in DataFolderTest.testMove: after vetoing the move // of a data folder, the bogus item for the temporary new folder // (e.g. BB/AAA/A1) is left in the toNotify pool forever. This // point is reached; remove it now. -jglick if (toNotify.remove(item)) { if (toNotify.isEmpty()) { task.cancel(); } notifyAll(); } return; } // refresh of parent folder if (refresh) { fo = fo.getParent (); if (fo != null) { Item item2 = (Item)map.get (fo); if (item2 != null) { DataFolder df = (DataFolder) item2.getDataObjectOrNull(); if (df != null) { VALIDATOR.refreshFolderOf (df); } } } } } /** Changes the primary file to new one. * @param item the item to change * @param newFile new primary file to set */ private synchronized void changePrimaryFile ( Item item, FileObject newFile ) { map.remove (item.primaryFile); item.primaryFile = newFile; map.put (newFile, item); countRegistration(newFile); } /** When the loader pool is changed, then all objects are rescanned. */ public void stateChanged (javax.swing.event.ChangeEvent ev) { Set set; synchronized (this) { // copy the values synchronously set = new HashSet (map.values ()); } set = createSetOfAllFiles (set); revalidate (set); } /** Create list of all files for given collection of data objects. * @param c collection of DataObjectPool.Item * @return set of files */ private static Set createSetOfAllFiles (Collection c) { HashSet set = new HashSet (c.size () * 7); Iterator it = c.iterator(); while (it.hasNext()) { Item item = (Item)it.next (); DataObject obj = item.getDataObjectOrNull (); if (obj != null) { set.addAll (obj.files ()); } } return set; } /** Remove DataObjects which became invalid thanks * to unmounting a FileSystem */ private void removeInvalidObjects() { Set files; synchronized (this) { files = new HashSet (map.values ()); } files = createSetOfAllFiles (files); VALIDATOR.removeInvalidObject(files); } // // Repository listener changes // /** Called when new file system is added to the pool. * @param ev event describing the action */ public void fileSystemAdded(RepositoryEvent ev) { ev.getFileSystem().addPropertyChangeListener( getPOOL() ); } /** Called when a file system is removed from the pool. * @param ev event describing the action */ public void fileSystemRemoved(RepositoryEvent ev) { ev.getFileSystem().removePropertyChangeListener( getPOOL() ); removeInvalidObjects(); } /** Called when a file system pool is reordered. */ public void fileSystemPoolReordered(RepositoryReorderedEvent ev) { } /** Called when a file system property changed. * If it's property root, check validity. * @param ev event describing the action */ public void propertyChange (final PropertyChangeEvent ev) { if (FileSystem.PROP_SYSTEM_NAME.equals (ev.getPropertyName ())) { removeInvalidObjects(); } if (FileSystem.PROP_ROOT.equals (ev.getPropertyName ())) { removeInvalidObjects(); } } /** Returns all currently existing data * objects. * * @return iterator of DataObjectPool.Item */ Iterator getActiveDataObjects () { synchronized (this) { ArrayList alist = new ArrayList(map.values()); return alist.iterator(); } } /** One item in object pool. */ static final class Item extends Object { /** initial value of obj field. */ private static final Reference REFERENCE_NOT_SET = new WeakReference(null); /** weak reference data object with this primary file */ private Reference obj = REFERENCE_NOT_SET; /** primary file */ FileObject primaryFile; // [PENDING] hack to check the stack when the DataObject has been created // private Exception stack; /** @param fo primary file * @param pool object pool */ public Item (FileObject fo) { this.primaryFile = fo; // [PENDING] // stores stack /* java.io.StringWriter sw = new java.io.StringWriter (); stack = new Exception (); } // [PENDING] toString returns original stack public String toString () { return stack.toString ();*/ } /** Setter for the data object. Called immediatelly as possible. * @param obj the data object for this item */ public void setDataObject (DataObject obj) { this.obj = new ItemReference (obj, this); if (obj != null && !obj.getPrimaryFile ().isValid ()) { // if the primary file is already invalid => // mark the object as invalid deregister (false); } synchronized (DataObjectPool.getPOOL()) { DataObjectPool.getPOOL().notifyAll(); } } /** Getter for the data object. * @return the data object or null */ DataObject getDataObjectOrNull () { synchronized (DataObjectPool.getPOOL()) { while (this.obj == REFERENCE_NOT_SET) { try { DataObjectPool.getPOOL().wait (); } catch (InterruptedException exc) { } } } return this.obj == null ? null : (DataObject)this.obj.get (); } /** Getter for the data object. * @return the data object * @exception IllegalStateException if the data object has been lost * due to weak references (should not happen) */ public DataObject getDataObject () { DataObject obj = getDataObjectOrNull (); if (obj == null) { throw new IllegalStateException (); } return obj; } /** Deregister one reference. * @param refresh true if the parent folder should be refreshed */ public void deregister (boolean refresh) { getPOOL().deregister (this, refresh); } /** Changes the primary file to new one. * @param newFile new primary file to set */ public void changePrimaryFile (FileObject newFile) { getPOOL().changePrimaryFile (this, newFile); } /** Is the item valid? */ public boolean isValid () { if (getPOOL().map.get (primaryFile) == this) { return primaryFile.isValid(); } else { return false; } } public String toString () { DataObject obj = (DataObject)this.obj.get (); if (obj == null) { return "nothing[" + primaryFile + "]"; // NOI18N } return obj.toString (); } } /** WeakReference - references a DataObject, strongly references an Item */ static final class ItemReference extends WeakReference implements Runnable { /** Reference to an Item */ private Item item; ItemReference(DataObject dobject, Item item) { super(dobject, org.openide.util.Utilities.activeReferenceQueue()); this.item = item; } /** Does the cleanup of the reference */ public void run () { item.deregister(false); item = null; } } /** Validator to allow rescan of files. */ private static final class Validator extends Object implements DataLoader.RecognizedFiles { /** set of all files that should be revalidated (FileObject) */ private Set files; /** current thread that is in the validator */ private Thread current; /** number of threads waiting to enter the validation */ private int waiters; /** Number of calls to enter by current thread minus 1 */ private int reenterCount; /** set of files that has been marked recognized (FileObject) */ private HashSet recognizedFiles; /** set with all objects that refused to be discarded (DataObject) */ private HashSet refusingObjects; /** set of files that has been registered during revalidation */ private HashSet createdFiles; Validator() {} /** Enters the section. * @param set mutable set of files that should be processed * @return the set of files concatenated with any previous sets */ private synchronized Set enter (Set set) { if (current == Thread.currentThread ()) { reenterCount++; } else { waiters++; while (current != null) { try { wait (); } catch (InterruptedException ex) { } } current = Thread.currentThread (); waiters--; } if (files == null) { files = set; } else { files.addAll (set); } return files; } /** Leaves the critical section. */ private synchronized void exit () { if (reenterCount == 0) { current = null; if (waiters == 0) { files = null; } notify (); } else { reenterCount--; } } /** If there is another waiting thread, then I can * cancel my computation. */ private synchronized boolean goOn () { return waiters == 0; } /** Called to either refresh folder, or register the folder to be * refreshed later is validation is in progress. */ public void refreshFolderOf (DataFolder df) { if (createdFiles == null) { // no validator in progress FolderList.changedDataSystem (df.getPrimaryFile ()); } } /** Mark this file as being recognized. It will be excluded * from further processing. * * @param fo file object to exclude */ public void markRecognized (FileObject fo) { recognizedFiles.add (fo); } public void notifyRegistered (FileObject fo) { if (createdFiles != null) { createdFiles.add (fo); } } /** Reregister new object for already existing file object. * @param obj old object existing * @param loader loader of new object to create * @return true if the old object has been discarded and new one can * be created */ public boolean reregister (DataObject obj, DataLoader loader) { if (recognizedFiles == null) { // revalidation not in progress return false; } if (obj.getLoader () == loader) { // no change in loader => return false; } if (createdFiles.contains (obj.getPrimaryFile ())) { // if the file already has been created return false; } if (refusingObjects.contains (obj)) { // the object has been refused before return false; } return true; } /** Rescans all fileobjects in given set. * @param s mutable set of FileObjects * @return set of objects that refused to be revalidated */ public Set revalidate (Set s) { // ----------------- fix of #30559 START if ((s.size() == 1) && (current == Thread.currentThread ())) { if (files != null && files.contains(s.iterator().next())) { return new HashSet(); } } // ----------------- fix of #30559 END // holds all created object, so they are not garbage // collected till this method ends LinkedList createObjects = new LinkedList (); try { s = enter (s); recognizedFiles = new HashSet (); refusingObjects = new HashSet (); createdFiles = new HashSet (); HashSet allFS = new HashSet (java.util.Arrays.asList( Repository.getDefault().toArray() )); DataLoaderPool pool = (DataLoaderPool)Lookup.getDefault().lookup (DataLoaderPool.class); Iterator it = s.iterator (); while (it.hasNext () && goOn ()) { try { FileObject fo = (FileObject)it.next (); if (!recognizedFiles.contains (fo)) { // first of all test if the file is on a valid filesystem boolean invalidate; try { FileSystem fs = fo.getFileSystem (); invalidate = !allFS.contains (fs); } catch (FileStateInvalidException ex) { invalidate = true; } // the previous data object should be canceled DataObject orig = getPOOL().find (fo); if (orig == null) { // go on continue; } if (!invalidate) { // findDataObject // is not using method DataObjectPool.find to locate data object // directly for primary file, that is good DataObject obj = pool.findDataObject (fo, this); createObjects.add (obj); invalidate = obj != orig; } if (invalidate) { it.remove(); try { orig.setValid (false); } catch (java.beans.PropertyVetoException ex) { refusingObjects.add (orig); } } } } catch (DataObjectExistsException ex) { // this should be no problem here } catch (java.io.IOException ioe) { ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ioe); } catch (ConcurrentModificationException cme) { // not very nice but the only way I could come up to handle this: // java.util.ConcurrentModificationException // at java.util.HashMap$HashIterator.remove(HashMap.java:755) // at org.openide.loaders.DataObjectPool$Validator.revalidate(DataObjectPool.java:916) // at org.openide.loaders.DataObjectPool.revalidate(DataObjectPool.java:203) // at org.openide.loaders.DataObjectPool.stateChanged(DataObjectPool.java:527) // at org.openide.loaders.DataLoaderPool$1.run(DataLoaderPool.java:128) // at org.openide.util.Task.run(Task.java:136) //[catch] at org.openide.util.RequestProcessor$Processor.run(RequestProcessor.java:635) // is to ignore the exception and continue it = s.iterator(); } } return refusingObjects; } finally { recognizedFiles = null; refusingObjects = null; createdFiles = null; exit (); if ( s.size() > 1 ) getPOOL().refreshAllFolders (); } } /** Remove DataObjects which became invalid thanks * to unmounting a FileSystem */ void removeInvalidObject(Set files) { try { files = enter(files); HashSet allFS = new HashSet(); FileSystem[] fss = Repository.getDefault().toArray(); for (int i = 0; i < fss.length; i++) { allFS.add(fss[i]); } Iterator it = files.iterator(); while (it.hasNext() && goOn()) { FileObject fo = (FileObject) it.next(); boolean invalidate = !fo.isValid(); if ( !invalidate ) { try { FileSystem fs = fo.getFileSystem (); invalidate = !allFS.contains (fs); } catch (FileStateInvalidException ex) { invalidate = true; } } DataObject orig = null; synchronized (getPOOL()) { Item itm = (Item)getPOOL().map.get (fo); if (itm == null) { continue; } orig = itm.getDataObjectOrNull (); } if (invalidate && orig != null) { it.remove(); try { orig.setValid (false); } catch (java.beans.PropertyVetoException ex) { // silently ignore? ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex); } } } } finally { exit(); } } } // end of Validator }