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