001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.core.impl.user.directory; 017 018import java.sql.Connection; 019import java.sql.PreparedStatement; 020import java.sql.ResultSet; 021import java.sql.SQLException; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.configuration.Configuration; 033import org.apache.avalon.framework.configuration.ConfigurationException; 034import org.apache.avalon.framework.context.Context; 035import org.apache.avalon.framework.context.ContextException; 036import org.apache.avalon.framework.context.Contextualizable; 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.avalon.framework.service.Serviceable; 040import org.apache.commons.codec.digest.DigestUtils; 041import org.apache.commons.lang.RandomStringUtils; 042import org.apache.commons.lang.StringUtils; 043import org.apache.commons.lang3.ArrayUtils; 044import org.apache.excalibur.source.SourceResolver; 045 046import org.ametys.core.ObservationConstants; 047import org.ametys.core.datasource.ConnectionHelper; 048import org.ametys.core.observation.Event; 049import org.ametys.core.observation.ObservationManager; 050import org.ametys.core.script.SQLScriptHelper; 051import org.ametys.core.user.CurrentUserProvider; 052import org.ametys.core.user.InvalidModificationException; 053import org.ametys.core.user.User; 054import org.ametys.core.user.UserIdentity; 055import org.ametys.core.user.directory.ModifiableUserDirectory; 056import org.ametys.core.util.CachingComponent; 057import org.ametys.plugins.core.impl.user.jdbc.JdbcParameter; 058import org.ametys.runtime.i18n.I18nizableText; 059import org.ametys.runtime.parameter.AbstractParameterParser; 060import org.ametys.runtime.parameter.DefaultValidator; 061import org.ametys.runtime.parameter.Enumerator; 062import org.ametys.runtime.parameter.Errors; 063import org.ametys.runtime.parameter.Parameter; 064import org.ametys.runtime.parameter.ParameterHelper; 065import org.ametys.runtime.parameter.ParameterHelper.ParameterType; 066import org.ametys.runtime.parameter.Validator; 067import org.ametys.runtime.plugin.component.PluginAware; 068import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 069 070/** 071 * Use a jdbc driver for getting the list of users, modifying them and also 072 * authenticate them.<br> 073 * Passwords need to be encrypted with MD5 and encoded in base64.<br> 074 */ 075public class JdbcUserDirectory extends CachingComponent<User> implements ModifiableUserDirectory, Component, Serviceable, Contextualizable, PluginAware 076{ 077 /** The base plugin (for i18n key) */ 078 protected static final String BASE_PLUGIN_NAME = "core"; 079 080 static final String[] __COLUMNS = new String[] {"login", "password", "firstname", "lastname", "email"}; 081 082 /** Name of the parameter holding the datasource id */ 083 private static final String __DATASOURCE_PARAM_NAME = "runtime.users.jdbc.datasource"; 084 /** Name of the parameter holding the table users' name */ 085 private static final String __USERS_TABLE_PARAM_NAME = "runtime.users.jdbc.table"; 086 087 private static final String __COLUMN_LOGIN = "login"; 088 private static final String __COLUMN_PASSWORD = "password"; 089 private static final String __COLUMN_FIRSTNAME = "firstname"; 090 private static final String __COLUMN_LASTNAME = "lastname"; 091 private static final String __COLUMN_EMAIL = "email"; 092 private static final String __COLUMN_SALT = "salt"; 093 094 /** The identifier of data source */ 095 protected String _dataSourceId; 096 /** The name of users' SQL table */ 097 protected String _userTableName; 098 099 /** Model */ 100 protected Map<String, JdbcParameter> _model; 101 102 /** Plugin name */ 103 protected String _pluginName; 104 105 /** The avalon service manager */ 106 protected ServiceManager _manager; 107 108 /** The avalon context */ 109 protected Context _context; 110 111 /** The cocoon source resolver */ 112 protected SourceResolver _sourceResolver; 113 114 // ComponentManager for the Validators 115 private ThreadSafeComponentManager<Validator> _validatorManager; 116 117 //ComponentManager for the Enumerators 118 private ThreadSafeComponentManager<Enumerator> _enumeratorManager; 119 120 private ObservationManager _observationManager; 121 private CurrentUserProvider _currentUserProvider; 122 123 private String _udModelId; 124 private Map<String, Object> _paramValues; 125 private String _populationId; 126 127 private String _label; 128 129 private String _id; 130 131 private boolean _lazyInitialized; 132 133 @Override 134 public void setPluginInfo(String pluginName, String featureName, String id) 135 { 136 _pluginName = pluginName; 137 } 138 139 @Override 140 public void contextualize(Context context) throws ContextException 141 { 142 _context = context; 143 } 144 145 @Override 146 public void service(ServiceManager manager) throws ServiceException 147 { 148 _manager = manager; 149 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 150 } 151 152 @Override 153 public void dispose() 154 { 155 super.dispose(); 156 157 _enumeratorManager.dispose(); 158 _enumeratorManager = null; 159 160 _validatorManager.dispose(); 161 _validatorManager = null; 162 } 163 164 public String getId() 165 { 166 return _id; 167 } 168 169 public String getLabel() 170 { 171 return _label; 172 } 173 174 @Override 175 public void init(String id, String udModelId, Map<String, Object> paramValues, String label) 176 { 177 _id = id; 178 _udModelId = udModelId; 179 _paramValues = paramValues; 180 _label = label; 181 182 _userTableName = (String) paramValues.get(__USERS_TABLE_PARAM_NAME); 183 _dataSourceId = (String) paramValues.get(__DATASOURCE_PARAM_NAME); 184 185 configureModelParameters(); 186 } 187 188 /** 189 * Lazy lookup the {@link ObservationManager} 190 * @return the observation manager 191 */ 192 protected ObservationManager getObservationManager() 193 { 194 if (_observationManager == null) 195 { 196 try 197 { 198 _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE); 199 } 200 catch (ServiceException e) 201 { 202 // We may be in safe mode 203 getLogger().error("Unable to lookup ObservationManager component. Is safe mode ? It can not be retrieve in safe mode ", e); 204 } 205 } 206 return _observationManager; 207 } 208 209 /** 210 * Lazy lookup the {@link CurrentUserProvider} 211 * @return the current user provider 212 */ 213 protected CurrentUserProvider getCurrentUserProvider() 214 { 215 if (_currentUserProvider == null) 216 { 217 try 218 { 219 _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE); 220 } 221 catch (ServiceException e) 222 { 223 throw new RuntimeException("Unable to lookup CurrentUserProvider component", e); 224 } 225 } 226 return _currentUserProvider; 227 } 228 229 /** 230 * Get the connection to the database 231 * @return the SQL connection 232 */ 233 @SuppressWarnings("unchecked") 234 protected Connection getSQLConnection() 235 { 236 Connection connection = ConnectionHelper.getConnection(_dataSourceId); 237 238 if (!_lazyInitialized) 239 { 240 try 241 { 242 SQLScriptHelper.createTableIfNotExists(connection, _userTableName, "plugin:core://scripts/%s/jdbc_users.template.sql", _sourceResolver, 243 (Map) ArrayUtils.toMap(new String[][] {{"%TABLENAME%", _userTableName}})); 244 } 245 catch (Exception e) 246 { 247 getLogger().error("The tables requires by the " + this.getClass().getName() + " could not be created. A degraded behavior will occur", e); 248 } 249 250 _lazyInitialized = true; 251 } 252 253 return connection; 254 } 255 256 @Override 257 public void setPopulationId(String populationId) 258 { 259 _populationId = populationId; 260 } 261 262 @Override 263 public String getPopulationId() 264 { 265 return _populationId; 266 } 267 268 @Override 269 public Map<String, Object> getParameterValues() 270 { 271 return _paramValues; 272 } 273 274 @Override 275 public String getUserDirectoryModelId() 276 { 277 return _udModelId; 278 } 279 280 @SuppressWarnings("unchecked") 281 @Override 282 public Collection<User> getUsers() 283 { 284 return getUsers(Integer.MAX_VALUE, 0, Collections.EMPTY_MAP); 285 } 286 287 @Override 288 public List<User> getUsers(int count, int offset, Map<String, Object> parameters) 289 { 290 String pattern = StringUtils.defaultIfEmpty((String) parameters.get("pattern"), null); 291 int boundedCount = count >= 0 ? count : Integer.MAX_VALUE; 292 int boundedOffset = offset >= 0 ? offset : 0; 293 294 SelectUsersJdbcQueryExecutor<List<User>> queryExecutor = new SelectUsersJdbcQueryExecutor<List<User>>(pattern, boundedCount, boundedOffset) 295 { 296 @Override 297 protected List<User> processResultSet(ResultSet rs) throws SQLException 298 { 299 return _getUsersProcessResultSet(rs); 300 } 301 }; 302 303 return queryExecutor.run(); 304 } 305 306 @Override 307 public User getUser(String login) 308 { 309 if (isCacheEnabled()) 310 { 311 User user = getObjectFromCache(login); 312 if (user != null) 313 { 314 return user; 315 } 316 } 317 318 SelectUserJdbcQueryExecutor<User> queryExecutor = new SelectUserJdbcQueryExecutor<User>(login) 319 { 320 @Override 321 protected User processResultSet(ResultSet rs) throws SQLException 322 { 323 return _getUserProcessResultSet(rs, login); 324 } 325 }; 326 327 return queryExecutor.run(); 328 } 329 330 @Override 331 public boolean checkCredentials(String login, String password) 332 { 333 boolean updateNeeded = false; 334 335 Connection con = null; 336 PreparedStatement stmt = null; 337 ResultSet rs = null; 338 try 339 { 340 // Connect to the database with connection pool 341 con = getSQLConnection(); 342 343 // Build request for authenticating the user 344 String sql = "SELECT " + __COLUMN_LOGIN + ", " + __COLUMN_PASSWORD + ", " + __COLUMN_SALT + " FROM " + _userTableName + " WHERE " + __COLUMN_LOGIN + " = ?"; 345 if (getLogger().isDebugEnabled()) 346 { 347 getLogger().debug(sql); 348 } 349 350 stmt = con.prepareStatement(sql); 351 stmt.setString(1, login); 352 353 // Do the request 354 rs = stmt.executeQuery(); 355 356 if (rs.next()) 357 { 358 String storedPassword = rs.getString(__COLUMN_PASSWORD); 359 String salt = rs.getString(__COLUMN_SALT); 360 361 if (salt == null && _isMD5Encrypted(storedPassword)) 362 { 363 String encryptedPassword = org.ametys.core.util.StringUtils.md5Base64(password); 364 365 if (encryptedPassword == null) 366 { 367 getLogger().error("Unable to encrypt password"); 368 return false; 369 } 370 371 if (storedPassword.equals(encryptedPassword)) 372 { 373 updateNeeded = true; 374 return true; 375 } 376 377 return false; 378 } 379 else 380 { 381 String encryptedPassword = DigestUtils.sha512Hex(salt + password); 382 383 if (encryptedPassword == null) 384 { 385 getLogger().error("Unable to encrypt password"); 386 return false; 387 } 388 389 return storedPassword.equals(encryptedPassword); 390 } 391 } 392 393 return false; 394 } 395 catch (SQLException e) 396 { 397 getLogger().error("Error during the connection to the database", e); 398 return false; 399 } 400 finally 401 { 402 // Close connections 403 ConnectionHelper.cleanup(rs); 404 ConnectionHelper.cleanup(stmt); 405 ConnectionHelper.cleanup(con); 406 407 if (updateNeeded) 408 { 409 _updateToSSHAPassword(login, password); 410 } 411 } 412 } 413 414 @Override 415 public void add(Map<String, String> userInformation) throws InvalidModificationException 416 { 417 Connection con = null; 418 PreparedStatement stmt = null; 419 420 if (getLogger().isDebugEnabled()) 421 { 422 getLogger().debug("Starting adding a new user"); 423 } 424 425 // Check the presence of all parameters 426 Map<String, Errors> errorFields = validate(userInformation); 427 428 if (errorFields.size() > 0) 429 { 430 throw new InvalidModificationException("The creation of user failed because of invalid parameter values", errorFields); 431 } 432 433 String login = userInformation.get("login"); 434 435 try 436 { 437 // Connect to the database with connection pool 438 con = getSQLConnection(); 439 440 stmt = createAddStatement(con, userInformation); 441 442 // Do the request and check the result 443 if (stmt.executeUpdate() != 1) 444 { 445 if (getLogger().isWarnEnabled()) 446 { 447 getLogger().warn("The user to remove '" + login + "' was not removed."); 448 } 449 throw new InvalidModificationException("Error no user inserted"); 450 } 451 452 if (getObservationManager() != null) 453 { 454 // Observation manager can be null in safe mode 455 Map<String, Object> eventParams = new HashMap<>(); 456 eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, _populationId)); 457 getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_ADDED, getCurrentUserProvider().getUser(), eventParams)); 458 } 459 } 460 catch (SQLException e) 461 { 462 getLogger().error("Error communication with database", e); 463 throw new InvalidModificationException("Error during the communication with the database", e); 464 } 465 finally 466 { 467 // Close connections 468 ConnectionHelper.cleanup(stmt); 469 ConnectionHelper.cleanup(con); 470 } 471 472 } 473 474 @Override 475 public Map<String, Errors> validate(Map<String, String> userInformation) 476 { 477 Map<String, Errors> errorFields = new HashMap<>(); 478 for (JdbcParameter parameter : _model.values()) 479 { 480 String untypedvalue = userInformation.get(parameter.getId()); 481 Object typedvalue = ParameterHelper.castValue(untypedvalue, parameter.getType()); 482 Validator validator = parameter.getValidator(); 483 484 if (validator != null) 485 { 486 Errors errors = new Errors(); 487 validator.validate(typedvalue, errors); 488 489 if (errors.hasErrors()) 490 { 491 if (getLogger().isDebugEnabled()) 492 { 493 getLogger().debug("The field '" + parameter.getId() + "' is not valid"); 494 } 495 496 errorFields.put(parameter.getId(), errors); 497 } 498 } 499 } 500 return errorFields; 501 } 502 503 @Override 504 public void update(Map<String, String> userInformation) throws InvalidModificationException 505 { 506 Connection con = null; 507 PreparedStatement stmt = null; 508 509 Map<String, Errors> errorFields = new HashMap<>(); 510 for (String id : userInformation.keySet()) 511 { 512 JdbcParameter parameter = _model.get(id); 513 if (parameter != null) 514 { 515 String untypedvalue = userInformation.get(parameter.getId()); 516 Object typedvalue = ParameterHelper.castValue(untypedvalue, parameter.getType()); 517 Validator validator = parameter.getValidator(); 518 519 if (validator != null) 520 { 521 Errors errors = new Errors(); 522 validator.validate(typedvalue, errors); 523 524 if (errors.hasErrors()) 525 { 526 if (getLogger().isDebugEnabled()) 527 { 528 getLogger().debug("The field '" + parameter.getId() + "' is not valid"); 529 } 530 531 errorFields.put(parameter.getId(), errors); 532 } 533 } 534 } 535 } 536 537 if (errorFields.size() > 0) 538 { 539 throw new InvalidModificationException("The modification of user failed because of invalid parameter values", errorFields); 540 } 541 542 String login = userInformation.get("login"); 543 if (StringUtils.isEmpty(login)) 544 { 545 throw new InvalidModificationException("Cannot update without login information"); 546 } 547 548 try 549 { 550 // Connect to the database with connection pool 551 con = getSQLConnection(); 552 553 stmt = createModifyStatement(con, userInformation); 554 555 // Do the request 556 if (stmt.executeUpdate() != 1) 557 { 558 throw new InvalidModificationException("Error. User '" + login + "' not updated"); 559 } 560 561 if (getObservationManager() != null) 562 { 563 // Observation manager can be null in safe mode 564 Map<String, Object> eventParams = new HashMap<>(); 565 eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, _populationId)); 566 getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_UPDATED, getCurrentUserProvider().getUser(), eventParams)); 567 } 568 569 if (isCacheEnabled()) 570 { 571 removeObjectFromCache(login); 572 } 573 } 574 catch (SQLException e) 575 { 576 getLogger().error("Error communication with database", e); 577 throw new InvalidModificationException("Error communication with database", e); 578 } 579 finally 580 { 581 // Close connections 582 ConnectionHelper.cleanup(stmt); 583 ConnectionHelper.cleanup(con); 584 } 585 } 586 587 @Override 588 public void remove(String login) throws InvalidModificationException 589 { 590 Connection con = null; 591 PreparedStatement stmt = null; 592 593 try 594 { 595 // Connect to the database with connection pool 596 con = getSQLConnection(); 597 598 // Build request for removing the user 599 String sqlRequest = "DELETE FROM " + _userTableName + " WHERE " + __COLUMN_LOGIN + " = ?"; 600 if (getLogger().isDebugEnabled()) 601 { 602 getLogger().debug(sqlRequest); 603 } 604 605 stmt = con.prepareStatement(sqlRequest); 606 stmt.setString(1, login); 607 608 // Do the request and check the result 609 if (stmt.executeUpdate() != 1) 610 { 611 throw new InvalidModificationException("Error user was not deleted"); 612 } 613 614 if (getObservationManager() != null) 615 { 616 // Observation manager can be null in safe mode 617 Map<String, Object> eventParams = new HashMap<>(); 618 eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, _populationId)); 619 getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_DELETED, getCurrentUserProvider().getUser(), eventParams)); 620 } 621 622 if (isCacheEnabled()) 623 { 624 removeObjectFromCache(login); 625 } 626 } 627 catch (SQLException e) 628 { 629 throw new InvalidModificationException("Error during the communication with the database", e); 630 } 631 finally 632 { 633 // Close connections 634 ConnectionHelper.cleanup(stmt); 635 ConnectionHelper.cleanup(con); 636 } 637 } 638 639 @Override 640 public Collection< ? extends Parameter<ParameterType>> getModel() 641 { 642 return Collections.unmodifiableCollection(_model.values()); 643 } 644 645 /** 646 * Configure the edition model parameters 647 */ 648 protected void configureModelParameters() 649 { 650 _validatorManager = new ThreadSafeComponentManager<>(); 651 _validatorManager.setLogger(getLogger()); 652 _validatorManager.contextualize(_context); 653 _validatorManager.service(_manager); 654 655 _enumeratorManager = new ThreadSafeComponentManager<>(); 656 _enumeratorManager.setLogger(getLogger()); 657 _enumeratorManager.contextualize(_context); 658 _enumeratorManager.service(_manager); 659 660 _model = new LinkedHashMap<>(); 661 662 JdbcParameterParser jdbcParameterParser = new JdbcParameterParser(_enumeratorManager, _validatorManager); 663 664 for (String column : __COLUMNS) 665 { 666 JdbcParameter parameter = _configureParameter(jdbcParameterParser, column, column); 667 if (parameter != null) 668 { 669 _model.put(parameter.getId(), parameter); 670 } 671 } 672 673 try 674 { 675 jdbcParameterParser.lookupComponents(); 676 } 677 catch (Exception e) 678 { 679 getLogger().error("Unable to lookup parameter local components", e); 680 } 681 } 682 683 /** 684 * Configure the parameter (for special handling) 685 * @param jdbcParameterParser the {@link JdbcParameter} parser. 686 * @param id Id the of the parameter 687 * @param column Column name of the parameter 688 * @return The parameter created 689 */ 690 protected JdbcParameter _configureParameter(JdbcParameterParser jdbcParameterParser, String id, final String column) 691 { 692 JdbcParameter parameter = new JdbcParameter(); 693 694 String catalog = "plugin." + BASE_PLUGIN_NAME; 695 696 if ("password".equals(id)) 697 { 698 parameter.setPluginName(BASE_PLUGIN_NAME); 699 parameter.setLabel(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_PASSWORD_LABEL")); 700 parameter.setDescription(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_PASSWORD_DESCRIPTION")); 701 parameter.setType(ParameterType.PASSWORD); 702 parameter.setValidator(new DefaultValidator(null, true)); 703 } 704 else if ("login".equals(id)) 705 { 706 parameter.setPluginName(BASE_PLUGIN_NAME); 707 parameter.setLabel(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_LABEL")); 708 parameter.setDescription(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_DESCRIPTION")); 709 parameter.setType(ParameterType.STRING); 710 I18nizableText invalidText = new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_INVALID"); 711 parameter.setValidator(new DefaultValidator("^[a-zA-Z0-9_\\-\\.@]{3,64}$", invalidText, true)); 712 } 713 else if ("lastname".equals(id)) 714 { 715 parameter.setPluginName(BASE_PLUGIN_NAME); 716 parameter.setLabel(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_LASTNAME_LABEL")); 717 parameter.setDescription(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_LASTNAME_DESCRIPTION")); 718 parameter.setType(ParameterType.STRING); 719 parameter.setValidator(new DefaultValidator(null, true)); 720 } 721 else if ("firstname".equals(id)) 722 { 723 parameter.setPluginName(BASE_PLUGIN_NAME); 724 parameter.setLabel(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_FIRSTNAME_LABEL")); 725 parameter.setDescription(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_FIRSTNAME_DESCRIPTION")); 726 parameter.setType(ParameterType.STRING); 727 parameter.setValidator(new DefaultValidator(null, true)); 728 } 729 else if ("email".equals(id)) 730 { 731 parameter.setPluginName(BASE_PLUGIN_NAME); 732 parameter.setLabel(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_LABEL")); 733 parameter.setDescription(new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_DESCRIPTION")); 734 parameter.setType(ParameterType.STRING); 735 I18nizableText invalidText = new I18nizableText(catalog, "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_INVALID"); 736 parameter.setValidator(new DefaultValidator("^([\\w\\-\\.])+@([\\w\\-\\.])+\\.([a-zA-Z])+$", invalidText, false)); 737 } 738 else 739 { 740 return null; 741 } 742 743 parameter.setId(id); 744 parameter.setColumn(column); 745 746 return parameter; 747 } 748 749 /** 750 * Get the mandatory predicate to use when querying users by pattern. 751 * @param pattern The pattern to match, can be null. 752 * @return a {@link JdbcPredicate}, can be null. 753 */ 754 protected JdbcPredicate _getMandatoryPredicate(String pattern) 755 { 756 return null; 757 } 758 759 /** 760 * Get the pattern to match user login 761 * @param pattern the pattern 762 * @return the pattern to match user login 763 */ 764 protected String _getPatternToMatch(String pattern) 765 { 766 if (pattern != null) 767 { 768 return "%" + pattern + "%"; 769 } 770 return null; 771 } 772 773 /** 774 * Determines if the password is encrypted with MD5 algorithm 775 * @param password The encrypted password 776 * @return true if the password is encrypted with MD5 algorithm 777 */ 778 protected boolean _isMD5Encrypted(String password) 779 { 780 return password.length() == 24; 781 } 782 783 /** 784 * Generate a salt key and encrypt the password with the sha2 algorithm 785 * @param login The user login 786 * @param password The user pasword 787 */ 788 protected void _updateToSSHAPassword(String login, String password) 789 { 790 Connection con = null; 791 PreparedStatement stmt = null; 792 ResultSet rs = null; 793 794 try 795 { 796 con = getSQLConnection(); 797 798 String generateSaltKey = RandomStringUtils.randomAlphanumeric(48); 799 String newEncryptedPassword = DigestUtils.sha512Hex(generateSaltKey + password); 800 801 String sqlUpdate = "UPDATE " + _userTableName + " SET " + __COLUMN_PASSWORD + " = ?, " + __COLUMN_SALT + " = ? WHERE " + __COLUMN_LOGIN + " = ?"; 802 if (getLogger().isDebugEnabled()) 803 { 804 getLogger().debug(sqlUpdate); 805 } 806 807 stmt = con.prepareStatement(sqlUpdate); 808 stmt.setString(1, newEncryptedPassword); 809 stmt.setString(2, generateSaltKey); 810 stmt.setString(3, login); 811 812 stmt.execute(); 813 } 814 catch (SQLException e) 815 { 816 getLogger().error("Error during the connection to the database", e); 817 } 818 finally 819 { 820 // Close connections 821 ConnectionHelper.cleanup(rs); 822 ConnectionHelper.cleanup(stmt); 823 ConnectionHelper.cleanup(con); 824 } 825 } 826 827 /** 828 * Create Add statement 829 * @param con The sql connection 830 * @param userInformation the user informations 831 * @return The statement 832 * @throws SQLException if an error occurred 833 */ 834 protected PreparedStatement createAddStatement(Connection con, Map<String, String> userInformation) throws SQLException 835 { 836 String beginClause = "INSERT INTO " + _userTableName + " ("; 837 String middleClause = ") VALUES ("; 838 String endClause = ")"; 839 840 StringBuffer intoClause = new StringBuffer(); 841 StringBuffer valueClause = new StringBuffer(); 842 843 intoClause.append(__COLUMN_SALT); 844 valueClause.append("?"); 845 846 for (String column : __COLUMNS) 847 { 848 intoClause.append(", " + column); 849 valueClause.append(", ?"); 850 } 851 852 String sqlRequest = beginClause + intoClause.toString() + middleClause + valueClause + endClause; 853 if (getLogger().isDebugEnabled()) 854 { 855 getLogger().debug(sqlRequest); 856 } 857 858 PreparedStatement stmt = con.prepareStatement(sqlRequest); 859 860 int i = 1; 861 String generatedSaltKey = RandomStringUtils.randomAlphanumeric(48); 862 stmt.setString(i++, generatedSaltKey); 863 864 for (String column : __COLUMNS) 865 { 866 if ("password".equals(column)) 867 { 868 String encryptedPassword = DigestUtils.sha512Hex(generatedSaltKey + userInformation.get(column)); 869 if (encryptedPassword == null) 870 { 871 String message = "Cannot encode password"; 872 getLogger().error(message); 873 throw new SQLException(message); 874 } 875 stmt.setString(i++, encryptedPassword); 876 } 877 else 878 { 879 stmt.setString(i++, userInformation.get(column)); 880 } 881 } 882 883 return stmt; 884 } 885 886 /** 887 * Create statement to update database 888 * @param con The sql connection 889 * @param userInformation The user information 890 * @return The statement 891 * @throws SQLException if an error occurred 892 */ 893 protected PreparedStatement createModifyStatement(Connection con, Map<String, String> userInformation) throws SQLException 894 { 895 // Build request for editing the user 896 String beginClause = "UPDATE " + _userTableName + " SET "; 897 String endClause = " WHERE " + __COLUMN_LOGIN + " = ?"; 898 899 StringBuffer columnNames = new StringBuffer(""); 900 901 boolean passwordUpdate = false; 902 for (String id : userInformation.keySet()) 903 { 904 if (ArrayUtils.contains(__COLUMNS, id) && !"login".equals(id) && !("password".equals(id) && (userInformation.get(id) == null))) 905 { 906 if ("password".equals(id)) 907 { 908 passwordUpdate = true; 909 } 910 911 if (columnNames.length() > 0) 912 { 913 columnNames.append(", "); 914 } 915 columnNames.append(id + " = ?"); 916 } 917 } 918 919 if (passwordUpdate) 920 { 921 columnNames.append(", " + __COLUMN_SALT + " = ?"); 922 } 923 924 String sqlRequest = beginClause + columnNames.toString() + endClause; 925 if (getLogger().isDebugEnabled()) 926 { 927 getLogger().debug(sqlRequest); 928 } 929 930 PreparedStatement stmt = con.prepareStatement(sqlRequest); 931 _fillModifyStatement(stmt, userInformation); 932 933 return stmt; 934 } 935 936 /** 937 * Fill the statement with the user informations 938 * @param stmt The statement of the sql request 939 * @param userInformation the user informations 940 * @throws SQLException if an error occurred 941 */ 942 protected void _fillModifyStatement(PreparedStatement stmt, Map<String, String> userInformation) throws SQLException 943 { 944 int index = 1; 945 946 String generateSaltKey = RandomStringUtils.randomAlphanumeric(48); 947 boolean passwordUpdate = false; 948 949 for (String id : userInformation.keySet()) 950 { 951 if (ArrayUtils.contains(__COLUMNS, id) && !"login".equals(id)) 952 { 953 if ("password".equals(id)) 954 { 955 if (userInformation.get(id) != null) 956 { 957 String encryptedPassword = DigestUtils.sha512Hex(generateSaltKey + userInformation.get(id)); 958 if (encryptedPassword == null) 959 { 960 String message = "Cannot encrypt password"; 961 getLogger().error(message); 962 throw new SQLException(message); 963 } 964 stmt.setString(index++, encryptedPassword); 965 passwordUpdate = true; 966 } 967 } 968 else 969 { 970 stmt.setString(index++, userInformation.get(id)); 971 } 972 } 973 } 974 975 if (passwordUpdate) 976 { 977 stmt.setString(index++, generateSaltKey); 978 } 979 980 stmt.setString(index++, userInformation.get("login")); 981 } 982 983 /** 984 * Populate the user list with the result set 985 * @param rs The result set 986 * @return The user list 987 * @throws SQLException If an SQL exception occurs 988 */ 989 protected List<User> _getUsersProcessResultSet(ResultSet rs) throws SQLException 990 { 991 List<User> users = new ArrayList<>(); 992 993 while (rs.next()) 994 { 995 User user = null; 996 997 // Try to get in cache 998 if (isCacheEnabled()) 999 { 1000 String login = rs.getString(__COLUMN_LOGIN); 1001 user = getObjectFromCache(login); 1002 } 1003 1004 // Or create from result set 1005 if (user == null) 1006 { 1007 user = _createUserFromResultSet(rs); 1008 1009 if (isCacheEnabled()) 1010 { 1011 addObjectInCache(user.getIdentity().getLogin(), user); 1012 } 1013 } 1014 1015 users.add(user); 1016 } 1017 1018 return users; 1019 } 1020 1021 /** 1022 * Create the user implementation from the result set of the request 1023 * 1024 * @param rs The result set where you can use get methods 1025 * @return The user refleting the current cursor position in the result set 1026 * @throws SQLException if an error occurred 1027 */ 1028 protected User _createUserFromResultSet(ResultSet rs) throws SQLException 1029 { 1030 String login = rs.getString(__COLUMN_LOGIN); 1031 String lastName = rs.getString(__COLUMN_LASTNAME); 1032 String firstName = rs.getString(__COLUMN_FIRSTNAME); 1033 String email = rs.getString(__COLUMN_EMAIL); 1034 1035 return new User(new UserIdentity(login, _populationId), lastName, firstName, email, this); 1036 } 1037 1038 /** 1039 * Retrieve an user from a result set 1040 * @param rs The result set 1041 * @param login The user login 1042 * @return The retrieved user or null if not found 1043 * @throws SQLException If an SQL Exception occurs 1044 */ 1045 protected User _getUserProcessResultSet(ResultSet rs, String login) throws SQLException 1046 { 1047 if (rs.next()) 1048 { 1049 // Retrieve information of the user 1050 User user = _createUserFromResultSet(rs); 1051 1052 if (isCacheEnabled()) 1053 { 1054 addObjectInCache(login, user); 1055 } 1056 1057 return user; 1058 } 1059 else 1060 { 1061 // no user with this login in the database 1062 return null; 1063 } 1064 } 1065 1066 // ------------------------------ 1067 // INNER CLASSE 1068 // ------------------------------ 1069 /** 1070 * An internal query executor. 1071 * @param <T> The type of the queried object 1072 */ 1073 protected abstract class AbstractJdbcQueryExecutor<T> 1074 { 1075 /** 1076 * Main function, run the query process. Will not throw exception. Use 1077 * runWithException to catch non SQL exception thrown by 1078 * {@link #processResultSet(ResultSet)} 1079 * @return The queried object or null 1080 */ 1081 @SuppressWarnings("synthetic-access") 1082 public T run() 1083 { 1084 try 1085 { 1086 return runWithException(); 1087 } 1088 catch (Exception e) 1089 { 1090 getLogger().error("Exception during a query execution", e); 1091 throw new RuntimeException("Exception during a query execution", e); 1092 } 1093 } 1094 1095 /** 1096 * Main function, run the query process. 1097 * @return The queried object or null 1098 * @throws Exception All non SQLException will be thrown 1099 */ 1100 @SuppressWarnings("synthetic-access") 1101 public T runWithException() throws Exception 1102 { 1103 Connection connection = null; 1104 PreparedStatement stmt = null; 1105 ResultSet rs = null; 1106 1107 try 1108 { 1109 connection = getSQLConnection(); 1110 1111 String sql = getSqlQuery(connection); 1112 1113 if (getLogger().isDebugEnabled()) 1114 { 1115 getLogger().debug("Executing SQL query: " + sql); 1116 } 1117 1118 stmt = prepareStatement(connection, sql); 1119 rs = executeQuery(stmt); 1120 1121 return processResultSet(rs); 1122 } 1123 catch (SQLException e) 1124 { 1125 getLogger().error("Error during the communication with the database", e); 1126 throw new RuntimeException("Error during the communication with the database", e); 1127 } 1128 finally 1129 { 1130 ConnectionHelper.cleanup(rs); 1131 ConnectionHelper.cleanup(stmt); 1132 ConnectionHelper.cleanup(connection); 1133 } 1134 } 1135 1136 /** 1137 * Must return the SQL query to execute 1138 * @param connection The pool connection 1139 * @return The SQL query 1140 */ 1141 protected abstract String getSqlQuery(Connection connection); 1142 1143 /** 1144 * Prepare the statement to execute 1145 * @param connection The pool connection 1146 * @param sql The SQL query 1147 * @return The prepared statement, ready to be executed 1148 * @throws SQLException If an SQL Exception occurs 1149 */ 1150 protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException 1151 { 1152 return connection.prepareStatement(sql); 1153 } 1154 1155 /** 1156 * Execute the prepared statement and retrieves the result set. 1157 * @param stmt The prepared statement 1158 * @return The result set 1159 * @throws SQLException If an SQL Exception occurs 1160 */ 1161 protected ResultSet executeQuery(PreparedStatement stmt) throws SQLException 1162 { 1163 return stmt.executeQuery(); 1164 } 1165 1166 /** 1167 * Process the result set 1168 * @param rs The result set 1169 * @return The queried object or null 1170 * @throws SQLException If an SQL exception occurs 1171 * @throws Exception Other exception will be thrown when using {@link #runWithException()} 1172 */ 1173 protected T processResultSet(ResultSet rs) throws SQLException, Exception 1174 { 1175 return null; 1176 } 1177 } 1178 1179 /** 1180 * Query executor in order to select an user 1181 * @param <T> The type of the queried object 1182 */ 1183 protected class SelectUserJdbcQueryExecutor<T> extends AbstractJdbcQueryExecutor<T> 1184 { 1185 /** The user login */ 1186 protected String _login; 1187 1188 /** 1189 * The constructor 1190 * @param login The user login 1191 */ 1192 protected SelectUserJdbcQueryExecutor(String login) 1193 { 1194 _login = login; 1195 } 1196 1197 @Override 1198 protected String getSqlQuery(Connection connection) 1199 { 1200 // Build SQL request 1201 StringBuilder selectClause = new StringBuilder(); 1202 for (String id : __COLUMNS) 1203 { 1204 if (selectClause.length() > 0) 1205 { 1206 selectClause.append(", "); 1207 } 1208 selectClause.append(id); 1209 } 1210 1211 StringBuilder sql = new StringBuilder("SELECT "); 1212 sql.append(selectClause).append(" FROM ").append(_userTableName); 1213 sql.append(" WHERE login = ?"); 1214 1215 return sql.toString(); 1216 } 1217 1218 @Override 1219 protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException 1220 { 1221 PreparedStatement stmt = super.prepareStatement(connection, sql); 1222 1223 stmt.setString(1, _login); 1224 return stmt; 1225 } 1226 } 1227 1228 /** 1229 * Query executor in order to select users 1230 * @param <T> The type of the queried object 1231 */ 1232 protected class SelectUsersJdbcQueryExecutor<T> extends AbstractJdbcQueryExecutor<T> 1233 { 1234 /** The pattern to match (none if null) */ 1235 protected String _pattern; 1236 /** The maximum number of users to select */ 1237 protected int _length; 1238 /** The offset to start with, first is 0 */ 1239 protected int _offset; 1240 1241 /** The mandatory predicate to use when querying users by pattern */ 1242 protected JdbcPredicate _mandatoryPredicate; 1243 /** The pattern to match, extracted from the pattern */ 1244 protected String _patternToMatch; 1245 1246 /** 1247 * The constructor 1248 * @param pattern The pattern to match (none if null). 1249 * @param length The maximum number of users to select. 1250 * @param offset The offset to start with, first is 0. 1251 */ 1252 protected SelectUsersJdbcQueryExecutor(String pattern, int length, int offset) 1253 { 1254 _pattern = pattern; 1255 _length = length; 1256 _offset = offset; 1257 } 1258 1259 @Override 1260 protected String getSqlQuery(Connection connection) 1261 { 1262 // Build SQL request 1263 StringBuilder selectClause = new StringBuilder(); 1264 for (String column : __COLUMNS) 1265 { 1266 if (selectClause.length() > 0) 1267 { 1268 selectClause.append(", "); 1269 } 1270 selectClause.append(column); 1271 } 1272 1273 StringBuilder sql = new StringBuilder("SELECT "); 1274 sql.append(selectClause).append(" FROM ").append(_userTableName); 1275 1276 // Add the pattern 1277 _mandatoryPredicate = _getMandatoryPredicate(_pattern); 1278 if (_mandatoryPredicate != null) 1279 { 1280 sql.append(" WHERE ").append(_mandatoryPredicate.getPredicate()); 1281 } 1282 1283 _patternToMatch = _getPatternToMatch(_pattern); 1284 if (_patternToMatch != null) 1285 { 1286 if (ConnectionHelper.DATABASE_DERBY.equals(ConnectionHelper.getDatabaseType(connection))) 1287 { 1288 // The LIKE operator in Derby is case sensitive 1289 sql.append(_mandatoryPredicate != null ? " AND (" : " WHERE ") 1290 .append("UPPER(").append(__COLUMN_LOGIN).append(") LIKE UPPER(?) OR ") 1291 .append("UPPER(").append(__COLUMN_LASTNAME).append(") LIKE UPPER(?) OR ") 1292 .append("UPPER(").append(__COLUMN_FIRSTNAME).append(") LIKE UPPER(?)"); 1293 } 1294 else 1295 { 1296 sql.append(_mandatoryPredicate != null ? " AND (" : " WHERE ") 1297 .append(__COLUMN_LOGIN).append(" LIKE ? OR ") 1298 .append(__COLUMN_LASTNAME).append(" LIKE ? OR ") 1299 .append(__COLUMN_FIRSTNAME).append(" LIKE ?"); 1300 } 1301 1302 if (_mandatoryPredicate != null) 1303 { 1304 sql.append(')'); 1305 } 1306 } 1307 1308 // Add length filters 1309 sql = _addQuerySize(_length, _offset, connection, selectClause, sql); 1310 1311 return sql.toString(); 1312 } 1313 1314 @SuppressWarnings("synthetic-access") 1315 private StringBuilder _addQuerySize(int length, int offset, Connection con, StringBuilder selectClause, StringBuilder sql) 1316 { 1317 // Do not add anything if not necessary 1318 if (length == Integer.MAX_VALUE && offset == 0) 1319 { 1320 return sql; 1321 } 1322 1323 String dbType = ConnectionHelper.getDatabaseType(con); 1324 1325 if (ConnectionHelper.DATABASE_MYSQL.equals(dbType) || ConnectionHelper.DATABASE_POSTGRES.equals(dbType) || ConnectionHelper.DATABASE_HSQLDB.equals(dbType)) 1326 { 1327 sql.append(" LIMIT " + length + " OFFSET " + offset); 1328 return sql; 1329 } 1330 else if (ConnectionHelper.DATABASE_ORACLE.equals(dbType)) 1331 { 1332 return new StringBuilder("select " + selectClause.toString() + " from (select rownum r, " + selectClause.toString() + " from (" + sql.toString() 1333 + ")) where r BETWEEN " + (offset + 1) + " AND " + (offset + length)); 1334 } 1335 else if (ConnectionHelper.DATABASE_DERBY.equals(dbType)) 1336 { 1337 return new StringBuilder("select ").append(selectClause) 1338 .append(" from (select ROW_NUMBER() OVER () AS ROWNUM, ").append(selectClause.toString()) 1339 .append(" from (").append(sql.toString()).append(") AS TR ) AS TRR where ROWNUM BETWEEN ") 1340 .append(offset + 1).append(" AND ").append(offset + length); 1341 } 1342 else if (getLogger().isWarnEnabled()) 1343 { 1344 getLogger().warn("The request will not have the limit and offset set, since its type is unknown"); 1345 } 1346 1347 return sql; 1348 } 1349 1350 @Override 1351 protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException 1352 { 1353 PreparedStatement stmt = super.prepareStatement(connection, sql); 1354 1355 int i = 1; 1356 // Value the parameters if there is a pattern 1357 if (_mandatoryPredicate != null) 1358 { 1359 for (String value : _mandatoryPredicate.getValues()) 1360 { 1361 stmt.setString(i++, value); 1362 } 1363 } 1364 1365 if (_patternToMatch != null) 1366 { 1367 // One for the login, one for the lastname. 1368 stmt.setString(i++, _patternToMatch); 1369 stmt.setString(i++, _patternToMatch); 1370 // FIXME 1371 //if (_parameters.containsKey("firstname")) 1372 //{ 1373 stmt.setString(i++, _patternToMatch); 1374 //} 1375 } 1376 1377 return stmt; 1378 } 1379 } 1380 1381 /** 1382 * {@link AbstractParameterParser} for parsing {@link JdbcParameter}. 1383 */ 1384 protected static class JdbcParameterParser extends AbstractParameterParser<JdbcParameter, ParameterType> 1385 { 1386 JdbcParameterParser(ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager) 1387 { 1388 super(enumeratorManager, validatorManager); 1389 } 1390 1391 @Override 1392 protected JdbcParameter _createParameter(Configuration parameterConfig) throws ConfigurationException 1393 { 1394 return new JdbcParameter(); 1395 } 1396 1397 @Override 1398 protected String _parseId(Configuration parameterConfig) throws ConfigurationException 1399 { 1400 return parameterConfig.getAttribute("id"); 1401 } 1402 1403 @Override 1404 protected ParameterType _parseType(Configuration parameterConfig) throws ConfigurationException 1405 { 1406 try 1407 { 1408 return ParameterType.valueOf(parameterConfig.getAttribute("type").toUpperCase()); 1409 } 1410 catch (IllegalArgumentException e) 1411 { 1412 throw new ConfigurationException("Invalid type", parameterConfig, e); 1413 } 1414 } 1415 1416 @Override 1417 protected Object _parseDefaultValue(Configuration parameterConfig, JdbcParameter jdbcParameter) 1418 { 1419 String defaultValue = parameterConfig.getChild("default-value").getValue(null); 1420 return ParameterHelper.castValue(defaultValue, jdbcParameter.getType()); 1421 } 1422 } 1423 1424 /** 1425 * Class representing a SQL predicate (to use in a WHERE or HAVING clause), 1426 * with optional string parameters. 1427 */ 1428 public class JdbcPredicate 1429 { 1430 1431 /** The predicate string with optional "?" placeholders. */ 1432 protected String _predicate; 1433 /** The predicate parameter values. */ 1434 protected List<String> _predicateParamValues; 1435 1436 /** 1437 * Build a JDBC predicate. 1438 * @param predicate the predicate string. 1439 * @param values the parameter values. 1440 */ 1441 public JdbcPredicate(String predicate, String... values) 1442 { 1443 this(predicate, Arrays.asList(values)); 1444 } 1445 1446 /** 1447 * Build a JDBC predicate. 1448 * @param predicate the predicate string. 1449 * @param values the parameter values. 1450 */ 1451 public JdbcPredicate(String predicate, List<String> values) 1452 { 1453 this._predicate = predicate; 1454 this._predicateParamValues = values; 1455 } 1456 1457 /** 1458 * Get the predicate. 1459 * @return the predicate 1460 */ 1461 public String getPredicate() 1462 { 1463 return _predicate; 1464 } 1465 1466 /** 1467 * Set the predicate. 1468 * @param predicate the predicate to set 1469 */ 1470 public void setPredicate(String predicate) 1471 { 1472 this._predicate = predicate; 1473 } 1474 1475 /** 1476 * Get the parameter values. 1477 * @return the parameter values. 1478 */ 1479 public List<String> getValues() 1480 { 1481 return _predicateParamValues; 1482 } 1483 1484 /** 1485 * Set the parameter values. 1486 * @param values the parameter values to set. 1487 */ 1488 public void setValues(List<String> values) 1489 { 1490 this._predicateParamValues = values; 1491 } 1492 } 1493 1494}