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