/* * 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.filesystems; import java.io.*; import java.util.*; import java.lang.ref.*; import org.openide.util.enums.*; /** Implementation of the file object that simplyfies common * tasks with hierarchy of objects for AbstractFileObject and MultiFileObject. * * @author Jaroslav Tulach, */ abstract class AbstractFolder extends FileObject { /** empty array */ private static final AbstractFolder[] EMPTY_ARRAY = new AbstractFolder[0]; /** default extension separator */ private static final char EXT_SEP = '.'; /** empty hash map to mark that we are initialized */ private static final HashMap EMPTY = new HashMap (0); /** file system */ private FileSystem system; /** name of the file (only name and extension) */ protected String name; /** strong reference to parent (can be null for root) */ protected final AbstractFolder parent; /** Stores the system name of the file system to test * validity later. */ boolean validFlag; /** If root changes, all AbstractFolders in hierarchy are invalidated. *@see #isValid()*/ private AbstractFolder validRoot = null; /** list of children */ private String[] children; /** map that assignes file object to names. (String, Reference (AbstractFileObject)) */ private HashMap map; /** listeners */ private ListenerList listeners; /** Constructor. Takes reference to file system this file belongs to. * * @param fs the file system * @param parent the parent object (folder) * @param name name of the object (e.g. <code>filename.ext</code>) */ public AbstractFolder( FileSystem fs, AbstractFolder parent, String name ) { this.system = fs; this.parent = parent; this.name = name; validFlag = true; if (parent != null) validRoot = (AbstractFolder) fs.getRoot(); } /* Get the name without extension of this file or folder. * Period at first position is not considered as extension-separator * @return name of the file or folder(in its enclosing folder) */ public final String getName () { int i = name.lastIndexOf ('.'); /** period at first position is not considered as extension-separator */ return i <= 0 ? name : name.substring (0, i); } /* Get the extension of this file or folder. * Period at first position is not considered as extension-separator * This is the string after the last dot of the full name, if any. * * @return extension of the file or folder(if any) or empty string if there is none */ public final String getExt () { int i = name.lastIndexOf ('.') + 1; /** period at first position is not considered as extension-separator */ return i <= 1 || i == name.length () ? "" : name.substring (i); // NOI18N } /** Overrides the get name and ext method to make it faster then * default implementation. */ public final String getNameExt () { return name; } /** Overriden in AbstractFolder */ final boolean isHasExtOverride() { return true; } /** Overriden in AbstractFolder */ boolean hasExtOverride(String ext) { if (ext == null) { return false; } /** period at first position is not considered as extension-separator */ if (name.length() - ext.length() <= 1) { return false; } boolean ret = name.endsWith(ext); if (! ret) { return false; } if (name.charAt(name.length() - ext.length() - 1) != '.') { return false; } return true; } /* Getter for the right file system */ public final FileSystem getFileSystem () { return system; } // // Info // /* Test whether this object is the root folder. * The root should always be a folder. * @return true if the object is the root of a file system */ public final boolean isRoot () { return parent == null; } /* 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. * * @return true if the file object is valid */ public final boolean isValid () { // valid if (parent == null) { return true; } boolean isValidRoot = getFileSystem ().getRoot () == validRoot; return validFlag && isValidRoot; } // // List // /* Get parent folder. * The returned object will satisfy {@link #isFolder}. * * @return the parent folder or <code>null</code> if this object {@link #isRoot}. */ public final FileObject getParent () { return parent; } /* Get all children of this folder (files and subfolders). If the file does not have children * (does not exist or is not a folder) then an empty array should be returned. No particular order is assumed. * * @return array of direct children * @see #getChildren(boolean) * @see #getFolders * @see #getData */ public final synchronized FileObject[] getChildren () { check (); if (children == null) { return new FileObject[0]; } int size = children.length; FileObject[] arr = new FileObject[size]; for (int i = 0; i < size; i++) { arr[i] = getChild (children[i]); } return arr; } /** Tries to find a resource. * @param en enumeration of strings to scan * @return found object or null */ final FileObject find (Enumeration en) { AbstractFolder fo = this; while (fo != null && en.hasMoreElements ()) { // try to go on // lock to provide safety for getChild synchronized (fo) { // JST: Better to call the check only here, // than in getChild, than it is not called // so often. fo.check (); fo = fo.getChild ((String)en.nextElement ()); } } // no next requirements or not found return fo; } /** Tries to find a resource if it exists in memory. * @param en enumeration of strings to scan * @return found object or null */ final FileObject findIfExists (Enumeration en) { Reference r = findRefIfExists(en); return(r == null? null : (FileObject)r.get()); } /** Tries to find a resource if it exists in memory. * @param en enumeration of strings to scan * @return found object or null */ final Reference findRefIfExists (Enumeration en) { AbstractFolder fo = this; while (fo != null && en.hasMoreElements ()) { if (fo.map == null) { // this object is not initialized yet return null; } // try to go on // lock to provide safety for getChild synchronized (fo) { String name = (String)en.nextElement (); if (en.hasMoreElements()){ fo = fo.getChild(name); }else{ return((Reference)fo.map.get(name)); } } } // no next requirements or not found return null; } /** Finds one child for given name . * @param name the name of the child * @return the file object or null if it does not exist */ protected final AbstractFolder getChild (String name) { Reference r = (Reference)map.get (name); if (r == null) { return null; } AbstractFolder fo = (AbstractFolder)(r.get ()); if (fo == null) { // object does not exist => have to recreate it fo = createFile (name); map.put (name, createReference(fo)); } return fo; } /** Get method for children array . * @return array of children */ final String[] getChildrenArray () { return children;} /** Creates Reference. In FileSystem, which subclasses AbstractFileSystem, you can overload method * createReference(FileObject fo) to achieve another type of Reference (weak, strong etc.) * @param fo FileObject * @return Reference to FileObject */ protected Reference createReference(FileObject fo){ return(new WeakReference (fo)); } /** Obtains enumeration of all existing subfiles. */ final synchronized AbstractFolder[] subfiles () { if (map == null) { return EMPTY_ARRAY; } Iterator it = map.values ().iterator (); ArrayList ll = new ArrayList(map.size() + 2); while (it.hasNext ()) { Reference r = (Reference)it.next (); if (r == null) { continue; } AbstractFolder fo = (AbstractFolder)r.get (); if (fo != null /*&& (!fo.isFolder () || fo.map != null)*/) { // if the file object exists and either is not folder (then // we have to check the time) or it is folder and it has // some children // => use it ll.add (fo); } } return (AbstractFolder[])ll.toArray (EMPTY_ARRAY); } final boolean isInitialized () { return this.map != null; } /** Creates enumeration of existing subfiles in all tree * of files. * * @param rec should it be recursive or not * @return enumeration of AbstractFolders */ final Enumeration existingSubFiles (boolean rec) { if (!rec) { return new ArrayEnumeration (subfiles ()); } else { QueueEnumeration en = new QueueEnumeration () { public void process (Object o) { AbstractFolder af = (AbstractFolder)o; this.put (af.subfiles ()); } }; en.put (this); return en; } } /* helper method for MFO.setAttribute. MFO can disable firing from underlaying * layers. Should be reviewed in 3.4 or later *@see MultiFileObject#setAttribute*/ abstract void setAttribute(String attrName, Object value, boolean fire) throws IOException; /** Retrieve file or folder contained in this folder by name. * <em>Note</em> that neither file nor folder is created on disk. * @param name basename of the file or folder (in this folder) * @param ext extension of the file; <CODE>null</CODE> or <code>""</code> * if the file should have no extension or if folder is requested * @return the object representing this file or <CODE>null</CODE> if the file * or folder does not exist * @exception IllegalArgumentException if <code>this</code> is not a folder */ public final synchronized FileObject getFileObject (String name, String ext) { check (); if (ext == null || ext.equals ("")) { // NOI18N return getChild (name); } else { StringBuffer sb = new StringBuffer(name.length() + 1 + ((ext == null) ? 0 : ext.length())); sb.append(name).append(EXT_SEP).append(ext); return getChild (sb.toString()); } } /* Refresh the contents of a folder. Rescans the list of children names. */ public void refresh(boolean expected) { if (!isInitialized () && isFolder ()) return; refresh (null, null, true, expected); } // // Listeners section // /* Add new listener to this object. * @param l the listener */ public final void addFileChangeListener(FileChangeListener fcl) { synchronized (EMPTY_ARRAY) { if (listeners == null) { listeners = new ListenerList (FileChangeListener.class); } } listeners.add(fcl); } /* Remove listener from this object. * @param l the listener */ public final void removeFileChangeListener (FileChangeListener fcl) { if (listeners != null) { listeners.remove (fcl); } } /** Fires event */ protected final void fileDeleted0(FileEvent fileevent) { super.fireFileDeletedEvent(listeners (), fileevent); if(fileevent.getFile().equals(this) && parent != null) { FileEvent ev = new FileEvent(parent, fileevent.getFile(),fileevent.isExpected ()); parent.fileDeleted0(ev); } } /** Fires event */ protected final void fileCreated0(FileEvent fileevent, boolean flag) { if(flag) super.fireFileDataCreatedEvent(listeners (), fileevent); else super.fireFileFolderCreatedEvent(listeners (), fileevent); if(fileevent.getFile().equals(this) && parent != null) { FileEvent ev = new FileEvent(parent, fileevent.getFile(), fileevent.isExpected ()); parent.fileCreated0 (ev, flag); } } /** Creates nad fires event */ protected final void fileCreated0(FileObject src, FileObject file, boolean expected) { FileEvent ev = new FileEvent (src, file, expected); fileCreated0(ev, false); } /** Fires event */ protected final void fileChanged0 (FileEvent fileevent) { super.fireFileChangedEvent(listeners (), fileevent); if(fileevent.getFile().equals(this) && parent != null) { FileEvent ev = new FileEvent(parent, fileevent.getFile(), fileevent.isExpected ()); parent.fileChanged0 (ev); } } /** Fires event - but doesn`t fork events*/ final void fileChanged1 (FileEvent fileevent) { super.fireFileChangedEvent(listeners (), fileevent); } /** Fires event */ protected final void fileRenamed0 (FileRenameEvent filerenameevent) { super.fireFileRenamedEvent(listeners (), filerenameevent); if(filerenameevent.getFile().equals(this) && parent != null) { FileRenameEvent ev = new FileRenameEvent( parent, filerenameevent.getFile(), filerenameevent.getName(), filerenameevent.getExt(), filerenameevent.isExpected () ); parent.fileRenamed0 (ev); } } /** Fires event */ protected final void fileAttributeChanged0 (FileAttributeEvent fileattributeevent) { super.fireFileAttributeChangedEvent(listeners (), fileattributeevent); if(fileattributeevent.getFile().equals(this) && parent != null) { FileAttributeEvent ev = new FileAttributeEvent( parent, fileattributeevent.getFile(), fileattributeevent.getName(), fileattributeevent.getOldValue(), fileattributeevent.getNewValue(), fileattributeevent.isExpected () ); parent.fileAttributeChanged0 (ev); } } /** @return true if there is a listener */ protected final boolean hasListeners () { boolean fsHas = getFileSystem().getFCLSupport ().hasListeners (); boolean repHas = false; Repository rep = getFileSystem().getRepository (); if (rep != null) repHas = rep.getFCLSupport ().hasListeners (); return (listeners != null && listeners.getAllListeners ().length != 0) || repHas || fsHas; } /** @return true if this folder or its parent have listeners */ protected final boolean hasAtLeastOneListeners () { return hasListeners () || (parent != null && parent.hasListeners ()); } /** @return enumeration of all listeners. */ private final Enumeration listeners () { if (listeners == null) { return EmptyEnumeration.EMPTY; } else { return new FilterEnumeration (new ArrayEnumeration (listeners.getAllListeners ())) { public boolean accept (Object o) { return o != FileChangeListener.class; } }; } } // // Refreshing the state of the object // /** Test if the file has been checked and if not, refreshes its * content. */ private final void check () { if (map == null) { refresh (null, null, false, false); if (map == null) { // create empty map to mark that we are initialized map = EMPTY; } } } /** Refresh the content of file. Ignores changes to the files provided, * instead returns its file object. * @param added do not notify addition of this file * @param removed do not notify removing of this file */ protected final void refresh (String added, String removed) { this.refresh(added, removed, false); } /** Refresh the content of file. Ignores changes to the files provided, * instead returns its file object. * @param added do not notify addition of this file * @param removed do not notify removing of this file * @param reuseChildren a flag reuse children? */ protected final void refresh (String added, String removed, boolean reuseChildren) { if (reuseChildren && removed != null) { String[] nc = new String[children.length]; System.arraycopy(children, 0, nc, 0, children.length); for (int i = nc.length; --i >= 0; ) { if (removed.equals(nc[i])) { nc[i] = null; break; } } refresh (added, removed, true, false, nc); } else { refresh (added, removed, true, false, null); } } /** Method that allows subclasses to return its children. * * @return names (name . ext) of subfiles */ protected abstract String[] list (); /** Method to create a file object for given subfile. * @param name of the subfile * @return the file object */ protected abstract AbstractFolder createFile (String name); /** Refresh the content of file. Ignores changes to the files provided, * instead returns its file object. * @param added do not notify addition of this file * @param removed do not notify removing of this file * @param fire true if we should fire changes * @param expected true if the change has been expected by the user */ protected void refresh ( String added, String removed, boolean fire, boolean expected ) { this.refresh(added, removed, fire, expected, null); } void registerChild (String name) { synchronized (this) { if (map == null) check (); Object o = map.put(name, new WeakReference(null)); if (o != null) { map.put(name, o); } else { String newChildren[] = new String [children.length + 1]; System.arraycopy(children, 0, newChildren, 0, children.length); newChildren [children.length] = name; children = newChildren; } } } /** Refresh the content of file. Ignores changes to the files provided, * instead returns its file object. * @param added do not notify addition of this file * @param removed do not notify removing of this file * @param fire true if we should fire changes * @param expected true if the change has been expected by the user * @param list contains list of children */ protected final void refreshFolder ( String added, String removed, boolean fire, boolean expected, String[] list ) { try { getFileSystem ().beginAtomicAction (); synchronized (this) { if (list == null) { list = list(); } // refresh of folder checks children String[] newChildren = list; if (newChildren == null && parent == null) { // if root => we have to have children newChildren = new String[0]; } if (children == null && newChildren == null) { // no change and we are still date file return; } // System.out.println ("Refresh: " + this + " fire: " + fire); // NOI18N // Thread.dumpStack (); // new map (String, AbstractFileObject) HashMap m; // set of added files (String) Set add; // moreover map will contain only such files that disappeared int newChildrenContainNull = 0; if (newChildren != null) { int size = newChildren.length; m = new HashMap (size * 4 / 3 + 1, 0.75f); add = new HashSet (size * 4 / 3 + 1, 0.75f); Object replaceInSteadOfRemoved = map != null ? map.get (removed) : null; for (int i = 0; i < size; i++) { String ch = newChildren[i]; if (ch == null) { // ignore this and newChildrenContainNull++; continue; } Reference old = map == null ? null : (Reference)map.remove (ch); /** #18340 - ch.equals(removed) may seem a little confusing because * one would expect that children doesn`t contain removed. * But in CVS filesystems renaming of FileObject means that * two FileObjects will be present (old - needs checkout, * new - localy added). */ boolean isSpecialRename = (added != null && ch.equals(removed)); if (old == null || isSpecialRename) { // create new empty reference old = new WeakReference (null); add.add (ch); } m.put (ch, old); } if (added != null && replaceInSteadOfRemoved != null) { m.put (added, replaceInSteadOfRemoved); } } else { m = new HashMap (0); add = Collections.EMPTY_SET; } // maps (String, AbstractFileObject) between objects that has diappeared HashMap disappeared = null; if (fire) { // a set of files to notify that has been added if (added != null) { add.remove (added); } if (map != null) { // // MAP IS CHANGING TO (String, AbstractFileObject) // this should be fine cause map is replaced immediatelly after // this loop // disappeared = map; Iterator it = map.entrySet ().iterator (); while (it.hasNext ()) { Map.Entry entry = (Map.Entry)it.next (); // uses the value and replaces it immediatelly entry.setValue (getChild ((String)entry.getKey ())); } // remove the removed if (removed != null) { disappeared.remove (removed); } if (disappeared.isEmpty ()) { disappeared = null; } } } // use the new map // Map now contains the right content map = m; if (newChildrenContainNull != 0) { // exclude nulls from the array String[] arr = new String[newChildren.length - newChildrenContainNull]; int j = 0; for (int i = 0; i < newChildren.length; i++) { if (newChildren[i] != null) { arr[j++] = newChildren[i]; } } children = arr; } else { // ok, no nulls in the array children = newChildren; } if (fire && !add.isEmpty () && hasAtLeastOneListeners ()) { // fire these files has been added for (Iterator it = add.iterator (); it.hasNext (); ) { String name = (String)it.next (); AbstractFolder fo = getChild (name); if (fo == null) continue; fileCreated0 (this, fo, expected); // a change in children // changed = true; } } if (fire && disappeared != null) { // fire these files has been removed for (Iterator it = disappeared.values ().iterator (); it.hasNext (); ) { AbstractFolder fo = (AbstractFolder)it.next (); fo.validFlag = false; if (hasAtLeastOneListeners () || fo.hasAtLeastOneListeners ()) { FileEvent ev = new FileEvent (fo , fo, expected); fo.fileDeleted0 (ev); } // a change happened // changed = true; } } } } finally { getFileSystem ().finishAtomicAction (); } } /** Refresh the content of file. Ignores changes to the files provided, * instead returns its file object. * @param added do not notify addition of this file * @param removed do not notify removing of this file * @param fire true if we should fire changes * @param expected true if the change has been expected by the user * @param list contains list of children */ protected void refresh ( String added, String removed, boolean fire, boolean expected, String[] list ) { if (isFolder ()) { refreshFolder (added, removed, fire, expected, list); } } /** Notification that the output stream has been closed. * @fireFileChange defines if should be fired fileChanged event after close of stream */ protected void outputStreamClosed (boolean fireFileChange) { if (fireFileChange) fileChanged0 (new FileEvent (AbstractFolder.this)); } // // Serialization // public final Object writeReplace () { return new AbstractFileObject.Replace (getFileSystem ().getSystemName (), getPath ()); } }