/*
* 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 org.openide.util.actions.SystemAction;
import org.openide.util.enums.*;
/** The base for all filesystems that are build above a top of
* other ones. This system expects at most one filesystem it should write
* to and any number of filesystems to read from.
*
* If there is more versions of one file than the one from writable filesystem
* is prefered or the read only systems are scanned in the given order.
*
* @author Jaroslav Tulach
*/
public class MultiFileSystem extends FileSystem {
static final long serialVersionUID =-767493828111559560L;
/** what extension to add to file that mask another ones */
static final String MASK = "_hidden"; // NOI18N
/** array of fs. the filesystem at position 0 can be null, because
* it is writable filesystem. Others are only for read access
*/
private FileSystem[] systems;
/** @see #getPropagateMasks */
private boolean propagateMasks = false;
/** root */
private transient MultiFileObject root;
/** index of the filesystem with write access */
private static final int WRITE_SYSTEM_INDEX = 0;
/** Creates new empty MultiFileSystem. Useful only for
* subclasses.
*/
protected MultiFileSystem () {
this (new FileSystem[1]);
}
/** Creates new MultiFileSystem.
* @param fileSystems array of filesystems (can contain nulls)
*/
public MultiFileSystem (FileSystem[] fileSystems) {
this.systems = (FileSystem [])fileSystems.clone ();
}
/**
* Actually implements contract of FileSystem.refresh().
*/
public void refresh (boolean expected) {
Enumeration en = getMultiRoot ().existingSubFiles (true);
while (en.hasMoreElements()) {
FileObject fo = (FileObject)en.nextElement();
fo.refresh(expected);
}
}
/** Changes the filesystems that this system delegates to
*
* @param fileSystems array of filesystems
*/
protected final void setDelegates (FileSystem[] fileSystems) {
// save for notification
FileSystem[] oldSystems = systems;
// set them
this.systems = fileSystems;
getMultiRoot ().updateAllAfterSetDelegates(oldSystems);
List oldList = Arrays.asList (oldSystems);
List newList = Arrays.asList (systems);
// notify removed filesystems
HashSet toRemove = new HashSet(oldList);
toRemove.removeAll(newList);
for (Iterator iter = toRemove.iterator(); iter.hasNext(); ) {
FileSystem fs = ((FileSystem)iter.next());
if (fs != null)
fs.removeNotify();
}
// notify added filesystems
HashSet toAdd = new HashSet(newList);
toAdd.removeAll(oldList);
for (Iterator iter = toAdd.iterator(); iter.hasNext(); ) {
FileSystem fs = ((FileSystem)iter.next());
if (fs != null)
fs.addNotify();
}
}
/** All filesystem that this system delegates to.
* @return the array of delegates
*/
protected final FileSystem[] getDelegates () {
return systems;
}
/** Will mask files that are not used be listed as children?
* @return <code>true</code> if so
*/
public final boolean getPropagateMasks () {
return propagateMasks;
}
/** Set whether unused mask files should be listed as children.
* @param pm <code>true</code> if so
*/
protected final void setPropagateMasks (boolean pm) {
propagateMasks = pm;
}
/** This filesystem is readonly if it has not writable system.
*/
public boolean isReadOnly () {
return systems[WRITE_SYSTEM_INDEX] == null || systems[WRITE_SYSTEM_INDEX].isReadOnly ();
}
/** The name of the filesystem.
*/
public String getDisplayName () {
return getString ("CTL_MultiFileSystem");
}
/** Root of the filesystem.
*/
public FileObject getRoot () {
return getMultiRoot ();
}
/** Root of the filesystem.
*/
private MultiFileObject getMultiRoot () {
synchronized (MultiFileSystem.class) {
if (root == null) {
root = new MultiFileObject (this);
}
return root;
}
}
/** Merge actions from all delegates.
*/
public SystemAction[] getActions () {
ArrayList al = new ArrayList(101); // randomly choosen constant
HashSet uniq = new HashSet(101); // not that randommly choosen
FileSystem[] del = this.getDelegates();
for (int i=0; i<del.length; i++) {
if (del[i] == null) continue;
SystemAction[] acts = del[i].getActions();
for (int j=0; j<acts.length; j++) {
if (uniq.add(acts[j])) al.add(acts[j]);
}
}
return (SystemAction[])al.toArray(new SystemAction[al.size ()]);
}
public SystemAction[] getActions (final Set foSet) {
final ArrayList al = new ArrayList(101); // randomly choosen constant
final HashSet uniq = new HashSet(101); // not that randommly choosen
final FileSystem[] del = this.getDelegates();
for (int i=0; i<del.length; i++) {
if (del[i] == null) continue;
final SystemAction[] acts = del[i].getActions(foSet);
for (int j=0; j<acts.length; j++) {
if (uniq.add(acts[j])) al.add(acts[j]);
}
}
return (SystemAction[])al.toArray(new SystemAction[al.size ()]);
}
/* Finds file when its name is provided.
*
* @param aPackage package name where each package is separated by a dot
* @param name name of the file (without dots) or <CODE>null</CODE> if
* one want to obtain name of package and not file in it
* @param ext extension of the file or <CODE>null</CODE> if one needs
* package and not file name
*
* @warning when one of name or ext is <CODE>null</CODE> then name and
* ext should be ignored and scan should look only for a package
*
* @return FileObject that represents file with given name or
* <CODE>null</CODE> if the file does not exist
*/
public FileObject find (String aPackage, String name, String ext) {
// create enumeration of name to look for
StringTokenizer st = new StringTokenizer (aPackage, "."); // NOI18N
Enumeration en;
if (name == null || ext == null) {
en = st;
} else {
en = new SequenceEnumeration (
st,
new SingletonEnumeration (name + '.' + ext)
);
}
// tries to find it (can return null)
return getMultiRoot ().find (en);
}
/* Finds file when its resource name is given.
* The name has the usual format for the {@link ClassLoader#getResource(String)}
* method. So it may consist of "package1/package2/filename.ext".
* If there is no package, it may consist only of "filename.ext".
*
* @param name resource name
*
* @return FileObject that represents file with given name or
* <CODE>null</CODE> if the file does not exist
*/
public FileObject findResource (String name) {
if (name.length () == 0) {
return getMultiRoot ();
} else {
StringTokenizer tok = new StringTokenizer (name, "/"); // NOI18N
return getMultiRoot ().find (tok);
}
}
//
// Helper methods for subclasses
//
/** For given file object finds the filesystem that the object is placed on.
* The object must be created by this filesystem orherwise IllegalArgumentException
* is thrown.
*
* @param fo file object
* @return the filesystem (from the list we delegate to) the object has file on
* @exception IllegalArgumentException if the file object is not represented in this filesystem
*/
protected final FileSystem findSystem (FileObject fo) throws IllegalArgumentException {
try {
if (fo instanceof MultiFileObject) {
MultiFileObject mfo = (MultiFileObject)fo;
return mfo.getLeaderFileSystem ();
}
} catch (FileStateInvalidException ex) {
// can happen if there is no delegate, I do not know what to return
// better, but we should not throw the exception
return this;
}
throw new IllegalArgumentException (fo.getPath());
}
/** Marks a resource as hidden. It will not be listed in the list of files.
* Uses createMaskOn method to determine on which filesystem to mark the file.
*
* @param res resource name of file to hide or show
* @param hide true if we should hide the file/false otherwise
* @exception IOException if it is not possible
*/
protected final void hideResource (String res, boolean hide) throws IOException {
if (hide) {
// mask file
maskFile (createWritableOn (res), res);
} else {
unmaskFile (createWritableOn (res), res);
}
}
/** Finds all hidden files on given filesystem. The methods scans all files for
* ones with hidden extension and returns enumeration of names of files
* that are hidden.
*
* @param folder folder to start at
* @param rec proceed recursivelly
* @return enumeration of String with names of hidden files
*/
protected static Enumeration hiddenFiles (FileObject folder, boolean rec) {
Enumeration allFiles = folder.getChildren (rec);
Enumeration allNull = new AlterEnumeration (allFiles) {
public Object alter (Object fo) {
String sf = ((FileObject)fo).getPath();
if (sf.endsWith (MASK)) {
return sf.substring (0, sf.length () - MASK.length ());
} else {
return null;
}
}
};
return new FilterEnumeration (allNull);
}
//
// methods for subclass customization
//
/** Finds a resource on given filesystem. The default
* implementation simply uses FileSystem.findResource, but
* subclasses may override this method to hide/show some
* resources.
*
* @param fs the filesystem to scan on
* @param res the resource name to look for
* @return the file object or null
*/
protected FileObject findResourceOn (FileSystem fs, String res) {
return fs.findResource (res);
}
/** Finds the system to create writable version of the file on.
*
* @param name name of the file (full)
* @return the first one
* @exception IOException if the filesystem is readonly
*/
protected FileSystem createWritableOn (String name) throws IOException {
if (systems[WRITE_SYSTEM_INDEX] == null || systems[WRITE_SYSTEM_INDEX].isReadOnly ()) {
FSException.io ("EXC_FSisRO", getDisplayName ()); // NOI18N
}
return systems[WRITE_SYSTEM_INDEX];
}
/** Special case of createWritableOn (@see #createWritableOn).
*
* @param oldName original name of the file (full)
* @param newName name new of the file (full)
* @return the first one
* @exception IOException if the filesystem is readonly
* @since 1.34
*/
protected FileSystem createWritableOnForRename (String oldName, String newName) throws IOException {
return createWritableOn (newName);
}
/** When a file is about to be locked this method is consulted to
* choose which delegates should be locked. By default this method
* returns only one filesystem; the same returned by createWritableOn.
* <P>
* If an delegate resides on a filesystem returned in the resulting
* set, it will be locked. All others will remain unlocked.
*
* @param name the resource name to lock
* @return set of filesystems
* @exception IOException if the resource cannot be locked
*/
protected java.util.Set createLocksOn (String name) throws IOException {
FileSystem writable = createWritableOn (name);
return java.util.Collections.singleton(writable);
}
/** Notification that a file has migrated from one filesystem
* to another. Usually when somebody writes to file on readonly file
* system and the file has to be copied to write one.
* <P>
* This method allows subclasses to fire for example FileSystem.PROP_STATUS
* change to notify that annotation of this file should change.
*
* @param fo file object that change its actual filesystem
*/
protected void notifyMigration (FileObject fo) {
}
/** Notification that a file has been marked unimportant.
*
*
* @param fo file object that change its actual filesystem
*/
protected void markUnimportant (FileObject fo) {
}
/** Lets any sub filesystems prepare the environment.
* If they do not support it, it does not care.
*/
public void prepareEnvironment (FileSystem.Environment env)
throws EnvironmentNotSupportedException {
FileSystem[] layers = getDelegates ();
for (int i = 0; i < layers.length; i++) {
if (layers[i] != null) {
try {
layers[i].prepareEnvironment (env);
} catch (EnvironmentNotSupportedException ense) {
// Fine.
}
}
}
}
/** Notifies all encapsulated filesystems in advance
* to superclass behaviour. */
public void addNotify () {
super.addNotify();
for (int i = 0; i < systems.length; i++) {
if (systems[i] != null) {
systems[i].addNotify();
}
}
}
/** Notifies all encapsulated filesystems in advance
* to superclass behaviour. */
public void removeNotify () {
super.removeNotify();
for (int i = 0; i < systems.length; i++) {
if (systems[i] != null) {
systems[i].removeNotify();
}
}
}
//
// Private methods
//
/** Receives name of a resource and array of three elements and
* splits the name into folder, name and extension.
*
* @param res resource name
* @param store array to store data to
*/
private static String[] split (String res, String[] store) {
if (store == null) {
store = new String[3];
}
int file = res.lastIndexOf ('/');
int dot = res.lastIndexOf ('.');
if (file == -1) {
store[0] = ""; // NOI18N
} else {
store[0] = res.substring (0, file);
}
file++;
if (dot == -1) {
store[1] = res.substring (file);
store[2] = ""; // NOI18N
} else {
store[1] = res.substring (file, dot);
store[2] = res.substring (dot + 1);
}
return store;
}
/** Computes a list of FileObjects in the right order
* that can represent this instance.
*
* @param name of resource to find
* @return enumeration of FileObject
*/
Enumeration delegates (final String name) {
Enumeration en = new ArrayEnumeration (systems);
Enumeration objsAndNulls = new AlterEnumeration (en) {
public Object alter (Object o) {
FileSystem fs = (FileSystem)o;
if (fs == null) {
return null;
} else {
return findResourceOn (fs, name);
}
}
};
return new FilterEnumeration (objsAndNulls);
}
/** Creates a file object that will mask the given file.
* @param fs filesystem to work on
* @param res resource name of the file
* @exception IOException if it fails
*/
void maskFile (FileSystem fs, String res) throws IOException {
FileObject where = findResourceOn (fs,fs.getRoot().getPath ());
FileUtil.createData (where, res + MASK);
}
/** Deletes a file object that will mask the given file.
* @param fs filesystem to work on
* @param res resource name of the file
* @exception IOException if it fails
*/
void unmaskFile (FileSystem fs, String res) throws IOException {
FileObject fo = findResourceOn (fs,res + MASK);
if (fo != null) {
FileLock lock = fo.lock ();
try {
fo.delete (lock);
} finally {
lock.releaseLock ();
}
}
}
/** Deletes a all mask files that mask the given file. All
* higher levels then fs are checked and mask is deleted if necessary
* @param fs filesystem where res is placed
* @param res resource name of the file that should be unmasked
* @exception IOException if it fails
*/
void unmaskFileOnAll (FileSystem fs,String res) throws IOException {
FileSystem[] fss = this.getDelegates();
for (int i = 0; i < fss.length ; i++) {
if (fss[i] == null || fss[i].isReadOnly())
continue;
unmaskFile (fss[i], res);
/** unamsk on all higher levels, which mask files on fs-layer */
if (fss[i] == fs) return;
}
}
static boolean isMaskFile(FileObject fo) {
return fo.getExt().endsWith(MASK);
}
}