alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 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-2004 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.openide.filesystems;

import java.beans.*;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.List;

import org.openide.util.actions.SystemAction;
import org.openide.util.NbBundle;

/** Interface that provides basic information about a virtual
* filesystem in the IDE. Classes that implement it
* should follow JavaBean conventions because when a new
* instance of a filesystem class is inserted into the system, it should
* permit the user to modify it with standard Bean properties.
* 

* Implementing classes should also have associated subclasses of {@link FileObject}. *

Although the class is serializable, only the {@link #isHidden hidden state} and {@link #getSystemName system name} * are serialized, and the deserialized object is by default {@link #isValid invalid} (and may be a distinct * object from a valid filesystem in the Repository). If you wish to safely deserialize a file * system, you should after deserialization try to replace it with a filesystem of the * {@link Repository#findFileSystem same name} in the Repository. * @author Jaroslav Tulach */ public abstract class FileSystem implements java.io.Serializable { /** generated Serialized Version UID */ static final long serialVersionUID = -8931487924240189180L; /** Property name indicating validity of filesystem. */ public static final String PROP_VALID = "valid"; // NOI18N /** * Property name indicating whether filesystem is hidden. * @deprecated The property is now hidden. */ public static final String PROP_HIDDEN = "hidden"; // NOI18N /** * Property name giving internal system name of filesystem. * @deprecated This system name should now be avoided in favor of identifying files persistently by URL. */ public static final String PROP_SYSTEM_NAME = "systemName"; // NOI18N /** Property name giving display name of filesystem. * @since 2.1 */ public static final String PROP_DISPLAY_NAME = "displayName"; // NOI18N /** Property name giving root folder of filesystem. */ public static final String PROP_ROOT = "root"; // NOI18N /** Property name giving read-only state. */ public static final String PROP_READ_ONLY = "readOnly"; // NOI18N /** Property name giving capabilities state. @deprecated No more capabilities. */ static final String PROP_CAPABILITIES = "capabilities"; // NOI18N /** is this filesystem valid? * It can be invalid if there is another filesystem with the * same name in the filesystem pool. */ transient private boolean valid = false; /** True if the filesystem is assigned to pool. * Is modified from Repository methods. */ transient boolean assigned = false; /**Repository that contains this FileSystem or null*/ private transient Repository repository = null; private transient FCLSupport fclSupport; /** Describes capabilities of the filesystem. */ private FileSystemCapability capability; /** property listener on FileSystemCapability. */ private transient PropertyChangeListener capabilityListener; /** hidden flag */ private boolean hidden = false; /** system name */ private String systemName = "".intern (); // NOI18N /** Utility field used by event firing mechanism. */ private transient ListenerList fileStatusList; private transient ListenerList vetoableChangeList; private transient PropertyChangeSupport changeSupport; /** Used for synchronization purpose*/ private static Object internLock = new Object (); /** Default constructor. */ public FileSystem () { } /** Should check for external modifications. All existing FileObjects will be * refreshed. For folders it should reread the content of disk, * for data file it should check for the last time the file has been modified. * * The default implementation is to do nothing, in contradiction to the rest * of the description. Unless subclasses override it, the method does not work. * * @param expected should the file events be marked as expected change or not? * @see FileEvent#isExpected * @since 2.16 */ public void refresh (boolean expected) { } /** Test whether filesystem is valid. * Generally invalidity would be caused by a name conflict in the filesystem pool. * @return true if the filesystem is valid */ public final boolean isValid () { return valid; } /** Setter for validity. Accessible only from filesystem pool. * @param v the new value */ final void setValid (boolean v) { if (v != valid) { valid = v; firePropertyChange (PROP_VALID, !v ? Boolean.TRUE : Boolean.FALSE, v ? Boolean.TRUE : Boolean.FALSE, Boolean.FALSE); } } /** Set hidden state of the object. * A hidden filesystem is not presented to the user in the Repository list (though it may be present in the Repository Settings list). * * @param hide true if the filesystem should be hidden * @deprecated This property is now useless. */ public final void setHidden (boolean hide) { if (hide != hidden) { hidden = hide; firePropertyChange (PROP_HIDDEN, !hide ? Boolean.TRUE : Boolean.FALSE, hide ? Boolean.TRUE : Boolean.FALSE); } } /** Getter for the hidden property. * @deprecated This property is now useless. */ public final boolean isHidden () { return hidden; } /** Tests whether filesystem will survive reloading of system pool. * If true then when * {@link Repository} is reloading its content, it preserves this * filesystem in the pool. *

* This can be used when the pool contains system level and user level * filesystems. The system ones should be preserved when the user changes * the content (for example when he is loading a new project). *

The default implementation returns false. * * @return true if the filesystem should be persistent * @deprecated This property is long since useless. */ protected boolean isPersistent () { return false; } /** Provides a name for the system that can be presented to the user. *

* This call should never be used to attempt to identify the file root * of the filesystem. On some systems it may happen to look the same but this is a * coincidence and may well change in the future. Either check whether * you are working with a {@link LocalFileSystem} or similar implementation and use * {@link LocalFileSystem#getRootDirectory}; or better, try * {@link FileUtil#toFile} which is designed to do this correctly. *

Note: for most purposes it is probably a bad idea to use * this method. Instead look at {@link FileUtil#getFileDisplayName}. * @return user presentable name of the filesystem */ public abstract String getDisplayName (); /** Internal (system) name of the filesystem. * Should uniquely identify the filesystem, as it will * be used during serialization of its files. The preferred way of doing this is to concatenate the * name of the filesystem type (e.g. the class) and the textual form of its parameters. *

* A change of the system name should be interpreted as a change of the internal * state of the filesystem. For example, if the root directory is moved to different * location, one should rebuild representations for all files * in the system. *

* This call should never be used to attempt to identify the file root * of the filesystem. On Unix systems it may happen to look the same but this is a * coincidence and may well change in the future. Either check whether * you are working with a {@link LocalFileSystem} or similar implementation and use * {@link LocalFileSystem#getRootDirectory}; or better, try * {@link FileUtil#toFile} which is designed to do this correctly. * @return string with system name * @deprecated The system name should now be avoided in favor of identifying files persistently by URL. */ public final String getSystemName () { return systemName; } /** Changes system name of the filesystem. * This property is bound and constrained: first of all * all vetoable listeners are asked whether they agree with the change. If so, * the change is made and all change listeners are notified of * the change. * *

Warning: this method is protected so that only subclasses can change * the system name. * * @param name new system name * @exception PropertyVetoException if the change is not allowed by a listener * @deprecated The system name should now be avoided in favor of identifying files persistently by URL. */ protected final void setSystemName (String name) throws PropertyVetoException { synchronized (Repository.class) { if (systemName.equals (name)) { return; } // I must be the only one who works with system pool (that is listening) // on this interface fireVetoableChange (PROP_SYSTEM_NAME, systemName, name); String old = systemName; systemName = name.intern (); firePropertyChange (PROP_SYSTEM_NAME, old, systemName); /** backward compatibility for FileSystems that don`t fire * PROP_DISPLAY_NAME*/ firePropertyChange (PROP_DISPLAY_NAME, null, null); } } /** Returns true if the filesystem is default one of the IDE. * @see Repository#getDefaultFileSystem */ public final boolean isDefault () { return this == ExternalUtil.getRepository ().getDefaultFileSystem (); } /** Test if the filesystem is read-only or not. * @return true if the system is read-only */ public abstract boolean isReadOnly (); /** Getter for root folder in the filesystem. * * @return root folder of whole filesystem */ public abstract FileObject getRoot (); /** Finds file in the filesystem by name. *

* The default implementation converts dots in the package name into slashes, * concatenates the strings, adds any extension prefixed by a dot and calls * the {@link #findResource findResource} method. * *

Note: when both of name and ext are null then name and * extension should be ignored and scan should look only for a package. * * @param aPackage package name where each package component is separated by a dot * @param name name of the file (without dots) or null if * one wants to obtain a folder (package) and not a file in it * @param ext extension of the file (without leading dot) or null if one needs * a package and not a file * * @return a file object that represents a file with the given name or * null if the file does not exist * @deprecated Please use the ClassPath API instead, or use {@link #findResource} if you are not interested in classpaths. */ public FileObject find (String aPackage, String name, String ext) { assert FileUtil.assertDeprecatedMethod(); StringBuffer bf = new StringBuffer (); // append package and name if (!aPackage.equals ("")) { // NOI18N String p = aPackage.replace ('.', '/'); bf.append (p); bf.append ('/'); } // append name if (name != null) { bf.append (name); } // append extension if there is one if (ext != null) { bf.append ('.'); bf.append (ext); } return findResource (bf.toString ()); } /** Finds a file given its full resource path. * @param name the resource path, e.g. "dir/subdir/file.ext" or "dir/subdir" or "dir" * @return a file object with the given path or * null if no such file exists */ public abstract FileObject findResource (String name); /** Returns an array of actions that can be invoked on any file in * this filesystem. * These actions should preferably * support the {@link org.openide.util.actions.Presenter.Menu Menu}, * {@link org.openide.util.actions.Presenter.Popup Popup}, * and {@link org.openide.util.actions.Presenter.Toolbar Toolbar} presenters. * * @return array of available actions */ public abstract SystemAction[] getActions (); public SystemAction[] getActions (java.util.Set foSet) { return this.getActions(); } /** Reads object from stream and creates listeners. * @param in the input stream to read from * @exception IOException error during read * @exception ClassNotFoundException when class not found */ private void readObject (java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException { in.defaultReadObject (); if (capability != null) { capability.addPropertyChangeListener(getCapabilityChangeListener()); } } public String toString () { return getSystemName () + "[" + super.toString () + "]"; // NOI18N } /** Allows filesystems to set up the environment for external execution * and compilation. * Each filesystem can add its own values that * influence the environment. The set of operations that can modify * environment is described by the {@link Environment} interface. *

* The default implementation throws an exception to signal that it does not * support external compilation or execution. * * @param env the environment to setup * @exception EnvironmentNotSupportedException if external execution * and compilation cannot be supported * @deprecated Please use the ClassPath API instead. */ public void prepareEnvironment (Environment env) throws EnvironmentNotSupportedException { throw new EnvironmentNotSupportedException (this); } /** Get a status object that can annotate a set of files by changing the names or icons * associated with them. *

* The default implementation returns a status object making no modifications. * * @return the status object for this filesystem */ public Status getStatus () { return STATUS_NONE; } /** The object describing capabilities of this filesystem. * Subclasses cannot override it. * @deprecated Capabilities are no longer used. */ public final FileSystemCapability getCapability () { if (capability == null) { capability = new FileSystemCapability.Bean(); capability.addPropertyChangeListener(getCapabilityChangeListener()); } return capability; } /** Allows subclasses to change a set of capabilities of the * filesystem. * @param capability the capability to use * @deprecated Capabilities are no longer used. */ protected final void setCapability (FileSystemCapability capability) { if (this.capability != null) this.capability.removePropertyChangeListener(getCapabilityChangeListener ()); this.capability = capability; if (this.capability != null) this.capability.addPropertyChangeListener (getCapabilityChangeListener ()); } /** Executes atomic action. The atomic action represents a set of * operations constituting one logical unit. It is guaranteed that during * execution of such an action no events about changes in the filesystem * will be fired. *

* Warning: the action should not take a significant amount of time, and should finish as soon as * possible--otherwise all event notifications will be blocked. *

Warning: do not be misled by the name of this method; * it does not require the filesystem to treat the changes as an atomic block of * commits in the database sense! That is, if an exception is thrown in the middle * of the action, partial results will not be undone (in general this would be * impossible to implement for all filesystems anyway). * @param run the action to run * @exception IOException if there is an IOException thrown in the actions' {@link AtomicAction#run run} * method */ public final void runAtomicAction (final AtomicAction run) throws IOException { getEventControl ().runAtomicAction (run); } /** * Begin of block, that should be performed without firing events. * Firing of events is postponed after end of block . * There is strong necessity to use always both methods: beginAtomicAction * and finishAtomicAction. It is recomended use it in try - finally block. * @param run Events fired from this atomic action will be marked as events * that were fired from this run. */ void beginAtomicAction (FileSystem.AtomicAction run) { getEventControl ().beginAtomicAction (run); } void beginAtomicAction () { beginAtomicAction (null); } /** * End of block, that should be performed without firing events. * Firing of events is postponed after end of block . * There is strong necessity to use always both methods: beginAtomicAction * and finishAtomicAction. It is recomended use it in try - finally block. */ void finishAtomicAction () { getEventControl ().finishAtomicAction (); } /** * Inside atomicAction adds an event dispatcher to the queue of FS events * and firing of events is postponed. If not event handlers are called directly. * @param run dispatcher to run */ void dispatchEvent (EventDispatcher run) { getEventControl ().dispatchEvent (run); } /** returns property listener on FileSystemCapability. */ private synchronized PropertyChangeListener getCapabilityChangeListener () { if (capabilityListener == null) { capabilityListener = new PropertyChangeListener() { public void propertyChange(java.beans.PropertyChangeEvent propertyChangeEvent) { firePropertyChange(PROP_CAPABILITIES, propertyChangeEvent.getOldValue() , propertyChangeEvent.getNewValue()); } }; } return capabilityListener; } private transient static ThreadLocal thrLocal = new ThreadLocal (); private final EventControl getEventControl () { EventControl evnCtrl = (EventControl)thrLocal.get(); if (evnCtrl == null) thrLocal.set(evnCtrl = new EventControl ()); return evnCtrl; } /** Registers FileStatusListener to receive events. * The implementation registers the listener only when getStatus () is * overriden to return a special value. * * @param listener The listener to register. */ public final void addFileStatusListener ( org.openide.filesystems.FileStatusListener listener ) { synchronized (internLock) { // JST: Ok? Do not register listeners when the fs cannot change status? if (getStatus () == STATUS_NONE) return; if (fileStatusList == null) fileStatusList = new ListenerList (FileStatusListener.class); fileStatusList.add (listener); } } /** Removes FileStatusListener from the list of listeners. *@param listener The listener to remove. */ public final void removeFileStatusListener ( org.openide.filesystems.FileStatusListener listener ) { if (fileStatusList == null) return; fileStatusList.remove (listener); } /** Notifies all registered listeners about change of status of some files. * * @param event The event to be fired */ protected final void fireFileStatusChanged(FileStatusEvent event) { if (fileStatusList == null) return; List listeners = java.util.Arrays.asList(fileStatusList.getAllListeners ()); dispatchEvent(new FileStatusDispatcher(listeners,event)); } /** Adds listener for the veto of property change. * @param listener the listener */ public final void addVetoableChangeListener( java.beans.VetoableChangeListener listener ) { synchronized (internLock) { if (vetoableChangeList == null) vetoableChangeList = new ListenerList (VetoableChangeListener.class); vetoableChangeList.add (listener); } } /** Removes listener for the veto of property change. * @param listener the listener */ public final void removeVetoableChangeListener( java.beans.VetoableChangeListener listener ) { if (vetoableChangeList == null) return; vetoableChangeList.remove (listener); } /** Fires property vetoable event. * @param name name of the property * @param o old value of the property * @param n new value of the property * @exception PropertyVetoException if an listener vetoed the change */ protected final void fireVetoableChange ( java.lang.String name, java.lang.Object o, java.lang.Object n ) throws PropertyVetoException { if (vetoableChangeList == null) return; java.beans.PropertyChangeEvent e = null; Object[] listeners = vetoableChangeList.getAllListeners (); for (int i = 0; i < listeners.length; i++) { if (e == null) e = new java.beans.PropertyChangeEvent (this, name, o, n); ((java.beans.VetoableChangeListener)listeners[i]).vetoableChange (e); } } /** Registers PropertyChangeListener to receive events. *@param listener The listener to register. */ public final void addPropertyChangeListener(PropertyChangeListener listener) { synchronized (internLock) { if (changeSupport == null) changeSupport = new PropertyChangeSupport(this); } changeSupport.addPropertyChangeListener(listener); } /** Removes PropertyChangeListener from the list of listeners. *@param listener The listener to remove. */ public final void removePropertyChangeListener(PropertyChangeListener listener) { if (changeSupport != null) changeSupport.removePropertyChangeListener(listener); } /** Fires property change event. * @param name name of the property * @param o old value of the property * @param n new value of the property */ protected final void firePropertyChange (String name, Object o, Object n) { firePropertyChange (name, o, n, null); } final void firePropertyChange (String name, Object o, Object n, Object propagationId) { if (changeSupport == null) return; if (o != null && n != null && o.equals(n)) return; PropertyChangeEvent e = new PropertyChangeEvent(this, name, o, n); e.setPropagationId(propagationId); changeSupport.firePropertyChange(e); } /** Notifies this filesystem that it has been added to the repository. * Various initialization tasks could go here. The default implementation does nothing. *

Note that this method is advisory and serves as an optimization * to avoid retaining resources for too long etc. Filesystems should maintain correct * semantics regardless of whether and when this method is called. */ public void addNotify () { } /** Notifies this filesystem that it has been removed from the repository. * Concrete filesystem implementations could perform clean-up here. * The default implementation does nothing. *

Note that this method is advisory and serves as an optimization * to avoid retaining resources for too long etc. Filesystems should maintain correct * semantics regardless of whether and when this method is called. */ public void removeNotify () { } /** An action that it is to be called atomically with respect to filesystem event notification. * During its execution (via {@link FileSystem#runAtomicAction runAtomicAction}) * no events about changes in filesystems are fired. *

Nomenclature warning: the action is by no means "atomic" * in the usual sense of the word, i.e. either running to completion or rolling * back. There is no rollback support. The actual semantic property here is * close to "isolation" - the action appears as a single operation as far as * listeners are concerned - but not quite, since it is perfectly possible for * some other thread to see half of the action if it happens to run during * that time. Generally it is a mistake to assume that using AtomicAction gives * you any kind of consistency guarantees; rather, it avoids producing change * events too early and thus causing listener code to run before it should. */ public static interface AtomicAction { /** Executed when it is guaranteed that no events about changes * in filesystems will be notified. * * @exception IOException if there is an error during execution */ public void run () throws IOException; } /** Interface that allows filesystems to set up the Java environment * for external execution and compilation. * Currently just used to append entries to the external class path. * @deprecated Please use the ClassPath API instead. */ public static abstract class Environment extends Object { public Environment() { assert FileUtil.assertDeprecatedMethod(); } /** Adds one element to the class path environment variable. * @param classPathElement string representing the one element * @deprecated Please use the ClassPath API instead. */ public void addClassPath (String classPathElement) { } } /** Allows a filesystem to annotate a group of files (typically comprising a data object) with additional markers. *

This could be useful, for * example, for a filesystem supporting version control. * It could annotate names and icons of data nodes according to whether the files were current, locked, etc. */ public static interface Status { /** Annotate the name of a file cluster. * @param name the name suggested by default * @param files an immutable set of {@link FileObject}s belonging to this filesystem * @return the annotated name (may be the same as the passed-in name) * @exception ClassCastException if the files in the set are not of valid types */ public String annotateName (String name, java.util.Set files); /** Annotate the icon of a file cluster. *

Please do not modify the original; create a derivative icon image, * using a weak-reference cache if necessary. * @param icon the icon suggested by default * @param iconType an icon type from {@link java.beans.BeanInfo} * @param files an immutable set of {@link FileObject}s belonging to this filesystem * @return the annotated icon (may be the same as the passed-in icon) * @exception ClassCastException if the files in the set are not of valid types */ public java.awt.Image annotateIcon (java.awt.Image icon, int iconType, java.util.Set files); } /** Extension interface for Status provides HTML-formatted annotations. * Principally this is used to deëmphasize status text by presenting * it in a lighter color, by placing it inside * <font color=!controlShadow> tags. Note that it is preferable to * use logical colors (such as controlShadow) which are resolved by calling * UIManager.getColor(key) — this way they will always fit with the * look and feel. To use a logical color, prefix the color name with a * ! character. *

* Please use only the limited markup subset of HTML supported by the * lightweight HTML renderer. * @see org.openide.awt.HtmlRenderer * @since 4.30 */ public static interface HtmlStatus extends Status { /** Annotate a name such that the returned value contains HTML markup. * The return value less the html content should typically be the same * as the return value from annotateName(). This is used, * for example, by VCS filesystems to deëphasize the status information * included in the file name by using a light grey font color. *

* For consistency with Node.getHtmlDisplayName(), * filesystems that proxy other filesystems (and so must implement * this interface to supply HTML annotations) should return null if * the filesystem they proxy does not provide an implementation of * HTMLStatus. * @since 4.30 * @see org.openide.awt.HtmlRenderer * @see DataNode.getHtmlDisplayName() * @see org.openide.nodes.Node#getHtmlDisplayName **/ public String annotateNameHtml (String name, java.util.Set files); } /** Empty status */ private static final Status STATUS_NONE = new Status () { public String annotateName (String name, java.util.Set files) { return name; } public java.awt.Image annotateIcon (java.awt.Image icon, int iconType, java.util.Set files) { return icon; } }; /** Class used to notify events for the filesystem. */ static abstract class EventDispatcher extends Object implements Runnable { public final void run () { dispatch (false); } /** @param onlyPriority if true then invokes only priority listeners * else all listeners are invoked. */ protected abstract void dispatch (boolean onlyPriority); /** @param propID */ protected abstract void setAtomicActionLink (EventControl.AtomicActionLink propID); } private static class FileStatusDispatcher extends EventDispatcher { private java.util.List listeners; private FileStatusEvent fStatusEvent; public FileStatusDispatcher( java.util.List listeners, FileStatusEvent fStatusEvent) { this.listeners = listeners; this.fStatusEvent = fStatusEvent; } protected void dispatch(boolean onlyPriority) { if (onlyPriority) return; for (int i = 0; i < listeners.size(); i++) { FileStatusListener fStatusListener = (FileStatusListener) listeners.get(i); fStatusListener.annotationChanged(fStatusEvent); } } protected void setAtomicActionLink(EventControl.AtomicActionLink propID) { /** empty no fireFrom in FileStatusEvent*/ } } /** Getter for the resource string * @param s the resource name * @return the resource */ static String getString(String s) { /*This call to getBundle should ensure that currentClassLoader is not used to load resources from. This should prevent from deadlock, that occured: one waits for FileObject and has resource, second one waits for resource and has FileObject*/ return NbBundle.getBundle("org.openide.filesystems.Bundle", java.util.Locale.getDefault(), FileSystem.class.getClassLoader ()).getString (s); } /** Creates message for given string property with one parameter. * @param s resource name * @param obj the parameter to the message * @return the string for that text */ static String getString (String s, Object obj) { return MessageFormat.format (getString (s), new Object[] { obj }); } /** Creates message for given string property with two parameters. * @param s resource name * @param obj1 the parameter to the message * @param obj2 the parameter to the message * @return the string for that text */ static String getString (String s, Object obj1, Object obj2) { return MessageFormat.format (getString (s), new Object[] { obj1, obj2 }); } /** Creates message for given string property with three parameters. * @param s resource name * @param obj1 the parameter to the message * @param obj2 the parameter to the message * @param obj3 the parameter to the message * @return the string for that text */ static String getString (String s, Object obj1, Object obj2, Object obj3) { return MessageFormat.format (getString (s), new Object[] { obj1, obj2, obj3 }); } /** getter for Repository * @return Repository that contains this FileSystem or null if FileSystem * is not part of any Repository */ final Repository getRepository() { return repository; } void setRepository(Repository rep) { repository = rep; } final FCLSupport getFCLSupport() { synchronized (FCLSupport.class) { if (fclSupport == null) fclSupport = new FCLSupport (); } return fclSupport; } /** Add new listener to this object. * @param fcl the listener * @since 2.8 */ public final void addFileChangeListener(FileChangeListener fcl) { getFCLSupport ().addFileChangeListener(fcl); } /** Remove listener from this object. * @param fcl the listener * @since 2.8 */ public final void removeFileChangeListener(FileChangeListener fcl) { getFCLSupport ().removeFileChangeListener(fcl); } }

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.