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