View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17  
18  package org.apache.james.userrepository;
19  
20  import java.io.File;
21  import java.sql.Connection;
22  import java.sql.DatabaseMetaData;
23  import java.sql.PreparedStatement;
24  import java.sql.ResultSet;
25  import java.sql.SQLException;
26  import java.util.Collection;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  
34  import org.apache.avalon.cornerstone.services.datasources.DataSourceSelector;
35  import org.apache.avalon.excalibur.datasource.DataSourceComponent;
36  import org.apache.avalon.framework.CascadingRuntimeException;
37  import org.apache.avalon.framework.activity.Initializable;
38  import org.apache.avalon.framework.configuration.Configurable;
39  import org.apache.avalon.framework.configuration.Configuration;
40  import org.apache.avalon.framework.configuration.ConfigurationException;
41  import org.apache.avalon.framework.context.Context;
42  import org.apache.avalon.framework.context.ContextException;
43  import org.apache.avalon.framework.context.Contextualizable;
44  import org.apache.avalon.framework.service.Serviceable;
45  import org.apache.avalon.framework.service.ServiceManager;
46  import org.apache.avalon.framework.service.ServiceException;
47  import org.apache.james.context.AvalonContextUtilities;
48  import org.apache.james.util.JDBCUtil;
49  import org.apache.james.util.SqlResources;
50  import org.apache.james.services.User;
51  
52  /***
53   * An abstract base class for creating UserRepository implementations
54   * which use a database for persistence.
55   *
56   * To implement a new UserRepository using by extending this class,
57   * you need to implement the 3 abstract methods defined below,
58   * and define the required SQL statements in an SQLResources
59   * file.
60   *
61   * The SQL statements used by this implementation are:
62   * <TABLE>
63   * <TH><TD><B>Required</B></TD></TH>
64   * <TR><TD>select</TD><TD>Select all users.</TD></TR>
65   * <TR><TD>insert</TD><TD>Insert a user.</TD></TR>
66   * <TR><TD>update</TD><TD>Update a user.</TD></TR>
67   * <TR><TD>delete</TD><TD>Delete a user by name.</TD></TR>
68   * <TR><TD>createTable</TD><TD>Create the users table.</TD></TR>
69   * <TH><TD><B>Optional</B></TD></TH>
70   * <TR><TD>selectByLowercaseName</TD><TD>Select a user by name (case-insensitive lowercase).</TD></TR>
71   * </TABLE>
72   *
73   */
74  public abstract class AbstractJdbcUsersRepository extends AbstractUsersRepository
75      implements Contextualizable, Serviceable, Configurable, Initializable
76  {
77      /***
78       * The Avalon context used by the instance
79       */
80      protected Context context;
81  
82      protected Map m_sqlParameters;
83  
84      private String m_sqlFileName;
85  
86      private String m_datasourceName;
87  
88      private DataSourceSelector m_datasources;
89  
90      private DataSourceComponent m_datasource;
91  
92      // Fetches all Users from the db.
93      private String m_getUsersSql;
94  
95      // This fetch a user by name, ensuring case-insensitive matching.
96      private String m_userByNameCaseInsensitiveSql;
97  
98      // Insert, update and delete sql statements are not guaranteed
99      //  to be case-insensitive; this is handled in code.
100     private String m_insertUserSql;
101     private String m_updateUserSql;
102     private String m_deleteUserSql;
103 
104     // Creates a single table with "username" the Primary Key.
105     private String m_createUserTableSql;
106 
107     // The JDBCUtil helper class
108     private JDBCUtil theJDBCUtil;
109 
110     /***
111      * @see org.apache.avalon.framework.context.Contextualizable#contextualize(Context)
112      */
113     public void contextualize(final Context context)
114             throws ContextException {
115         this.context = context;
116     }
117 
118     /***
119      * @see org.apache.avalon.framework.service.Serviceable#compose(ServiceManager)
120      */
121     public void service( final ServiceManager componentManager )
122         throws ServiceException
123     {
124         StringBuffer logBuffer = null;
125         if (getLogger().isDebugEnabled())
126         {
127             logBuffer =
128                 new StringBuffer(64)
129                         .append(this.getClass().getName())
130                         .append(".compose()");
131             getLogger().debug( logBuffer.toString() );
132         }
133 
134         m_datasources =
135             (DataSourceSelector)componentManager.lookup( DataSourceSelector.ROLE );
136     }
137 
138     /***
139      * Configures the UserRepository for JDBC access.<br>
140      * <br>
141      * Requires a configuration element in the .conf.xml file of the form:<br>
142      * <br>
143      * <pre>
144      *  &lt;repository name="LocalUsers"
145      *      class="org.apache.james.userrepository.JamesUsersJdbcRepository"&gt;
146      *      &lt;!-- Name of the datasource to use --&gt;
147      *      &lt;data-source&gt;MailDb&lt;/data-source&gt;
148      *      &lt;!-- File to load the SQL definitions from -->
149      *      &lt;sqlFile>dist/conf/sqlResources.xml&lt;/sqlFile&gt;
150      *      &lt;!-- replacement parameters for the sql file --&gt;
151      *      &lt;sqlParameters table="JamesUsers"/&gt;
152      *  &lt;/repository&gt;
153      * </pre>
154      */
155     public void configure(Configuration configuration) throws ConfigurationException
156     {
157         StringBuffer logBuffer = null;
158         if (getLogger().isDebugEnabled()) {
159             logBuffer =
160                 new StringBuffer(64)
161                         .append(this.getClass().getName())
162                         .append(".configure()");
163             getLogger().debug( logBuffer.toString() );
164         }
165 
166         // Parse the DestinationURL for the name of the datasource,
167         // the table to use, and the (optional) repository Key.
168         String destUrl = configuration.getAttribute("destinationURL");
169         // normalise the destination, to simplify processing.
170         if ( ! destUrl.endsWith("/") ) {
171             destUrl += "/";
172         }
173         // Split on "/", starting after "db://"
174         List urlParams = new ArrayList();
175         int start = 5;
176         int end = destUrl.indexOf('/', start);
177         while ( end > -1 ) {
178             urlParams.add(destUrl.substring(start, end));
179             start = end + 1;
180             end = destUrl.indexOf('/', start);
181         }
182 
183         // Build SqlParameters and get datasource name from URL parameters
184         m_sqlParameters = new HashMap();
185         switch ( urlParams.size() ) {
186         case 3:
187             m_sqlParameters.put("key", urlParams.get(2));
188         case 2:
189             m_sqlParameters.put("table", urlParams.get(1));
190         case 1:
191             m_datasourceName = (String)urlParams.get(0);
192             break;
193         default:
194             throw new ConfigurationException
195                 ("Malformed destinationURL - " +
196                  "Must be of the format \"db://<data-source>[/<table>[/<key>]]\".");
197         }
198 
199         if (getLogger().isDebugEnabled()) {
200             logBuffer =
201                 new StringBuffer(128)
202                         .append("Parsed URL: table = '")
203                         .append(m_sqlParameters.get("table"))
204                         .append("', key = '")
205                         .append(m_sqlParameters.get("key"))
206                         .append("'");
207             getLogger().debug(logBuffer.toString());
208         }
209 
210         // Get the SQL file location
211         m_sqlFileName = configuration.getChild("sqlFile", true).getValue();
212         if (!m_sqlFileName.startsWith("file://")) {
213             throw new ConfigurationException
214                 ("Malformed sqlFile - Must be of the format \"file://<filename>\".");
215         }
216 
217         // Get other sql parameters from the configuration object,
218         // if any.
219         Configuration sqlParamsConfig = configuration.getChild("sqlParameters");
220         String[] paramNames = sqlParamsConfig.getAttributeNames();
221         for (int i = 0; i < paramNames.length; i++ ) {
222             String paramName = paramNames[i];
223             String paramValue = sqlParamsConfig.getAttribute(paramName);
224             m_sqlParameters.put(paramName, paramValue);
225         }
226     }
227 
228     /***
229      * <p>Initialises the JDBC repository.</p>
230      * <p>1) Tests the connection to the database.</p>
231      * <p>2) Loads SQL strings from the SQL definition file,
232      *     choosing the appropriate SQL for this connection,
233      *     and performing parameter substitution,</p>
234      * <p>3) Initialises the database with the required tables, if necessary.</p>
235      *
236      * @throws Exception if an error occurs
237      */
238     public void initialize() throws Exception
239     {
240         StringBuffer logBuffer = null;
241         if (getLogger().isDebugEnabled()) {
242             logBuffer =
243                 new StringBuffer(128)
244                         .append(this.getClass().getName())
245                         .append(".initialize()");
246             getLogger().debug( logBuffer.toString() );
247         }
248 
249         theJDBCUtil =
250             new JDBCUtil() {
251                 protected void delegatedLog(String logString) {
252                     AbstractJdbcUsersRepository.this.getLogger().warn("AbstractJdbcUsersRepository: " + logString);
253                 }
254             };
255 
256         // Get the data-source required.
257         m_datasource = (DataSourceComponent)m_datasources.select(m_datasourceName);
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             File sqlFile = null;
265 
266             try {
267                 sqlFile = AvalonContextUtilities.getFile(context, m_sqlFileName);
268             } catch (Exception e) {
269                 getLogger().fatalError(e.getMessage(), e);
270                 throw e;
271             }
272 
273             if (getLogger().isDebugEnabled()) {
274                 logBuffer =
275                     new StringBuffer(256)
276                             .append("Reading SQL resources from file: ")
277                             .append(sqlFile.getAbsolutePath())
278                             .append(", section ")
279                             .append(this.getClass().getName())
280                             .append(".");
281                 getLogger().debug(logBuffer.toString());
282             }
283 
284             SqlResources sqlStatements = new SqlResources();
285             sqlStatements.init(sqlFile, this.getClass().getName(),
286                                conn, m_sqlParameters);
287 
288             // Create the SQL Strings to use for this table.
289             // Fetches all Users from the db.
290             m_getUsersSql = sqlStatements.getSqlString("select", true);
291 
292             // Get a user by lowercase name. (optional)
293             // If not provided, the entire list is iterated to find a user.
294             m_userByNameCaseInsensitiveSql =
295                 sqlStatements.getSqlString("selectByLowercaseName");
296 
297             // Insert, update and delete are not guaranteed to be case-insensitive
298             // Will always be called with correct case in username..
299             m_insertUserSql = sqlStatements.getSqlString("insert", true);
300             m_updateUserSql = sqlStatements.getSqlString("update", true);
301             m_deleteUserSql = sqlStatements.getSqlString("delete", true);
302 
303             // Creates a single table with "username" the Primary Key.
304             m_createUserTableSql = sqlStatements.getSqlString("createTable", true);
305 
306             // Check if the required table exists. If not, create it.
307             // The table name is defined in the SqlResources.
308             String tableName = sqlStatements.getSqlString("tableName", true);
309 
310             // Need to ask in the case that identifiers are stored, ask the 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;
315             if ( dbMetaData.storesLowerCaseIdentifiers() ) {
316                 tableName = tableName.toLowerCase(Locale.US);
317             }
318             else if ( dbMetaData.storesUpperCaseIdentifiers() ) {
319                 tableName = tableName.toUpperCase(Locale.US);
320             }
321             */
322 
323             // Try UPPER, lower, and MixedCase, to see if the table is there.
324             if (! theJDBCUtil.tableExists(dbMetaData, tableName))
325             {
326                 // Users table doesn't exist - create it.
327                 PreparedStatement createStatement = null;
328                 try {
329                     createStatement =
330                         conn.prepareStatement(m_createUserTableSql);
331                     createStatement.execute();
332                 } finally {
333                     theJDBCUtil.closeJDBCStatement(createStatement);
334                 }
335 
336                 logBuffer =
337                     new StringBuffer(128)
338                             .append(this.getClass().getName())
339                             .append(": Created table \'")
340                             .append(tableName)
341                             .append("\'.");
342                 getLogger().info(logBuffer.toString());
343             } else {
344                 if (getLogger().isDebugEnabled()) {
345                     getLogger().debug("Using table: " + tableName);
346                 }
347             }
348 
349         }
350         finally {
351             theJDBCUtil.closeJDBCConnection( conn );
352         }
353     }
354 
355     /***
356      * Produces the complete list of User names, with correct case.
357      * @return a <code>List</code> of <code>String</code>s representing
358      *         user names.
359      */
360     protected List listUserNames() {
361         Collection users = getAllUsers();
362         List userNames = new ArrayList(users.size());
363         for (Iterator it = users.iterator(); it.hasNext(); ) {
364             userNames.add(((User)it.next()).getUserName());
365         }
366         users.clear();
367         return userNames;
368     }
369 
370     //
371     // Superclass methods - overridden from AbstractUsersRepository
372     //
373     /***
374      * Returns a list populated with all of the Users in the repository.
375      * @return an <code>Iterator</code> of <code>JamesUser</code>s.
376      */
377     protected Iterator listAllUsers() {
378         return getAllUsers().iterator();
379     }
380 
381     /***
382      * Returns a list populated with all of the Users in the repository.
383      * @return a <code>Collection</code> of <code>JamesUser</code>s.
384      */
385     private Collection getAllUsers() {
386         List userList = new ArrayList(); // Build the users into this list.
387 
388         Connection conn = openConnection();
389         PreparedStatement getUsersStatement = null;
390         ResultSet rsUsers = null;
391         try {
392             // Get a ResultSet containing all users.
393             getUsersStatement = 
394                                conn.prepareStatement(m_getUsersSql);
395             rsUsers = getUsersStatement.executeQuery();
396 
397             // Loop through and build a User for every row.
398             while ( rsUsers.next() ) {
399                 User user = readUserFromResultSet(rsUsers);
400                 userList.add(user);
401             }
402         }
403         catch ( SQLException sqlExc) {
404             sqlExc.printStackTrace();
405             throw new CascadingRuntimeException("Error accessing database", sqlExc);
406         }
407         finally {
408             theJDBCUtil.closeJDBCResultSet(rsUsers);
409             theJDBCUtil.closeJDBCStatement(getUsersStatement);
410             theJDBCUtil.closeJDBCConnection(conn);
411         }
412 
413         return userList;
414     }
415 
416     /***
417      * Adds a user to the underlying Repository.
418      * The user name must not clash with an existing user.
419      *
420      * @param user the user to be added
421      */
422     protected void doAddUser(User user) {
423         Connection conn = openConnection();
424         PreparedStatement addUserStatement = null;
425 
426         // Insert into the database.
427         try {
428             // Get a PreparedStatement for the insert.
429             addUserStatement =
430                 conn.prepareStatement(m_insertUserSql);
431 
432             setUserForInsertStatement(user, addUserStatement);
433 
434             addUserStatement.execute();
435         }
436         catch ( SQLException sqlExc) {
437             sqlExc.printStackTrace();
438             throw new CascadingRuntimeException("Error accessing database", sqlExc);
439         } finally {
440             theJDBCUtil.closeJDBCStatement(addUserStatement);
441             theJDBCUtil.closeJDBCConnection(conn);
442         }
443     }
444 
445     /***
446      * Removes a user from the underlying repository.
447      * If the user doesn't exist this method doesn't throw
448      * an exception.
449      *
450      * @param user the user to be removed
451      */
452     protected void doRemoveUser(User user) {
453         String username = user.getUserName();
454 
455         Connection conn = openConnection();
456         PreparedStatement removeUserStatement = null;
457 
458         // Delete from the database.
459         try {
460             removeUserStatement = conn.prepareStatement(m_deleteUserSql);
461             removeUserStatement.setString(1, username);
462             removeUserStatement.execute();
463         }
464         catch ( SQLException sqlExc ) {
465             sqlExc.printStackTrace();
466             throw new CascadingRuntimeException("Error accessing database", sqlExc);
467         } finally {
468             theJDBCUtil.closeJDBCStatement(removeUserStatement);
469             theJDBCUtil.closeJDBCConnection(conn);
470         }
471     }
472 
473     /***
474      * Updates a user record to match the supplied User.
475      *
476      * @param user the updated user record
477      */
478     protected void doUpdateUser(User user)
479     {
480         Connection conn = openConnection();
481         PreparedStatement updateUserStatement = null;
482 
483         // Update the database.
484         try {
485             updateUserStatement = conn.prepareStatement(m_updateUserSql);
486             setUserForUpdateStatement(user, updateUserStatement);
487             updateUserStatement.execute();
488         }
489         catch ( SQLException sqlExc ) {
490             sqlExc.printStackTrace();
491             throw new CascadingRuntimeException("Error accessing database", sqlExc);
492         } finally {
493             theJDBCUtil.closeJDBCStatement(updateUserStatement);
494             theJDBCUtil.closeJDBCConnection(conn);
495         }
496     }
497 
498     /***
499      * Gets a user by name, ignoring case if specified.
500      * If the specified SQL statement has been defined, this method
501      * overrides the basic implementation in AbstractUsersRepository
502      * to increase performance.
503      *
504      * @param name the name of the user being retrieved
505      * @param ignoreCase whether the name is regarded as case-insensitive
506      *
507      * @return the user being retrieved, null if the user doesn't exist
508      */
509     protected User getUserByName(String name, boolean ignoreCase)
510     {
511         // See if this statement has been set, if not, use
512         // simple superclass method.
513         if ( m_userByNameCaseInsensitiveSql == null ) {
514             return super.getUserByName(name, ignoreCase);
515         }
516 
517         // Always get the user via case-insensitive SQL,
518         // then check case if necessary.
519         Connection conn = openConnection();
520         PreparedStatement getUsersStatement = null;
521         ResultSet rsUsers = null;
522         try {
523             // Get a ResultSet containing all users.
524             String sql = m_userByNameCaseInsensitiveSql;
525             getUsersStatement = conn.prepareStatement(sql);
526 
527             getUsersStatement.setString(1, name.toLowerCase(Locale.US));
528 
529             rsUsers = getUsersStatement.executeQuery();
530 
531             // For case-insensitive matching, the first matching user will be returned.
532             User user = null;
533             while ( rsUsers.next() ) {
534                 User rowUser = readUserFromResultSet(rsUsers);
535                 String actualName = rowUser.getUserName();
536 
537                 // Check case before we assume it's the right one.
538                 if ( ignoreCase || actualName.equals(name) ) {
539                     user = rowUser;
540                     break;
541                 }
542             }
543             return user;
544         }
545         catch ( SQLException sqlExc ) {
546             sqlExc.printStackTrace();
547             throw new CascadingRuntimeException("Error accessing database", sqlExc);
548         }
549         finally {
550             theJDBCUtil.closeJDBCResultSet(rsUsers);
551             theJDBCUtil.closeJDBCStatement(getUsersStatement);
552             theJDBCUtil.closeJDBCConnection(conn);
553         }
554     }
555 
556 
557     /***
558      * Reads properties for a User from an open ResultSet.
559      * Subclass implementations of this method must have knowledge of the fields
560      * presented by the "select" and "selectByLowercaseName" SQL statements.
561      * These implemenations may generate a subclass-specific User instance.
562      *
563      * @param rsUsers A ResultSet with a User record in the current row.
564      * @return A User instance
565      * @throws SQLException
566      *                   if an exception occurs reading from the ResultSet
567      */
568     protected abstract User readUserFromResultSet(ResultSet rsUsers)
569         throws SQLException;
570 
571     /***
572      * Set parameters of a PreparedStatement object with
573      * property values from a User instance.
574      * Implementations of this method have knowledge of the parameter
575      * ordering of the "insert" SQL statement definition.
576      *
577      * @param user       a User instance, which should be an implementation class which
578      *                   is handled by this Repostory implementation.
579      * @param userInsert a PreparedStatement initialised with SQL taken from the "insert" SQL definition.
580      * @throws SQLException
581      *                   if an exception occurs while setting parameter values.
582      */
583     protected abstract void setUserForInsertStatement(User user,
584                                                       PreparedStatement userInsert)
585         throws SQLException;
586 
587     /***
588      * Set parameters of a PreparedStatement object with
589      * property values from a User instance.
590      * Implementations of this method have knowledge of the parameter
591      * ordering of the "update" SQL statement definition.
592      *
593      * @param user       a User instance, which should be an implementation class which
594      *                   is handled by this Repostory implementation.
595      * @param userUpdate a PreparedStatement initialised with SQL taken from the "update" SQL definition.
596      * @throws SQLException
597      *                   if an exception occurs while setting parameter values.
598      */
599     protected abstract void setUserForUpdateStatement(User user,
600                                                       PreparedStatement userUpdate)
601         throws SQLException;
602 
603     /***
604      * Opens a connection, throwing a runtime exception if a SQLException is
605      * encountered in the process.
606      *
607      * @return the new connection
608      */
609     private Connection openConnection()
610     {
611         try {
612             return m_datasource.getConnection();
613         }
614         catch (SQLException sqle) {
615             throw new CascadingRuntimeException(
616                 "An exception occurred getting a database connection.", sqle);
617         }
618     }
619 }
620 
621