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