001    /* 
002        Licensed to the Apache Software Foundation (ASF) under one
003        or more contributor license agreements.  See the NOTICE file
004        distributed with this work for additional information
005        regarding copyright ownership.  The ASF licenses this file
006        to you under the Apache License, Version 2.0 (the
007        "License"); you may not use this file except in compliance
008        with the License.  You may obtain a copy of the License at
009    
010           http://www.apache.org/licenses/LICENSE-2.0
011    
012        Unless required by applicable law or agreed to in writing,
013        software distributed under the License is distributed on an
014        "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015        KIND, either express or implied.  See the License for the
016        specific language governing permissions and limitations
017        under the License.  
018     */
019    package org.apache.wiki.auth.user;
020    
021    import java.io.*;
022    import java.security.Principal;
023    import java.sql.*;
024    import java.util.*;
025    import java.util.Date;
026    
027    import javax.naming.Context;
028    import javax.naming.InitialContext;
029    import javax.naming.NamingException;
030    import javax.sql.DataSource;
031    
032    import org.apache.wiki.WikiEngine;
033    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
034    import org.apache.wiki.auth.NoSuchPrincipalException;
035    import org.apache.wiki.auth.WikiPrincipal;
036    import org.apache.wiki.auth.WikiSecurityException;
037    import org.apache.wiki.util.Serializer;
038    
039    /**
040     * <p>
041     * Implementation of UserDatabase that persists {@link DefaultUserProfile}
042     * objects to a JDBC DataSource, as might typically be provided by a web
043     * container. This implementation looks up the JDBC DataSource using JNDI. The
044     * JNDI name of the datasource, backing table and mapped columns used by this 
045     * class can be overridden by adding settings in <code>jspwiki.properties</code>.
046     * </p>
047     * <p>
048     * Configurable properties are these:
049     * </p>
050     * <table>
051     * <tr> <thead>
052     * <th>Property</th>
053     * <th>Default</th>
054     * <th>Definition</th>
055     * <thead> </tr>
056     * <tr>
057     * <td><code>jspwiki.userdatabase.datasource</code></td>
058     * <td><code>jdbc/UserDatabase</code></td>
059     * <td>The JNDI name of the DataSource</td>
060     * </tr>
061     * <tr>
062     * <td><code>jspwiki.userdatabase.table</code></td>
063     * <td><code>users</code></td>
064     * <td>The table that stores the user profiles</td>
065     * </tr>
066     * <tr>
067     * <td><code>jspwiki.userdatabase.attributes</code></td>
068     * <td><code>attributes</code></td>
069     * <td>The CLOB column containing the profile's custom attributes, stored as key/value strings, each separated by newline.</td>
070     * </tr>
071     * <tr>
072     * <td><code>jspwiki.userdatabase.created</code></td>
073     * <td><code>created</code></td>
074     * <td>The column containing the profile's creation timestamp</td>
075     * </tr>
076     * <tr>
077     * <td><code>jspwiki.userdatabase.email</code></td>
078     * <td><code>email</code></td>
079     * <td>The column containing the user's e-mail address</td>
080     * </tr>
081     * <tr>
082     * <td><code>jspwiki.userdatabase.fullName</code></td>
083     * <td><code>full_name</code></td>
084     * <td>The column containing the user's full name</td>
085     * </tr>
086     * <tr>
087     * <td><code>jspwiki.userdatabase.loginName</code></td>
088     * <td><code>login_name</code></td>
089     * <td>The column containing the user's login id</td>
090     * </tr>
091     * <tr>
092     * <td><code>jspwiki.userdatabase.password</code></td>
093     * <td><code>password</code></td>
094     * <td>The column containing the user's password</td>
095     * </tr>
096     * <tr>
097     * <td><code>jspwiki.userdatabase.modified</code></td>
098     * <td><code>modified</code></td>
099     * <td>The column containing the profile's last-modified timestamp</td>
100     * </tr>
101     * <tr>
102     * <td><code>jspwiki.userdatabase.uid</code></td>
103     * <td><code>uid</code></td>
104     * <td>The column containing the profile's unique identifier, as a long integer</td>
105     * </tr>
106     * <tr>
107     * <td><code>jspwiki.userdatabase.wikiName</code></td>
108     * <td><code>wiki_name</code></td>
109     * <td>The column containing the user's wiki name</td>
110     * </tr>
111     * <tr>
112     * <td><code>jspwiki.userdatabase.lockExpiry</code></td>
113     * <td><code>lock_expiry</code></td>
114     * <td>The column containing the date/time when the profile, if locked, should be unlocked.</td>
115     * </tr>
116     * <tr>
117     * <td><code>jspwiki.userdatabase.roleTable</code></td>
118     * <td><code>roles</code></td>
119     * <td>The table that stores user roles. When a new user is created, a new
120     * record is inserted containing user's initial role. The table will have an ID
121     * column whose name and values correspond to the contents of the user table's
122     * login name column. It will also contain a role column (see next row).</td>
123     * </tr>
124     * <tr>
125     * <td><code>jspwiki.userdatabase.role</code></td>
126     * <td><code>role</code></td>
127     * <td>The column in the role table that stores user roles. When a new user is
128     * created, this column will be populated with the value
129     * <code>Authenticated</code>. Once created, JDBCUserDatabase does not use
130     * this column again; it is provided strictly for the convenience of
131     * container-managed authentication services.</td>
132     * </tr>
133     * </table>
134     * <p>
135     * This class hashes passwords using SHA-1. All of the underying SQL commands
136     * used by this class are implemented using prepared statements, so it is immune
137     * to SQL injection attacks.
138     * </p>
139     * <p>
140     * This class is typically used in conjunction with a web container's JNDI
141     * resource factory. For example, Tomcat provides a basic
142     * JNDI factory for registering DataSources. To give JSPWiki access to the JNDI
143     * resource named by <code></code>, you would declare the datasource resource
144     * similar to this:
145     * </p>
146     * <blockquote><code>&lt;Context ...&gt;<br/>
147     *  &nbsp;&nbsp;...<br/>
148     *  &nbsp;&nbsp;&lt;Resource name="jdbc/UserDatabase" auth="Container"<br/>
149     *  &nbsp;&nbsp;&nbsp;&nbsp;type="javax.sql.DataSource" username="dbusername" password="dbpassword"<br/>
150     *  &nbsp;&nbsp;&nbsp;&nbsp;driverClassName="org.hsql.jdbcDriver" url="jdbc:HypersonicSQL:database"<br/>
151     *  &nbsp;&nbsp;&nbsp;&nbsp;maxActive="8" maxIdle="4"/&gt;<br/>
152     *  &nbsp;...<br/>
153     * &lt;/Context&gt;</code></blockquote>
154     * <p>
155     * To configure JSPWiki to use JDBC support, first create a database 
156     * with a structure similar to that provided by the HSQL and PostgreSQL 
157     * scripts in src/main/config/db.  If you have different table or column 
158     * names you can either alias them with a database view and have JSPWiki
159     * use the views, or alter the WEB-INF/jspwiki.properties file: the 
160     * jspwiki.userdatabase.* and jspwiki.groupdatabase.* properties change the
161     * names of the tables and columns that JSPWiki uses.
162     * </p>
163     * <p>
164     * A JNDI datasource (named jdbc/UserDatabase by default but can be configured 
165     * in the jspwiki.properties file) will need to be created in your servlet container.
166     * JDBC driver JARs should be added, e.g. in Tomcat's <code>lib</code>
167     * directory. For more Tomcat JNDI configuration examples, see <a
168     * href="http://tomcat.apache.org/tomcat-7.0-doc/jndi-resources-howto.html">
169     * http://tomcat.apache.org/tomcat-7.0-doc/jndi-resources-howto.html</a>.
170     * Once done, restart JSPWiki in the servlet container for it to read the 
171     * new properties and switch to JDBC authentication.
172     * </p>
173     * <p>
174     * JDBCUserDatabase commits changes as transactions if the back-end database
175     * supports them. If the database supports transactions, user profile changes
176     * are saved to permanent storage only when the {@link #commit()} method is
177     * called. If the database does <em>not</em> support transactions, then
178     * changes are made immediately (during the {@link #save(UserProfile)} method),
179     * and the {@linkplain #commit()} method no-ops. Thus, callers should always
180     * call the {@linkplain #commit()} method after saving a profile to guarantee
181     * that changes are applied.
182     * </p>
183     * 
184     * @since 2.3
185     */
186    public class JDBCUserDatabase extends AbstractUserDatabase
187    {
188    
189        private static final String NOTHING = "";
190    
191        public static final String DEFAULT_DB_ATTRIBUTES = "attributes";
192    
193        public static final String DEFAULT_DB_CREATED = "created";
194    
195        public static final String DEFAULT_DB_EMAIL = "email";
196    
197        public static final String DEFAULT_DB_FULL_NAME = "full_name";
198    
199        public static final String DEFAULT_DB_JNDI_NAME = "jdbc/UserDatabase";
200    
201        public static final String DEFAULT_DB_LOCK_EXPIRY = "lock_expiry";
202    
203        public static final String DEFAULT_DB_MODIFIED = "modified";
204    
205        public static final String DEFAULT_DB_ROLE = "role";
206    
207        public static final String DEFAULT_DB_ROLE_TABLE = "roles";
208    
209        public static final String DEFAULT_DB_TABLE = "users";
210    
211        public static final String DEFAULT_DB_LOGIN_NAME = "login_name";
212    
213        public static final String DEFAULT_DB_PASSWORD = "password";
214    
215        public static final String DEFAULT_DB_UID = "uid";
216    
217        public static final String DEFAULT_DB_WIKI_NAME = "wiki_name";
218    
219        public static final String PROP_DB_ATTRIBUTES = "jspwiki.userdatabase.attributes";
220    
221        public static final String PROP_DB_CREATED = "jspwiki.userdatabase.created";
222    
223        public static final String PROP_DB_EMAIL = "jspwiki.userdatabase.email";
224    
225        public static final String PROP_DB_FULL_NAME = "jspwiki.userdatabase.fullName";
226    
227        public static final String PROP_DB_DATASOURCE = "jspwiki.userdatabase.datasource";
228    
229        public static final String PROP_DB_LOCK_EXPIRY = "jspwiki.userdatabase.lockExpiry";
230    
231        public static final String PROP_DB_LOGIN_NAME = "jspwiki.userdatabase.loginName";
232    
233        public static final String PROP_DB_MODIFIED = "jspwiki.userdatabase.modified";
234    
235        public static final String PROP_DB_PASSWORD = "jspwiki.userdatabase.password";
236    
237        public static final String PROP_DB_UID = "jspwiki.userdatabase.uid";
238    
239        public static final String PROP_DB_ROLE = "jspwiki.userdatabase.role";
240    
241        public static final String PROP_DB_ROLE_TABLE = "jspwiki.userdatabase.roleTable";
242    
243        public static final String PROP_DB_TABLE = "jspwiki.userdatabase.table";
244    
245        public static final String PROP_DB_WIKI_NAME = "jspwiki.userdatabase.wikiName";
246    
247        private DataSource m_ds = null;
248    
249        private String m_deleteUserByLoginName = null;
250    
251        private String m_deleteRoleByLoginName = null;
252    
253        private String m_findByEmail = null;
254    
255        private String m_findByFullName = null;
256    
257        private String m_findByLoginName = null;
258    
259        private String m_findByUid = null;
260    
261        private String m_findByWikiName = null;
262    
263        private String m_renameProfile = null;
264    
265        private String m_renameRoles = null;
266    
267        private String m_updateProfile = null;
268    
269        private String m_findAll = null;
270    
271        private String m_findRoles = null;
272    
273        private String m_insertProfile = null;
274    
275        private String m_insertRole = null;
276    
277        private String m_attributes = null;
278    
279        private String m_email = null;
280    
281        private String m_fullName = null;
282    
283        private String m_lockExpiry = null;
284    
285        private String m_loginName = null;
286    
287        private String m_password = null;
288    
289        private String m_uid = null;
290        
291        private String m_wikiName = null;
292    
293        private String m_created = null;
294    
295        private String m_modified = null;
296    
297        private boolean m_supportsCommits = false;
298    
299        /**
300         * Looks up and deletes the first {@link UserProfile} in the user database
301         * that matches a profile having a given login name. If the user database
302         * does not contain a user with a matching attribute, throws a
303         * {@link NoSuchPrincipalException}. This method is intended to be atomic;
304         * results cannot be partially committed. If the commit fails, it should
305         * roll back its state appropriately. Implementing classes that persist to
306         * the file system may wish to make this method <code>synchronized</code>.
307         * 
308         * @param loginName the login name of the user profile that shall be deleted
309         */
310        public void deleteByLoginName( String loginName ) throws NoSuchPrincipalException, WikiSecurityException
311        {
312            // Get the existing user; if not found, throws NoSuchPrincipalException
313            findByLoginName( loginName );
314            Connection conn = null;
315    
316            try
317            {
318                // Open the database connection
319                conn = m_ds.getConnection();
320                if( m_supportsCommits )
321                {
322                    conn.setAutoCommit( false );
323                }
324    
325                PreparedStatement ps;
326                // Delete user record
327                ps = conn.prepareStatement( m_deleteUserByLoginName );
328                ps.setString( 1, loginName );
329                ps.execute();
330                ps.close();
331    
332                // Delete role record
333                ps = conn.prepareStatement( m_deleteRoleByLoginName );
334                ps.setString( 1, loginName );
335                ps.execute();
336                ps.close();
337    
338                // Commit and close connection
339                if( m_supportsCommits )
340                {
341                    conn.commit();
342                }
343            }
344            catch( SQLException e )
345            {
346                throw new WikiSecurityException( e.getMessage(), e );
347            }
348            finally
349            {
350                try
351                {
352                    if( conn != null ) conn.close();
353                }
354                catch( Exception e )
355                {
356                }
357            }
358        }
359    
360        /**
361         * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String)
362         */
363        public UserProfile findByEmail( String index ) throws NoSuchPrincipalException
364        {
365            return findByPreparedStatement( m_findByEmail, index );
366        }
367    
368        /**
369         * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
370         */
371        public UserProfile findByFullName( String index ) throws NoSuchPrincipalException
372        {
373            return findByPreparedStatement( m_findByFullName, index );
374        }
375    
376        /**
377         * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
378         */
379        public UserProfile findByLoginName( String index ) throws NoSuchPrincipalException
380        {
381            return findByPreparedStatement( m_findByLoginName, index );
382        }
383    
384        /**
385         * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(String)
386         */
387        public UserProfile findByUid( String uid ) throws NoSuchPrincipalException
388        {
389            return findByPreparedStatement( m_findByUid, uid );
390        }
391    
392        /**
393         * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(String)
394         */
395        public UserProfile findByWikiName( String index ) throws NoSuchPrincipalException
396        {
397            return findByPreparedStatement( m_findByWikiName, index );
398        }
399    
400        /**
401         * Returns all WikiNames that are stored in the UserDatabase as an array of
402         * WikiPrincipal objects. If the database does not contain any profiles,
403         * this method will return a zero-length array.
404         * 
405         * @return the WikiNames
406         */
407        public Principal[] getWikiNames() throws WikiSecurityException
408        {
409            Set<Principal> principals = new HashSet<Principal>();
410            Connection conn = null;
411            try
412            {
413                conn = m_ds.getConnection();
414                PreparedStatement ps = conn.prepareStatement( m_findAll );
415                ResultSet rs = ps.executeQuery();
416                while ( rs.next() )
417                {
418                    String wikiName = rs.getString( m_wikiName );
419                    if( wikiName == null )
420                    {
421                        log.warn( "Detected null wiki name in XMLUserDataBase. Check your user database." );
422                    }
423                    else
424                    {
425                        Principal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME );
426                        principals.add( principal );
427                    }
428                }
429                ps.close();
430            }
431            catch( SQLException e )
432            {
433                throw new WikiSecurityException( e.getMessage(), e );
434            }
435            finally
436            {
437                try
438                {
439                    if( conn != null ) conn.close();
440                }
441                catch( Exception e )
442                {
443                }
444            }
445    
446            return principals.toArray( new Principal[principals.size()] );
447        }
448    
449        /**
450         * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine,
451         *      java.util.Properties)
452         */
453        public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException
454        {
455            String userTable;
456            String role;
457            String roleTable;
458    
459            String jndiName = props.getProperty( PROP_DB_DATASOURCE, DEFAULT_DB_JNDI_NAME );
460            try
461            {
462                Context initCtx = new InitialContext();
463                Context ctx = (Context) initCtx.lookup( "java:comp/env" );
464                m_ds = (DataSource) ctx.lookup( jndiName );
465    
466                // Prepare the SQL selectors
467                userTable = props.getProperty( PROP_DB_TABLE, DEFAULT_DB_TABLE );
468                m_email = props.getProperty( PROP_DB_EMAIL, DEFAULT_DB_EMAIL );
469                m_fullName = props.getProperty( PROP_DB_FULL_NAME, DEFAULT_DB_FULL_NAME );
470                m_lockExpiry = props.getProperty( PROP_DB_LOCK_EXPIRY, DEFAULT_DB_LOCK_EXPIRY );
471                m_loginName = props.getProperty( PROP_DB_LOGIN_NAME, DEFAULT_DB_LOGIN_NAME );
472                m_password = props.getProperty( PROP_DB_PASSWORD, DEFAULT_DB_PASSWORD );
473                m_uid = props.getProperty( PROP_DB_UID, DEFAULT_DB_UID );
474                m_wikiName = props.getProperty( PROP_DB_WIKI_NAME, DEFAULT_DB_WIKI_NAME );
475                m_created = props.getProperty( PROP_DB_CREATED, DEFAULT_DB_CREATED );
476                m_modified = props.getProperty( PROP_DB_MODIFIED, DEFAULT_DB_MODIFIED );
477                m_attributes = props.getProperty( PROP_DB_ATTRIBUTES, DEFAULT_DB_ATTRIBUTES );
478    
479                m_findAll = "SELECT * FROM " + userTable;
480                m_findByEmail = "SELECT * FROM " + userTable + " WHERE " + m_email + "=?";
481                m_findByFullName = "SELECT * FROM " + userTable + " WHERE " + m_fullName + "=?";
482                m_findByLoginName = "SELECT * FROM " + userTable + " WHERE " + m_loginName + "=?";
483                m_findByUid = "SELECT * FROM " + userTable + " WHERE " + m_uid + "=?";
484                m_findByWikiName = "SELECT * FROM " + userTable + " WHERE " + m_wikiName + "=?";
485    
486                // The user insert SQL prepared statement
487                m_insertProfile = "INSERT INTO " + userTable + " ("
488                                  + m_uid + ","
489                                  + m_email + ","
490                                  + m_fullName + ","
491                                  + m_password + ","
492                                  + m_wikiName + ","
493                                  + m_modified + ","
494                                  + m_loginName + ","
495                                  + m_attributes + ","
496                                  + m_created
497                                  + ") VALUES (?,?,?,?,?,?,?,?,?)";
498                
499                // The user update SQL prepared statement
500                m_updateProfile = "UPDATE " + userTable + " SET "
501                                  + m_uid + "=?,"
502                                  + m_email + "=?,"
503                                  + m_fullName + "=?,"
504                                  + m_password + "=?,"
505                                  + m_wikiName + "=?,"
506                                  + m_modified + "=?,"
507                                  + m_loginName + "=?,"
508                                  + m_attributes + "=?,"
509                                  + m_lockExpiry + "=? "
510                                  + "WHERE " + m_loginName + "=?";
511    
512                // Prepare the role insert SQL
513                roleTable = props.getProperty( PROP_DB_ROLE_TABLE, DEFAULT_DB_ROLE_TABLE );
514                role = props.getProperty( PROP_DB_ROLE, DEFAULT_DB_ROLE );
515                m_insertRole = "INSERT INTO " + roleTable + " (" + m_loginName + "," + role + ") VALUES (?,?)";
516                m_findRoles = "SELECT * FROM " + roleTable + " WHERE " + m_loginName + "=?";
517    
518                // Prepare the user delete SQL
519                m_deleteUserByLoginName = "DELETE FROM " + userTable + " WHERE " + m_loginName + "=?";
520    
521                // Prepare the role delete SQL
522                m_deleteRoleByLoginName = "DELETE FROM " + roleTable + " WHERE " + m_loginName + "=?";
523    
524                // Prepare the rename user/roles SQL
525                m_renameProfile = "UPDATE " + userTable + " SET " + m_loginName + "=?," + m_modified + "=? WHERE " + m_loginName
526                                  + "=?";
527                m_renameRoles = "UPDATE " + roleTable + " SET " + m_loginName + "=? WHERE " + m_loginName + "=?";
528            }
529            catch( NamingException e )
530            {
531                log.error( "JDBCUserDatabase initialization error: " + e.getMessage() );
532                throw new NoRequiredPropertyException( PROP_DB_DATASOURCE, "JDBCUserDatabase initialization error: " + e.getMessage() );
533            }
534    
535            // Test connection by doing a quickie select
536            Connection conn = null;
537            try
538            {
539                conn = m_ds.getConnection();
540                PreparedStatement ps = conn.prepareStatement( m_findAll );
541                ps.close();
542            }
543            catch( SQLException e )
544            {
545                log.error( "DB connectivity error: " + e.getMessage() );
546                throw new WikiSecurityException("DB connectivity error: " + e.getMessage(), e );
547            }
548            finally
549            {
550                try
551                {
552                    if( conn != null ) conn.close();
553                }
554                catch( Exception e )
555                {
556                }
557            }
558            log.info( "JDBCUserDatabase initialized from JNDI DataSource: " + jndiName );
559    
560            // Determine if the datasource supports commits
561            try
562            {
563                conn = m_ds.getConnection();
564                DatabaseMetaData dmd = conn.getMetaData();
565                if( dmd.supportsTransactions() )
566                {
567                    m_supportsCommits = true;
568                    conn.setAutoCommit( false );
569                    log.info( "JDBCUserDatabase supports transactions. Good; we will use them." );
570                }
571            }
572            catch( SQLException e )
573            {
574                log.warn( "JDBCUserDatabase warning: user database doesn't seem to support transactions. Reason: " + e.getMessage() );
575            }
576            finally
577            {
578                try
579                {
580                    if( conn != null ) conn.close();
581                }
582                catch( Exception e )
583                {
584                }
585            }
586        }
587    
588        /**
589         * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String)
590         */
591        public void rename( String loginName, String newName )
592                                                              throws NoSuchPrincipalException,
593                                                                  DuplicateUserException,
594                                                                  WikiSecurityException
595        {
596            // Get the existing user; if not found, throws NoSuchPrincipalException
597            UserProfile profile = findByLoginName( loginName );
598    
599            // Get user with the proposed name; if found, it's a collision
600            try
601            {
602                UserProfile otherProfile = findByLoginName( newName );
603                if( otherProfile != null )
604                {
605                    throw new DuplicateUserException( "security.error.cannot.rename", newName );
606                }
607            }
608            catch( NoSuchPrincipalException e )
609            {
610                // Good! That means it's safe to save using the new name
611            }
612    
613            Connection conn = null;
614            try
615            {
616                // Open the database connection
617                conn = m_ds.getConnection();
618                if( m_supportsCommits )
619                {
620                    conn.setAutoCommit( false );
621                }
622    
623                Timestamp ts = new Timestamp( System.currentTimeMillis() );
624                Date modDate = new Date( ts.getTime() );
625    
626                // Change the login ID for the user record
627                PreparedStatement ps = conn.prepareStatement( m_renameProfile );
628                ps.setString( 1, newName );
629                ps.setTimestamp( 2, ts );
630                ps.setString( 3, loginName );
631                ps.execute();
632                ps.close();
633    
634                // Change the login ID for the role records
635                ps = conn.prepareStatement( m_renameRoles );
636                ps.setString( 1, newName );
637                ps.setString( 2, loginName );
638                ps.execute();
639                ps.close();
640    
641                // Set the profile name and mod time
642                profile.setLoginName( newName );
643                profile.setLastModified( modDate );
644    
645                // Commit and close connection
646                if( m_supportsCommits )
647                {
648                    conn.commit();
649                }
650            }
651            catch( SQLException e )
652            {
653                throw new WikiSecurityException( e.getMessage(), e );
654            }
655            finally
656            {
657                try
658                {
659                    if( conn != null ) conn.close();
660                }
661                catch( Exception e )
662                {
663                }
664            }
665        }
666    
667        /**
668         * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
669         */
670        public void save( UserProfile profile ) throws WikiSecurityException
671        {
672            String initialRole = "Authenticated";
673    
674            // Figure out which prepared statement to use & execute it
675            String loginName = profile.getLoginName();
676            PreparedStatement ps = null;
677            UserProfile existingProfile = null;
678    
679            try
680            {
681                existingProfile = findByLoginName( loginName );
682            }
683            catch( NoSuchPrincipalException e )
684            {
685                // Existing profile will be null
686            }
687    
688            // Get a clean password from the passed profile.
689            // Blank password is the same as null, which means we re-use the
690            // existing one.
691            String password = profile.getPassword();
692            String existingPassword = (existingProfile == null) ? null : existingProfile.getPassword();
693            if( NOTHING.equals( password ) )
694            {
695                password = null;
696            }
697            if( password == null )
698            {
699                password = existingPassword;
700            }
701    
702            // If password changed, hash it before we save
703            if( !password.equals( existingPassword ) )
704            {
705                password = getHash( password );
706            }
707    
708            Connection conn = null;
709            try
710            {
711                // Open the database connection
712                conn = m_ds.getConnection();
713                if( m_supportsCommits )
714                {
715                    conn.setAutoCommit( false );
716                }
717    
718                Timestamp ts = new Timestamp( System.currentTimeMillis() );
719                Date modDate = new Date( ts.getTime() );
720                java.sql.Date lockExpiry = profile.getLockExpiry() == null ? null : new java.sql.Date( profile.getLockExpiry().getTime() );
721                if( existingProfile == null )
722                {
723                    // User is new: insert new user record
724                    ps = conn.prepareStatement( m_insertProfile );
725                    ps.setString( 1, profile.getUid() );
726                    ps.setString( 2, profile.getEmail() );
727                    ps.setString( 3, profile.getFullname() );
728                    ps.setString( 4, password );
729                    ps.setString( 5, profile.getWikiName() );
730                    ps.setTimestamp( 6, ts );
731                    ps.setString( 7, profile.getLoginName() );
732                    try
733                    {
734                        ps.setString( 8, Serializer.serializeToBase64( profile.getAttributes() ) );
735                    }
736                    catch ( IOException e )
737                    {
738                        throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e );
739                    }
740                    ps.setTimestamp( 9, ts );
741                    ps.execute();
742                    ps.close();
743    
744                    // Insert new role record
745                    ps = conn.prepareStatement( m_findRoles );
746                    ps.setString( 1, profile.getLoginName() );
747                    ResultSet rs = ps.executeQuery();
748                    int roles = 0;
749                    while ( rs.next() )
750                    {
751                        roles++;
752                    }
753                    ps.close();
754                    if( roles == 0 )
755                    {
756                        ps = conn.prepareStatement( m_insertRole );
757                        ps.setString( 1, profile.getLoginName() );
758                        ps.setString( 2, initialRole );
759                        ps.execute();
760                        ps.close();
761                    }
762    
763                    // Set the profile creation time
764                    profile.setCreated( modDate );
765                }
766                else
767                {
768                    // User exists: modify existing record
769                    ps = conn.prepareStatement( m_updateProfile );
770                    ps.setString( 1, profile.getUid() );
771                    ps.setString( 2, profile.getEmail() );
772                    ps.setString( 3, profile.getFullname() );
773                    ps.setString( 4, password );
774                    ps.setString( 5, profile.getWikiName() );
775                    ps.setTimestamp( 6, ts );
776                    ps.setString( 7, profile.getLoginName() );
777                    try
778                    {
779                        ps.setString( 8, Serializer.serializeToBase64( profile.getAttributes() ) );
780                    }
781                    catch ( IOException e )
782                    {
783                        throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e );
784                    }
785                    ps.setDate( 9, lockExpiry );
786                    ps.setString( 10, profile.getLoginName() );
787                    ps.execute();
788                    ps.close();
789                }
790                // Set the profile mod time
791                profile.setLastModified( modDate );
792    
793                // Commit and close connection
794                if( m_supportsCommits )
795                {
796                    conn.commit();
797                }
798            }
799            catch( SQLException e )
800            {
801                throw new WikiSecurityException( e.getMessage(), e );
802            }
803            finally
804            {
805                try
806                {
807                    if( conn != null ) conn.close();
808                }
809                catch( Exception e )
810                {
811                }
812            }
813        }
814    
815        /**
816         * Private method that returns the first {@link UserProfile} matching a
817         * named column's value. This method will also set the UID if it has not yet been set.     
818         * @param sql the SQL statement that should be prepared; it must have one parameter
819         * to set (either a String or a Long)
820         * @param index the value to match
821         * @return the resolved UserProfile
822         * @throws SQLException
823         */
824        private UserProfile findByPreparedStatement( String sql, Object index ) throws NoSuchPrincipalException
825        {
826            UserProfile profile = null;
827            boolean found = false;
828            boolean unique = true;
829            Connection conn = null;
830            try
831            {
832                // Open the database connection
833                conn = m_ds.getConnection();
834                if( m_supportsCommits )
835                {
836                    conn.setAutoCommit( false );
837                }
838    
839                PreparedStatement ps = conn.prepareStatement( sql );
840                
841                // Set the parameter to search by
842                if ( index instanceof String )
843                {
844                    ps.setString( 1, (String)index );
845                }
846                else if ( index instanceof Long )
847                {
848                    ps.setLong( 1, ( (Long)index).longValue() );
849                }
850                else 
851                {
852                    throw new IllegalArgumentException( "Index type not recognized!" );
853                }
854                
855                // Go and get the record!
856                ResultSet rs = ps.executeQuery();
857                while ( rs.next() )
858                {
859                    if( profile != null )
860                    {
861                        unique = false;
862                        break;
863                    }
864                    profile = newProfile();
865                    
866                    // Fetch the basic user attributes
867                    profile.setUid( rs.getString( m_uid ) );
868                    if ( profile.getUid() == null )
869                    {
870                        profile.setUid( generateUid( this ) );
871                    }
872                    profile.setCreated( rs.getTimestamp( m_created ) );
873                    profile.setEmail( rs.getString( m_email ) );
874                    profile.setFullname( rs.getString( m_fullName ) );
875                    profile.setLastModified( rs.getTimestamp( m_modified ) );
876                    Date lockExpiry = rs.getDate( m_lockExpiry );
877                    profile.setLockExpiry( rs.wasNull() ? null : lockExpiry );
878                    profile.setLoginName( rs.getString( m_loginName ) );
879                    profile.setPassword( rs.getString( m_password ) );
880                    
881                    // Fetch the user attributes
882                    String rawAttributes = rs.getString( m_attributes );
883                    if ( rawAttributes != null )
884                    {
885                        try
886                        {
887                            Map<String,? extends Serializable> attributes = Serializer.deserializeFromBase64( rawAttributes );
888                            profile.getAttributes().putAll( attributes );
889                        }
890                        catch ( IOException e )
891                        {
892                            log.error( "Could not parse user profile attributes!", e );
893                        }
894                    }
895                    found = true;
896                }
897                ps.close();
898            }
899            catch( SQLException e )
900            {
901                throw new NoSuchPrincipalException( e.getMessage() );
902            }
903            finally
904            {
905                try
906                {
907                    if( conn != null ) conn.close();
908                }
909                catch( Exception e )
910                {
911                }
912            }
913    
914            if( !found )
915            {
916                throw new NoSuchPrincipalException( "Could not find profile in database!" );
917            }
918            if( !unique )
919            {
920                throw new NoSuchPrincipalException( "More than one profile in database!" );
921            }
922            return profile;
923    
924        }
925    
926    }