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

package org.netbeans;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

/**
 * Command Line Interface and User Directory Locker support class.
 * Subclasses may be registered into the system to handle special command-line options.
 * To be registered, use META-INF/services/org.netbeans.CLIHandler
 * in a JAR file in the startup or dynamic class path (e.g. lib/ext/
 * or lib/).
 * @author Jaroslav Tulach
 * @since org.netbeans.core/1 1.18
 * @see "#32054"
 * @see Specification
 */
public abstract class CLIHandler extends Object {
    /** lenght of the key used for connecting */
    private static final int KEY_LENGTH = 10;
    /** ok reply */
    private static final int REPLY_OK = 1;
    /** sends exit code */
    private static final int REPLY_EXIT = 2;
    /** fail reply */
    private static final int REPLY_FAIL = 0;
    /** the server is active, but cannot compute the value now */
    private static final int REPLY_DELAY = 3;
    
    /** request to read from input stream */
    private static final int REPLY_READ = 10;
    /** request to write */
    private static final int REPLY_WRITE = 11;
    /** request to find out how much data is available */
    private static final int REPLY_AVAILABLE = 12;
    
    /**
     * Used during bootstrap sequence. Should only be used by core, not modules.
     */
    public static final int WHEN_BOOT = 1;
    /**
     * Used during later initialization or while NetBeans is up and running.
     */
    public static final int WHEN_INIT = 2;
    /** Testing output of the threads.
     */
    private static StringBuffer OUTPUT;
    
    private int when;
    
    /**
     * Create a CLI handler and indicate its preferred timing.
     * @param when when to run the handler: {@link #WHEN_BOOT} or {@link #WHEN_INIT}
     */
    protected CLIHandler(int when) {
        this.when = when;
    }
    
    /**
     * Process some set of command-line arguments.
     * Unrecognized or null arguments should be ignored.
     * Recognized arguments should be nulled out.
     * @param args arguments
     * @return error value or 0 if everything is all right
     */
    protected abstract int cli(Args args);
    
    private static void showHelp(PrintWriter w, List handlers) {
//        w.println("  -? or --help          Show this help information.");
        Iterator it = handlers.iterator();
        while (it.hasNext()) {
            ((CLIHandler)it.next()).usage(w);
        }
    }
    
    /**
     * Print usage information for this handler.
     * @param w a writer to print to
     */
    protected abstract void usage(PrintWriter w);
    
    /** For testing purposes we can block the
     * algorithm in any place in the initialize method.
     */
    private static void enterState(int state, Integer block) {
        StringBuffer output = OUTPUT;
        if (output != null) {
            synchronized (output) {
                // for easier debugging of CLIHandlerTest
                output.append ("state: ");
                output.append (state);
                output.append (" thread: ");
                output.append (Thread.currentThread());
                if (block == null) {
                    output.append ('\n');
                }
            }
        }
        
        if (block == null) return;

        
        synchronized (block) {
            if (state == block.intValue()) {
                if (output != null) {
                    output.append (" blocked\n");
                }
                block.notifyAll();
                try {
                    block.wait();
                } catch (InterruptedException ex) {
                    throw new IllegalStateException();
                }
            } else {
                if (output != null) {
                    output.append (" not blocked\n");
                }
            }
        }
    }
    
    /** Notification of available handlers.
     * @return non-zero if one of the handlers fails
     */
    private static int notifyHandlers(Args args, List handlers, int when, boolean failOnUnknownOptions, boolean consume) {
        try {
            //System.err.println("notifyHandlers: handlers=" + handlers + " when=" + when + " args=" + Arrays.asList(args.getArguments()));
            String[] argv = args.getArguments();
            for (int i = 0; i < argv.length; i++) {
                assert argv[i] != null;
                if (argv[i].equals("-?") || argv[i].equals("--help") || argv[i].equals ("-help")) { // NOI18N
                    PrintWriter w = new PrintWriter(args.getOutputStream());
                    showHelp(w, handlers);
                    w.flush();
                    return 2;
                }
            }
            int r = 0;
            Iterator it = handlers.iterator();
            while (it.hasNext()) {
                CLIHandler h = (CLIHandler)it.next();
                if (h.when != when) continue;

                r = h.cli(args);
                //System.err.println("notifyHandlers: exit code " + r + " from " + h);
                if (r != 0) {
                    return r;
                }
            }
            if (failOnUnknownOptions) {
                argv = args.getArguments();
                for (int i = 0; i < argv.length; i++) {
                    if (argv[i] != null) {
                        // Unhandled option.
                        PrintWriter w = new PrintWriter(args.getOutputStream());
                        w.println("Ignored unknown option: " + argv[i]); // NOI18N

                        // XXX(-ttran) not good, this doesn't show the help for
                        // switches handled by the launcher
                        //
                        //showHelp(w, handlers);
                        
                        w.flush();
                        return 2;
                    }
                }
            }
            return 0;
        } finally {
            args.reset(consume);
        }
    }
    
    /**
     * Represents result of initialization.
     * @see #initialize(String[], ClassLoader)
     * @see #initialize(Args, Integer, List)
     */
    static final class Status {
        public static final int CANNOT_CONNECT = -255;
        
        private final File lockFile;
        private final int port;
        private int exitCode;
        /**
         * General failure.
         */
        Status() {
            this(0);
        }
        /**
         * Failure due to a parse problem.
         * @param c bad status code (not 0)
         * @see #cli(Args)
         */
        Status(int c) {
            this(null, 0, c);
        }
        /**
         * Some measure of success.
         * @param l the lock file (not null)
         * @param p the server port (not 0)
         * @param c a status code (0 or not)
         */
        Status(File l, int p, int c) {
            lockFile = l;
            port = p;
            exitCode = c;
        }
        
        /**
         * Get the lock file, if available.
         * @return the lock file, or null if there is none
         */
        public File getLockFile() {
            return lockFile;
        }
        /**
         * Get the server port, if available.
         * @return a port number for the server, or 0 if there is no port open
         */
        public int getServerPort() {
            return port;
        }
        /**
         * Get the CLI parse status.
         * @return 0 for success, some other value for error conditions
         */
        public int getExitCode() {
            return exitCode;
        }
    }
    
    
    /** Initializes the system by creating lock file.
     *
     * @param args the command line arguments to recognize
     * @param classloader to find command CLIHandlers in
     * @param failOnUnknownOptions if true, fail (status 2) if some options are not recognized (also checks for -? and -help)
     * @param cleanLockFile removes lock file if it appears to be dead
     * @return the file to be used as lock file or null parsing of args failed
     */
    static Status initialize(
        String[] args, 
        InputStream is, 
        OutputStream os, 
        Main.BootClassLoader loader,
        boolean failOnUnknownOptions, 
        boolean cleanLockFile,
        Runnable runWhenHome
    ) {
        return initialize(
            new Args(args, is, os, System.getProperty ("user.dir")), 
            (Integer)null, 
            loader.allCLIs(), 
            failOnUnknownOptions, 
            cleanLockFile, 
            runWhenHome
        );
    }
    
    /**
     * What to do later when {@link #finishInitialization} is called.
     * May remain null, otherwise contains list of Execute
     */
    private static List doLater = new ArrayList ();
    static interface Execute {
        /** @return returns exit code */
        public int exec ();
    }
    
    /** Execute this runnable when finishInitialization method is called.
     */
    private static int registerFinishInstallation (Execute run) {
        boolean runNow;
    
        synchronized (CLIHandler.class) {
            if (doLater != null) {
                doLater.add (run);
                runNow = false;
            } else {
                runNow = true;
            }
        }
        
        if (runNow) {
            return run.exec ();
        }
        
        return 0;
    }

    /**
     * Run any {@link #WHEN_INIT} handlers that were passed to the original command line.
     * Should be called when the system is up and ready.
     * Cancels any existing actions, in case it is called twice.
     * @return the result of executing the handlers
     */
    static int finishInitialization (boolean recreate) {
        List toRun;
        synchronized (CLIHandler.class) {
            toRun = doLater;
            doLater = recreate ? new ArrayList () : null;
            if (!recreate) {
                CLIHandler.class.notifyAll ();
            }
        }
        
        if (toRun != null) {
            Iterator it = toRun.iterator ();
            while (it.hasNext ()) {
                Execute r = (Execute)it.next ();
                int result = r.exec ();
                if (result != 0) {
                    return result;
                }
            }
        }
        return 0;
    }
    
    /** Blocks for a while and waits if the finishInitialization method
     * was called.
     * @param timeout ms to wait
     * @return true if finishInitialization is over
     */
    private static synchronized boolean waitFinishInstallationIsOver (int timeout) {
        if (doLater != null) {
            try {
                CLIHandler.class.wait (timeout);
            } catch (InterruptedException ex) {
                // go on, never mind
            }
        }
        return doLater == null;
    }
    
    /** Registers debugging output for tests.
     */
    static void registerDebug (StringBuffer sb) {
        OUTPUT = sb;
    }
    
    /** Enhanced search for localhost address that works also behind VPN
     */
    private static InetAddress localHostAddress () throws IOException {
        java.net.NetworkInterface net = java.net.NetworkInterface.getByName ("lo");
        if (net == null || !net.getInetAddresses ().hasMoreElements ()) {
            return InetAddress.getLocalHost (); 
        } else {
            return (InetAddress)net.getInetAddresses ().nextElement ();
        }
    }
    
    /** Initializes the system by creating lock file.
     *
     * @param args the command line arguments to recognize
     * @param block the state we want to block in
     * @param handlers all handlers to use
     * @param failOnUnknownOptions if true, fail (status 2) if some options are not recognized (also checks for -? and -help)
     * @param cleanLockFile removes lock file if it appears to be dead
     * @param runWhenHome runnable to be executed when netbeans.user property is set
     * @return a status summary
     */
    static Status initialize(
        final Args args, Integer block, 
        final List handlers, 
        final boolean failOnUnknownOptions, 
        boolean cleanLockFile,
        Runnable runWhenHome
    ) {
        // initial parsing of args
        {
            int r = notifyHandlers(args, handlers, WHEN_BOOT, false, failOnUnknownOptions);
            if (r != 0) {
                return new Status(r);
            }
        }
        
        // get the value
        String home = System.getProperty("netbeans.user"); // NOI18N
        if (home == null) {
            home = System.getProperty("user.home"); // NOI18N
            System.setProperty ("netbeans.user", home); // NOI18N
        }
        
        if (runWhenHome != null) {
            // notify that we have successfully identified the home property
            runWhenHome.run ();
        }
        
        
        File lockFile = new File(home, "lock"); // NOI18N
        
        for (int i = 0; i < 5; i++) {
            // try few times to succeed
            try {
                if (lockFile.exists()) {
                    enterState(5, block);
                    throw new IOException("EXISTS"); // NOI18N
                }
                lockFile.getParentFile().mkdirs();
                lockFile.createNewFile();
                lockFile.deleteOnExit();
                try {
                    // try to make it only user-readable (on Unix)
                    // since people are likely to leave a+r on their userdir
                    File chmod = new File("/bin/chmod"); // NOI18N
                    if (!chmod.isFile()) {
                        // Linux uses /bin, Solaris /usr/bin, others hopefully one of those
                        chmod = new File("/usr/bin/chmod"); // NOI18N
                    }
                    if (chmod.isFile()) {
                        int chmoded = Runtime.getRuntime().exec(new String[] {
                            chmod.getAbsolutePath(),
                            "go-rwx", // NOI18N
                            lockFile.getAbsolutePath()
                        }).waitFor();
                        if (chmoded != 0) {
                            throw new IOException("could not run " + chmod + " go-rwx " + lockFile); // NOI18N
                        }
                    }
                } catch (InterruptedException e) {
                    throw (IOException)new IOException(e.toString()).initCause(e);
                }
                
                enterState(10, block);
                
                byte[] arr = new byte[KEY_LENGTH];
                try {
                    SecureRandom.getInstance("SHA1PRNG").nextBytes(arr); // NOI18N
                } catch (NoSuchAlgorithmException e) {
                    // #36966: IBM JDK doesn't have it.
                    try {
                        SecureRandom.getInstance("IBMSecureRandom").nextBytes(arr); // NOI18N
                    } catch (NoSuchAlgorithmException e2) {
                        // OK, disable server...
                        System.err.println("WARNING: remote IDE automation features cannot be cryptographically secured, so disabling; please reopen http://www.netbeans.org/issues/show_bug.cgi?id=36966"); // NOI18N
                        e.printStackTrace();
                        return new Status();
                    }
                }
                
                Server server = new Server(arr, block, handlers, failOnUnknownOptions);
                
                DataOutputStream os = new DataOutputStream(new FileOutputStream(lockFile));
                int p = server.getLocalPort();
                os.writeInt(p);
                
                enterState(20, block);
                
                os.write(arr);
                os.close();
                
                int execCode = registerFinishInstallation (new Execute () {
                    public int exec () {
                        return notifyHandlers(args, handlers, WHEN_INIT, failOnUnknownOptions, failOnUnknownOptions);
                    }
                });
                
                enterState(0, block);
                return new Status(lockFile, server.getLocalPort(), execCode);
            } catch (IOException ex) {
                if (!"EXISTS".equals(ex.getMessage())) { // NOI18N
                    ex.printStackTrace();
                }
                // already exists, try to read
                byte[] key = null;
                int port = -1;
                try {
                    DataInputStream is = new DataInputStream(new FileInputStream(lockFile));
                    port = is.readInt();
                    key = new byte[KEY_LENGTH];
                    is.readFully(key);
                    is.close();
                } catch (IOException ex2) {
                    // ok, try to read it once more
                }
                
                if (key != null && port != -1) {
                    try {
                        // ok, try to connect
                        enterState(28, block);
                        Socket socket = new Socket(localHostAddress (), port);
                        // wait max of 1s for reply
                        socket.setSoTimeout(5000);
                        DataOutputStream os = new DataOutputStream(socket.getOutputStream());
                        os.write(key);
                        os.flush();
                        
                        enterState(30, block);
                        
                        DataInputStream replyStream = new DataInputStream(socket.getInputStream());
                        COMMUNICATION: for (;;) {
                            enterState(32, block);
                            int reply = replyStream.read();
                            //System.err.println("reply=" + reply);
                            enterState(34, block);
                            
                            switch (reply) {
                                case REPLY_FAIL:
                                    enterState(36, block);
                                    break COMMUNICATION;
                                case REPLY_OK:
                                    enterState(38, block);
                                    // write the arguments
                                    String[] arr = args.getArguments();
                                    os.writeInt(arr.length);
                                    for (int a = 0; a < arr.length; a++) {
                                        os.writeUTF(arr[a]);
                                    }
                                    os.writeUTF (args.getCurrentDirectory().toString()); 
                                    os.flush();
                                    break;
                                case REPLY_EXIT:
                                    int exitCode = replyStream.readInt();
                                    if (exitCode == 0) {
                                        // to signal end of the world
                                        exitCode = -1;
                                    }
                                    
                                    os.close();
                                    replyStream.close();
                                    
                                    enterState(0, block);
                                    return new Status(lockFile, port, exitCode);
                                case REPLY_READ: {
                                    enterState(42, block);
                                    int howMuch = replyStream.readInt();
                                    byte[] byteArr = new byte[howMuch];
                                    int really = args.getInputStream().read(byteArr);
                                    os.write(really);
                                    if (really > 0) {
                                        os.write(byteArr, 0, really);
                                    }
                                    os.flush();
                                    break;
                                }
                                case REPLY_WRITE: {
                                    enterState(44, block);
                                    int howMuch = replyStream.readInt();
                                    byte[] byteArr = new byte[howMuch];
                                    replyStream.read(byteArr);
                                    args.getOutputStream().write(byteArr);
                                    break;
                                }
                                case REPLY_AVAILABLE:
                                    enterState(46, block);
                                    os.writeInt(args.getInputStream().available());
                                    os.flush();
                                    break;
                                case REPLY_DELAY:
                                    // ok, try once more
                                    break;
                                case -1:
                                    // EOF. Why does this happen?
                                    break;
                                default:
                                    assert false : reply;
                            }
                        }
                        
                        // connection ok, but secret key not recognized
                        // delete the lock file
                    } catch (java.net.SocketTimeoutException ex2) {
                        // connection failed, the port is dead
                        enterState(33, block);
                    } catch (java.net.ConnectException ex2) {
                        // connection failed, the port is dead
                        enterState(33, block);
                    } catch (IOException ex2) {
                        // some strange exception
                        ex2.printStackTrace();
                        enterState(33, block);
                    }
                    if (cleanLockFile) {
                        // remove the file and try once more
                        lockFile.delete();
                    } else {
                        return new Status (Status.CANNOT_CONNECT);
                    }
                }
            }
            
            try {
                enterState(83, block);
                Thread.sleep((int)(Math.random() * 1000.00));
                enterState(85, block);
            } catch (InterruptedException ex) {
                // means nothing
            }
        }
        
        // failure
        return new Status();
    }

    /** Class that represents available arguments to the CLI
     * handlers.
     */
    public static final class Args extends Object {
        private String[] args;
        private final String[] argsBackup;
        private InputStream is;
        private OutputStream os;
        private File currentDir;
        
        Args(String[] args, InputStream is, OutputStream os, String currentDir) {
            argsBackup = args;
            reset(false);
            this.is = is;
            this.os = os;
            this.currentDir = new File (currentDir);
        }
        
        /**
         * Restore the arguments list to a clean state.
         * If not consuming arguments, it is just set to the original list.
         * If consuming arguments, any nulled-out arguments are removed from the list.
         */
        void reset(boolean consume) {
            if (consume) {
                String[] a = args;
                if (a == null) {
                    a = argsBackup;
                }
                List l = new ArrayList(Arrays.asList(a));
                l.removeAll(Collections.singleton(null));
                args = (String[])l.toArray(new String[l.size()]);
            } else {
                args = (String[])argsBackup.clone();
            }
        }
        
        /**
         * Get the command-line arguments.
         * You may not modify the returned array except to set some elements
         * to null as you recognize them.
         * @return array of string arguments, may contain nulls
         */
        public String[] getArguments() {
            return args;
        }
        
        /**
         * Get an output stream to which data may be sent.
         * @return stream to write to
         */
        public OutputStream getOutputStream() {
            return os;
        }
        
        public File getCurrentDirectory () {
            return currentDir;
        }
        
        /**
         * Get an input stream that may supply additional data.
         * @return stream to read from
         */
        public InputStream getInputStream() {
            return is;
        }
    } // end of Args
    
    /** Server that creates local socket and communicates with it.
     */
    private static final class Server extends Thread {
        private byte[] key;
        private ServerSocket socket;
        private Integer block;
        private List handlers;
        private Socket work;
        private static volatile int counter;
        private final boolean failOnUnknownOptions;
        
        public Server(byte[] key, Integer block, List handlers, boolean failOnUnknownOptions) throws IOException {
            super("CLI Requests Server"); // NOI18N
            this.key = key;
            this.setDaemon(true);
            this.block = block;
            this.handlers = handlers;
            this.failOnUnknownOptions = failOnUnknownOptions;
            
            socket = new ServerSocket(0);
            
            start();
        }
        
        public Server(Socket request, byte[] key, Integer block, List handlers, boolean failOnUnknownOptions) throws IOException {
            super("CLI Handler Thread Handler: " + ++counter); // NOI18N
            this.key = key;
            this.setDaemon(true);
            this.block = block;
            this.handlers = handlers;
            this.work = request;
            this.failOnUnknownOptions = failOnUnknownOptions;
            
            start();
        }
        
        public int getLocalPort() {
            return socket.getLocalPort();
        }
        
        public void run() {
            if (work != null) {
                // I am a worker not listener server
                try {
                    handleConnect(work);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                return;
            }
            
            
            while (true) {
                try {
                    enterState(65, block);
                    Socket s = socket.accept();
                    
                    // spans new request handler
                    new Server(s, key, block, handlers, failOnUnknownOptions);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        
        
        private void handleConnect(Socket s) throws IOException {
            
            byte[] check = new byte[key.length];
            DataInputStream is = new DataInputStream(s.getInputStream());
            
            enterState(70, block);
            
            is.readFully(check);
            
            enterState(90, block);
            
            final DataOutputStream os = new DataOutputStream(s.getOutputStream());
            
            if (Arrays.equals(check, key)) {
                while (!waitFinishInstallationIsOver (2000)) {
                    os.write (REPLY_DELAY);
                    os.flush ();
                }
                
                enterState(93, block);
                os.write(REPLY_OK);
                os.flush();
                
                // continue with arguments
                int numberOfArguments = is.readInt();
                String[] args = new String[numberOfArguments];
                for (int i = 0; i < args.length; i++) {
                    args[i] = is.readUTF();
                }
                final String currentDir = is.readUTF ();
                
                final Args arguments = new Args(args, new IS(is, os), new OS(is, os), currentDir);

                class ComputingAndNotifying extends Thread {
                    public int res;
                    public boolean finished;
                    
                    public ComputingAndNotifying () {
                        super ("Computes values in handlers");
                    }
                    
                    public void run () {
                        try {
                            res = notifyHandlers (arguments, handlers, WHEN_INIT, failOnUnknownOptions, false);

                            if (res == 0) {
                                enterState (98, block);
                            } else {
                                enterState (99, block);
                            }
                        } finally {
                            synchronized (this) {
                                finished = true;
                                notifyAll ();
                            }
                        }
                    }
                    
                    public synchronized void waitForResultAndNotifyOthers () {
                        // execute the handlers in another thread
                        start ();
                        while (!finished) {
                            try {
                                wait (1000);
                                os.write (REPLY_DELAY);
                                os.flush ();
                            } catch (InterruptedException ex) {
                                ex.printStackTrace();
                            } catch (IOException ex) {
                                ex.printStackTrace();
                            }
                        }
                    }
                }
                ComputingAndNotifying r = new ComputingAndNotifying ();
                r.waitForResultAndNotifyOthers ();
                os.write(REPLY_EXIT);
                os.writeInt(r.res);
            } else {
                enterState(103, block);
                os.write(REPLY_FAIL);
            }
            
            
            enterState(120, block);
            
            os.close();
            is.close();
        }
        
        private static final class IS extends InputStream {
            private DataInputStream is;
            private DataOutputStream os;
            
            public IS(DataInputStream is, DataOutputStream os) {
                this.is = is;
                this.os = os;
            }
            
            public int read() throws IOException {
                byte[] arr = new byte[1];
                if (read(arr) == 1) {
                    return arr[0];
                } else {
                    return -1;
                }
            }
            
            public void close() throws IOException {
                super.close();
            }
            
            public int available() throws IOException {
                // ask for data
                os.write(REPLY_AVAILABLE);
                os.flush();
                // read provided data
                return is.readInt();
            }
            
            public int read(byte[] b) throws IOException {
                return read(b, 0, b.length);
            }
            
            public int read(byte[] b, int off, int len) throws IOException {
                // ask for data
                os.write(REPLY_READ);
                os.writeInt(len);
                os.flush();
                // read provided data
                int really = is.read ();
                if (really > 0) {
                    return is.read(b, off, really);
                } else {
                    return really;
                }
            }
            
        } // end of IS
        
        private static final class OS extends OutputStream {
            private DataOutputStream os;
            
            public OS(DataInputStream is, DataOutputStream os) {
                this.os = os;
            }
            
            public void write(int b) throws IOException {
                byte[] arr = { (byte)b };
                write(arr);
            }
            
            public void write(byte[] b) throws IOException {
                write(b, 0, b.length);
            }
            
            public void close() throws IOException {
                super.close();
            }
            
            public void flush() throws IOException {
                os.flush();
            }
            
            public void write(byte[] b, int off, int len) throws IOException {
                os.write(REPLY_WRITE);
                os.writeInt(len);
                os.write(b, off, len);
            }
            
        } // end of OS
        
    } // end of Server
    
    
}
... 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.