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
020package org.apache.james.user.jdbc;
021
022import java.io.InputStream;
023import java.sql.Connection;
024import java.sql.DatabaseMetaData;
025import java.sql.PreparedStatement;
026import java.sql.ResultSet;
027import java.sql.SQLException;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.HashMap;
031import java.util.Iterator;
032import java.util.List;
033import java.util.Locale;
034import java.util.Map;
035
036import javax.annotation.PostConstruct;
037import javax.inject.Inject;
038import javax.sql.DataSource;
039
040import org.apache.commons.configuration.ConfigurationException;
041import org.apache.commons.configuration.HierarchicalConfiguration;
042import org.apache.james.filesystem.api.FileSystem;
043import org.apache.james.user.api.UsersRepositoryException;
044import org.apache.james.user.api.model.User;
045import org.apache.james.user.lib.AbstractJamesUsersRepository;
046import org.apache.james.util.sql.JDBCUtil;
047import org.apache.james.util.sql.SqlResources;
048
049/**
050 * An abstract base class for creating UserRepository implementations which use
051 * a database for persistence.
052 * 
053 * To implement a new UserRepository using by extending this class, you need to
054 * implement the 3 abstract methods defined below, and define the required SQL
055 * statements in an SQLResources file.
056 * 
057 * The SQL statements used by this implementation are:
058 * <table>
059 * <tr>
060 * <td><b>Required</b></td>
061 * <td></td>
062 * </tr>
063 * <tr>
064 * <td>select</td>
065 * <td>Select all users.</td>
066 * </tr>
067 * <tr>
068 * <td>insert</td>
069 * <td>Insert a user.</td>
070 * </tr>
071 * <tr>
072 * <td>update</td>
073 * <td>Update a user.</td>
074 * </tr>
075 * <tr>
076 * <td>delete</td>
077 * <td>Delete a user by name.</td>
078 * </tr>
079 * <tr>
080 * <td>createTable</td>
081 * <td>Create the users table.</td>
082 * </tr>
083 * <tr>
084 * <td><b>Optional</b></td>
085 * <td></td>
086 * </tr>
087 * <tr>
088 * <td>selectByLowercaseName</td>
089 * <td>Select a user by name (case-insensitive lowercase).</td>
090 * </tr>
091 * </table>
092 */
093@Deprecated
094public abstract class AbstractJdbcUsersRepository extends AbstractJamesUsersRepository {
095
096    protected Map<String, String> m_sqlParameters;
097
098    private String m_sqlFileName;
099
100    private DataSource m_datasource;
101
102    // Fetches all Users from the db.
103    private String m_getUsersSql;
104
105    // This fetch a user by name, ensuring case-insensitive matching.
106    private String m_userByNameCaseInsensitiveSql;
107
108    // Insert, update and delete sql statements are not guaranteed
109    // to be case-insensitive; this is handled in code.
110    private String m_insertUserSql;
111
112    private String m_updateUserSql;
113
114    private String m_deleteUserSql;
115
116    // The JDBCUtil helper class
117    private JDBCUtil theJDBCUtil;
118
119    private FileSystem fileSystem;
120
121    /**
122     * Removes a user from the repository
123     * 
124     * @param userName
125     *            the user to be removed
126     * @throws UsersRepositoryException
127     */
128    public void removeUser(String userName) throws UsersRepositoryException {
129        User user = getUserByName(userName);
130        if (user != null) {
131            doRemoveUser(user);
132        } else {
133            throw new UsersRepositoryException("User " + userName + " does not exist");
134        }
135    }
136
137    /**
138     * Get the user object with the specified user name. Return null if no such
139     * user.
140     * 
141     * @param name
142     *            the name of the user to retrieve
143     * 
144     * @return the user if found, null otherwise
145     * 
146     * @since James 1.2.2
147     */
148    public User getUserByName(String name) throws UsersRepositoryException {
149        return getUserByName(name, ignoreCase);
150    }
151
152    /**
153     * Returns whether or not this user is in the repository
154     * 
155     * @return true or false
156     */
157    public boolean contains(String name) throws UsersRepositoryException {
158        User user = getUserByName(name, ignoreCase);
159        return (user != null);
160    }
161
162    /**
163     * Returns whether or not this user is in the repository. Names are matched
164     * on a case insensitive basis.
165     * 
166     * @return true or false
167     */
168    public boolean containsCaseInsensitive(String name) throws UsersRepositoryException {
169        User user = getUserByName(name, true);
170        return (user != null);
171    }
172
173    /**
174     * Test if user with name 'name' has password 'password'.
175     * 
176     * @param name
177     *            the name of the user to be tested
178     * @param password
179     *            the password to be tested
180     * 
181     * @return true if the test is successful, false if the password is
182     *         incorrect or the user doesn't exist
183     * @since James 1.2.2
184     */
185    public boolean test(String name, String password) throws UsersRepositoryException {
186        User user = getUserByName(name, ignoreCase);
187        return user != null && user.verifyPassword(password);
188    }
189
190    /**
191     * Returns a count of the users in the repository.
192     * 
193     * @return the number of users in the repository
194     */
195    public int countUsers() throws UsersRepositoryException {
196        List<String> usernames = listUserNames();
197        return usernames.size();
198    }
199
200    /**
201     * List users in repository.
202     * 
203     * @return Iterator over a collection of Strings, each being one user in the
204     *         repository.
205     */
206    public Iterator<String> list() throws UsersRepositoryException {
207        return listUserNames().iterator();
208    }
209
210    /**
211     * Set the DataSourceSelector
212     * 
213     * @param m_datasource
214     *            the DataSourceSelector
215     */
216    @Inject
217    public void setDatasource(DataSource m_datasource) {
218        this.m_datasource = m_datasource;
219    }
220
221    /**
222     * Sets the filesystem service
223     * 
224     * @param system
225     *            the new service
226     */
227    @Inject
228    public void setFileSystem(FileSystem system) {
229        this.fileSystem = system;
230    }
231
232    /**
233     * Initialises the JDBC repository.
234     * <ol>
235     * <li>Tests the connection to the database.</li>
236     * <li>Loads SQL strings from the SQL definition file, choosing the
237     * appropriate SQL for this connection, and performing parameter
238     * substitution,</li>
239     * <li>Initialises the database with the required tables, if necessary.</li>
240     * </ol>
241     * 
242     * @throws Exception
243     *             if an error occurs
244     */
245    @PostConstruct
246    public void init() throws Exception {
247        StringBuffer logBuffer;
248        if (getLogger().isDebugEnabled()) {
249            logBuffer = new StringBuffer(128).append(this.getClass().getName()).append(".initialize()");
250            getLogger().debug(logBuffer.toString());
251        }
252
253        theJDBCUtil = new JDBCUtil() {
254            protected void delegatedLog(String logString) {
255                AbstractJdbcUsersRepository.this.getLogger().warn("AbstractJdbcUsersRepository: " + logString);
256            }
257        };
258
259        // Test the connection to the database, by getting the DatabaseMetaData.
260        Connection conn = openConnection();
261        try {
262            DatabaseMetaData dbMetaData = conn.getMetaData();
263
264            InputStream sqlFile;
265
266            try {
267                sqlFile = fileSystem.getResource(m_sqlFileName);
268            } catch (Exception e) {
269                getLogger().error(e.getMessage(), e);
270                throw e;
271            }
272
273            if (getLogger().isDebugEnabled()) {
274                logBuffer = new StringBuffer(256).append("Reading SQL resources from: ").append(m_sqlFileName).append(", section ").append(this.getClass().getName()).append(".");
275                getLogger().debug(logBuffer.toString());
276            }
277
278            SqlResources sqlStatements = new SqlResources();
279            sqlStatements.init(sqlFile, this.getClass().getName(), conn, m_sqlParameters);
280
281            // Create the SQL Strings to use for this table.
282            // Fetches all Users from the db.
283            m_getUsersSql = sqlStatements.getSqlString("select", true);
284
285            // Get a user by lowercase name. (optional)
286            // If not provided, the entire list is iterated to find a user.
287            m_userByNameCaseInsensitiveSql = sqlStatements.getSqlString("selectByLowercaseName");
288
289            // Insert, update and delete are not guaranteed to be
290            // case-insensitive
291            // Will always be called with correct case in username..
292            m_insertUserSql = sqlStatements.getSqlString("insert", true);
293            m_updateUserSql = sqlStatements.getSqlString("update", true);
294            m_deleteUserSql = sqlStatements.getSqlString("delete", true);
295
296            // Creates a single table with "username" the Primary Key.
297            String createUserTableSql = sqlStatements.getSqlString("createTable", true);
298
299            // Check if the required table exists. If not, create it.
300            // The table name is defined in the SqlResources.
301            String tableName = sqlStatements.getSqlString("tableName", true);
302
303            // Need to ask in the case that identifiers are stored, ask the
304            // DatabaseMetaInfo.
305            // NB this should work, but some drivers (eg mm MySQL)
306            // don't return the right details, hence the hackery below.
307            /*
308             * String tableName = m_tableName; if (
309             * dbMetaData.storesLowerCaseIdentifiers() ) { tableName =
310             * tableName.toLowerCase(Locale.US); } else if (
311             * dbMetaData.storesUpperCaseIdentifiers() ) { tableName =
312             * tableName.toUpperCase(Locale.US); }
313             */
314
315            // Try UPPER, lower, and MixedCase, to see if the table is there.
316            if (!theJDBCUtil.tableExists(dbMetaData, tableName)) {
317                // Users table doesn't exist - create it.
318                PreparedStatement createStatement = null;
319                try {
320                    createStatement = conn.prepareStatement(createUserTableSql);
321                    createStatement.execute();
322                } finally {
323                    theJDBCUtil.closeJDBCStatement(createStatement);
324                }
325
326                logBuffer = new StringBuffer(128).append(this.getClass().getName()).append(": Created table \'").append(tableName).append("\'.");
327                getLogger().info(logBuffer.toString());
328            } else {
329                if (getLogger().isDebugEnabled()) {
330                    getLogger().debug("Using table: " + tableName);
331                }
332            }
333
334        } finally {
335            theJDBCUtil.closeJDBCConnection(conn);
336        }
337    }
338
339    /**
340     * <p>
341     * Configures the UserRepository for JDBC access.
342     * </p>
343     * <p>
344     * Requires a configuration element in the .conf.xml file of the form:
345     * </p>
346     * 
347     * <pre>
348     *   &lt;repository name=&quot;so even &quot;
349     *       class=&quot;org.apache.james.userrepository.JamesUsersJdbcRepository&quot;&gt;
350     *       &lt;!-- Name of the datasource to use --&gt;
351     *       &lt;data-source&gt;MailDb&lt;/data-source&gt;
352     *       &lt;!-- File to load the SQL definitions from --&gt;
353     *       &lt;sqlFile&gt;dist/conf/sqlResources.xml&lt;/sqlFile&gt;
354     *       &lt;!-- replacement parameters for the sql file --&gt;
355     *       &lt;sqlParameters table=&quot;JamesUsers&quot;/&gt;
356     *   &lt;/repository&gt;
357     * </pre>
358     * 
359     * @see org.apache.james.user.lib.AbstractJamesUsersRepository#doConfigure(org.apache.commons.configuration.HierarchicalConfiguration)
360     */
361    protected void doConfigure(HierarchicalConfiguration configuration) throws ConfigurationException {
362        StringBuffer logBuffer;
363        if (getLogger().isDebugEnabled()) {
364            logBuffer = new StringBuffer(64).append(this.getClass().getName()).append(".configure()");
365            getLogger().debug(logBuffer.toString());
366        }
367
368        // Parse the DestinationURL for the name of the datasource,
369        // the table to use, and the (optional) repository Key.
370        String destUrl = configuration.getString("[@destinationURL]", null);
371        // throw an exception if the attribute is missing
372        if (destUrl == null)
373            throw new ConfigurationException("destinationURL attribute is missing from Configuration");
374
375        // normalise the destination, to simplify processing.
376        if (!destUrl.endsWith("/")) {
377            destUrl += "/";
378        }
379        // Split on "/", starting after "db://"
380        List<String> urlParams = new ArrayList<String>();
381        int start = 5;
382        int end = destUrl.indexOf('/', start);
383        while (end > -1) {
384            urlParams.add(destUrl.substring(start, end));
385            start = end + 1;
386            end = destUrl.indexOf('/', start);
387        }
388
389        // Build SqlParameters and get datasource name from URL parameters
390        m_sqlParameters = new HashMap<String, String>();
391        switch (urlParams.size()) {
392        case 3:
393            m_sqlParameters.put("key", urlParams.get(2));
394        case 2:
395            m_sqlParameters.put("table", urlParams.get(1));
396        case 1:
397            urlParams.get(0);
398            break;
399        default:
400            throw new ConfigurationException("Malformed destinationURL - " + "Must be of the format \"db://<data-source>[/<table>[/<key>]]\".");
401        }
402
403        if (getLogger().isDebugEnabled()) {
404            logBuffer = new StringBuffer(128).append("Parsed URL: table = '").append(m_sqlParameters.get("table")).append("', key = '").append(m_sqlParameters.get("key")).append("'");
405            getLogger().debug(logBuffer.toString());
406        }
407
408        // Get the SQL file location
409        m_sqlFileName = configuration.getString("sqlFile", null);
410
411        // Get other sql parameters from the configuration object,
412        // if any.
413        Iterator<String> paramIt = configuration.getKeys("sqlParameters");
414        while (paramIt.hasNext()) {
415            String rawName = paramIt.next();
416            String paramName = paramIt.next().substring("sqlParameters.[@".length(), rawName.length() - 1);
417            String paramValue = configuration.getString(rawName);
418            m_sqlParameters.put(paramName, paramValue);
419        }
420    }
421
422    /**
423     * Produces the complete list of User names, with correct case.
424     * 
425     * @return a <code>List</code> of <code>String</code>s representing user
426     *         names.
427     */
428    protected List<String> listUserNames() throws UsersRepositoryException {
429        Collection<User> users = getAllUsers();
430        List<String> userNames = new ArrayList<String>(users.size());
431        for (User user : users) {
432            userNames.add(user.getUserName());
433        }
434        users.clear();
435        return userNames;
436    }
437
438    /**
439     * Returns a list populated with all of the Users in the repository.
440     * 
441     * @return an <code>Iterator</code> of <code>User</code>s.
442     */
443    protected Iterator<User> listAllUsers() throws UsersRepositoryException {
444        return getAllUsers().iterator();
445    }
446
447    /**
448     * Returns a list populated with all of the Users in the repository.
449     * 
450     * @return a <code>Collection</code> of <code>JamesUser</code>s.
451     * @throws UsersRepositoryException
452     */
453    private Collection<User> getAllUsers() throws UsersRepositoryException {
454        List<User> userList = new ArrayList<User>(); // Build the users into
455                                                     // this list.
456
457        Connection conn = null;
458        PreparedStatement getUsersStatement = null;
459        ResultSet rsUsers = null;
460        try {
461            conn = openConnection();
462            // Get a ResultSet containing all users.
463            getUsersStatement = conn.prepareStatement(m_getUsersSql);
464            rsUsers = getUsersStatement.executeQuery();
465
466            // Loop through and build a User for every row.
467            while (rsUsers.next()) {
468                User user = readUserFromResultSet(rsUsers);
469                userList.add(user);
470            }
471        } catch (SQLException sqlExc) {
472            sqlExc.printStackTrace();
473            throw new UsersRepositoryException("Error accessing database", sqlExc);
474        } finally {
475            theJDBCUtil.closeJDBCResultSet(rsUsers);
476            theJDBCUtil.closeJDBCStatement(getUsersStatement);
477            theJDBCUtil.closeJDBCConnection(conn);
478        }
479
480        return userList;
481    }
482
483    /**
484     * Adds a user to the underlying Repository. The user name must not clash
485     * with an existing user.
486     * 
487     * @param user
488     *            the user to add
489     * @throws UsersRepositoryException
490     */
491    protected void doAddUser(User user) throws UsersRepositoryException {
492        Connection conn = null;
493        PreparedStatement addUserStatement = null;
494
495        // Insert into the database.
496        try {
497            conn = openConnection();
498            // Get a PreparedStatement for the insert.
499            addUserStatement = conn.prepareStatement(m_insertUserSql);
500
501            setUserForInsertStatement(user, addUserStatement);
502
503            addUserStatement.execute();
504        } catch (SQLException sqlExc) {
505            sqlExc.printStackTrace();
506            throw new UsersRepositoryException("Error accessing database", sqlExc);
507        } finally {
508            theJDBCUtil.closeJDBCStatement(addUserStatement);
509            theJDBCUtil.closeJDBCConnection(conn);
510        }
511    }
512
513    /**
514     * Removes a user from the underlying repository. If the user doesn't exist,
515     * returns ok.
516     * 
517     * @param user
518     *            the user to remove
519     * @throws UsersRepositoryException
520     */
521    protected void doRemoveUser(User user) throws UsersRepositoryException {
522        String username = user.getUserName();
523
524        Connection conn = null;
525        PreparedStatement removeUserStatement = null;
526
527        // Delete from the database.
528        try {
529            conn = openConnection();
530            removeUserStatement = conn.prepareStatement(m_deleteUserSql);
531            removeUserStatement.setString(1, username);
532            removeUserStatement.execute();
533        } catch (SQLException sqlExc) {
534            sqlExc.printStackTrace();
535            throw new UsersRepositoryException("Error accessing database", sqlExc);
536        } finally {
537            theJDBCUtil.closeJDBCStatement(removeUserStatement);
538            theJDBCUtil.closeJDBCConnection(conn);
539        }
540    }
541
542    /**
543     * Updates a user record to match the supplied User.
544     * 
545     * @param user
546     *            the user to update
547     * @throws UsersRepositoryException
548     */
549    protected void doUpdateUser(User user) throws UsersRepositoryException {
550        Connection conn = null;
551        PreparedStatement updateUserStatement = null;
552
553        // Update the database.
554        try {
555            conn = openConnection();
556            updateUserStatement = conn.prepareStatement(m_updateUserSql);
557            setUserForUpdateStatement(user, updateUserStatement);
558            updateUserStatement.execute();
559        } catch (SQLException sqlExc) {
560            sqlExc.printStackTrace();
561            throw new UsersRepositoryException("Error accessing database", sqlExc);
562        } finally {
563            theJDBCUtil.closeJDBCStatement(updateUserStatement);
564            theJDBCUtil.closeJDBCConnection(conn);
565        }
566    }
567
568    /**
569     * Gets a user by name, ignoring case if specified. This implementation gets
570     * the entire set of users, and scrolls through searching for one matching
571     * <code>name</code>.
572     * 
573     * @param name
574     *            the name of the user being retrieved
575     * @param ignoreCase
576     *            whether the name is regarded as case-insensitive
577     * 
578     * @return the user being retrieved, null if the user doesn't exist
579     * @throws UsersRepositoryException
580     */
581    protected User getUserByNameIterating(String name, boolean ignoreCase) throws UsersRepositoryException {
582        // Just iterate through all of the users until we find one matching.
583        Iterator<User> users = listAllUsers();
584        while (users.hasNext()) {
585            User user = users.next();
586            String username = user.getUserName();
587            if ((!ignoreCase && username.equals(name)) || (ignoreCase && username.equalsIgnoreCase(name))) {
588                return user;
589            }
590        }
591        // Not found - return null
592        return null;
593    }
594
595    /**
596     * Gets a user by name, ignoring case if specified. If the specified SQL
597     * statement has been defined, this method overrides the basic
598     * implementation in AbstractJamesUsersRepository to increase performance.
599     * 
600     * @param name
601     *            the name of the user being retrieved
602     * @param ignoreCase
603     *            whether the name is regarded as case-insensitive
604     * 
605     * @return the user being retrieved, null if the user doesn't exist
606     * @throws UsersRepositoryException
607     */
608    protected User getUserByName(String name, boolean ignoreCase) throws UsersRepositoryException {
609        // See if this statement has been set, if not, use
610        // simple superclass method.
611        if (m_userByNameCaseInsensitiveSql == null) {
612            return getUserByNameIterating(name, ignoreCase);
613        }
614
615        // Always get the user via case-insensitive SQL,
616        // then check case if necessary.
617        Connection conn = null;
618        PreparedStatement getUsersStatement = null;
619        ResultSet rsUsers = null;
620        try {
621            conn = openConnection();
622            // Get a ResultSet containing all users.
623            String sql = m_userByNameCaseInsensitiveSql;
624            getUsersStatement = conn.prepareStatement(sql);
625
626            getUsersStatement.setString(1, name.toLowerCase(Locale.US));
627
628            rsUsers = getUsersStatement.executeQuery();
629
630            // For case-insensitive matching, the first matching user will be
631            // returned.
632            User user = null;
633            while (rsUsers.next()) {
634                User rowUser = readUserFromResultSet(rsUsers);
635                String actualName = rowUser.getUserName();
636
637                // Check case before we assume it's the right one.
638                if (ignoreCase || actualName.equals(name)) {
639                    user = rowUser;
640                    break;
641                }
642            }
643            return user;
644        } catch (SQLException sqlExc) {
645            sqlExc.printStackTrace();
646            throw new UsersRepositoryException("Error accessing database", sqlExc);
647        } finally {
648            theJDBCUtil.closeJDBCResultSet(rsUsers);
649            theJDBCUtil.closeJDBCStatement(getUsersStatement);
650            theJDBCUtil.closeJDBCConnection(conn);
651        }
652    }
653
654    /**
655     * Reads properties for a User from an open ResultSet. Subclass
656     * implementations of this method must have knowledge of the fields
657     * presented by the "select" and "selectByLowercaseName" SQL statements.
658     * These implemenations may generate a subclass-specific User instance.
659     * 
660     * @param rsUsers
661     *            A ResultSet with a User record in the current row.
662     * @return A User instance
663     * @throws SQLException
664     *             if an exception occurs reading from the ResultSet
665     */
666    protected abstract User readUserFromResultSet(ResultSet rsUsers) throws SQLException;
667
668    /**
669     * Set parameters of a PreparedStatement object with property values from a
670     * User instance. Implementations of this method have knowledge of the
671     * parameter ordering of the "insert" SQL statement definition.
672     * 
673     * @param user
674     *            a User instance, which should be an implementation class which
675     *            is handled by this Repostory implementation.
676     * @param userInsert
677     *            a PreparedStatement initialised with SQL taken from the
678     *            "insert" SQL definition.
679     * @throws SQLException
680     *             if an exception occurs while setting parameter values.
681     */
682    protected abstract void setUserForInsertStatement(User user, PreparedStatement userInsert) throws SQLException;
683
684    /**
685     * Set parameters of a PreparedStatement object with property values from a
686     * User instance. Implementations of this method have knowledge of the
687     * parameter ordering of the "update" SQL statement definition.
688     * 
689     * @param user
690     *            a User instance, which should be an implementation class which
691     *            is handled by this Repostory implementation.
692     * @param userUpdate
693     *            a PreparedStatement initialised with SQL taken from the
694     *            "update" SQL definition.
695     * @throws SQLException
696     *             if an exception occurs while setting parameter values.
697     */
698    protected abstract void setUserForUpdateStatement(User user, PreparedStatement userUpdate) throws SQLException;
699
700    /**
701     * Opens a connection, throwing a runtime exception if a SQLException is
702     * encountered in the process.
703     * 
704     * @return the new connection
705     * @throws SQLException
706     */
707    private Connection openConnection() throws SQLException {
708        return m_datasource.getConnection();
709
710    }
711}