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
93 private String m_getUsersSql;
94
95
96 private String m_userByNameCaseInsensitiveSql;
97
98
99
100 private String m_insertUserSql;
101 private String m_updateUserSql;
102 private String m_deleteUserSql;
103
104
105 private String m_createUserTableSql;
106
107
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 * <repository name="LocalUsers"
145 * class="org.apache.james.userrepository.JamesUsersJdbcRepository">
146 * <!-- Name of the datasource to use -->
147 * <data-source>MailDb</data-source>
148 * <!-- File to load the SQL definitions from -->
149 * <sqlFile>dist/conf/sqlResources.xml</sqlFile>
150 * <!-- replacement parameters for the sql file -->
151 * <sqlParameters table="JamesUsers"/>
152 * </repository>
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
167
168 String destUrl = configuration.getAttribute("destinationURL");
169
170 if ( ! destUrl.endsWith("/") ) {
171 destUrl += "/";
172 }
173
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
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
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
218
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
257 m_datasource = (DataSourceComponent)m_datasources.select(m_datasourceName);
258
259
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
289
290 m_getUsersSql = sqlStatements.getSqlString("select", true);
291
292
293
294 m_userByNameCaseInsensitiveSql =
295 sqlStatements.getSqlString("selectByLowercaseName");
296
297
298
299 m_insertUserSql = sqlStatements.getSqlString("insert", true);
300 m_updateUserSql = sqlStatements.getSqlString("update", true);
301 m_deleteUserSql = sqlStatements.getSqlString("delete", true);
302
303
304 m_createUserTableSql = sqlStatements.getSqlString("createTable", true);
305
306
307
308 String tableName = sqlStatements.getSqlString("tableName", true);
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324 if (! theJDBCUtil.tableExists(dbMetaData, tableName))
325 {
326
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
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();
387
388 Connection conn = openConnection();
389 PreparedStatement getUsersStatement = null;
390 ResultSet rsUsers = null;
391 try {
392
393 getUsersStatement =
394 conn.prepareStatement(m_getUsersSql);
395 rsUsers = getUsersStatement.executeQuery();
396
397
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
427 try {
428
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
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
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
512
513 if ( m_userByNameCaseInsensitiveSql == null ) {
514 return super.getUserByName(name, ignoreCase);
515 }
516
517
518
519 Connection conn = openConnection();
520 PreparedStatement getUsersStatement = null;
521 ResultSet rsUsers = null;
522 try {
523
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
532 User user = null;
533 while ( rsUsers.next() ) {
534 User rowUser = readUserFromResultSet(rsUsers);
535 String actualName = rowUser.getUserName();
536
537
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