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