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