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