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 * <repository name="so even " 349 * class="org.apache.james.userrepository.JamesUsersJdbcRepository"> 350 * <!-- Name of the datasource to use --> 351 * <data-source>MailDb</data-source> 352 * <!-- File to load the SQL definitions from --> 353 * <sqlFile>dist/conf/sqlResources.xml</sqlFile> 354 * <!-- replacement parameters for the sql file --> 355 * <sqlParameters table="JamesUsers"/> 356 * </repository> 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}