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 */
019
020package org.nuxeo.ecm.core.storage.sql;
021
022import java.io.File;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.OutputStream;
026import java.io.PrintStream;
027import java.io.Serializable;
028import java.security.MessageDigest;
029import java.security.NoSuchAlgorithmException;
030import java.sql.Connection;
031import java.sql.DatabaseMetaData;
032import java.sql.ResultSet;
033import java.sql.SQLException;
034import java.sql.Statement;
035import java.util.Calendar;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.LinkedList;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043import java.util.Map.Entry;
044import java.util.concurrent.CopyOnWriteArrayList;
045
046import javax.naming.NamingException;
047
048import org.apache.commons.logging.Log;
049import org.apache.commons.logging.LogFactory;
050import org.nuxeo.common.Environment;
051import org.nuxeo.ecm.core.api.NuxeoException;
052import org.nuxeo.ecm.core.api.lock.LockManager;
053import org.nuxeo.ecm.core.api.repository.FulltextConfiguration;
054import org.nuxeo.ecm.core.storage.FulltextDescriptor;
055import org.nuxeo.ecm.core.storage.lock.LockManagerService;
056import org.nuxeo.ecm.core.storage.sql.Model.IdType;
057import org.nuxeo.ecm.core.storage.sql.Session.PathResolver;
058import org.nuxeo.ecm.core.storage.sql.coremodel.SQLSession;
059import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCConnection;
060import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger;
061import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCMapper;
062import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo;
063import org.nuxeo.ecm.core.storage.sql.jdbc.TableUpgrader;
064import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
065import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database;
066import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
067import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
068import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.DialectOracle;
069import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.SQLStatement.ListCollector;
070import org.nuxeo.runtime.RuntimeMessage;
071import org.nuxeo.runtime.RuntimeMessage.Level;
072import org.nuxeo.runtime.RuntimeMessage.Source;
073import org.nuxeo.runtime.api.Framework;
074import org.nuxeo.runtime.cluster.ClusterService;
075import org.nuxeo.runtime.datasource.ConnectionHelper;
076import org.nuxeo.runtime.datasource.DataSourceHelper;
077import org.nuxeo.runtime.metrics.MetricsService;
078import org.nuxeo.runtime.transaction.TransactionHelper;
079
080import io.dropwizard.metrics5.Counter;
081import io.dropwizard.metrics5.Gauge;
082import io.dropwizard.metrics5.MetricName;
083import io.dropwizard.metrics5.MetricRegistry;
084import io.dropwizard.metrics5.SharedMetricRegistries;
085
086/**
087 * {@link Repository} implementation, to be extended by backend-specific initialization code.
088 */
089public class RepositoryImpl implements Repository, org.nuxeo.ecm.core.model.Repository {
090
091    private static final Log log = LogFactory.getLog(RepositoryImpl.class);
092
093    public static final String TEST_UPGRADE = "testUpgrade";
094
095    // property in sql.txt file
096    public static final String TEST_UPGRADE_VERSIONS = "testUpgradeVersions";
097
098    public static final String TEST_UPGRADE_LAST_CONTRIBUTOR = "testUpgradeLastContributor";
099
100    public static final String TEST_UPGRADE_LOCKS = "testUpgradeLocks";
101
102    public static final String TEST_UPGRADE_SYS_CHANGE_TOKEN = "testUpgradeSysChangeToken";
103
104    public static Map<String, Serializable> testProps = new HashMap<>();
105
106    protected final RepositoryDescriptor repositoryDescriptor;
107
108    private final Collection<SessionImpl> sessions;
109
110    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
111
112    protected final Counter sessionCount;
113
114    private LockManager lockManager;
115
116    /**
117     * @since 7.4 : used to know if the LockManager was provided by this repository or externally
118     */
119    protected boolean selfRegisteredLockManager = false;
120
121    /** Propagator of invalidations to all mappers' caches. */
122    // public for tests
123    public final VCSInvalidationsPropagator invalidationsPropagator;
124
125    protected VCSClusterInvalidator clusterInvalidator;
126
127    public boolean requiresClusterSQL;
128
129    private Model model;
130
131    protected SQLInfo sqlInfo;
132
133    public RepositoryImpl(RepositoryDescriptor repositoryDescriptor) {
134        this.repositoryDescriptor = repositoryDescriptor;
135        sessions = new CopyOnWriteArrayList<>();
136        invalidationsPropagator = new VCSInvalidationsPropagator();
137
138        sessionCount = registry.counter(MetricName.build("nuxeo", "repositories", "repository", "sessions")
139                                                  .tagged("repository", repositoryDescriptor.name));
140        createMetricsGauges();
141
142        initRepository();
143    }
144
145    protected void createMetricsGauges() {
146        MetricName gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "size")
147                                         .tagged("repository", repositoryDescriptor.name);
148        registry.remove(gaugeName);
149        registry.register(gaugeName, new Gauge<Long>() {
150            @Override
151            public Long getValue() {
152                return getCacheSize();
153            }
154        });
155        gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "pristine")
156                              .tagged("repository", repositoryDescriptor.name);
157        registry.remove(gaugeName);
158        registry.register(gaugeName, new Gauge<Long>() {
159            @Override
160            public Long getValue() {
161                return getCachePristineSize();
162            }
163        });
164        gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "selection")
165                              .tagged("repository", repositoryDescriptor.name);
166        registry.remove(gaugeName);
167        registry.register(gaugeName, new Gauge<Long>() {
168            @Override
169            public Long getValue() {
170                return getCacheSelectionSize();
171            }
172        });
173        gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "mapper")
174                .tagged("repository", repositoryDescriptor.name);
175        registry.remove(gaugeName);
176        registry.register(gaugeName, new Gauge<Long>() {
177            @Override
178            public Long getValue() {
179                return getCacheMapperSize();
180            }
181        });
182    }
183
184    protected Mapper createCachingMapper(Model model, Mapper mapper) {
185        try {
186            Class<? extends CachingMapper> cachingMapperClass = getCachingMapperClass();
187            if (cachingMapperClass == null) {
188                return mapper;
189            }
190            CachingMapper cachingMapper = cachingMapperClass.getDeclaredConstructor().newInstance();
191            cachingMapper.initialize(getName(), model, mapper, invalidationsPropagator,
192                    repositoryDescriptor.cachingMapperProperties);
193            return cachingMapper;
194        } catch (ReflectiveOperationException e) {
195            throw new NuxeoException(e);
196        }
197    }
198
199    protected Class<? extends CachingMapper> getCachingMapperClass() {
200        if (!repositoryDescriptor.getCachingMapperEnabled()) {
201            return null;
202        }
203        Class<? extends CachingMapper> cachingMapperClass = repositoryDescriptor.cachingMapperClass;
204        if (cachingMapperClass == null) {
205            // default cache
206            cachingMapperClass = SoftRefCachingMapper.class;
207        }
208        return cachingMapperClass;
209    }
210
211    public RepositoryDescriptor getRepositoryDescriptor() {
212        return repositoryDescriptor;
213    }
214
215    public LockManager getLockManager() {
216        return lockManager;
217    }
218
219    public Model getModel() {
220        return model;
221    }
222
223    /** @since 11.1 */
224    public SQLInfo getSQLInfo() {
225        return sqlInfo;
226    }
227
228    public VCSInvalidationsPropagator getInvalidationsPropagator() {
229        return invalidationsPropagator;
230    }
231
232    public boolean isChangeTokenEnabled() {
233        return repositoryDescriptor.isChangeTokenEnabled();
234    }
235
236    @Override
237    public SQLSession getSession() {
238        return new SQLSession(getConnection(), this); // NOSONAR
239    }
240
241    /**
242     * Gets a new connection.
243     *
244     * @return the session
245     */
246    @Override
247    public synchronized SessionImpl getConnection() {
248        if (Framework.getRuntime().isShuttingDown()) {
249            throw new IllegalStateException("Cannot open connection, runtime is shutting down");
250        }
251        SessionPathResolver pathResolver = new SessionPathResolver();
252        Mapper mapper = new JDBCMapper(model, pathResolver, sqlInfo, clusterInvalidator, this);
253        mapper = createCachingMapper(model, mapper);
254        SessionImpl session = new SessionImpl(this, model, mapper);
255        pathResolver.setSession(session);
256
257        sessions.add(session);
258        sessionCount.inc();
259        return session;
260    }
261
262    // callback by session at close time
263    protected void closeSession(SessionImpl session) {
264        sessions.remove(session);
265        sessionCount.dec();
266    }
267
268    protected void initRepository() {
269        log.debug("Initializing");
270        prepareClusterInvalidator(); // sets requiresClusterSQL used by backend init
271
272        // check datasource
273        String dataSourceName = JDBCConnection.getDataSourceName(repositoryDescriptor.name);
274        try {
275            DataSourceHelper.getDataSource(dataSourceName);
276        } catch (NamingException cause) {
277            throw new NuxeoException("Cannot acquire datasource: " + dataSourceName, cause);
278        }
279
280        // check connection and get dialect
281        Dialect dialect;
282        try (Connection connection = ConnectionHelper.getConnection(dataSourceName)) {
283            dialect = Dialect.createDialect(connection, repositoryDescriptor);
284        } catch (SQLException cause) {
285            throw new NuxeoException("Cannot get connection from datasource: " + dataSourceName, cause);
286        }
287
288        // model setup
289        ModelSetup modelSetup = new ModelSetup();
290        modelSetup.materializeFulltextSyntheticColumn = dialect.getMaterializeFulltextSyntheticColumn();
291        modelSetup.supportsArrayColumns = dialect.supportsArrayColumns();
292        switch (dialect.getIdType()) {
293        case VARCHAR:
294        case UUID:
295            modelSetup.idType = IdType.STRING;
296            break;
297        case SEQUENCE:
298            modelSetup.idType = IdType.LONG;
299            break;
300        default:
301            throw new AssertionError(dialect.getIdType().toString());
302        }
303        modelSetup.repositoryDescriptor = repositoryDescriptor;
304
305        // Model and SQLInfo
306        model = new Model(modelSetup);
307        sqlInfo = new SQLInfo(model, dialect, requiresClusterSQL);
308
309        // DDL mode
310        String ddlMode = repositoryDescriptor.getDDLMode();
311        if (ddlMode == null) {
312            // compat
313            ddlMode = repositoryDescriptor.getNoDDL() ? RepositoryDescriptor.DDL_MODE_IGNORE
314                    : RepositoryDescriptor.DDL_MODE_EXECUTE;
315        }
316
317        // create database
318        if (ddlMode.equals(RepositoryDescriptor.DDL_MODE_IGNORE)) {
319            log.info("Skipping database creation");
320        } else {
321            createDatabase(ddlMode);
322        }
323        if (log.isDebugEnabled()) {
324            FulltextDescriptor fulltextDescriptor = repositoryDescriptor.getFulltextDescriptor();
325            log.debug(String.format("Database ready, fulltext: disabled=%b storedInBlob=%b searchDisabled=%b.",
326                    fulltextDescriptor.getFulltextDisabled(), fulltextDescriptor.getFulltextStoredInBlob(),
327                    fulltextDescriptor.getFulltextSearchDisabled()));
328        }
329
330        initLockManager();
331        initClusterInvalidator();
332
333        // log once which mapper cache is being used
334        Class<? extends CachingMapper> cachingMapperClass = getCachingMapperClass();
335        if (cachingMapperClass == null) {
336            log.warn("VCS Mapper cache is disabled.");
337        } else {
338            log.info("VCS Mapper cache using: " + cachingMapperClass.getName());
339        }
340
341        initRootNode();
342    }
343
344    protected void initRootNode() {
345        // access a session once so that SessionImpl.computeRootNode can create the root node
346        try (SessionImpl session = getConnection()) {
347            // nothing
348        }
349    }
350
351    protected String getLockManagerName() {
352        // TODO configure in repo descriptor
353        return getName();
354    }
355
356    protected void initLockManager() {
357        String lockManagerName = getLockManagerName();
358        LockManagerService lockManagerService = Framework.getService(LockManagerService.class);
359        lockManager = lockManagerService.getLockManager(lockManagerName);
360        if (lockManager == null) {
361            // no descriptor
362            // default to a VCSLockManager
363            lockManager = new VCSLockManager(this);
364            lockManagerService.registerLockManager(lockManagerName, lockManager);
365            selfRegisteredLockManager = true;
366        } else {
367            selfRegisteredLockManager = false;
368        }
369        log.info("Repository " + getName() + " using lock manager " + lockManager);
370    }
371
372    protected void prepareClusterInvalidator() {
373        if (Framework.getService(ClusterService.class).isEnabled()) {
374            clusterInvalidator = createClusterInvalidator();
375            requiresClusterSQL = clusterInvalidator.requiresClusterSQL();
376        }
377    }
378
379    protected VCSClusterInvalidator createClusterInvalidator() {
380        Class<? extends VCSClusterInvalidator> klass = repositoryDescriptor.clusterInvalidatorClass;
381        if (klass == null) {
382            klass = VCSPubSubInvalidator.class;
383        }
384        try {
385            return klass.getDeclaredConstructor().newInstance();
386        } catch (ReflectiveOperationException e) {
387            throw new NuxeoException(e);
388        }
389    }
390
391    protected void initClusterInvalidator() {
392        if (clusterInvalidator != null) {
393            String nodeId = Framework.getService(ClusterService.class).getNodeId();
394            clusterInvalidator.initialize(nodeId, this);
395        }
396    }
397
398    public static class SessionPathResolver implements PathResolver {
399
400        private Session session;
401
402        protected void setSession(Session session) {
403            this.session = session;
404        }
405
406        @Override
407        public Serializable getIdForPath(String path) {
408            Node node = session.getNodeByPath(path, null);
409            return node == null ? null : node.getId();
410        }
411    }
412
413    /*
414     * ----- Repository -----
415     */
416
417    @Override
418    public void shutdown() {
419        close();
420    }
421
422    @Override
423    public synchronized void close() {
424        closeAllSessions();
425        model = null;
426        if (clusterInvalidator != null) {
427            clusterInvalidator.close();
428        }
429
430        if (selfRegisteredLockManager) {
431            LockManagerService lms = Framework.getService(LockManagerService.class);
432            if (lms != null) {
433                lms.unregisterLockManager(getLockManagerName());
434            }
435        }
436    }
437
438    protected synchronized void closeAllSessions() {
439        for (SessionImpl session : sessions) {
440            session.closeSession();
441        }
442        sessions.clear();
443        sessionCount.dec(sessionCount.getCount());
444        if (lockManager != null) {
445            lockManager.closeLockManager();
446        }
447    }
448
449    /*
450     * ----- RepositoryManagement -----
451     */
452
453    @Override
454    public String getName() {
455        return repositoryDescriptor.name;
456    }
457
458    @Override
459    public int clearCaches() {
460        int n = 0;
461        for (SessionImpl session : sessions) {
462            n += session.clearCaches();
463        }
464        if (lockManager != null) {
465            lockManager.clearLockManagerCaches();
466        }
467        return n;
468    }
469
470    @Override
471    public long getCacheSize() {
472        long size = 0;
473        for (SessionImpl session : sessions) {
474            size += session.getCacheSize();
475        }
476        return size;
477    }
478
479    public long getCacheMapperSize() {
480        long size = 0;
481        for (SessionImpl session : sessions) {
482            size += session.getCacheMapperSize();
483        }
484        return size;
485    }
486
487    @Override
488    public long getCachePristineSize() {
489        long size = 0;
490        for (SessionImpl session : sessions) {
491            size += session.getCachePristineSize();
492        }
493        return size;
494    }
495
496    @Override
497    public long getCacheSelectionSize() {
498        long size = 0;
499        for (SessionImpl session : sessions) {
500            size += session.getCacheSelectionSize();
501        }
502        return size;
503    }
504
505    @Override
506    public void processClusterInvalidationsNext() {
507        // TODO pass through or something
508    }
509
510    @Override
511    public void markReferencedBinaries() {
512        try (SessionImpl session = getConnection()) {
513            session.markReferencedBinaries();
514        }
515    }
516
517    @Override
518    public int cleanupDeletedDocuments(int max, Calendar beforeTime) {
519        if (!repositoryDescriptor.getSoftDeleteEnabled()) {
520            return 0;
521        }
522        try (SessionImpl session = getConnection()) {
523            return session.cleanupDeletedDocuments(max, beforeTime);
524        }
525    }
526
527    @Override
528    public FulltextConfiguration getFulltextConfiguration() {
529        return model.getFulltextConfiguration();
530    }
531
532    /**
533     * Creates the necessary structures in the database.
534     *
535     * @param ddlMode the DDL execution mode
536     */
537    protected void createDatabase(String ddlMode) {
538        // some databases (SQL Server) can't create tables/indexes/etc in a transaction, so suspend it
539        runWithoutTransaction(() -> createDatabaseNoTx(ddlMode));
540    }
541
542    protected void createDatabaseNoTx(String ddlMode) {
543        String dataSourceName = "repository_" + getName();
544        try {
545            // open connection in noSharing mode
546            try (Connection connection = ConnectionHelper.getConnection(dataSourceName, true)) {
547                sqlInfo.dialect.performPostOpenStatements(connection);
548                if (!connection.getAutoCommit()) {
549                    throw new NuxeoException("connection should not run in transactional mode for DDL operations");
550                }
551                createTables(connection, ddlMode);
552            }
553        } catch (SQLException e) {
554            throw new NuxeoException(e);
555        }
556    }
557
558    protected String getTableName(String origName) {
559        if (sqlInfo.dialect instanceof DialectOracle) {
560            if (origName.length() > 30) {
561                StringBuilder sb = new StringBuilder(origName.length());
562                try {
563                    MessageDigest digest = MessageDigest.getInstance("MD5");
564                    sb.append(origName.substring(0, 15));
565                    sb.append('_');
566                    digest.update(origName.getBytes());
567                    sb.append(Dialect.toHexString(digest.digest()).substring(0, 12));
568                    return sb.toString();
569                } catch (NoSuchAlgorithmException e) {
570                    throw new RuntimeException("Error", e);
571                }
572            }
573        }
574        return origName;
575    }
576
577    protected void createTables(Connection connection, String ddlMode) throws SQLException {
578        JDBCLogger logger = new JDBCLogger(getName());
579        Dialect dialect = sqlInfo.dialect;
580        ListCollector ddlCollector = new ListCollector();
581
582        sqlInfo.executeSQLStatements(null, ddlMode, connection, logger, ddlCollector); // for missing category
583        sqlInfo.executeSQLStatements("first", ddlMode, connection, logger, ddlCollector);
584        sqlInfo.executeSQLStatements("beforeTableCreation", ddlMode, connection, logger, ddlCollector);
585        if (testProps.containsKey(TEST_UPGRADE)) {
586            // create "old" tables
587            sqlInfo.executeSQLStatements("testUpgrade", ddlMode, connection, logger, null); // do not collect
588        }
589
590        String schemaName = dialect.getConnectionSchema(connection);
591        DatabaseMetaData metadata = connection.getMetaData();
592        Set<String> tableNames = findTableNames(metadata, schemaName);
593        Database database = sqlInfo.getDatabase();
594        Map<String, List<Column>> added = new HashMap<>();
595
596        for (Table table : database.getTables()) {
597            String tableName = getTableName(table.getPhysicalName());
598            if (!tableNames.contains(tableName.toUpperCase())) {
599                /*
600                 * Create missing table.
601                 */
602                ddlCollector.add(table.getCreateSql());
603                ddlCollector.addAll(table.getPostCreateSqls(model));
604                added.put(table.getKey(), null); // null = table created
605                sqlInfo.sqlStatementsProperties.put("create_table_" + tableName.toLowerCase(), Boolean.TRUE);
606            } else {
607                /*
608                 * Get existing columns.
609                 */
610                Map<String, Integer> columnTypes = new HashMap<>();
611                Map<String, String> columnTypeNames = new HashMap<>();
612                Map<String, Integer> columnTypeSizes = new HashMap<>();
613                try (ResultSet rs = metadata.getColumns(null, schemaName, tableName, "%")) {
614                    while (rs.next()) {
615                        String schema = rs.getString("TABLE_SCHEM");
616                        if (schema != null) { // null for MySQL, doh!
617                            if ("INFORMATION_SCHEMA".equals(schema.toUpperCase())) {
618                                // H2 returns some system tables (locks)
619                                continue;
620                            }
621                        }
622                        String columnName = rs.getString("COLUMN_NAME").toUpperCase();
623                        columnTypes.put(columnName, Integer.valueOf(rs.getInt("DATA_TYPE")));
624                        columnTypeNames.put(columnName, rs.getString("TYPE_NAME"));
625                        columnTypeSizes.put(columnName, Integer.valueOf(rs.getInt("COLUMN_SIZE")));
626                    }
627                }
628                /*
629                 * Update types and create missing columns.
630                 */
631                List<Column> addedColumns = new LinkedList<>();
632                for (Column column : table.getColumns()) {
633                    String upperName = column.getPhysicalName().toUpperCase();
634                    Integer type = columnTypes.remove(upperName);
635                    if (type == null) {
636                        log.warn("Adding missing column in database: " + column.getFullQuotedName());
637                        ddlCollector.add(table.getAddColumnSql(column));
638                        ddlCollector.addAll(table.getPostAddSqls(column, model));
639                        addedColumns.add(column);
640                    } else {
641                        String actualName = columnTypeNames.get(upperName);
642                        Integer actualSize = columnTypeSizes.get(upperName);
643                        String message = column.checkJdbcType(type, actualName, actualSize);
644                        if (message != null) {
645                            log.error(message);
646                            Framework.getRuntime()
647                                     .getMessageHandler()
648                                     .addMessage(new RuntimeMessage(Level.ERROR, message, Source.CODE,
649                                             this.getClass().getName()));
650                        }
651                    }
652                }
653                for (String col : dialect.getIgnoredColumns(table)) {
654                    columnTypes.remove(col.toUpperCase());
655                }
656                if (!columnTypes.isEmpty()) {
657                    log.warn("Database contains additional unused columns for table " + table.getQuotedName() + ": "
658                            + String.join(", ", columnTypes.keySet()));
659                }
660                if (!addedColumns.isEmpty()) {
661                    if (added.containsKey(table.getKey())) {
662                        throw new AssertionError();
663                    }
664                    added.put(table.getKey(), addedColumns);
665                }
666            }
667        }
668
669        if (testProps.containsKey(TEST_UPGRADE)) {
670            // create "old" content in tables
671            sqlInfo.executeSQLStatements("testUpgradeOldTables", ddlMode, connection, logger, ddlCollector);
672        }
673
674        // run upgrade for each table if added columns or test
675        if (!added.isEmpty()) {
676            TableUpgrader tableUpgrader = createTableUpgrader(connection, logger);
677            for (Entry<String, List<Column>> en : added.entrySet()) {
678                List<Column> addedColumns = en.getValue();
679                String tableKey = en.getKey();
680                tableUpgrader.upgrade(tableKey, addedColumns, ddlMode, ddlCollector);
681            }
682        }
683
684        sqlInfo.executeSQLStatements("afterTableCreation", ddlMode, connection, logger, ddlCollector);
685        sqlInfo.executeSQLStatements("last", ddlMode, connection, logger, ddlCollector);
686
687        // aclr_permission check for PostgreSQL
688        dialect.performAdditionalStatements(connection);
689
690        /*
691         * Execute all the collected DDL, or dump it if requested, depending on ddlMode
692         */
693
694        // ddlMode may be:
695        // ignore (not treated here, nothing done)
696        // dump (implies execute)
697        // dump,execute
698        // dump,ignore (no execute)
699        // execute
700        // abort (implies dump)
701        // compat can be used instead of execute to always recreate stored procedures
702
703        List<String> ddl = ddlCollector.getStrings();
704        boolean ignore = ddlMode.contains(RepositoryDescriptor.DDL_MODE_IGNORE);
705        boolean dump = ddlMode.contains(RepositoryDescriptor.DDL_MODE_DUMP);
706        boolean abort = ddlMode.contains(RepositoryDescriptor.DDL_MODE_ABORT);
707        if (dump || abort) {
708            /*
709             * Dump DDL if not empty.
710             */
711            if (!ddl.isEmpty()) {
712                File dumpFile = new File(Environment.getDefault().getLog(), "ddl-vcs-" + getName() + ".sql");
713                try (OutputStream out = new FileOutputStream(dumpFile); PrintStream ps = new PrintStream(out)) {
714                    for (String sql : dialect.getDumpStart()) {
715                        ps.println(sql);
716                    }
717                    for (String sql : ddl) {
718                        sql = sql.trim();
719                        if (sql.endsWith(";")) {
720                            sql = sql.substring(0, sql.length() - 1);
721                        }
722                        ps.println(dialect.getSQLForDump(sql));
723                    }
724                    for (String sql : dialect.getDumpStop()) {
725                        ps.println(sql);
726                    }
727                } catch (IOException e) {
728                    throw new NuxeoException(e);
729                }
730                /*
731                 * Abort if requested.
732                 */
733                if (abort) {
734                    log.error("Dumped DDL to: " + dumpFile);
735                    throw new NuxeoException(
736                            "Database initialization failed for: " + getName() + ", DDL must be executed: " + dumpFile);
737                }
738            }
739        }
740        if (!ignore) {
741            /*
742             * Execute DDL.
743             */
744            try (Statement st = connection.createStatement()) {
745                for (String sql : ddl) {
746                    logger.log(sql.replace("\n", "\n    ")); // indented
747                    try {
748                        st.execute(sql);
749                    } catch (SQLException e) {
750                        throw new SQLException("Error executing: " + sql + " : " + e.getMessage(), e);
751                    }
752                }
753            }
754            /*
755             * Execute post-DDL stuff.
756             */
757            try (Statement st = connection.createStatement()) {
758                for (String sql : dialect.getStartupSqls(model, sqlInfo.database)) {
759                    logger.log(sql.replace("\n", "\n    ")); // indented
760                    try {
761                        st.execute(sql);
762                    } catch (SQLException e) {
763                        throw new SQLException("Error executing: " + sql + " : " + e.getMessage(), e);
764                    }
765                }
766            }
767        }
768    }
769
770    protected TableUpgrader createTableUpgrader(Connection connection, JDBCLogger logger) {
771        TableUpgrader tableUpgrader = new TableUpgrader(sqlInfo, connection, logger);
772        tableUpgrader.add(Model.VERSION_TABLE_NAME, Model.VERSION_IS_LATEST_KEY, "upgradeVersions",
773                TEST_UPGRADE_VERSIONS);
774        tableUpgrader.add("dublincore", "lastContributor", "upgradeLastContributor", TEST_UPGRADE_LAST_CONTRIBUTOR);
775        tableUpgrader.add(Model.LOCK_TABLE_NAME, Model.LOCK_OWNER_KEY, "upgradeLocks", TEST_UPGRADE_LOCKS);
776        tableUpgrader.add(Model.HIER_TABLE_NAME, Model.MAIN_SYS_CHANGE_TOKEN_KEY, "upgradeSysChangeToken",
777                TEST_UPGRADE_SYS_CHANGE_TOKEN);
778        return tableUpgrader;
779    }
780
781    /** Finds uppercase table names. */
782    protected static Set<String> findTableNames(DatabaseMetaData metadata, String schemaName) throws SQLException {
783        Set<String> tableNames = new HashSet<>();
784        ResultSet rs = metadata.getTables(null, schemaName, "%", new String[] { "TABLE" });
785        while (rs.next()) {
786            String tableName = rs.getString("TABLE_NAME");
787            tableNames.add(tableName.toUpperCase());
788        }
789        rs.close();
790        return tableNames;
791    }
792
793    // completely stops the current transaction while running something
794    protected static void runWithoutTransaction(Runnable runnable) {
795        boolean rollback = TransactionHelper.isTransactionMarkedRollback();
796        boolean hasTransaction = TransactionHelper.isTransactionActiveOrMarkedRollback();
797        if (hasTransaction) {
798            TransactionHelper.commitOrRollbackTransaction();
799        }
800        boolean completedAbruptly = true;
801        try {
802            runnable.run();
803            completedAbruptly = false;
804        } finally {
805            if (hasTransaction) {
806                try {
807                    TransactionHelper.startTransaction();
808                } finally {
809                    if (completedAbruptly || rollback) {
810                        TransactionHelper.setTransactionRollbackOnly();
811                    }
812                }
813            }
814        }
815    }
816
817}