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