001/* 002 * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * George Lefter 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.directory.sql; 021 022import static org.nuxeo.ecm.directory.sql.SQLDirectory.TENANT_ID_FIELD; 023 024import java.io.Serializable; 025import java.sql.Connection; 026import java.sql.PreparedStatement; 027import java.sql.ResultSet; 028import java.sql.SQLException; 029import java.sql.Statement; 030import java.util.ArrayList; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.Iterator; 034import java.util.LinkedHashMap; 035import java.util.LinkedList; 036import java.util.List; 037import java.util.Map; 038import java.util.Map.Entry; 039import java.util.stream.Collectors; 040import java.util.Set; 041 042import org.apache.commons.lang.StringUtils; 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.nuxeo.ecm.core.api.ConcurrentUpdateException; 046import org.nuxeo.ecm.core.api.DocumentModel; 047import org.nuxeo.ecm.core.api.DocumentModelList; 048import org.nuxeo.ecm.core.api.NuxeoPrincipal; 049import org.nuxeo.ecm.core.api.PropertyException; 050import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 051import org.nuxeo.ecm.core.api.local.ClientLoginModule; 052import org.nuxeo.ecm.core.api.model.Property; 053import org.nuxeo.ecm.core.api.security.SecurityConstants; 054import org.nuxeo.ecm.core.schema.types.Field; 055import org.nuxeo.ecm.core.storage.sql.ColumnSpec; 056import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger; 057import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; 058import org.nuxeo.ecm.core.storage.sql.jdbc.db.Delete; 059import org.nuxeo.ecm.core.storage.sql.jdbc.db.Insert; 060import org.nuxeo.ecm.core.storage.sql.jdbc.db.Select; 061import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; 062import org.nuxeo.ecm.core.storage.sql.jdbc.db.Update; 063import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect; 064import org.nuxeo.ecm.core.utils.SIDGenerator; 065import org.nuxeo.ecm.directory.BaseSession; 066import org.nuxeo.ecm.directory.DirectoryException; 067import org.nuxeo.ecm.directory.EntrySource; 068import org.nuxeo.ecm.directory.OperationNotAllowedException; 069import org.nuxeo.ecm.directory.PasswordHelper; 070import org.nuxeo.ecm.directory.Reference; 071import org.nuxeo.ecm.directory.sql.filter.SQLComplexFilter; 072 073/** 074 * This class represents a session against an SQLDirectory. 075 */ 076public class SQLSession extends BaseSession { 077 078 private static final Log log = LogFactory.getLog(SQLSession.class); 079 080 // set to false for debugging 081 private static final boolean HIDE_PASSWORD_IN_LOGS = true; 082 083 final Table table; 084 085 private final boolean computeMultiTenantId; 086 087 protected SQLStaticFilter[] staticFilters; 088 089 String sid; 090 091 Connection sqlConnection; 092 093 private final Dialect dialect; 094 095 protected JDBCLogger logger = new JDBCLogger("SQLDirectory"); 096 097 public SQLSession(SQLDirectory directory, SQLDirectoryDescriptor config) throws DirectoryException { 098 super(directory, TableReference.class); 099 table = directory.getTable(); 100 dialect = directory.getDialect(); 101 sid = String.valueOf(SIDGenerator.next()); 102 staticFilters = config.getStaticFilters(); 103 computeMultiTenantId = config.isComputeMultiTenantId(); 104 acquireConnection(); 105 } 106 107 @Override 108 public SQLDirectory getDirectory() { 109 return (SQLDirectory) directory; 110 } 111 112 @Override 113 public DocumentModel getEntryFromSource(String id, boolean fetchReferences) throws DirectoryException { 114 acquireConnection(); 115 // String sql = String.format("SELECT * FROM %s WHERE %s = ?", 116 // tableName, idField); 117 Select select = new Select(table); 118 select.setFrom(table.getQuotedName()); 119 select.setWhat(getReadColumnsSQL()); 120 121 String whereClause = table.getPrimaryColumn().getQuotedName() + " = ?"; 122 whereClause = addFilterWhereClause(whereClause); 123 124 select.setWhere(whereClause); 125 String sql = select.getStatement(); 126 127 if (logger.isLogEnabled()) { 128 List<Serializable> values = new ArrayList<>(); 129 values.add(id); 130 addFilterValuesForLog(values); 131 logger.logSQL(sql, values); 132 } 133 134 PreparedStatement ps = null; 135 try { 136 ps = sqlConnection.prepareStatement(sql); 137 setFieldValue(ps, 1, table.getPrimaryColumn(), id); 138 addFilterValues(ps, 2); 139 140 Map<String, Object> fieldMap = new HashMap<>(); 141 try (ResultSet rs = ps.executeQuery()) { 142 if (!rs.next()) { 143 return null; 144 } 145 for (Column column : getReadColumns()) { 146 Object value = getFieldValue(rs, column); 147 fieldMap.put(column.getKey(), value); 148 } 149 if (logger.isLogEnabled()) { 150 logger.logResultSet(rs, getReadColumns()); 151 } 152 } 153 154 if (isMultiTenant()) { 155 // check that the entry is from the current tenant, or no tenant 156 // at all 157 String tenantId = getCurrentTenantId(); 158 if (!StringUtils.isBlank(tenantId)) { 159 String entryTenantId = (String) fieldMap.get(TENANT_ID_FIELD); 160 if (!StringUtils.isBlank(entryTenantId)) { 161 if (!entryTenantId.equals(tenantId)) { 162 return null; 163 } 164 } 165 } 166 } 167 168 DocumentModel entry = fieldMapToDocumentModel(fieldMap); 169 170 // fetch the reference fields 171 if (fetchReferences) { 172 Map<String, List<String>> targetIdsMap = new HashMap<>(); 173 for (Reference reference : directory.getReferences()) { 174 List<String> targetIds = reference.getTargetIdsForSource(entry.getId()); 175 targetIds = new ArrayList<>(targetIds); 176 Collections.sort(targetIds); 177 String fieldName = reference.getFieldName(); 178 if (targetIdsMap.containsKey(fieldName)) { 179 targetIdsMap.get(fieldName).addAll(targetIds); 180 } else { 181 targetIdsMap.put(fieldName, targetIds); 182 } 183 } 184 for (Entry<String, List<String>> en : targetIdsMap.entrySet()) { 185 String fieldName = en.getKey(); 186 List<String> targetIds = en.getValue(); 187 try { 188 entry.setProperty(schemaName, fieldName, targetIds); 189 } catch (PropertyException e) { 190 throw new DirectoryException(e); 191 } 192 } 193 } 194 return entry; 195 } catch (SQLException e) { 196 throw new DirectoryException("getEntry failed", e); 197 } finally { 198 try { 199 if (ps != null) { 200 ps.close(); 201 } 202 } catch (SQLException sqle) { 203 throw new DirectoryException(sqle); 204 } 205 } 206 } 207 208 protected List<Column> getReadColumns() { 209 return readAllColumns ? getDirectory().readColumnsAll : getDirectory().readColumns; 210 } 211 212 protected String getReadColumnsSQL() { 213 return readAllColumns ? getDirectory().readColumnsAllSQL : getDirectory().readColumnsSQL; 214 } 215 216 protected DocumentModel fieldMapToDocumentModel(Map<String, Object> fieldMap) { 217 String idFieldName = directory.getSchemaFieldMap().get(getIdField()).getName().getPrefixedName(); 218 // If the prefixed id is not here, try to get without prefix 219 // It may happen when we gentry from sql 220 if (!fieldMap.containsKey(idFieldName)) { 221 idFieldName = getIdField(); 222 } 223 224 String id = String.valueOf(fieldMap.get(idFieldName)); 225 try { 226 DocumentModel docModel = BaseSession.createEntryModel(sid, schemaName, id, fieldMap, isReadOnly()); 227 return docModel; 228 } catch (PropertyException e) { 229 log.error(e, e); 230 return null; 231 } 232 } 233 234 private void acquireConnection() throws DirectoryException { 235 try { 236 if (sqlConnection == null || sqlConnection.isClosed()) { 237 sqlConnection = getDirectory().getConnection(); 238 } 239 } catch (SQLException e) { 240 throw new DirectoryException("Cannot connect to SQL directory '" + directory.getName() + "': " 241 + e.getMessage(), e); 242 } 243 } 244 245 /** 246 * Checks the SQL error we got and determine if a concurrent update happened. Throws if that's the case. 247 * 248 * @param e the exception 249 * @since 7.10-HF04, 8.2 250 */ 251 protected void checkConcurrentUpdate(Throwable e) throws ConcurrentUpdateException { 252 if (dialect.isConcurrentUpdateException(e)) { 253 throw new ConcurrentUpdateException(e); 254 } 255 } 256 257 protected String addFilterWhereClause(String whereClause) throws DirectoryException { 258 if (staticFilters.length == 0) { 259 return whereClause; 260 } 261 if (whereClause != null && whereClause.trim().length() > 0) { 262 whereClause = whereClause + " AND "; 263 } else { 264 whereClause = ""; 265 } 266 for (int i = 0; i < staticFilters.length; i++) { 267 SQLStaticFilter filter = staticFilters[i]; 268 whereClause += filter.getDirectoryColumn(table, getDirectory().useNativeCase()).getQuotedName(); 269 whereClause += " " + filter.getOperator() + " "; 270 whereClause += "? "; 271 272 if (i < staticFilters.length - 1) { 273 whereClause = whereClause + " AND "; 274 } 275 } 276 return whereClause; 277 } 278 279 protected void addFilterValues(PreparedStatement ps, int startIdx) throws DirectoryException { 280 for (int i = 0; i < staticFilters.length; i++) { 281 SQLStaticFilter filter = staticFilters[i]; 282 setFieldValue(ps, startIdx + i, filter.getDirectoryColumn(table, getDirectory().useNativeCase()), 283 filter.getValue()); 284 } 285 } 286 287 protected void addFilterValuesForLog(List<Serializable> values) { 288 for (int i = 0; i < staticFilters.length; i++) { 289 values.add(staticFilters[i].getValue()); 290 } 291 } 292 293 /** 294 * Internal method to read the hashed password for authentication. 295 * 296 * @since 9.1 297 */ 298 protected String getPassword(String id) { 299 acquireConnection(); 300 301 Select select = new Select(table); 302 select.setFrom(table.getQuotedName()); 303 List<Column> whatColumns = new ArrayList<>(2); 304 whatColumns.add(table.getColumn(getPasswordField())); 305 if (isMultiTenant()) { 306 whatColumns.add(table.getColumn(TENANT_ID_FIELD)); 307 } 308 String what = whatColumns.stream().map(Column::getQuotedName).collect(Collectors.joining(", ")); 309 select.setWhat(what); 310 String whereClause = table.getPrimaryColumn().getQuotedName() + " = ?"; 311 whereClause = addFilterWhereClause(whereClause); 312 select.setWhere(whereClause); 313 String sql = select.getStatement(); 314 315 if (logger.isLogEnabled()) { 316 List<Serializable> values = new ArrayList<>(); 317 values.add(id); 318 addFilterValuesForLog(values); 319 logger.logSQL(sql, values); 320 } 321 322 try (PreparedStatement ps = sqlConnection.prepareStatement(sql)) { 323 setFieldValue(ps, 1, table.getPrimaryColumn(), id); 324 addFilterValues(ps, 2); 325 try (ResultSet rs = ps.executeQuery()) { 326 if (!rs.next()) { 327 return null; 328 } 329 if (isMultiTenant()) { 330 // check that the entry is from the current tenant, or no tenant at all 331 String tenantId = getCurrentTenantId(); 332 if (!StringUtils.isBlank(tenantId)) { 333 String entryTenantId = (String) getFieldValue(rs, table.getColumn(TENANT_ID_FIELD)); 334 if (!StringUtils.isBlank(entryTenantId)) { 335 if (!entryTenantId.equals(tenantId)) { 336 return null; 337 } 338 } 339 } 340 } 341 String password = (String) getFieldValue(rs, table.getColumn(getPasswordField())); 342 if (logger.isLogEnabled()) { 343 String value = HIDE_PASSWORD_IN_LOGS ? "********" : password; 344 logger.logMap(Collections.singletonMap(getPasswordField(), value)); 345 } 346 return password; 347 } 348 } catch (SQLException e) { 349 throw new DirectoryException("getPassword failed", e); 350 } 351 } 352 353 @Override 354 public void deleteEntry(String id) { 355 acquireConnection(); 356 if (!canDeleteMultiTenantEntry(id)) { 357 throw new OperationNotAllowedException("Operation not allowed in the current tenant context", 358 "label.directory.error.multi.tenant.operationNotAllowed", null); 359 } 360 super.deleteEntry(id); 361 } 362 363 protected boolean canDeleteMultiTenantEntry(String entryId) throws DirectoryException { 364 if (isMultiTenant()) { 365 // can only delete entry from the current tenant 366 String tenantId = getCurrentTenantId(); 367 if (!StringUtils.isBlank(tenantId)) { 368 DocumentModel entry = getEntry(entryId); 369 String entryTenantId = (String) entry.getProperty(schemaName, TENANT_ID_FIELD); 370 if (StringUtils.isBlank(entryTenantId) || !entryTenantId.equals(tenantId)) { 371 if (log.isDebugEnabled()) { 372 log.debug(String.format("Trying to delete entry '%s' not part of current tenant '%s'", entryId, 373 tenantId)); 374 } 375 return false; 376 } 377 } 378 } 379 return true; 380 } 381 382 @Override 383 public void deleteEntry(String id, Map<String, String> map) throws DirectoryException { 384 checkPermission(SecurityConstants.WRITE); 385 acquireConnection(); 386 387 if (!canDeleteMultiTenantEntry(id)) { 388 throw new DirectoryException("Operation not allowed in the current tenant context"); 389 } 390 391 // Assume in this case that there are no References to this entry. 392 PreparedStatement ps = null; 393 try { 394 Delete delete = new Delete(table); 395 StringBuilder whereClause = new StringBuilder(); 396 List<Serializable> values = new ArrayList<>(1 + map.size()); 397 398 whereClause.append(table.getPrimaryColumn().getQuotedName()); 399 whereClause.append(" = ?"); 400 values.add(id); 401 for (Entry<String, String> e : map.entrySet()) { 402 String key = e.getKey(); 403 String value = e.getValue(); 404 whereClause.append(" AND "); 405 Column col = table.getColumn(key); 406 if (col == null) { 407 throw new IllegalArgumentException("Unknown column " + key); 408 } 409 whereClause.append(col.getQuotedName()); 410 if (value == null) { 411 whereClause.append(" IS NULL"); 412 } else { 413 whereClause.append(" = ?"); 414 values.add(value); 415 } 416 } 417 delete.setWhere(whereClause.toString()); 418 String sql = delete.getStatement(); 419 420 if (logger.isLogEnabled()) { 421 logger.logSQL(sql, values); 422 } 423 424 ps = sqlConnection.prepareStatement(sql); 425 for (int i = 0; i < values.size(); i++) { 426 if (i == 0) { 427 setFieldValue(ps, 1, table.getPrimaryColumn(), values.get(i)); 428 } else { 429 ps.setString(1 + i, (String) values.get(i)); 430 } 431 } 432 ps.execute(); 433 } catch (SQLException e) { 434 checkConcurrentUpdate(e); 435 throw new DirectoryException("deleteEntry failed", e); 436 } finally { 437 try { 438 if (ps != null) { 439 ps.close(); 440 } 441 } catch (SQLException sqle) { 442 throw new DirectoryException(sqle); 443 } 444 } 445 getDirectory().invalidateCaches(); 446 } 447 448 @Override 449 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy, 450 boolean fetchReferences) { 451 return query(filter, fulltext, orderBy, fetchReferences, -1, -1); 452 } 453 454 @Override 455 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy, 456 boolean fetchReferences, int limit, int offset) throws DirectoryException { 457 if (!hasPermission(SecurityConstants.READ)) { 458 return new DocumentModelListImpl(); 459 } 460 acquireConnection(); 461 Map<String, Object> filterMap = new LinkedHashMap<>(filter); 462 filterMap.remove(getPasswordField()); // cannot filter on password 463 464 if (isMultiTenant()) { 465 // filter entries on the tenantId field also 466 String tenantId = getCurrentTenantId(); 467 if (!StringUtils.isBlank(tenantId)) { 468 filterMap.put(TENANT_ID_FIELD, tenantId); 469 } 470 } 471 472 try { 473 // build count query statement 474 StringBuilder whereClause = new StringBuilder(); 475 String separator = ""; 476 List<Column> orderedColumns = new LinkedList<>(); 477 for (String columnName : filterMap.keySet()) { 478 479 if (getDirectory().isReference(columnName)) { 480 log.warn(columnName + " is a reference and will be ignored" + " as a query criterion"); 481 continue; 482 } 483 484 Object value = filterMap.get(columnName); 485 Column column = table.getColumn(columnName); 486 if (null == column) { 487 // this might happen if we have a case like a chain 488 // selection and a directory without parent column 489 throw new DirectoryException("cannot find column '" + columnName + "' for table: " + table); 490 } 491 String leftSide = column.getQuotedName(); 492 String rightSide = "?"; 493 String operator; 494 boolean substring = fulltext != null && fulltext.contains(columnName); 495 if ("".equals(value) && dialect.hasNullEmptyString() && !substring) { 496 // see NXP-6172, empty values are Null in Oracle 497 value = null; 498 } 499 if (value != null) { 500 if (value instanceof SQLComplexFilter) { 501 SQLComplexFilter complexFilter = (SQLComplexFilter) value; 502 operator = complexFilter.getOperator(); 503 rightSide = complexFilter.getRightSide(); 504 } else if (substring) { 505 // NB : remove double % in like query NXGED-833 506 String searchedValue = null; 507 switch (substringMatchType) { 508 case subany: 509 searchedValue = '%' + String.valueOf(value).toLowerCase() + '%'; 510 break; 511 case subinitial: 512 searchedValue = String.valueOf(value).toLowerCase() + '%'; 513 break; 514 case subfinal: 515 searchedValue = '%' + String.valueOf(value).toLowerCase(); 516 break; 517 } 518 filterMap.put(columnName, searchedValue); 519 if (dialect.supportsIlike()) { 520 operator = " ILIKE "; // postgresql rules 521 } else { 522 leftSide = "LOWER(" + leftSide + ')'; 523 operator = " LIKE "; 524 } 525 } else { 526 operator = " = "; 527 } 528 } else { 529 operator = " IS NULL"; 530 } 531 whereClause.append(separator).append(leftSide).append(operator); 532 if (value != null) { 533 whereClause.append(rightSide); 534 orderedColumns.add(column); 535 } 536 separator = " AND "; 537 } 538 539 int queryLimitSize = getDirectory().getDescriptor().getQuerySizeLimit(); 540 boolean trucatedResults = false; 541 if (queryLimitSize != 0 && (limit <= 0 || limit > queryLimitSize)) { 542 PreparedStatement ps = null; 543 try { 544 // create a preparedStatement for counting and bind the 545 // values 546 // String countQuery = new StringBuilder("SELECT count(*) 547 // FROM ") 548 // .append(table.getQuotedName(dialect)).append(whereClause).toString(); 549 Select select = new Select(table); 550 select.setWhat("count(*)"); 551 select.setFrom(table.getQuotedName()); 552 553 String where = whereClause.toString(); 554 where = addFilterWhereClause(where); 555 select.setWhere(where); 556 557 String countQuery = select.getStatement(); 558 if (logger.isLogEnabled()) { 559 List<Serializable> values = new ArrayList<>(orderedColumns.size()); 560 for (Column column : orderedColumns) { 561 Object value = filterMap.get(column.getKey()); 562 values.add((Serializable) value); 563 } 564 addFilterValuesForLog(values); 565 logger.logSQL(countQuery, values); 566 } 567 ps = sqlConnection.prepareStatement(countQuery); 568 fillPreparedStatementFields(filterMap, orderedColumns, ps); 569 570 int count; 571 try (ResultSet rs = ps.executeQuery()) { 572 rs.next(); 573 count = rs.getInt(1); 574 } 575 if (logger.isLogEnabled()) { 576 logger.logCount(count); 577 } 578 if (count > queryLimitSize) { 579 trucatedResults = true; 580 limit = queryLimitSize; 581 log.error("Displayed results will be truncated because too many rows in result: " + count); 582 // throw new SizeLimitExceededException("too many rows in result: " + count); 583 } 584 } finally { 585 if (ps != null) { 586 ps.close(); 587 } 588 } 589 } 590 591 // create a preparedStatement and bind the values 592 // String query = new StringBuilder("SELECT * FROM 593 // ").append(tableName).append( 594 // whereClause).toString(); 595 596 Select select = new Select(table); 597 select.setWhat(getReadColumnsSQL()); 598 select.setFrom(table.getQuotedName()); 599 600 String where = whereClause.toString(); 601 where = addFilterWhereClause(where); 602 select.setWhere(where); 603 604 StringBuilder orderby = new StringBuilder(128); 605 if (orderBy != null) { 606 for (Iterator<Map.Entry<String, String>> it = orderBy.entrySet().iterator(); it.hasNext();) { 607 Entry<String, String> entry = it.next(); 608 orderby.append(dialect.openQuote()) 609 .append(entry.getKey()) 610 .append(dialect.closeQuote()) 611 .append(' ') 612 .append(entry.getValue()); 613 if (it.hasNext()) { 614 orderby.append(','); 615 } 616 } 617 } 618 select.setOrderBy(orderby.toString()); 619 String query = select.getStatement(); 620 boolean manualLimitOffset; 621 if (limit <= 0) { 622 manualLimitOffset = false; 623 } else { 624 if (offset < 0) { 625 offset = 0; 626 } 627 if (dialect.supportsPaging()) { 628 query = dialect.addPagingClause(query, limit, offset); 629 manualLimitOffset = false; 630 } else { 631 manualLimitOffset = true; 632 } 633 } 634 635 if (logger.isLogEnabled()) { 636 List<Serializable> values = new ArrayList<>(orderedColumns.size()); 637 for (Column column : orderedColumns) { 638 Object value = filterMap.get(column.getKey()); 639 values.add((Serializable) value); 640 } 641 addFilterValuesForLog(values); 642 logger.logSQL(query, values); 643 } 644 645 try (PreparedStatement ps = sqlConnection.prepareStatement(query)) { 646 fillPreparedStatementFields(filterMap, orderedColumns, ps); 647 648 // execute the query and create a documentModel list 649 ResultSet rs = ps.executeQuery(); 650 DocumentModelList list = new DocumentModelListImpl(); 651 while (rs.next()) { 652 653 // fetch values for stored fields 654 Map<String, Object> map = new HashMap<>(); 655 for (Column column : getReadColumns()) { 656 Object o = getFieldValue(rs, column); 657 map.put(column.getKey(), o); 658 } 659 660 DocumentModel docModel = fieldMapToDocumentModel(map); 661 662 // fetch the reference fields 663 if (fetchReferences) { 664 Map<String, List<String>> targetIdsMap = new HashMap<>(); 665 for (Reference reference : directory.getReferences()) { 666 List<String> targetIds = reference.getTargetIdsForSource(docModel.getId()); 667 String fieldName = reference.getFieldName(); 668 if (targetIdsMap.containsKey(fieldName)) { 669 targetIdsMap.get(fieldName).addAll(targetIds); 670 } else { 671 targetIdsMap.put(fieldName, targetIds); 672 } 673 } 674 for (Entry<String, List<String>> en : targetIdsMap.entrySet()) { 675 String fieldName = en.getKey(); 676 List<String> targetIds = en.getValue(); 677 docModel.setProperty(schemaName, fieldName, targetIds); 678 } 679 } 680 list.add(docModel); 681 } 682 rs.close(); 683 if (manualLimitOffset) { 684 int totalSize = list.size(); 685 if (offset > 0) { 686 if (offset >= totalSize) { 687 list = new DocumentModelListImpl(); 688 } else { 689 list = new DocumentModelListImpl(list.subList(offset, totalSize)); 690 } 691 } 692 if (list.size() > limit) { // list.size() not totalSize, we may have an offset already 693 list = new DocumentModelListImpl(list.subList(0, limit)); 694 } 695 ((DocumentModelListImpl) list).setTotalSize(totalSize); 696 } 697 if (trucatedResults) { 698 ((DocumentModelListImpl) list).setTotalSize(-2); 699 } 700 return list; 701 } 702 703 } catch (SQLException e) { 704 try { 705 sqlConnection.close(); 706 } catch (SQLException e1) { 707 } 708 throw new DirectoryException("query failed", e); 709 } 710 } 711 712 @Override 713 protected DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) { 714 // Make a copy of fieldMap to avoid modifying it 715 fieldMap = new HashMap<>(fieldMap); 716 717 Map<String, Field> schemaFieldMap = directory.getSchemaFieldMap(); 718 Field schemaIdField = schemaFieldMap.get(getIdField()); 719 720 String idFieldName = schemaIdField.getName().getPrefixedName(); 721 722 acquireConnection(); 723 if (autoincrementId) { 724 fieldMap.remove(idFieldName); 725 } else { 726 // check id that was given 727 Object rawId = fieldMap.get(idFieldName); 728 if (rawId == null) { 729 throw new DirectoryException("Missing id"); 730 } 731 String id = String.valueOf(rawId); 732 if (hasEntry(id)) { 733 throw new DirectoryException(String.format("Entry with id %s already exists", id)); 734 } 735 736 if (isMultiTenant()) { 737 String tenantId = getCurrentTenantId(); 738 if (!StringUtils.isBlank(tenantId)) { 739 fieldMap.put(TENANT_ID_FIELD, tenantId); 740 if (computeMultiTenantId) { 741 fieldMap.put(idFieldName, computeMultiTenantDirectoryId(tenantId, id)); 742 } 743 } 744 } 745 } 746 747 List<Column> columnList = new ArrayList<>(table.getColumns()); 748 Column idColumn = null; 749 for (Iterator<Column> i = columnList.iterator(); i.hasNext();) { 750 Column column = i.next(); 751 if (column.isIdentity()) { 752 idColumn = column; 753 } 754 String prefixedName = schemaFieldMap.get(column.getKey()).getName().getPrefixedName(); 755 756 if (!fieldMap.containsKey(prefixedName)) { 757 Field prefixedField = schemaFieldMap.get(prefixedName); 758 if (prefixedField != null && prefixedField.getDefaultValue() != null) { 759 fieldMap.put(prefixedName, prefixedField.getDefaultValue()); 760 } else { 761 i.remove(); 762 } 763 } 764 } 765 Insert insert = new Insert(table); 766 for (Column column : columnList) { 767 insert.addColumn(column); 768 } 769 // needed for Oracle for empty map insert 770 insert.addIdentityColumn(idColumn); 771 String sql = insert.getStatement(); 772 773 if (logger.isLogEnabled()) { 774 List<Serializable> values = new ArrayList<>(columnList.size()); 775 for (Column column : columnList) { 776 String prefixField = schemaFieldMap.get(column.getKey()).getName().getPrefixedName(); 777 Object value = fieldMap.get(prefixField); 778 Serializable v; 779 if (HIDE_PASSWORD_IN_LOGS && column.getKey().equals(getPasswordField())) { 780 v = "********"; // hide password in logs 781 } else { 782 v = fieldValueForWrite(value, column); 783 } 784 values.add(v); 785 } 786 logger.logSQL(sql, values); 787 } 788 789 DocumentModel entry; 790 PreparedStatement ps = null; 791 Statement st = null; 792 try { 793 if (autoincrementId && dialect.hasIdentityGeneratedKey()) { 794 ps = sqlConnection.prepareStatement(sql, new String[] { getIdField() }); 795 } else { 796 ps = sqlConnection.prepareStatement(sql); 797 } 798 int index = 1; 799 for (Column column : columnList) { 800 String prefixField = schemaFieldMap.get(column.getKey()).getName().getPrefixedName(); 801 Object value = fieldMap.get(prefixField); 802 setFieldValue(ps, index, column, value); 803 index++; 804 } 805 ps.execute(); 806 if (autoincrementId) { 807 Column column = table.getColumn(getIdField()); 808 ResultSet rs; 809 if (dialect.hasIdentityGeneratedKey()) { 810 rs = ps.getGeneratedKeys(); 811 } else { 812 // needs specific statements 813 sql = dialect.getIdentityGeneratedKeySql(column); 814 st = sqlConnection.createStatement(); 815 rs = st.executeQuery(sql); 816 } 817 if (!rs.next()) { 818 throw new DirectoryException("Cannot get generated key"); 819 } 820 if (logger.isLogEnabled()) { 821 logger.logResultSet(rs, Collections.singletonList(column)); 822 } 823 Serializable rawId = column.getFromResultSet(rs, 1); 824 fieldMap.put(idFieldName, rawId); 825 rs.close(); 826 } 827 entry = fieldMapToDocumentModel(fieldMap); 828 } catch (SQLException e) { 829 checkConcurrentUpdate(e); 830 throw new DirectoryException("createEntry failed", e); 831 } finally { 832 try { 833 if (ps != null) { 834 ps.close(); 835 } 836 if (st != null) { 837 st.close(); 838 } 839 } catch (SQLException sqle) { 840 throw new DirectoryException(sqle); 841 } 842 } 843 844 return entry; 845 } 846 847 @Override 848 protected List<String> updateEntryWithoutReferences(DocumentModel docModel) throws DirectoryException { 849 acquireConnection(); 850 List<Column> storedColumnList = new LinkedList<>(); 851 List<String> referenceFieldList = new LinkedList<>(); 852 853 if (isMultiTenant()) { 854 // can only update entry from the current tenant 855 String tenantId = getCurrentTenantId(); 856 if (!StringUtils.isBlank(tenantId)) { 857 String entryTenantId = (String) docModel.getProperty(schemaName, TENANT_ID_FIELD); 858 if (StringUtils.isBlank(entryTenantId) || !entryTenantId.equals(tenantId)) { 859 if (log.isDebugEnabled()) { 860 log.debug(String.format("Trying to update entry '%s' not part of current tenant '%s'", 861 docModel.getId(), tenantId)); 862 } 863 throw new OperationNotAllowedException("Operation not allowed in the current tenant context", 864 "label.directory.error.multi.tenant.operationNotAllowed", null); 865 } 866 } 867 } 868 869 // collect fields to update 870 for (String fieldName : directory.getSchemaFieldMap().keySet()) { 871 if (fieldName.equals(getIdField())) { 872 continue; 873 } 874 Property prop = docModel.getPropertyObject(schemaName, fieldName); 875 if (!prop.isDirty()) { 876 continue; 877 } 878 if (fieldName.equals(getPasswordField()) && StringUtils.isEmpty((String) prop.getValue())) { 879 // null/empty password means unchanged 880 continue; 881 } 882 if (getDirectory().isReference(fieldName)) { 883 referenceFieldList.add(fieldName); 884 } else { 885 storedColumnList.add(table.getColumn(fieldName)); 886 } 887 } 888 889 if (!storedColumnList.isEmpty()) { 890 // update stored fields 891 // String whereString = StringUtils.join( 892 // storedFieldPredicateList.iterator(), ", "); 893 // String sql = String.format("UPDATE %s SET %s WHERE %s = ?", 894 // tableName, whereString, 895 // primaryColumn); 896 897 Update update = new Update(table); 898 update.setUpdatedColumns(storedColumnList); 899 String whereString = table.getPrimaryColumn().getQuotedName() + " = ?"; 900 update.setWhere(whereString); 901 String sql = update.getStatement(); 902 903 if (logger.isLogEnabled()) { 904 List<Serializable> values = new ArrayList<>(storedColumnList.size()); 905 for (Column column : storedColumnList) { 906 Object value = docModel.getProperty(schemaName, column.getKey()); 907 if (HIDE_PASSWORD_IN_LOGS && column.getKey().equals(getPasswordField())) { 908 value = "********"; // hide password in logs 909 } 910 values.add((Serializable) value); 911 } 912 values.add(docModel.getId()); 913 logger.logSQL(sql, values); 914 } 915 916 PreparedStatement ps = null; 917 try { 918 ps = sqlConnection.prepareStatement(sql); 919 920 int index = 1; 921 // TODO: how can I reset dirty fields? 922 for (Column column : storedColumnList) { 923 Object value = docModel.getProperty(schemaName, column.getKey()); 924 setFieldValue(ps, index, column, value); 925 index++; 926 } 927 setFieldValue(ps, index, table.getPrimaryColumn(), docModel.getId()); 928 ps.execute(); 929 } catch (SQLException e) { 930 checkConcurrentUpdate(e); 931 throw new DirectoryException("updateEntry failed for " + docModel.getId(), e); 932 } finally { 933 try { 934 if (ps != null) { 935 ps.close(); 936 } 937 } catch (SQLException sqle) { 938 throw new DirectoryException(sqle); 939 } 940 } 941 } 942 943 return referenceFieldList; 944 } 945 946 @Override 947 public void deleteEntryWithoutReferences(String id) throws DirectoryException { 948 // second step: clean stored fields 949 PreparedStatement ps = null; 950 try { 951 Delete delete = new Delete(table); 952 String whereString = table.getPrimaryColumn().getQuotedName() + " = ?"; 953 delete.setWhere(whereString); 954 String sql = delete.getStatement(); 955 if (logger.isLogEnabled()) { 956 logger.logSQL(sql, Collections.singleton(id)); 957 } 958 ps = sqlConnection.prepareStatement(sql); 959 setFieldValue(ps, 1, table.getPrimaryColumn(), id); 960 ps.execute(); 961 } catch (SQLException e) { 962 checkConcurrentUpdate(e); 963 throw new DirectoryException("deleteEntry failed", e); 964 } finally { 965 try { 966 if (ps != null) { 967 ps.close(); 968 } 969 } catch (SQLException sqle) { 970 throw new DirectoryException(sqle); 971 } 972 } 973 } 974 975 protected void fillPreparedStatementFields(Map<String, Object> filterMap, List<Column> orderedColumns, 976 PreparedStatement ps) throws DirectoryException { 977 int index = 1; 978 for (Column column : orderedColumns) { 979 Object value = filterMap.get(column.getKey()); 980 981 if (value instanceof SQLComplexFilter) { 982 index = ((SQLComplexFilter) value).setFieldValue(ps, index, column); 983 } else { 984 setFieldValue(ps, index, column, value); 985 index++; 986 } 987 } 988 addFilterValues(ps, index); 989 } 990 991 private Object getFieldValue(ResultSet rs, Column column) throws DirectoryException { 992 try { 993 int index = rs.findColumn(column.getPhysicalName()); 994 return column.getFromResultSet(rs, index); 995 } catch (SQLException e) { 996 throw new DirectoryException("getFieldValue failed", e); 997 } 998 } 999 1000 private void setFieldValue(PreparedStatement ps, int index, Column column, Object value) throws DirectoryException { 1001 try { 1002 column.setToPreparedStatement(ps, index, fieldValueForWrite(value, column)); 1003 } catch (SQLException e) { 1004 throw new DirectoryException("setFieldValue failed", e); 1005 } 1006 } 1007 1008 protected Serializable fieldValueForWrite(Object value, Column column) { 1009 ColumnSpec spec = column.getType().spec; 1010 if (value instanceof String) { 1011 if (spec == ColumnSpec.LONG || spec == ColumnSpec.AUTOINC) { 1012 // allow storing string into integer/long key 1013 return Long.valueOf((String) value); 1014 } 1015 if (column.getKey().equals(getPasswordField())) { 1016 // hash password if not already hashed 1017 String password = (String) value; 1018 if (!PasswordHelper.isHashed(password)) { 1019 password = PasswordHelper.hashPassword(password, passwordHashAlgorithm); 1020 } 1021 return password; 1022 } 1023 } else if (value instanceof Number) { 1024 if (spec == ColumnSpec.LONG || spec == ColumnSpec.AUTOINC) { 1025 // canonicalize to Long 1026 if (value instanceof Integer) { 1027 return Long.valueOf(((Integer) value).longValue()); 1028 } 1029 } else if (spec == ColumnSpec.STRING) { 1030 // allow storing number in string field 1031 return value.toString(); 1032 } 1033 } 1034 return (Serializable) value; 1035 } 1036 1037 @Override 1038 public void close() throws DirectoryException { 1039 try { 1040 if (!sqlConnection.isClosed()) { 1041 sqlConnection.close(); 1042 } 1043 } catch (SQLException e) { 1044 throw new DirectoryException("close failed", e); 1045 } finally { 1046 getDirectory().removeSession(this); 1047 } 1048 } 1049 1050 /** 1051 * Enable connection status checking on SQL directory connections 1052 * 1053 * @since 5.7.2 1054 */ 1055 public boolean isLive() throws DirectoryException { 1056 try { 1057 return !sqlConnection.isClosed(); 1058 } catch (SQLException e) { 1059 throw new DirectoryException("Cannot check connection status of " + this, e); 1060 } 1061 } 1062 1063 @Override 1064 public boolean authenticate(String username, String password) { 1065 String storedPassword = getPassword(username); 1066 return PasswordHelper.verifyPassword(password, storedPassword); 1067 } 1068 1069 @Override 1070 public boolean isAuthenticating() { 1071 return directory.getSchemaFieldMap().containsKey(getPasswordField()); 1072 } 1073 1074 @Override 1075 public boolean hasEntry(String id) { 1076 acquireConnection(); 1077 Select select = new Select(table); 1078 select.setFrom(table.getQuotedName()); 1079 select.setWhat("1"); 1080 select.setWhere(table.getPrimaryColumn().getQuotedName() + " = ?"); 1081 String sql = select.getStatement(); 1082 1083 if (logger.isLogEnabled()) { 1084 logger.logSQL(sql, Collections.singleton(id)); 1085 } 1086 1087 PreparedStatement ps = null; 1088 ResultSet rs = null; 1089 try { 1090 ps = sqlConnection.prepareStatement(sql); 1091 setFieldValue(ps, 1, table.getPrimaryColumn(), id); 1092 rs = ps.executeQuery(); 1093 boolean has = rs.next(); 1094 if (logger.isLogEnabled()) { 1095 logger.logCount(has ? 1 : 0); 1096 } 1097 return has; 1098 } catch (SQLException e) { 1099 throw new DirectoryException("hasEntry failed", e); 1100 } finally { 1101 try { 1102 if (rs != null) { 1103 rs.close(); 1104 } 1105 if (ps != null) { 1106 ps.close(); 1107 } 1108 } catch (SQLException sqle) { 1109 throw new DirectoryException(sqle); 1110 } 1111 } 1112 } 1113 1114 /** 1115 * Returns {@code true} if this directory supports multi tenancy, {@code false} otherwise. 1116 */ 1117 protected boolean isMultiTenant() { 1118 return directory.isMultiTenant(); 1119 } 1120 1121 /** 1122 * Returns the tenant id of the logged user if any, {@code null} otherwise. 1123 */ 1124 protected String getCurrentTenantId() { 1125 NuxeoPrincipal principal = ClientLoginModule.getCurrentPrincipal(); 1126 return principal != null ? principal.getTenantId() : null; 1127 } 1128 1129 @Override 1130 public String toString() { 1131 return "SQLSession [directory=" + directory.getName() + ", sid=" + sid + "]"; 1132 } 1133 1134}