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 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.storage.sql.jdbc; 020 021import java.io.Serializable; 022import java.sql.Array; 023import java.sql.BatchUpdateException; 024import java.sql.CallableStatement; 025import java.sql.PreparedStatement; 026import java.sql.ResultSet; 027import java.sql.SQLException; 028import java.sql.Statement; 029import java.sql.Types; 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.Calendar; 033import java.util.Collection; 034import java.util.Collections; 035import java.util.HashMap; 036import java.util.HashSet; 037import java.util.Iterator; 038import java.util.LinkedHashMap; 039import java.util.LinkedList; 040import java.util.List; 041import java.util.Map; 042import java.util.Map.Entry; 043import java.util.Set; 044import java.util.stream.Collectors; 045 046import org.apache.commons.lang3.tuple.Pair; 047import org.nuxeo.common.utils.BatchUtils; 048import org.nuxeo.ecm.core.api.ConcurrentUpdateException; 049import org.nuxeo.ecm.core.api.NuxeoException; 050import org.nuxeo.ecm.core.api.model.Delta; 051import org.nuxeo.ecm.core.storage.sql.VCSClusterInvalidator; 052import org.nuxeo.ecm.core.storage.sql.VCSInvalidations; 053import org.nuxeo.ecm.core.storage.sql.VCSInvalidationsPropagator; 054import org.nuxeo.ecm.core.storage.sql.Model; 055import org.nuxeo.ecm.core.storage.sql.PropertyType; 056import org.nuxeo.ecm.core.storage.sql.Row; 057import org.nuxeo.ecm.core.storage.sql.RowId; 058import org.nuxeo.ecm.core.storage.sql.RowMapper; 059import org.nuxeo.ecm.core.storage.sql.SelectionType; 060import org.nuxeo.ecm.core.storage.sql.SimpleFragment; 061import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.SQLInfoSelect; 062import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.SQLInfoSelection; 063import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; 064import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; 065import org.nuxeo.ecm.core.storage.sql.jdbc.db.Update; 066import org.nuxeo.runtime.api.Framework; 067import org.nuxeo.runtime.services.config.ConfigurationService; 068 069/** 070 * A {@link JDBCRowMapper} maps {@link Row}s to and from a JDBC database. 071 */ 072public class JDBCRowMapper extends JDBCConnection implements RowMapper { 073 074 public static final int UPDATE_BATCH_SIZE = 100; // also insert/delete 075 076 public static final int DEBUG_MAX_TREE = 50; 077 078 /** Property to determine whether collection appends delete all then re-insert, or are optimized for append. */ 079 public static final String COLLECTION_DELETE_BEFORE_APPEND_PROP = "org.nuxeo.vcs.list-delete-before-append"; 080 081 /** 082 * Cluster invalidator, or {@code null} if this mapper does not participate in invalidation propagation (cluster 083 * invalidator, lock manager). 084 */ 085 private final VCSClusterInvalidator clusterInvalidator; 086 087 private final VCSInvalidationsPropagator invalidationsPropagator; 088 089 private final boolean collectionDeleteBeforeAppend; 090 091 private final CollectionIO aclCollectionIO; 092 093 private final CollectionIO scalarCollectionIO; 094 095 public JDBCRowMapper(Model model, SQLInfo sqlInfo, VCSClusterInvalidator clusterInvalidator, 096 VCSInvalidationsPropagator invalidationsPropagator) { 097 super(model, sqlInfo); 098 this.clusterInvalidator = clusterInvalidator; 099 this.invalidationsPropagator = invalidationsPropagator; 100 ConfigurationService configurationService = Framework.getService(ConfigurationService.class); 101 collectionDeleteBeforeAppend = configurationService.isBooleanTrue(COLLECTION_DELETE_BEFORE_APPEND_PROP); 102 aclCollectionIO = new ACLCollectionIO(collectionDeleteBeforeAppend); 103 scalarCollectionIO = new ScalarCollectionIO(collectionDeleteBeforeAppend); 104 } 105 106 @Override 107 public VCSInvalidations receiveInvalidations() { 108 if (clusterInvalidator != null) { 109 VCSInvalidations invalidations = clusterInvalidator.receiveInvalidations(); 110 // send received invalidations to all mappers 111 if (invalidations != null && !invalidations.isEmpty()) { 112 invalidationsPropagator.propagateInvalidations(invalidations, null); 113 } 114 return invalidations; 115 } else { 116 return null; 117 } 118 } 119 120 @Override 121 public void sendInvalidations(VCSInvalidations invalidations) { 122 if (clusterInvalidator != null) { 123 clusterInvalidator.sendInvalidations(invalidations); 124 } 125 } 126 127 @Override 128 public void clearCache() { 129 // no cache 130 } 131 132 @Override 133 public long getCacheSize() { 134 return 0; 135 } 136 137 @Override 138 public void rollback() { 139 // nothing 140 } 141 142 protected CollectionIO getCollectionIO(String tableName) { 143 return tableName.equals(Model.ACL_TABLE_NAME) ? aclCollectionIO : scalarCollectionIO; 144 } 145 146 @Override 147 public Serializable generateNewId() { 148 try { 149 return dialect.getGeneratedId(connection); 150 } catch (SQLException e) { 151 throw new NuxeoException(e); 152 } 153 } 154 155 /* 156 * ----- RowIO ----- 157 */ 158 159 @Override 160 public List<? extends RowId> read(Collection<RowId> rowIds, boolean cacheOnly) { 161 List<RowId> res = new ArrayList<>(rowIds.size()); 162 if (cacheOnly) { 163 // return no data 164 for (RowId rowId : rowIds) { 165 res.add(new RowId(rowId)); 166 } 167 return res; 168 } 169 // reorganize by table 170 Map<String, Set<Serializable>> tableIds = new HashMap<>(); 171 for (RowId rowId : rowIds) { 172 tableIds.computeIfAbsent(rowId.tableName, k -> new HashSet<>()).add(rowId.id); 173 } 174 // read on each table 175 for (Entry<String, Set<Serializable>> en : tableIds.entrySet()) { 176 String tableName = en.getKey(); 177 Set<Serializable> ids = new HashSet<>(en.getValue()); 178 int size = ids.size(); 179 int chunkSize = sqlInfo.getMaximumArgsForIn(); 180 List<Row> rows; 181 if (size > chunkSize) { 182 List<Serializable> idList = new ArrayList<>(ids); 183 rows = new ArrayList<>(size); 184 for (int start = 0; start < size; start += chunkSize) { 185 int end = start + chunkSize; 186 if (end > size) { 187 end = size; 188 } 189 // needs to be Serializable -> copy 190 List<Serializable> chunkIds = new ArrayList<>(idList.subList(start, end)); 191 List<Row> chunkRows; 192 if (model.isCollectionFragment(tableName)) { 193 chunkRows = readCollectionArrays(tableName, chunkIds); 194 } else { 195 chunkRows = readSimpleRows(tableName, chunkIds); 196 } 197 rows.addAll(chunkRows); 198 } 199 } else { 200 if (model.isCollectionFragment(tableName)) { 201 rows = readCollectionArrays(tableName, ids); 202 } else { 203 rows = readSimpleRows(tableName, ids); 204 } 205 } 206 // check we have all the ids (readSimpleRows may have some 207 // missing) 208 for (Row row : rows) { 209 res.add(row); 210 ids.remove(row.id); 211 } 212 // for the missing ids record an empty RowId 213 for (Serializable id : ids) { 214 res.add(new RowId(tableName, id)); 215 } 216 } 217 return res; 218 } 219 220 /** 221 * Gets a list of rows for {@link SimpleFragment}s from the database, given the table name and the ids. 222 * 223 * @param tableName the table name 224 * @param ids the ids 225 * @return the list of rows, without the missing ones 226 */ 227 protected List<Row> readSimpleRows(String tableName, Collection<Serializable> ids) { 228 if (ids.isEmpty()) { 229 return Collections.emptyList(); 230 } 231 SQLInfoSelect select = sqlInfo.getSelectFragmentsByIds(tableName, ids.size()); 232 Map<String, Serializable> criteriaMap = Collections.singletonMap(Model.MAIN_KEY, (Serializable) ids); 233 return getSelectRows(tableName, select, criteriaMap, null, false); 234 } 235 236 /** 237 * Reads several collection rows, given a table name and the ids. 238 * 239 * @param tableName the table name 240 * @param ids the ids 241 */ 242 protected List<Row> readCollectionArrays(String tableName, Collection<Serializable> ids) { 243 if (ids.isEmpty()) { 244 return Collections.emptyList(); 245 } 246 String[] orderBys = { Model.MAIN_KEY, Model.COLL_TABLE_POS_KEY }; // clusters 247 // results 248 Set<String> skipColumns = new HashSet<>(Collections.singleton(Model.COLL_TABLE_POS_KEY)); 249 SQLInfoSelect select = sqlInfo.getSelectFragmentsByIds(tableName, ids.size(), orderBys, skipColumns); 250 251 String sql = select.sql; 252 if (logger.isLogEnabled()) { 253 logger.logSQL(sql, ids); 254 } 255 try (PreparedStatement ps = connection.prepareStatement(sql)) { 256 int i = 1; 257 for (Serializable id : ids) { 258 dialect.setId(ps, i++, id); 259 } 260 try (ResultSet rs = ps.executeQuery()) { 261 countExecute(); 262 263 // get all values from result set, separate by ids 264 // the result set is ordered by id, pos 265 CollectionIO io = getCollectionIO(tableName); 266 PropertyType ftype = model.getCollectionFragmentType(tableName); 267 PropertyType type = ftype.getArrayBaseType(); 268 Serializable curId = null; 269 List<Serializable> list = null; 270 Serializable[] returnId = new Serializable[1]; 271 int[] returnPos = { -1 }; 272 List<Row> res = new LinkedList<>(); 273 Set<Serializable> remainingIds = new HashSet<>(ids); 274 while (rs.next()) { 275 Serializable value = io.getCurrentFromResultSet(rs, select.whatColumns, model, returnId, returnPos); 276 Serializable newId = returnId[0]; 277 if (newId != null && !newId.equals(curId)) { 278 // flush old list 279 if (list != null) { 280 res.add(new Row(tableName, curId, type.collectionToArray(list))); 281 remainingIds.remove(curId); 282 } 283 curId = newId; 284 list = new ArrayList<>(); 285 } 286 list.add(value); // NOSONAR 287 } 288 if (curId != null && list != null) { 289 // flush last list 290 res.add(new Row(tableName, curId, type.collectionToArray(list))); 291 remainingIds.remove(curId); 292 } 293 294 // fill empty ones 295 if (!remainingIds.isEmpty()) { 296 Serializable[] emptyArray = ftype.getEmptyArray(); 297 for (Serializable id : remainingIds) { 298 res.add(new Row(tableName, id, emptyArray)); 299 } 300 } 301 if (logger.isLogEnabled()) { 302 for (Row row : res) { 303 logger.log(" -> " + row); 304 } 305 } 306 return res; 307 } 308 } catch (SQLException e) { 309 throw new NuxeoException("Could not select: " + sql, e); 310 } 311 } 312 313 /** 314 * Fetches the rows for a select with fixed criteria given as two maps (a criteriaMap whose values and up in the 315 * returned rows, and a joinMap for other criteria). 316 */ 317 protected List<Row> getSelectRows(String tableName, SQLInfoSelect select, Map<String, Serializable> criteriaMap, 318 Map<String, Serializable> joinMap, boolean limitToOne) { 319 List<Row> list = new LinkedList<>(); 320 if (select.whatColumns.isEmpty()) { 321 // happens when we fetch a fragment whose columns are all opaque 322 // check it's a by-id query 323 if (select.whereColumns.size() == 1 && Model.MAIN_KEY.equals(select.whereColumns.get(0).getKey()) 324 && joinMap == null) { 325 Row row = new Row(tableName, criteriaMap); 326 if (select.opaqueColumns != null) { 327 for (Column column : select.opaqueColumns) { 328 row.putNew(column.getKey(), Row.OPAQUE); 329 } 330 } 331 list.add(row); 332 return list; 333 } 334 // else do a useless select but the criteria are more complex and we 335 // can't shortcut 336 } 337 if (joinMap == null) { 338 joinMap = Collections.emptyMap(); 339 } 340 try (PreparedStatement ps = connection.prepareStatement(select.sql)) { 341 342 /* 343 * Compute where part. 344 */ 345 List<Serializable> debugValues = null; 346 if (logger.isLogEnabled()) { 347 debugValues = new LinkedList<>(); 348 } 349 int i = 1; 350 for (Column column : select.whereColumns) { 351 String key = column.getKey(); 352 Serializable v; 353 if (criteriaMap.containsKey(key)) { 354 v = criteriaMap.get(key); 355 } else if (joinMap.containsKey(key)) { 356 v = joinMap.get(key); 357 } else { 358 throw new RuntimeException(key); 359 } 360 if (v == null) { 361 throw new NuxeoException("Null value for key: " + key); 362 } 363 if (v instanceof Collection<?>) { 364 // allow insert of several values, for the IN (...) case 365 for (Object vv : (Collection<?>) v) { 366 column.setToPreparedStatement(ps, i++, (Serializable) vv); 367 if (debugValues != null) { 368 debugValues.add((Serializable) vv); 369 } 370 } 371 } else { 372 column.setToPreparedStatement(ps, i++, v); 373 if (debugValues != null) { 374 debugValues.add(v); 375 } 376 } 377 } 378 if (debugValues != null) { 379 logger.logSQL(select.sql, debugValues); 380 } 381 382 /* 383 * Execute query. 384 */ 385 try (ResultSet rs = ps.executeQuery()) { 386 countExecute(); 387 388 /* 389 * Construct the maps from the result set. 390 */ 391 while (rs.next()) { 392 // TODO using criteriaMap is wrong if it contains a Collection 393 Row row = new Row(tableName, criteriaMap); 394 i = 1; 395 for (Column column : select.whatColumns) { 396 row.put(column.getKey(), column.getFromResultSet(rs, i++)); 397 } 398 if (select.opaqueColumns != null) { 399 for (Column column : select.opaqueColumns) { 400 row.putNew(column.getKey(), Row.OPAQUE); 401 } 402 } 403 if (logger.isLogEnabled()) { 404 logger.logResultSet(rs, select.whatColumns); 405 } 406 list.add(row); 407 if (limitToOne) { 408 return list; 409 } 410 } 411 } 412 if (limitToOne) { 413 return Collections.emptyList(); 414 } 415 return list; 416 } catch (SQLException e) { 417 checkConcurrentUpdate(e); 418 throw new NuxeoException("Could not select: " + select.sql, e); 419 } 420 } 421 422 @Override 423 public void write(RowBatch batch) { 424 // do deletes first to avoid violating constraint of unique child name in parent 425 // when replacing a complex list element 426 if (!batch.deletes.isEmpty()) { 427 writeDeletes(batch.deletes); 428 } 429 // batch.deletesDependent not executed 430 if (!batch.creates.isEmpty()) { 431 writeCreates(batch.creates); 432 } 433 if (!batch.updates.isEmpty()) { 434 writeUpdates(batch.updates); 435 } 436 } 437 438 protected void writeCreates(List<Row> creates) { 439 // reorganize by table 440 Map<String, List<Row>> tableRows = new LinkedHashMap<>(); 441 // hierarchy table first because there are foreign keys to it 442 tableRows.put(Model.HIER_TABLE_NAME, new LinkedList<>()); 443 for (Row row : creates) { 444 tableRows.computeIfAbsent(row.tableName, k -> new LinkedList<>()).add(row); 445 } 446 // inserts on each table 447 for (Entry<String, List<Row>> en : tableRows.entrySet()) { 448 String tableName = en.getKey(); 449 List<Row> rows = en.getValue(); 450 if (model.isCollectionFragment(tableName)) { 451 List<RowUpdate> rowus = rows.stream().map(RowUpdate::new).collect(Collectors.toList()); 452 insertCollectionRows(tableName, rowus); 453 } else { 454 insertSimpleRows(tableName, rows); 455 } 456 } 457 } 458 459 protected void writeUpdates(Set<RowUpdate> updates) { 460 // we want to write in a consistent order to avoid simple deadlocks between two transactions 461 // so we write by sorted tables, then in each table by sorted ids 462 // reorganize by table 463 Map<String, List<RowUpdate>> tableRows = new HashMap<>(); 464 for (RowUpdate rowu : updates) { 465 tableRows.computeIfAbsent(rowu.row.tableName, k -> new ArrayList<>()).add(rowu); 466 } 467 List<String> tables = new ArrayList<>(); 468 tables.addAll(tableRows.keySet()); 469 // sort tables by name 470 Collections.sort(tables); 471 // updates on each table 472 for (String tableName : tables) { 473 List<RowUpdate> rows = tableRows.get(tableName); 474 // sort rows by id 475 Collections.sort(rows); 476 if (model.isCollectionFragment(tableName)) { 477 updateCollectionRows(tableName, rows); 478 } else { 479 updateSimpleRows(tableName, rows); 480 } 481 } 482 } 483 484 protected void writeDeletes(Collection<RowId> deletes) { 485 // reorganize by table 486 Map<String, Set<Serializable>> tableIds = new HashMap<>(); 487 for (RowId rowId : deletes) { 488 tableIds.computeIfAbsent(rowId.tableName, k -> new HashSet<>()).add(rowId.id); 489 } 490 // delete on each table 491 for (Entry<String, Set<Serializable>> en : tableIds.entrySet()) { 492 String tableName = en.getKey(); 493 Set<Serializable> ids = en.getValue(); 494 deleteRows(tableName, ids); 495 } 496 } 497 498 /** 499 * Inserts multiple rows, all for the same table. 500 */ 501 protected void insertSimpleRows(String tableName, List<Row> rows) { 502 if (rows.isEmpty()) { 503 return; 504 } 505 String sql = sqlInfo.getInsertSql(tableName); 506 if (sql == null) { 507 throw new NuxeoException("Unknown table: " + tableName); 508 } 509 boolean batched = supportsBatchUpdates && rows.size() > 1; 510 String loggedSql = batched ? sql + " -- BATCHED" : sql; 511 List<Column> columns = sqlInfo.getInsertColumns(tableName); 512 try (PreparedStatement ps = connection.prepareStatement(sql)) { 513 int batch = 0; 514 for (Iterator<Row> rowIt = rows.iterator(); rowIt.hasNext();) { 515 Row row = rowIt.next(); 516 if (logger.isLogEnabled()) { 517 logger.logSQL(loggedSql, columns, row); 518 } 519 int i = 1; 520 for (Column column : columns) { 521 column.setToPreparedStatement(ps, i++, row.get(column.getKey())); 522 } 523 if (batched) { 524 ps.addBatch(); 525 batch++; 526 if (batch % UPDATE_BATCH_SIZE == 0 || !rowIt.hasNext()) { 527 ps.executeBatch(); 528 countExecute(); 529 } 530 } else { 531 ps.execute(); 532 countExecute(); 533 } 534 } 535 } catch (SQLException e) { 536 if (e instanceof BatchUpdateException) { 537 BatchUpdateException bue = (BatchUpdateException) e; 538 if (e.getCause() == null && bue.getNextException() != null) { 539 // provide a readable cause in the stack trace 540 e.initCause(bue.getNextException()); 541 } 542 } 543 checkConcurrentUpdate(e); 544 throw new NuxeoException("Could not insert: " + sql, e); 545 } 546 } 547 548 /** 549 * Updates multiple collection rows, all for the same table. 550 */ 551 protected void insertCollectionRows(String tableName, List<RowUpdate> rowus) { 552 if (rowus.isEmpty()) { 553 return; 554 } 555 String sql = sqlInfo.getInsertSql(tableName); 556 List<Column> columns = sqlInfo.getInsertColumns(tableName); 557 CollectionIO io = getCollectionIO(tableName); 558 try (PreparedStatement ps = connection.prepareStatement(sql)) { 559 io.executeInserts(ps, rowus, columns, supportsBatchUpdates, sql, this); 560 } catch (SQLException e) { 561 checkConcurrentUpdate(e); 562 throw new NuxeoException("Could not insert: " + sql, e); 563 } 564 } 565 566 /** 567 * Updates multiple simple rows, all for the same table. 568 */ 569 protected void updateSimpleRows(String tableName, List<RowUpdate> rows) { 570 if (rows.isEmpty()) { 571 return; 572 } 573 574 // we want to allow batching, BUT we also want to keep the order of rows to avoid some deadlocks 575 // so we batch together successive row updates that use the same SQL 576 List<Pair<SQLInfoSelect, List<RowUpdate>>> batchedPairs = BatchUtils.groupByDerived(rows, 577 rowu -> sqlInfo.getUpdateById(tableName, rowu), (a, b) -> a.sql.equals(b.sql)); 578 579 // write by batch 580 for (Pair<SQLInfoSelect, List<RowUpdate>> pair : batchedPairs) { 581 SQLInfoSelect update = pair.getLeft(); 582 List<RowUpdate> rowUpdates = pair.getRight(); 583 boolean changeTokenEnabled = model.getRepositoryDescriptor().isChangeTokenEnabled(); 584 boolean batched = supportsBatchUpdates && rowUpdates.size() > 1 585 && (dialect.supportsBatchUpdateCount() || !changeTokenEnabled); 586 String loggedSql = batched ? update.sql + " -- BATCHED" : update.sql; 587 try (PreparedStatement ps = connection.prepareStatement(update.sql)) { 588 int batch = 0; 589 for (Iterator<RowUpdate> rowIt = rowUpdates.iterator(); rowIt.hasNext();) { 590 RowUpdate rowu = rowIt.next(); 591 if (logger.isLogEnabled()) { 592 logger.logSQL(loggedSql, update.whatColumns, rowu.row, update.whereColumns, rowu.conditions); 593 } 594 int i = 1; 595 for (Column column : update.whatColumns) { 596 Serializable value = rowu.row.get(column.getKey()); 597 if (value instanceof Delta) { 598 value = ((Delta) value).getDeltaValue(); 599 } 600 column.setToPreparedStatement(ps, i++, value); 601 } 602 boolean hasConditions = false; 603 for (Column column : update.whereColumns) { 604 // id or condition 605 String key = column.getKey(); 606 Serializable value; 607 if (key.equals(Model.MAIN_KEY)) { 608 value = rowu.row.get(key); 609 } else { 610 hasConditions = true; 611 value = rowu.conditions.get(key); 612 } 613 column.setToPreparedStatement(ps, i++, value); 614 } 615 if (batched) { 616 ps.addBatch(); 617 batch++; 618 if (batch % UPDATE_BATCH_SIZE == 0 || !rowIt.hasNext()) { 619 int[] counts = ps.executeBatch(); 620 countExecute(); 621 if (changeTokenEnabled && hasConditions) { 622 for (int j = 0; j < counts.length; j++) { 623 int count = counts[j]; 624 if (count != Statement.SUCCESS_NO_INFO && count != 1) { 625 Serializable id = rowUpdates.get(j).row.id; 626 logger.log(" -> CONCURRENT UPDATE: " + id); 627 throw new ConcurrentUpdateException(id.toString()); 628 } 629 } 630 } 631 } 632 } else { 633 int count = ps.executeUpdate(); 634 countExecute(); 635 if (changeTokenEnabled && hasConditions) { 636 if (count != Statement.SUCCESS_NO_INFO && count != 1) { 637 Serializable id = rowu.row.id; 638 logger.log(" -> CONCURRENT UPDATE: " + id); 639 throw new ConcurrentUpdateException(id.toString()); 640 } 641 } 642 } 643 } 644 } catch (SQLException e) { 645 checkConcurrentUpdate(e); 646 throw new NuxeoException("Could not update: " + update.sql, e); 647 } 648 } 649 } 650 651 protected void updateCollectionRows(String tableName, List<RowUpdate> rowus) { 652 Set<Serializable> deleteIds = new HashSet<>(); 653 for (RowUpdate rowu : rowus) { 654 if (rowu.pos == -1 || collectionDeleteBeforeAppend) { 655 deleteIds.add(rowu.row.id); 656 } 657 } 658 deleteRows(tableName, deleteIds); 659 insertCollectionRows(tableName, rowus); 660 } 661 662 /** 663 * Deletes multiple rows, all for the same table. 664 */ 665 protected void deleteRows(String tableName, Set<Serializable> ids) { 666 if (ids.isEmpty()) { 667 return; 668 } 669 int size = ids.size(); 670 int chunkSize = sqlInfo.getMaximumArgsForIn(); 671 if (size > chunkSize) { 672 List<Serializable> idList = new ArrayList<>(ids); 673 for (int start = 0; start < size; start += chunkSize) { 674 int end = start + chunkSize; 675 if (end > size) { 676 end = size; 677 } 678 // needs to be Serializable -> copy 679 List<Serializable> chunkIds = new ArrayList<>(idList.subList(start, end)); 680 deleteRowsDirect(tableName, chunkIds); 681 } 682 } else { 683 deleteRowsDirect(tableName, ids); 684 } 685 } 686 687 protected void deleteRowsSoft(List<NodeInfo> nodeInfos) { 688 try { 689 int size = nodeInfos.size(); 690 List<Serializable> ids = new ArrayList<>(size); 691 for (NodeInfo info : nodeInfos) { 692 ids.add(info.id); 693 } 694 int chunkSize = 100; // max size of ids array 695 if (size <= chunkSize) { 696 doSoftDeleteRows(ids); 697 } else { 698 for (int start = 0; start < size;) { 699 int end = start + chunkSize; 700 if (end > size) { 701 end = size; 702 } 703 doSoftDeleteRows(ids.subList(start, end)); 704 start = end; 705 } 706 } 707 } catch (SQLException e) { 708 throw new NuxeoException("Could not soft delete", e); 709 } 710 } 711 712 // not chunked 713 protected void doSoftDeleteRows(List<Serializable> ids) throws SQLException { 714 Serializable whereIds = newIdArray(ids); 715 Calendar now = Calendar.getInstance(); 716 String sql = sqlInfo.getSoftDeleteSql(); 717 if (logger.isLogEnabled()) { 718 logger.logSQL(sql, Arrays.asList(whereIds, now)); 719 } 720 try (PreparedStatement ps = connection.prepareStatement(sql)) { 721 setToPreparedStatementIdArray(ps, 1, whereIds); 722 dialect.setToPreparedStatementTimestamp(ps, 2, now, null); 723 ps.execute(); 724 countExecute(); 725 } 726 } 727 728 protected Serializable newIdArray(Collection<Serializable> ids) { 729 if (dialect.supportsArrays()) { 730 return ids.toArray(); // Object[] 731 } else { 732 // join with '|' 733 StringBuilder b = new StringBuilder(); 734 for (Serializable id : ids) { 735 b.append(id); 736 b.append('|'); 737 } 738 b.setLength(b.length() - 1); 739 return b.toString(); 740 } 741 } 742 743 protected void setToPreparedStatementIdArray(PreparedStatement ps, int index, Serializable idArray) 744 throws SQLException { 745 if (idArray instanceof String) { 746 ps.setString(index, (String) idArray); 747 } else { 748 Array array = dialect.createArrayOf(Types.OTHER, (Object[]) idArray, connection); 749 ps.setArray(index, array); 750 } 751 } 752 753 /** 754 * Clean up soft-deleted rows. 755 * <p> 756 * Rows deleted more recently than the beforeTime are left alone. Only a limited number of rows may be deleted, to 757 * prevent transaction during too long. 758 * 759 * @param max the maximum number of rows to delete at a time 760 * @param beforeTime the maximum deletion time of the rows to delete 761 * @return the number of rows deleted 762 */ 763 public int cleanupDeletedRows(int max, Calendar beforeTime) { 764 if (max < 0) { 765 max = 0; 766 } 767 String sql = sqlInfo.getSoftDeleteCleanupSql(); 768 if (logger.isLogEnabled()) { 769 logger.logSQL(sql, Arrays.<Serializable> asList(beforeTime, Long.valueOf(max))); 770 } 771 try { 772 if (sql.startsWith("{")) { 773 // callable statement 774 boolean outFirst = sql.startsWith("{?="); 775 int outIndex = outFirst ? 1 : 3; 776 int inIndex = outFirst ? 2 : 1; 777 try (CallableStatement cs = connection.prepareCall(sql)) { 778 cs.setInt(inIndex, max); 779 dialect.setToPreparedStatementTimestamp(cs, inIndex + 1, beforeTime, null); 780 cs.registerOutParameter(outIndex, Types.INTEGER); 781 cs.execute(); 782 int count = cs.getInt(outIndex); 783 logger.logCount(count); 784 return count; 785 } 786 } else { 787 // standard prepared statement with result set 788 try (PreparedStatement ps = connection.prepareStatement(sql)) { 789 ps.setInt(1, max); 790 dialect.setToPreparedStatementTimestamp(ps, 2, beforeTime, null); 791 try (ResultSet rs = ps.executeQuery()) { 792 countExecute(); 793 if (!rs.next()) { 794 throw new NuxeoException("Cannot get result"); 795 } 796 int count = rs.getInt(1); 797 logger.logCount(count); 798 return count; 799 } 800 } 801 } 802 } catch (SQLException e) { 803 throw new NuxeoException("Could not purge soft delete", e); 804 } 805 } 806 807 protected void deleteRowsDirect(String tableName, Collection<Serializable> ids) { 808 String sql = sqlInfo.getDeleteSql(tableName, ids.size()); 809 if (logger.isLogEnabled()) { 810 logger.logSQL(sql, ids); 811 } 812 try (PreparedStatement ps = connection.prepareStatement(sql)) { 813 int i = 1; 814 for (Serializable id : ids) { 815 dialect.setId(ps, i++, id); 816 } 817 int count = ps.executeUpdate(); 818 countExecute(); 819 logger.logCount(count); 820 } catch (SQLException e) { 821 checkConcurrentUpdate(e); 822 throw new NuxeoException("Could not delete: " + tableName, e); 823 } 824 } 825 826 @Override 827 public Row readSimpleRow(RowId rowId) { 828 SQLInfoSelect select = sqlInfo.selectFragmentById.get(rowId.tableName); 829 Map<String, Serializable> criteriaMap = Collections.singletonMap(Model.MAIN_KEY, rowId.id); 830 List<Row> maps = getSelectRows(rowId.tableName, select, criteriaMap, null, true); 831 return maps.isEmpty() ? null : maps.get(0); 832 } 833 834 @Override 835 public Map<String, String> getBinaryFulltext(RowId rowId) { 836 ArrayList<String> columns = new ArrayList<>(); 837 for (String index : model.getFulltextConfiguration().indexesAllBinary) { 838 String col = Model.FULLTEXT_BINARYTEXT_KEY + model.getFulltextIndexSuffix(index); 839 columns.add(col); 840 } 841 Serializable id = rowId.id; 842 Map<String, String> ret = new HashMap<>(columns.size()); 843 String sql = dialect.getBinaryFulltextSql(columns); 844 if (sql == null) { 845 logger.info("getBinaryFulltextSql not supported for dialect " + dialect); 846 return ret; 847 } 848 if (logger.isLogEnabled()) { 849 logger.logSQL(sql, Collections.singletonList(id)); 850 } 851 try (PreparedStatement ps = connection.prepareStatement(sql)) { 852 dialect.setId(ps, 1, id); 853 try (ResultSet rs = ps.executeQuery()) { 854 while (rs.next()) { 855 for (int i = 1; i <= columns.size(); i++) { 856 ret.put(columns.get(i - 1), rs.getString(i)); 857 } 858 } 859 if (logger.isLogEnabled()) { 860 logger.log(" -> " + ret); 861 } 862 } 863 return ret; 864 } catch (SQLException e) { 865 throw new NuxeoException("Could not select: " + sql, e); 866 } 867 } 868 869 @Override 870 public Serializable[] readCollectionRowArray(RowId rowId) { 871 String tableName = rowId.tableName; 872 Serializable id = rowId.id; 873 String sql = sqlInfo.selectFragmentById.get(tableName).sql; 874 if (logger.isLogEnabled()) { 875 logger.logSQL(sql, Collections.singletonList(id)); 876 } 877 try (PreparedStatement ps = connection.prepareStatement(sql)) { 878 List<Column> columns = sqlInfo.selectFragmentById.get(tableName).whatColumns; 879 dialect.setId(ps, 1, id); // assumes only one primary column 880 try (ResultSet rs = ps.executeQuery()) { 881 countExecute(); 882 883 // construct the resulting collection using each row 884 CollectionIO io = getCollectionIO(tableName); 885 List<Serializable> list = new ArrayList<>(); 886 Serializable[] returnId = new Serializable[1]; 887 int[] returnPos = { -1 }; 888 while (rs.next()) { 889 list.add(io.getCurrentFromResultSet(rs, columns, model, returnId, returnPos)); 890 } 891 PropertyType type = model.getCollectionFragmentType(tableName).getArrayBaseType(); 892 Serializable[] array = type.collectionToArray(list); 893 894 if (logger.isLogEnabled()) { 895 logger.log(" -> " + Arrays.asList(array)); 896 } 897 return array; 898 } 899 } catch (SQLException e) { 900 throw new NuxeoException("Could not select: " + sql, e); 901 } 902 } 903 904 @Override 905 public List<Row> readSelectionRows(SelectionType selType, Serializable selId, Serializable filter, 906 Serializable criterion, boolean limitToOne) { 907 SQLInfoSelection selInfo = sqlInfo.getSelection(selType); 908 Map<String, Serializable> criteriaMap = new HashMap<>(); 909 criteriaMap.put(selType.selKey, selId); 910 SQLInfoSelect select; 911 if (filter == null) { 912 select = selInfo.selectAll; 913 } else { 914 select = selInfo.selectFiltered; 915 criteriaMap.put(selType.filterKey, filter); 916 } 917 if (selType.criterionKey != null) { 918 criteriaMap.put(selType.criterionKey, criterion); 919 } 920 return getSelectRows(selType.tableName, select, criteriaMap, null, limitToOne); 921 } 922 923 @Override 924 public Set<Serializable> readSelectionsIds(SelectionType selType, List<Serializable> values) { 925 SQLInfoSelection selInfo = sqlInfo.getSelection(selType); 926 Map<String, Serializable> criteriaMap = new HashMap<>(); 927 Set<Serializable> ids = new HashSet<>(); 928 int size = values.size(); 929 int chunkSize = sqlInfo.getMaximumArgsForIn(); 930 if (size > chunkSize) { 931 for (int start = 0; start < size; start += chunkSize) { 932 int end = start + chunkSize; 933 if (end > size) { 934 end = size; 935 } 936 // needs to be Serializable -> copy 937 List<Serializable> chunkTodo = new ArrayList<>(values.subList(start, end)); 938 criteriaMap.put(selType.selKey, (Serializable) chunkTodo); 939 SQLInfoSelect select = selInfo.getSelectSelectionIds(chunkTodo.size()); 940 List<Row> rows = getSelectRows(selType.tableName, select, criteriaMap, null, false); 941 rows.forEach(row -> ids.add(row.id)); 942 } 943 } else { 944 criteriaMap.put(selType.selKey, (Serializable) values); 945 SQLInfoSelect select = selInfo.getSelectSelectionIds(values.size()); 946 List<Row> rows = getSelectRows(selType.tableName, select, criteriaMap, null, false); 947 rows.forEach(row -> ids.add(row.id)); 948 } 949 return ids; 950 } 951 952 @Override 953 public CopyResult copy(IdWithTypes source, Serializable destParentId, String destName, Row overwriteRow, 954 boolean excludeSpecialChildren, boolean excludeACL) { 955 // assert !model.separateMainTable; // other case not implemented 956 VCSInvalidations invalidations = new VCSInvalidations(); 957 try { 958 Map<Serializable, Serializable> idMap = new LinkedHashMap<>(); 959 Map<Serializable, IdWithTypes> idToTypes = new HashMap<>(); 960 // copy the hierarchy fragments recursively 961 Serializable overwriteId = overwriteRow == null ? null : overwriteRow.id; 962 if (overwriteId != null) { 963 // overwrite hier root with explicit values 964 String tableName = Model.HIER_TABLE_NAME; 965 updateSimpleRowWithValues(tableName, overwriteRow); 966 idMap.put(source.id, overwriteId); 967 // invalidate 968 invalidations.addModified(new RowId(tableName, overwriteId)); 969 } 970 // create the new hierarchy by copy 971 boolean resetVersion = destParentId != null; 972 Set<Serializable> recordIds = new HashSet<>(); 973 Serializable newRootId = copyHierRecursive(source, destParentId, destName, overwriteId, resetVersion, idMap, 974 idToTypes, recordIds, excludeSpecialChildren); 975 // invalidate children 976 Serializable invalParentId = overwriteId == null ? destParentId : overwriteId; 977 if (invalParentId != null) { // null for a new version 978 invalidations.addModified(new RowId(VCSInvalidations.PARENT, invalParentId)); 979 } 980 // copy all collected fragments 981 Set<Serializable> proxyIds = new HashSet<>(); 982 for (Entry<String, Set<Serializable>> entry : model.getPerFragmentIds(idToTypes).entrySet()) { 983 String tableName = entry.getKey(); 984 if (tableName.equals(Model.HIER_TABLE_NAME)) { 985 // already done 986 continue; 987 } 988 if (tableName.equals(Model.VERSION_TABLE_NAME)) { 989 // versions not fileable 990 // restore must not copy versions either 991 continue; 992 } 993 if (excludeACL && tableName.equals(Model.ACL_TABLE_NAME)) { 994 continue; 995 } 996 Set<Serializable> ids = entry.getValue(); 997 if (tableName.equals(Model.PROXY_TABLE_NAME)) { 998 for (Serializable id : ids) { 999 proxyIds.add(idMap.get(id)); // copied ids 1000 } 1001 } 1002 Boolean invalidation = copyRows(tableName, ids, idMap, overwriteId); 1003 if (invalidation != null) { 1004 // overwrote something 1005 // make sure things are properly invalidated in this and 1006 // other sessions 1007 if (Boolean.TRUE.equals(invalidation)) { 1008 invalidations.addModified(new RowId(tableName, overwriteId)); 1009 } else { 1010 invalidations.addDeleted(new RowId(tableName, overwriteId)); 1011 } 1012 } 1013 } 1014 return new CopyResult(newRootId, invalidations, proxyIds, recordIds); 1015 } catch (SQLException e) { 1016 throw new NuxeoException("Could not copy: " + source.id.toString(), e); 1017 } 1018 } 1019 1020 /** 1021 * Updates a row in the database with given explicit values. 1022 */ 1023 protected void updateSimpleRowWithValues(String tableName, Row row) { 1024 Update update = sqlInfo.getUpdateByIdForKeys(tableName, row.getKeys()); 1025 Table table = update.getTable(); 1026 String sql = update.getStatement(); 1027 try (PreparedStatement ps = connection.prepareStatement(sql)) { 1028 if (logger.isLogEnabled()) { 1029 List<Serializable> values = new LinkedList<>(); 1030 values.addAll(row.getValues()); 1031 values.add(row.id); // id last in SQL 1032 logger.logSQL(sql, values); 1033 } 1034 int i = 1; 1035 List<String> keys = row.getKeys(); 1036 List<Serializable> values = row.getValues(); 1037 int size = keys.size(); 1038 for (int r = 0; r < size; r++) { 1039 String key = keys.get(r); 1040 Serializable value = values.get(r); 1041 table.getColumn(key).setToPreparedStatement(ps, i++, value); 1042 } 1043 dialect.setId(ps, i, row.id); // id last in SQL 1044 ps.executeUpdate(); 1045 countExecute(); 1046 } catch (SQLException e) { 1047 throw new NuxeoException("Could not update: " + sql, e); 1048 } 1049 } 1050 1051 /** 1052 * Copies hierarchy from id to parentId, and recurses. 1053 * <p> 1054 * If name is {@code null}, then the original name is kept. 1055 * <p> 1056 * {@code idMap} is filled with info about the correspondence between original and copied ids. {@code idToTypes} is 1057 * filled with the type of each (source) fragment. {@code recordIds} is filled with the copied ids of documents that 1058 * used to be records. 1059 * <p> 1060 * TODO: this should be optimized to use a stored procedure. 1061 * 1062 * @param overwriteId when not {@code null}, the copy is done onto this existing node (skipped) 1063 * @return the new root id 1064 */ 1065 protected Serializable copyHierRecursive(IdWithTypes source, Serializable parentId, String name, 1066 Serializable overwriteId, boolean resetVersion, Map<Serializable, Serializable> idMap, 1067 Map<Serializable, IdWithTypes> idToTypes, Set<Serializable> recordIds, boolean excludeSpecialChildren) throws SQLException { 1068 idToTypes.put(source.id, source); 1069 Serializable newId; 1070 if (overwriteId == null) { 1071 newId = copyHier(source.id, parentId, name, resetVersion, idMap); 1072 } else { 1073 newId = overwriteId; 1074 idMap.put(source.id, newId); 1075 } 1076 if (source.isRecord) { 1077 recordIds.add(newId); 1078 } 1079 1080 // recurse in children, exclude regular children (in the case of a versionable folderish) 1081 boolean excludeRegularChildren = parentId == null; 1082 for (IdWithTypes child : getChildrenIdsWithTypes(source.id, excludeSpecialChildren, excludeRegularChildren)) { 1083 copyHierRecursive(child, newId, null, null, resetVersion, idMap, idToTypes, recordIds, excludeSpecialChildren); 1084 } 1085 return newId; 1086 } 1087 1088 /** 1089 * Copies hierarchy from id to a new child of parentId. 1090 * <p> 1091 * If name is {@code null}, then the original name is kept. 1092 * <p> 1093 * {@code idMap} is filled with info about the correspondence between original and copied ids. 1094 * 1095 * @return the new id 1096 */ 1097 protected Serializable copyHier(Serializable id, Serializable parentId, String name, boolean resetVersion, 1098 Map<Serializable, Serializable> idMap) throws SQLException { 1099 boolean explicitName = name != null; 1100 1101 SQLInfoSelect copy = sqlInfo.getCopyHier(explicitName, resetVersion); 1102 try (PreparedStatement ps = connection.prepareStatement(copy.sql)) { 1103 Serializable newId = generateNewId(); 1104 1105 List<Serializable> debugValues = null; 1106 if (logger.isLogEnabled()) { 1107 debugValues = new ArrayList<>(4); 1108 } 1109 int i = 1; 1110 for (Column column : copy.whatColumns) { 1111 String key = column.getKey(); 1112 Serializable v; 1113 if (key.equals(Model.HIER_PARENT_KEY)) { 1114 v = parentId; 1115 } else if (key.equals(Model.HIER_CHILD_NAME_KEY)) { 1116 // present if name explicitely set (first iteration) 1117 v = name; 1118 } else if (key.equals(Model.MAIN_KEY)) { 1119 // present if APP_UUID generation 1120 v = newId; 1121 } else if (key.equals(Model.MAIN_IS_RECORD_KEY)) { 1122 // records are reset on copy 1123 v = null; 1124 } else if (key.equals(Model.MAIN_BASE_VERSION_KEY) || key.equals(Model.MAIN_CHECKED_IN_KEY)) { 1125 v = null; 1126 } else if (key.equals(Model.MAIN_MINOR_VERSION_KEY) || key.equals(Model.MAIN_MAJOR_VERSION_KEY)) { 1127 // present if reset version (regular copy, not checkin) 1128 v = null; 1129 } else { 1130 throw new RuntimeException(column.toString()); 1131 } 1132 column.setToPreparedStatement(ps, i++, v); 1133 if (debugValues != null) { 1134 debugValues.add(v); 1135 } 1136 } 1137 // last parameter is for 'WHERE "id" = ?' 1138 Column whereColumn = copy.whereColumns.get(0); 1139 whereColumn.setToPreparedStatement(ps, i, id); 1140 if (debugValues != null) { 1141 debugValues.add(id); 1142 logger.logSQL(copy.sql, debugValues); 1143 } 1144 ps.executeUpdate(); 1145 countExecute(); 1146 1147 // TODO DB_IDENTITY 1148 // post insert fetch idrow 1149 1150 idMap.put(id, newId); 1151 return newId; 1152 } 1153 } 1154 1155 /** 1156 * Gets the children ids and types of a node. 1157 */ 1158 protected List<IdWithTypes> getChildrenIdsWithTypes(Serializable id, boolean excludeSpecialChildren, boolean excludeRegularChildren) throws SQLException { 1159 List<IdWithTypes> children = new LinkedList<>(); 1160 String sql = sqlInfo.getSelectChildrenIdsAndTypesSql(excludeSpecialChildren, excludeRegularChildren); 1161 if (logger.isLogEnabled()) { 1162 logger.logSQL(sql, Collections.singletonList(id)); 1163 } 1164 List<Column> columns = sqlInfo.getSelectChildrenIdsAndTypesWhatColumns(); 1165 try (PreparedStatement ps = connection.prepareStatement(sql)) { 1166 List<String> debugValues = null; 1167 if (logger.isLogEnabled()) { 1168 debugValues = new LinkedList<>(); 1169 } 1170 dialect.setId(ps, 1, id); // parent id 1171 try (ResultSet rs = ps.executeQuery()) { 1172 countExecute(); 1173 while (rs.next()) { 1174 Serializable childId = null; 1175 String childPrimaryType = null; 1176 String[] childMixinTypes = null; 1177 boolean isRecord = false; 1178 int i = 1; 1179 for (Column column : columns) { 1180 String key = column.getKey(); 1181 Serializable value = column.getFromResultSet(rs, i++); 1182 if (key.equals(Model.MAIN_KEY)) { 1183 childId = value; 1184 } else if (key.equals(Model.MAIN_PRIMARY_TYPE_KEY)) { 1185 childPrimaryType = (String) value; 1186 } else if (key.equals(Model.MAIN_MIXIN_TYPES_KEY)) { 1187 childMixinTypes = (String[]) value; 1188 } else if (key.equals(Model.MAIN_IS_RECORD_KEY)) { 1189 isRecord = Boolean.TRUE.equals(value); 1190 } 1191 } 1192 children.add(new IdWithTypes(childId, childPrimaryType, childMixinTypes, isRecord)); 1193 if (debugValues != null) { 1194 debugValues.add(childId + "/" + childPrimaryType + "/" + Arrays.toString(childMixinTypes)); 1195 } 1196 } 1197 } 1198 if (debugValues != null) { 1199 logger.log(" -> " + debugValues); 1200 } 1201 return children; 1202 } 1203 } 1204 1205 /** 1206 * Copy the rows from tableName with given ids into new ones with new ids given by idMap. 1207 * <p> 1208 * A new row with id {@code overwriteId} is first deleted. 1209 * 1210 * @return {@link Boolean#TRUE} for a modification or creation, {@link Boolean#FALSE} for a deletion, {@code null} 1211 * otherwise (still absent) 1212 */ 1213 protected Boolean copyRows(String tableName, Set<Serializable> ids, Map<Serializable, Serializable> idMap, 1214 Serializable overwriteId) throws SQLException { 1215 String copySql = sqlInfo.getCopySql(tableName); 1216 Column copyIdColumn = sqlInfo.getCopyIdColumn(tableName); 1217 String deleteSql = sqlInfo.getDeleteSql(tableName); 1218 try (PreparedStatement copyPs = connection.prepareStatement(copySql); 1219 PreparedStatement deletePs = connection.prepareStatement(deleteSql)) { 1220 boolean before = false; 1221 boolean after = false; 1222 for (Serializable id : ids) { 1223 Serializable newId = idMap.get(id); 1224 boolean overwrite = newId.equals(overwriteId); 1225 if (overwrite) { 1226 // remove existing first 1227 if (logger.isLogEnabled()) { 1228 logger.logSQL(deleteSql, Collections.singletonList(newId)); 1229 } 1230 dialect.setId(deletePs, 1, newId); 1231 int delCount = deletePs.executeUpdate(); 1232 countExecute(); 1233 before = delCount > 0; 1234 } 1235 copyIdColumn.setToPreparedStatement(copyPs, 1, newId); 1236 copyIdColumn.setToPreparedStatement(copyPs, 2, id); 1237 if (logger.isLogEnabled()) { 1238 logger.logSQL(copySql, Arrays.asList(newId, id)); 1239 } 1240 int copyCount = copyPs.executeUpdate(); 1241 countExecute(); 1242 if (overwrite) { 1243 after = copyCount > 0; 1244 } 1245 } 1246 // * , n -> mod (TRUE) 1247 // n , 0 -> del (FALSE) 1248 // 0 , 0 -> null 1249 return after ? Boolean.TRUE : (before ? Boolean.FALSE : null); 1250 } 1251 } 1252 1253 @Override 1254 public void remove(Serializable rootId, List<NodeInfo> nodeInfos) { 1255 if (sqlInfo.softDeleteEnabled) { 1256 deleteRowsSoft(nodeInfos); 1257 } else { 1258 deleteRowsDirect(Model.HIER_TABLE_NAME, Collections.singleton(rootId)); 1259 } 1260 } 1261 1262 @Override 1263 public List<NodeInfo> getDescendantsInfo(Serializable rootId) { 1264 if (!dialect.supportsFastDescendants()) { 1265 return getDescendantsInfoIterative(rootId); 1266 } 1267 List<NodeInfo> descendants = new LinkedList<>(); 1268 String sql = sqlInfo.getSelectDescendantsInfoSql(); 1269 if (logger.isLogEnabled()) { 1270 logger.logSQL(sql, Collections.singletonList(rootId)); 1271 } 1272 List<Column> columns = sqlInfo.getSelectDescendantsInfoWhatColumns(); 1273 try (PreparedStatement ps = connection.prepareStatement(sql)) { 1274 List<String> debugValues = null; 1275 if (logger.isLogEnabled()) { 1276 debugValues = new LinkedList<>(); 1277 } 1278 dialect.setId(ps, 1, rootId); // parent id 1279 try (ResultSet rs = ps.executeQuery()) { 1280 countExecute(); 1281 while (rs.next()) { 1282 NodeInfo info = getNodeInfo(rs, columns); 1283 descendants.add(info); 1284 if (debugValues != null) { 1285 if (debugValues.size() < DEBUG_MAX_TREE) { 1286 debugValues.add(info.id + "/" + info.primaryType); 1287 } 1288 } 1289 } 1290 } 1291 if (debugValues != null) { 1292 if (debugValues.size() >= DEBUG_MAX_TREE) { 1293 debugValues.add("... (" + descendants.size() + ") results"); 1294 } 1295 logger.log(" -> " + debugValues); 1296 } 1297 return descendants; 1298 } catch (SQLException e) { 1299 throw new NuxeoException("Failed to get descendants", e); 1300 } 1301 } 1302 1303 protected List<NodeInfo> getDescendantsInfoIterative(Serializable rootId) { 1304 Set<Serializable> done = new HashSet<>(); 1305 List<Serializable> todo = new ArrayList<>(Collections.singleton(rootId)); 1306 List<NodeInfo> descendants = new ArrayList<>(); 1307 while (!todo.isEmpty()) { 1308 List<NodeInfo> infos; 1309 int size = todo.size(); 1310 int chunkSize = sqlInfo.getMaximumArgsForIn(); 1311 if (size > chunkSize) { 1312 infos = new ArrayList<>(); 1313 for (int start = 0; start < size; start += chunkSize) { 1314 int end = start + chunkSize; 1315 if (end > size) { 1316 end = size; 1317 } 1318 // needs to be Serializable -> copy 1319 List<Serializable> chunkTodo = new ArrayList<>(todo.subList(start, end)); 1320 List<NodeInfo> chunkInfos = getChildrenNodeInfos(chunkTodo); 1321 infos.addAll(chunkInfos); 1322 } 1323 } else { 1324 infos = getChildrenNodeInfos(todo); 1325 } 1326 todo = new ArrayList<>(); 1327 for (NodeInfo info : infos) { 1328 Serializable id = info.id; 1329 if (!done.add(id)) { 1330 continue; 1331 } 1332 todo.add(id); 1333 descendants.add(info); 1334 } 1335 } 1336 return descendants; 1337 } 1338 1339 /** 1340 * Gets the children of a node as a list of NodeInfo. 1341 */ 1342 protected List<NodeInfo> getChildrenNodeInfos(Collection<Serializable> ids) { 1343 List<NodeInfo> children = new LinkedList<>(); 1344 SQLInfoSelect select = sqlInfo.getSelectChildrenNodeInfos(ids.size()); 1345 if (logger.isLogEnabled()) { 1346 logger.logSQL(select.sql, ids); 1347 } 1348 Column where = select.whereColumns.get(0); 1349 try (PreparedStatement ps = connection.prepareStatement(select.sql)) { 1350 List<String> debugValues = null; 1351 if (logger.isLogEnabled()) { 1352 debugValues = new LinkedList<>(); 1353 } 1354 int ii = 1; 1355 for (Serializable id : ids) { 1356 where.setToPreparedStatement(ps, ii++, id); 1357 } 1358 try (ResultSet rs = ps.executeQuery()) { 1359 countExecute(); 1360 while (rs.next()) { 1361 NodeInfo info = getNodeInfo(rs, select.whatColumns); 1362 children.add(info); 1363 if (debugValues != null) { 1364 if (debugValues.size() < DEBUG_MAX_TREE) { 1365 debugValues.add(info.id + "/" + info.primaryType); 1366 } 1367 } 1368 } 1369 } 1370 if (debugValues != null) { 1371 if (debugValues.size() >= DEBUG_MAX_TREE) { 1372 debugValues.add("... (" + children.size() + ") results"); 1373 } 1374 logger.log(" -> " + debugValues); 1375 } 1376 return children; 1377 } catch (SQLException e) { 1378 throw new NuxeoException("Failed to get descendants", e); 1379 } 1380 } 1381 1382 protected NodeInfo getNodeInfo(ResultSet rs, List<Column> columns) throws SQLException { 1383 Serializable id = null; 1384 Serializable parentId = null; 1385 String primaryType = null; 1386 Boolean isProperty = null; 1387 Serializable targetId = null; 1388 Serializable versionableId = null; 1389 Calendar retainUntil = null; 1390 boolean isRecord = false; 1391 boolean hasLegalHold = false; 1392 boolean isRetentionActive = false; 1393 int i = 1; 1394 for (Column column : columns) { 1395 String key = column.getKey(); 1396 Serializable value = column.getFromResultSet(rs, i++); 1397 if (key.equals(Model.MAIN_KEY)) { 1398 id = value; 1399 } else if (key.equals(Model.HIER_PARENT_KEY)) { 1400 parentId = value; 1401 } else if (key.equals(Model.MAIN_PRIMARY_TYPE_KEY)) { 1402 primaryType = (String) value; 1403 } else if (key.equals(Model.HIER_CHILD_ISPROPERTY_KEY)) { 1404 isProperty = (Boolean) value; 1405 } else if (key.equals(Model.PROXY_TARGET_KEY)) { 1406 targetId = value; 1407 } else if (key.equals(Model.PROXY_VERSIONABLE_KEY)) { 1408 versionableId = value; 1409 } else if (key.equals(Model.MAIN_IS_RECORD_KEY)) { 1410 isRecord = Boolean.TRUE.equals(value); 1411 } else if (key.equals(Model.MAIN_RETAIN_UNTIL_KEY)) { 1412 retainUntil = (Calendar) value; 1413 } else if (key.equals(Model.MAIN_HAS_LEGAL_HOLD_KEY)) { 1414 hasLegalHold = Boolean.TRUE.equals(value); 1415 } else if (key.equals(Model.MAIN_IS_RETENTION_ACTIVE_KEY)) { 1416 isRetentionActive = Boolean.TRUE.equals(value); 1417 } 1418 // no mixins (not useful to caller) 1419 // no versions (not fileable) 1420 } 1421 NodeInfo nodeInfo = new NodeInfo(id, parentId, primaryType, isProperty, versionableId, targetId, isRecord, 1422 retainUntil, hasLegalHold, isRetentionActive); 1423 return nodeInfo; 1424 } 1425 1426}