001package 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. 002 003import java.sql.*; 004import java.util.*; 005import java.util.logging.*; 006import org.postgresql.ds.*; 007import votorola.g.lang.*; 008import votorola.g.logging.*; 009 010 011/** An interface to a PostgreSQL relational database. 012 * 013 * @see ConstructionContext 014 */ 015public @ThreadRestricted("holds this") final class Database 016{ 017 018 // OPT: the lastest JDBC drivers are documented as thread safe. We might mark this 019 // interface thread-safe too. http://jdbc.postgresql.org/documentation/84/thread.html 020 // See also: http://mail.zelea.com/list/votorola/2012-September/001419.html 021 022 023 /** Partially constructs a Database. This is relatively inexpensive. The result is 024 * suitable for a hashtable lookup and not much else. Call {@linkplain 025 * #init(PGSimpleDataSource) init}(s) to complete it. 026 */ 027 public Database( final ConstructionContext cc ) 028 { 029 // adding a field here? you may want to add it to equals() and hashCode() too 030 name = cc.getName(); 031 serverName = cc.getServerName(); 032 serverPort = cc.getServerPort(); 033 username = cc.getUsername(); 034 userPassword = cc.getUserPassword(); 035 } 036 037 038 039 /** Finishes constructing this Database. This is relatively expensive. 040 * 041 * @throws IllegalStateException if init was already called. 042 */ 043 public @ThreadSafe synchronized void init( final PGSimpleDataSource s ) throws SQLException 044 { 045 if( connection != null ) throw new IllegalStateException(); 046 047 s.setServerName( serverName ); 048 s.setPortNumber( serverPort ); 049 s.setDatabaseName( name ); 050 s.setUser( username ); 051 if( userPassword != null ) s.setPassword( userPassword ); 052 connection = s.getConnection(); 053 054 statementCache = new HashMap<String,PreparedStatement>(); 055 } 056 057 058 059 // ------------------------------------------------------------------------------------ 060 061 062 /** Returns the database connection of this interface. Do not close it. 063 */ 064 public @Warning("thread restricted object") Connection connection() 065 { 066 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 067 return connection; 068 } 069 070 071 private Connection connection; // final after init() 072 073 074 075 /** Ensures the specified schema exists in this database, creating it if necessary. 076 */ 077 public void ensureSchema( final String name ) throws SQLException 078 { 079 assert Thread.holdsLock( Database.this ); 080 if( ensureSchema_set.contains( name )) return; // save expense of redundant call 081 082 final String key = Database.class.getName() + ":" + name + ".ensureSchema"; 083 PreparedStatement s = statementCache.get( key ); 084 if( s == null ) 085 { 086 s = connection.prepareStatement( "CREATE SCHEMA \"" + name + "\"" ); 087 statementCache.put( key, s ); 088 } 089 try 090 { 091 s.execute(); 092 } 093 catch( SQLException x ) { if( !"42P06".equals( x.getSQLState() )) throw x; } // 42P06 = DUPLICATE SCHEMA 094 ensureSchema_set.add( name ); 095 } 096 097 098 private final HashSet<String> ensureSchema_set = new HashSet<String>(); 099 100 101 102 /** Logs any warnings recorded in the connection, and clears them. 103 */ 104 public @ThreadSafe synchronized void logAndClearWarnings() // never properly tested 105 { 106 if( logAndClearWarnings_isDisabled ) return; 107 108 try 109 { 110 SQLWarning warning = connection.getWarnings(); 111 if( warning == null ) return; 112 113 connection.clearWarnings(); 114 final StringBuilder stringB = new StringBuilder(); 115 for( ;; ) 116 { 117 stringB.append( "cleared warning" ); 118 String state = warning.getSQLState(); 119 if( state != null ) // PostgreSQL's warning/exception message strings do not include this, so include it here 120 { 121 stringB.append( " (SQLState=" ); 122 stringB.append( state ); 123 stringB.append( ')' ); 124 } 125 stringB.append( ": " ); 126 stringB.append( warning.toString() ); 127 logger.fine( stringB.toString() ); 128 129 warning = warning.getNextWarning(); 130 if( warning == null ) break; 131 132 stringB.delete( 0, stringB.length() ); // clear for reuse 133 } 134 } 135 // catch( SQLException x ) { throw new VotorolaRuntimeException( x ); } 136 catch( SQLException x ) 137 { 138 logAndClearWarnings_isDisabled = true; 139 logger.log( LoggerX.FINE, /*message*/"disabling log of connection warnings, due to exception", x ); 140 } 141 } 142 143 144 private boolean logAndClearWarnings_isDisabled; 145 146 147 148 /** Maximum length of an SQL identifier, for PostgreSQL (default build). 149 */ 150 public static final int MAX_IDENTIFIER_LENGTH = 63; 151 152 153 154 /** A map of client-prepared statements for reuse with this database. 155 */ 156 public HashMap<String,PreparedStatement> statementCache() 157 { 158 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 159 return statementCache; 160 } 161 162 163 private HashMap<String,PreparedStatement> statementCache; // final after init() 164 165 166 167 // - O b j e c t ---------------------------------------------------------------------- 168 169 170 /** Returns true iff o is a database configured with the same name, server name, port 171 * and user. 172 */ 173 public @Override @ThreadSafe final boolean equals( Object o ) 174 { 175 if( o == null || !getClass().equals( o.getClass() )) return false; 176 177 final Database d = (Database)o; 178 return name.equals( d.name ) && serverName.equals( d.serverName ) 179 && serverPort == d.serverPort && username.equals( d.username ); 180 } 181 182 183 184 public @Override @ThreadSafe final int hashCode() 185 { 186 return name.hashCode() + serverName.hashCode() + serverPort*31 + username.hashCode(); 187 } 188 189 190 191 // ==================================================================================== 192 193 194 /** A context for configuring a PostgreSQL database. 195 */ 196 public static abstract @ThreadSafe class ConstructionContext 197 { 198 199 200 /** Returns the name of the database. 201 */ 202 public abstract String getName(); 203 204 205 206 /** Returns the name of the DBMS server that hosts the database. For example 207 * "localhost", or "db.somewhere.net". 208 * 209 * @see #setServerName(String) 210 */ 211 public String getServerName() { return serverName; } 212 213 214 private String serverName = "localhost"; 215 216 217 /** Sets the name of the DBMS server. The default value is "localhost". 218 * 219 * @see #getServerName() 220 */ 221 @ThreadRestricted("constructor") 222 public void setServerName( String serverName ) { this.serverName = serverName; } 223 224 225 226 /** Returns the communication port of the DBMS server that hosts the database. 227 * 228 * @see #setServerPort(int) 229 */ 230 public int getServerPort() { return serverPort; } 231 232 233 private int serverPort = 5432; 234 235 236 /** Sets the port of the PostgreSQL DBMS server. The default value is 5432. 237 * 238 * @see #getServerPort() 239 */ 240 @ThreadRestricted("constructor") 241 public void setServerPort( int serverPort ) { this.serverPort = serverPort; } 242 243 244 245 /** Returns the user name for the DBMS access account. 246 */ 247 public abstract String getUsername(); 248 249 250 251 /** Returns the user password for the DBMS access account; or null if none is 252 * required. (None is required for PostgreSQL authentication methods 'trust' and 253 * 'ident'.) See SQL 'CREATE USER'. 254 * 255 * @see #setUserPassword(String) 256 */ 257 public String getUserPassword() { return userPassword; } 258 259 260 private String userPassword = "localhost"; 261 262 263 /** Sets the user password. The default value is null. 264 * 265 * @see #getUserPassword() 266 */ 267 @ThreadRestricted("constructor") 268 public void setUserPassword( String userPassword ) { this.userPassword = userPassword; } 269 270 271 } 272 273 274 275//// P r i v a t e /////////////////////////////////////////////////////////////////////// 276 277 278 private static final Logger logger = LoggerX.i( Database.class ); 279 280 281 282 private final String name; 283 284 285 286 private final String serverName; 287 288 289 290 private final int serverPort; 291 292 293 294 private final String username; 295 296 297 298 private final String userPassword; 299 300 301}