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}