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