package votorola.g.sql; // Copyright 2007-2010, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import java.sql.*; import java.util.*; import java.util.logging.*; import org.postgresql.ds.*; import votorola.g.lang.*; import votorola.g.logging.*; /** An interface to a PostgreSQL relational database. * * @see ConstructionContext */ public @ThreadRestricted("holds this") final class Database { // OPT: the lastest JDBC drivers are documented as thread safe. We might mark this // interface thread-safe too. http://jdbc.postgresql.org/documentation/84/thread.html // See also: http://mail.zelea.com/list/votorola/2012-September/001419.html /** Partially constructs a Database. This is relatively inexpensive. The result is * suitable for a hashtable lookup and not much else. Call {@linkplain * #init(PGSimpleDataSource) init}(s) to complete it. */ public Database( final ConstructionContext cc ) { // adding a field here? you may want to add it to equals() and hashCode() too name = cc.getName(); serverName = cc.getServerName(); serverPort = cc.getServerPort(); username = cc.getUsername(); userPassword = cc.getUserPassword(); } /** Finishes constructing this Database. This is relatively expensive. * * @throws IllegalStateException if init was already called. */ public @ThreadSafe synchronized void init( final PGSimpleDataSource s ) throws SQLException { if( connection != null ) throw new IllegalStateException(); s.setServerName( serverName ); s.setPortNumber( serverPort ); s.setDatabaseName( name ); s.setUser( username ); if( userPassword != null ) s.setPassword( userPassword ); connection = s.getConnection(); statementCache = new HashMap(); } // ------------------------------------------------------------------------------------ /** Returns the database connection of this interface. Do not close it. */ public @Warning("thread restricted object") Connection connection() { assert Thread.holdsLock( Database.this ); // this method is actually thread safe, but the object is not, and here we assume it is about to be accessed return connection; } private Connection connection; // final after init() /** Ensures the specified schema exists in this database, creating it if necessary. */ public void ensureSchema( final String name ) throws SQLException { assert Thread.holdsLock( Database.this ); if( ensureSchema_set.contains( name )) return; // save expense of redundant call final String key = Database.class.getName() + ":" + name + ".ensureSchema"; PreparedStatement s = statementCache.get( key ); if( s == null ) { s = connection.prepareStatement( "CREATE SCHEMA \"" + name + "\"" ); statementCache.put( key, s ); } try { s.execute(); } catch( SQLException x ) { if( !"42P06".equals( x.getSQLState() )) throw x; } // 42P06 = DUPLICATE SCHEMA ensureSchema_set.add( name ); } private final HashSet ensureSchema_set = new HashSet(); /** Logs any warnings recorded in the connection, and clears them. */ public @ThreadSafe synchronized void logAndClearWarnings() // never properly tested { if( logAndClearWarnings_isDisabled ) return; try { SQLWarning warning = connection.getWarnings(); if( warning == null ) return; connection.clearWarnings(); final StringBuilder stringB = new StringBuilder(); for( ;; ) { stringB.append( "cleared warning" ); String state = warning.getSQLState(); if( state != null ) // PostgreSQL's warning/exception message strings do not include this, so include it here { stringB.append( " (SQLState=" ); stringB.append( state ); stringB.append( ')' ); } stringB.append( ": " ); stringB.append( warning.toString() ); logger.fine( stringB.toString() ); warning = warning.getNextWarning(); if( warning == null ) break; stringB.delete( 0, stringB.length() ); // clear for reuse } } // catch( SQLException x ) { throw new VotorolaRuntimeException( x ); } catch( SQLException x ) { logAndClearWarnings_isDisabled = true; logger.log( LoggerX.FINE, /*message*/"disabling log of connection warnings, due to exception", x ); } } private boolean logAndClearWarnings_isDisabled; /** Maximum length of an SQL identifier, for PostgreSQL (default build). */ public static final int MAX_IDENTIFIER_LENGTH = 63; /** A map of client-prepared statements for reuse with this database. */ public HashMap statementCache() { assert Thread.holdsLock( Database.this ); // this method is actually thread safe, but the object is not, and here we assume it is about to be accessed return statementCache; } private HashMap statementCache; // final after init() // - O b j e c t ---------------------------------------------------------------------- /** Returns true iff o is a database configured with the same name, server name, port * and user. */ public @Override @ThreadSafe final boolean equals( Object o ) { if( o == null || !getClass().equals( o.getClass() )) return false; final Database d = (Database)o; return name.equals( d.name ) && serverName.equals( d.serverName ) && serverPort == d.serverPort && username.equals( d.username ); } public @Override @ThreadSafe final int hashCode() { return name.hashCode() + serverName.hashCode() + serverPort*31 + username.hashCode(); } // ==================================================================================== /** A context for configuring a PostgreSQL database. */ public static abstract @ThreadSafe class ConstructionContext { /** Returns the name of the database. */ public abstract String getName(); /** Returns the name of the DBMS server that hosts the database. For example * "localhost", or "db.somewhere.net". * * @see #setServerName(String) */ public String getServerName() { return serverName; } private String serverName = "localhost"; /** Sets the name of the DBMS server. The default value is "localhost". * * @see #getServerName() */ @ThreadRestricted("constructor") public void setServerName( String serverName ) { this.serverName = serverName; } /** Returns the communication port of the DBMS server that hosts the database. * * @see #setServerPort(int) */ public int getServerPort() { return serverPort; } private int serverPort = 5432; /** Sets the port of the PostgreSQL DBMS server. The default value is 5432. * * @see #getServerPort() */ @ThreadRestricted("constructor") public void setServerPort( int serverPort ) { this.serverPort = serverPort; } /** Returns the user name for the DBMS access account. */ public abstract String getUsername(); /** Returns the user password for the DBMS access account; or null if none is * required. (None is required for PostgreSQL authentication methods 'trust' and * 'ident'.) See SQL 'CREATE USER'. * * @see #setUserPassword(String) */ public String getUserPassword() { return userPassword; } private String userPassword = "localhost"; /** Sets the user password. The default value is null. * * @see #getUserPassword() */ @ThreadRestricted("constructor") public void setUserPassword( String userPassword ) { this.userPassword = userPassword; } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static final Logger logger = LoggerX.i( Database.class ); private final String name; private final String serverName; private final int serverPort; private final String username; private final String userPassword; }