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