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