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