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

/*
 * MM JDBC Drivers for MySQL
 *
 * $Id: Connection.java,v 1.2 1998/08/25 00:53:46 mmatthew Exp $
 *
 * Copyright (C) 1998 Mark Matthews <mmatthew@worldserver.com>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 * 
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 *
 * See the COPYING file located in the top-level-directory of
 * the archive of this library for complete text of license.
 *
 * Some portions:
 *
 * Copyright (c) 1996 Bradley McLean / Jeffrey Medeiros
 * Modifications Copyright (c) 1996/1997 Martin Rode
 * Copyright (c) 1997 Peter T Mount
 */

/**
 * A Connection represents a session with a specific database.  Within the
 * context of a Connection, SQL statements are executed and results are
 * returned.
 *
 * <P>A Connection's database is able to provide information describing
 * its tables, its supported SQL grammar, its stored procedures, the
 * capabilities of this connection, etc.  This information is obtained
 * with the getMetaData method.
 *
 * <p>Note: MySQL does not support transactions, so all queries
 *                 are committed as they are executed.
 *
 * @see java.sql.Connection
 * @author Mark Matthews <mmatthew@worldserver.com>
 * @version $Id$
 */

package org.gjt.mm.mysql;

import java.io.UnsupportedEncodingException;

import java.sql.*;
import java.util.Properties;


public class Connection implements java.sql.Connection
{
    MysqlIO _IO                 = null;
    private boolean _isClosed   = true;
    
    private String  _Host       = null;
    private int     _port       = 3306;
    private String  _User       = null;
    private String  _Password   = null;
    private String  _Database   = null;
    
    private boolean _autoCommit = true;
    private boolean _readOnly   = false;

    private boolean _do_unicode = false;
    private String  _Encoding   = null;
    
    private String  _MyURL      = null;

    private int     _max_rows   = -1;
    private boolean _max_rows_changed = false;

    private org.gjt.mm.mysql.Driver _MyDriver;

    //
    // This is for the high availability :) routines 
    //

    private boolean _high_availability = false;
    private int     _max_reconnects    = 3;
    private double  _initial_timeout   = 2.0D;
    
    // The command used to "ping" the database.
    // Newer versions of MySQL server have a ping() command,
    // but this works for everything.

    private static final String _PING_COMMAND = "SELECT 1";

    /**
     * Connect to a MySQL Server.
     *
     * <p>Important Notice
     *
     * <br>Although this will connect to the database, user code should open
     * the connection via the DriverManager.getConnection() methods only.
     *
     * <br>This should only be called from the org.gjt.mm.mysql.Driver class.
     *
     * @param Host the hostname of the database server
     * @param port the port number the server is listening on
     * @param Info a Properties[] list holding the user and password
     * @param Database the database to connect to
     * @param Url the URL of the connection
     * @param D the Driver instantation of the connection
     * @return a valid connection profile
     * @exception java.sql.SQLException if a database access error occurs
     */

  public Connection(String Host, int port, Properties Info, String Database, 
                    String Url, Driver D) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {Host, new Integer(port), Info,
			   Database, Url, D};
	  Debug.methodCall(this, "constructor", Args);
      }

      if (Host == null) {
	  _Host = "localhost";
      }
      else {
	  _Host = new String(Host);
      }
      
      _port = port;
      
      if (Database == null) {
	  throw new SQLException("Malformed URL '" + Url + "'.", "S1000");
      }
      _Database = new String(Database);
      
      _MyURL = new String(Url);
      _MyDriver = D;
      
      String U = Info.getProperty("user");
      String P = Info.getProperty("password");
      
      if (U == null || U.equals(""))
	  _User = "nobody";
      else
	  _User = new String(U);
      
      if (P == null)
	  _Password = "";
      else
	  _Password = new String(P);
      
      // Check for driver specific properties
      
      if (Info.getProperty("autoReconnect") != null) {
	  _high_availability = Info.getProperty("autoReconnect").toUpperCase().equals("TRUE");
      }
      
      if (_high_availability) {
	  if (Info.getProperty("maxReconnects") != null) {
	      try {
		  int n = Integer.parseInt(Info.getProperty("maxReconnects"));
		  _max_reconnects = n;
	      }
	      catch (NumberFormatException NFE) {
		  throw new SQLException("Illegal parameter '" + 
					 Info.getProperty("maxReconnects") 
					 +"' for maxReconnects", "0S100");
	      }
	  }
	  
	  if (Info.getProperty("initialTimeout") != null) {
	      try {
		  double n = Integer.parseInt(Info.getProperty("intialTimeout"));
		  _initial_timeout = n;
	      }
	      catch (NumberFormatException NFE) {
		  throw new SQLException("Illegal parameter '" + 
					 Info.getProperty("initialTimeout") 
					 +"' for initialTimeout", "0S100");
	      }
	  }
      }
      
      if (Info.getProperty("maxRows") != null) {
	  try {
	      int n = Integer.parseInt(Info.getProperty("maxRows"));
	      
	      if (n == 0) {
		  n = -1;
	      } // adjust so that it will become MysqlDefs.MAX_ROWS
              // in execSQL()
	      _max_rows = n;
	  }
	  catch (NumberFormatException NFE) {
	      throw new SQLException("Illegal parameter '" + 
				     Info.getProperty("maxRows") 
				     +"' for maxRows", "0S100");
	  }
      }
      
      if (Info.getProperty("useUnicode") != null) {
	  String UseUnicode = Info.getProperty("useUnicode").toUpperCase();
	  if (UseUnicode.startsWith("TRUE")) {
	      _do_unicode = true;
	  }
	  if (Info.getProperty("characterEncoding") != null) {
	      _Encoding = Info.getProperty("characterEncoding");
	      
	      // Attempt to use the encoding, and bail out if it
	      // can't be used
	      try {
		  String TestString = "abc";
		  TestString.getBytes(_Encoding);
	      }
	      catch (UnsupportedEncodingException UE) {
		  throw new SQLException("Unsupported character encoding '" + 
					 _Encoding + "'.", "0S100"); 
	      }
	  }
      }
      
      if (Driver.debug)
	  System.out.println("Connect: " + _User + " to " + _Database);
      try {
	  _IO = new MysqlIO(Host, port);
	  _IO.init(_User, _Password);
	  _IO.sendCommand(MysqlDefs.INIT_DB, _Database, null);
	  _isClosed = false;
      } 
      catch (java.sql.SQLException E) {
	  throw E;
      }
      catch (Exception E) {
	  E.printStackTrace();
	  throw new java.sql.SQLException("Cannot connect to MySQL server on " + _Host + ":" + _port + ". Is there a MySQL server running on the machine/port you are trying to connect to? (" + E.getClass().getName() + ")", "08S01");
      }
  }
  
  /**
   * SQL statements without parameters are normally executed using
   * Statement objects.  If the same SQL statement is executed many
   * times, it is more efficient to use a PreparedStatement
   *
   * @return a new Statement object
   * @exception java.sql.SQLException passed through from the constructor
   */

    public java.sql.Statement createStatement() throws java.sql.SQLException
    {
	if (Driver.trace) {
	    Object[] Args = new Object[0];
	    Debug.methodCall(this, "createStatement", Args);
	}

      if (Driver.debug) {
       System.out.println(this + " creating statement.");
      }
        
      org.gjt.mm.mysql.Statement Stmt = new org.gjt.mm.mysql.Statement(this, _Database);

	if (_max_rows != -1) {
	    Stmt.setMaxRows(_max_rows);
	}

	if (Driver.trace) {
	    Debug.returnValue(this, "createStatement", Stmt);
	}
        
      return Stmt;
    }

  /**
   * A SQL statement with or without IN parameters can be pre-compiled
   * and stored in a PreparedStatement object.  This object can then
   * be used to efficiently execute this statement multiple times.
   * 
   * <p>
   * <B>Note: This method is optimized for handling parametric
   * SQL statements that benefit from precompilation if the driver
   * supports precompilation. 
   * In this case, the statement is not sent to the database until the
   * PreparedStatement is executed.  This has no direct effect on users;
   * however it does affect which method throws certain java.sql.SQLExceptions
   *
   * <p>
   * MySQL does not support precompilation of statements, so they
   * are handled by the driver. 
   *
   * @param sql a SQL statement that may contain one or more '?' IN
   *    parameter placeholders
   * @return a new PreparedStatement object containing the pre-compiled
   *    statement.
   * @exception java.sql.SQLException if a database access error occurs.
   */

  public java.sql.PreparedStatement prepareStatement(String Sql) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {Sql};
	  Debug.methodCall(this, "prepareStatement", Args);
      }
      PreparedStatement PStmt = new org.gjt.mm.mysql.PreparedStatement(this, Sql, _Database);

      if (Driver.trace) {
	  Debug.returnValue(this, "prepareStatement", PStmt);
      }

    return PStmt;
  }

  /**
   * A SQL stored procedure call statement is handled by creating a
   * CallableStatement for it.  The CallableStatement provides methods
   * for setting up its IN and OUT parameters and methods for executing
   * it.
   *
   * <B>Note: This method is optimised for handling stored procedure
   * call statements.  Some drivers may send the call statement to the
   * database when the prepareCall is done; others may wait until the
   * CallableStatement is executed.  This has no direct effect on users;
   * however, it does affect which method throws certain java.sql.SQLExceptions
   *
   * @param sql a SQL statement that may contain one or more '?' parameter
   *    placeholders.  Typically this statement is a JDBC function call
   *    escape string.
   * @return a new CallableStatement object containing the pre-compiled
   *    SQL statement
   * @exception java.sql.SQLException if a database access error occurs
   */

  public java.sql.CallableStatement prepareCall(String Sql) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {Sql};
	  Debug.methodCall(this, "prepareCall", Args);
      }
    throw new java.sql.SQLException("Callable statments not suppoted.", "S1C00"); 
  }

  /**
   * A driver may convert the JDBC sql grammar into its system's
   * native SQL grammar prior to sending it; nativeSQL returns the
   * native form of the statement that the driver would have sent.
   *
   * @param sql a SQL statement that may contain one or more '?'
   *    parameter placeholders
   * @return the native form of this statement
   * @exception java.sql.SQLException if a database access error occurs
   */

  public String nativeSQL(String Sql) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {Sql};
	  Debug.methodCall(this, "nativeSQL", Args);
	  Debug.returnValue(this, "nativeSQL", Sql);
      }

    return Sql;
  }

  /**
   * If a connection is in auto-commit mode, than all its SQL
   * statements will be executed and committed as individual
   * transactions.  Otherwise, its SQL statements are grouped
   * into transactions that are terminated by either commit()
   * or rollback().  By default, new connections are in auto-
   * commit mode.  The commit occurs when the statement completes
   * or the next execute occurs, whichever comes first.  In the
   * case of statements returning a ResultSet, the statement
   * completes when the last row of the ResultSet has been retrieved
   * or the ResultSet has been closed.  In advanced cases, a single
   * statement may return multiple results as well as output parameter
   * values.  Here the commit occurs when all results and output param
   * values have been retrieved.
   *
   * <p>Note: MySQL does not support transactions, so this
   *                 method is a no-op.
   *
   * @param autoCommit - true enables auto-commit; false disables it
   * @exception java.sql.SQLException if a database access error occurs
   */

  public void setAutoCommit(boolean autoCommit) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {new Boolean(autoCommit)};
	  Debug.methodCall(this, "setAutoCommit", Args);
      }

      if (autoCommit == false) {
	  throw new SQLException("Cannot disable AUTO_COMMIT", "08003");
      }

      return;
  }

  /**
   * gets the current auto-commit state
   *
   * @return Current state of the auto-commit mode
   * @exception java.sql.SQLException (why?)
   * @see setAutoCommit
   */

  public boolean getAutoCommit() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "getAutoCommit", Args);
	  Debug.returnValue(this, "getAutoCommit", new Boolean(_autoCommit));
      }

    return _autoCommit;
  }

  /**
   * The method commit() makes all changes made since the previous
   * commit/rollback permanent and releases any database locks currently
   * held by the Connection.  This method should only be used when
   * auto-commit has been disabled.  (If autoCommit == true, then we
   * just return anyhow)
   * 
   * <p>Note: MySQL does not support transactions, so this
   *                 method is a no-op.
   *
   * @exception java.sql.SQLException if a database access error occurs
   * @see setAutoCommit
   */

  public void commit() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "commit", Args);
      }

    return;
  }

  /**
   * The method rollback() drops all changes made since the previous
   * commit/rollback and releases any database locks currently held by
   * the Connection.
   *
   * <p>Note: MySQL does not support transactions, so this
   *                 method is a no-op.
   *
   * @exception java.sql.SQLException if a database access error occurs
   * @see commit
   */

  public void rollback() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "rollback", Args);
      }

          if (_isClosed) {
                throw new java.sql.SQLException("Rollback attempt on closed connection.", "08003");
          }
  }
  
  /**
   * In some cases, it is desirable to immediately release a Connection's
   * database and JDBC resources instead of waiting for them to be
   * automatically released (cant think why off the top of my head)
   *
   * <B>Note: A Connection is automatically closed when it is
   * garbage collected.  Certain fatal errors also result in a closed
   * connection.
   *
   * @exception java.sql.SQLException if a database access error occurs
   */

  public void close() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "close", Args);
      }
      
    if (_IO != null)
      {
        try {
            _IO.quit();
          } 
        catch (Exception e) {}
        _IO = null;
      }
    _isClosed = true;
  }
  
  /**
   * Tests to see if a Connection is closed
   *
   * @return the status of the connection
   * @exception java.sql.SQLException (why?)
   */

  public boolean isClosed() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "isClosed", Args);
	  Debug.returnValue(this, "isClosed", new Boolean(_isClosed));
      }

      if (!_isClosed) {
	 // Test the connection
    
        try {
            synchronized (_IO) {
		   execSQL(_PING_COMMAND, -1);
            }
        } 
        catch (Exception E) {
		_isClosed = true;
        }
      }
	
      return _isClosed;
  }

  /**
   * A connection's database is able to provide information describing
   * its tables, its supported SQL grammar, its stored procedures, the
   * capabilities of this connection, etc.  This information is made
   * available through a DatabaseMetaData object.
   *
   * @return a DatabaseMetaData object for this connection
   * @exception java.sql.SQLException if a database access error occurs
   */

  public java.sql.DatabaseMetaData getMetaData() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "getMetaData", Args);
      }

      org.gjt.mm.mysql.DatabaseMetaData DBMD = 
	  new org.gjt.mm.mysql.DatabaseMetaData(this, _Database);
      
      if (Driver.trace) {
	  Debug.returnValue(this, "getMetaData", DBMD);
      }

      return DBMD;
  }

  /**
   * You can put a connection in read-only mode as a hint to enable
   * database optimizations
   *
   * <B>Note: setReadOnly cannot be called while in the middle
   * of a transaction
   *
   * @param readOnly - true enables read-only mode; false disables it
   * @exception java.sql.SQLException if a database access error occurs
   */

  public void setReadOnly (boolean readOnly) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {new Boolean(readOnly)};
	  Debug.methodCall(this, "setReadOnly", Args);
	  Debug.returnValue(this, "setReadOnly", new Boolean(readOnly));
      }
    _readOnly = readOnly;
  } 

  /**
   * Tests to see if the connection is in Read Only Mode.  Note that
   * we cannot really put the database in read only mode, but we pretend
   * we can by returning the value of the readOnly flag
   *
   * @return true if the connection is read only
   * @exception java.sql.SQLException if a database access error occurs
   */

  public boolean isReadOnly() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "isReadOnly", Args);
	  Debug.returnValue(this, "isReadOnly", new Boolean(_readOnly));
      }

    return _readOnly;
  }

  /**
   * A sub-space of this Connection's database may be selected by
   * setting a catalog name.  If the driver does not support catalogs,
   * it will silently ignore this request
   *
   * <p>Note: MySQL's notion of catalogs are individual databases.
   *
   * @exception java.sql.SQLException if a database access error occurs
   */

  public void setCatalog(String Catalog) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {Catalog};
	  Debug.methodCall(this, "setCatalog", Args);
      }

        execSQL("USE " + Catalog, -1);
        _Database = Catalog;
  }
  
  /**
   * Return the connections current catalog name, or null if no
   * catalog name is set, or we dont support catalogs.
   *
   * <p>Note: MySQL's notion of catalogs are individual databases.
   * @return the current catalog name or null
   * @exception java.sql.SQLException if a database access error occurs
   */

  public String getCatalog() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "getCatalog", Args);
	  Debug.returnValue(this, "getCatalog", _Database);
      }

    return _Database;
  }

  /**
   * You can call this method to try to change the transaction
   * isolation level using one of the TRANSACTION_* values.
   *
   * <B>Note: setTransactionIsolation cannot be called while
   * in the middle of a transaction
   *
   * @param level one of the TRANSACTION_* isolation values with
   *    the exception of TRANSACTION_NONE; some databases may
   *    not support other values
   * @exception java.sql.SQLException if a database access error occurs
   * @see java.sql.DatabaseMetaData#supportsTransactionIsolationLevel
   */

  public void setTransactionIsolation(int level) throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = {new Integer(level)};
	  Debug.methodCall(this, "setTransactionIsolation", Args);
      }

    throw new java.sql.SQLException("Transaction Isolation Levels are not supported.", "S1C00");
  }
  
  /**
   * Get this Connection's current transaction isolation mode.
   *
   * @return the current TRANSACTION_* mode value
   * @exception java.sql.SQLException if a database access error occurs
   */

  public int getTransactionIsolation() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "getTransactionIsolation", Args);
	  Debug.returnValue(this, "getTransactionIsolation", new Integer(java.sql.Connection.TRANSACTION_SERIALIZABLE));
      }
    return java.sql.Connection.TRANSACTION_SERIALIZABLE;
  }
  
  /**
   * The first warning reported by calls on this Connection is
   * returned.
   *
   * <B>Note: Sebsequent warnings will be changed to this
   * java.sql.SQLWarning
   *
   * @return the first java.sql.SQLWarning or null
   * @exception java.sql.SQLException if a database access error occurs
   */

  public java.sql.SQLWarning getWarnings() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "getWarnings", Args);
	  Debug.returnValue(this, "getWarnings", null);
      }

    return null;
  }

  /**
   * After this call, getWarnings returns null until a new warning
   * is reported for this connection.
   *
   * @exception java.sql.SQLException if a database access error occurs
   */
  
  public void clearWarnings() throws java.sql.SQLException
  {
      if (Driver.trace) {
	  Object[] Args = new Object[0];
	  Debug.methodCall(this, "clearWarnings", Args);
      }

    // firstWarning = null;
  }

  // *********************************************************************
  //
  //                END OF PUBLIC INTERFACE
  //
  // *********************************************************************

  /**
   * Send a query to the server.  Returns one of the ResultSet
   * objects.
   *
   * This is synchronized, so Statement's queries
   * will be serialized.
   *
   * @param sql the SQL statement to be executed
   * @return a ResultSet holding the results
   * @exception java.sql.SQLException if a database error occurs
   */

  ResultSet execSQL(String Sql, int max_rows) 
      throws java.sql.SQLException
  {
	  return execSQL(Sql, max_rows, null);
  }
  
  ResultSet execSQL(String Sql, int max_rows, Buffer Packet) 
      throws java.sql.SQLException
  {  
      synchronized (_IO) {

	  if (_high_availability) {
	      try {
		  _IO.sqlQuery(_PING_COMMAND, MysqlDefs.MAX_ROWS);
	      }
	      catch (Exception Ex) {

		  double timeout = _initial_timeout;
		  boolean connection_good = false;

		  for (int i = 0; i < _max_reconnects; i++) {
        
		      try {
			  _IO = new MysqlIO(_Host, _port);
			  _IO.init(_User, _Password);
			  _IO.sendCommand(MysqlDefs.INIT_DB, _Database, null);
			  _IO.sqlQuery(_PING_COMMAND, MysqlDefs.MAX_ROWS);
					  
			  connection_good = true;
			  break;
		      } 
		      catch (Exception EEE) {}
                  
		      try {
			  Thread.currentThread().sleep((long)timeout * 1000);
			  timeout = timeout * timeout;
		      }
		      catch (InterruptedException IE) {}
		  }

		  if (!connection_good) { // We've really failed!
		      throw new SQLException("Server connection failure during transaction. \nAttemtped reconnect " + _max_reconnects + " times. Giving up.", "08001");
		  }
	      }
	  }

	  try {
	      int real_max_rows = ( max_rows == -1 ) ? 
		  MysqlDefs.MAX_ROWS : max_rows;

	      if (Packet == null) {
		  String Encoding = null;

		  if (useUnicode()) {
		      Encoding = getEncoding();
		  }

		  return _IO.sqlQuery(Sql, real_max_rows, Encoding);
	      }
	      else {
		  return _IO.sqlQueryDirect(Packet, real_max_rows);
	      }
	  }
	  catch (java.io.EOFException EOFE) {
	      throw new java.sql.SQLException("Lost connection to server during query", "08007");
	  }
	  catch (Exception E) {
	      String ExceptionType = E.getClass().getName();
	      String ExceptionMessage = E.getMessage();
    
	      throw new java.sql.SQLException("Error during query: Unexpected Exception: " + ExceptionType + " message given: " + ExceptionMessage, "S1000");
	  }
      }
  }

  String getURL()
  {
    return _MyURL;
  }

  String getUser()
  {
    return _User;
  }

  String getServerVersion()
  {
    return _IO.getServerVersion();
  }

    int getServerMajorVersion()
    {
        return _IO.getServerMajorVersion();
    }

    int getServerMinorVersion()
    {
        return _IO.getServerMinorVersion();
    }

    int getServerSubMinorVersion()
    {
        return _IO.getServerSubMinorVersion();
    }

    void maxRowsChanged()
    {
        _max_rows_changed = true;
    }

    boolean useMaxRows()
    {
        return _max_rows_changed;
    }

    boolean useUnicode()
    {
	return _do_unicode;
    }

    String getEncoding()
    {
	return _Encoding;
    }

    Object getMutex()
    {
	return _IO;
    }
}
... 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.