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