001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.fcrepo.config;
020
021import java.time.Instant;
022import java.time.LocalDateTime;
023import java.time.ZoneOffset;
024import java.util.Map;
025
026import javax.annotation.PostConstruct;
027import javax.sql.DataSource;
028
029import org.flywaydb.core.Flyway;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.springframework.beans.factory.annotation.Value;
033import org.springframework.context.annotation.Bean;
034import org.springframework.context.annotation.Configuration;
035import org.springframework.core.convert.converter.Converter;
036import org.springframework.core.convert.converter.ConverterRegistry;
037import org.springframework.core.convert.support.DefaultConversionService;
038import org.springframework.jdbc.datasource.DataSourceTransactionManager;
039import org.springframework.transaction.PlatformTransactionManager;
040import org.springframework.transaction.TransactionDefinition;
041import org.springframework.transaction.annotation.EnableTransactionManagement;
042import org.springframework.transaction.support.DefaultTransactionDefinition;
043import org.springframework.transaction.support.TransactionTemplate;
044
045import com.zaxxer.hikari.HikariDataSource;
046
047/**
048 * @author pwinckles
049 */
050@EnableTransactionManagement
051@Configuration
052public class DatabaseConfig extends BasePropsConfig {
053
054    private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseConfig.class);
055
056    private static final String H2_FILE = "fcrepo-h2";
057
058    @Value("${fcrepo.db.url:#{'jdbc:h2:'" +
059            " + fedoraPropsConfig.fedoraData.resolve('" + H2_FILE + "').toAbsolutePath().toString()" +
060            " + ';FILE_LOCK=SOCKET'}}")
061    private String dbUrl;
062
063    @Value("${fcrepo.db.user:}")
064    private String dbUser;
065
066    @Value("${fcrepo.db.password:}")
067    private String dbPassword;
068
069    @Value("${fcrepo.db.max.pool.size:10}")
070    private Integer maxPoolSize;
071
072    @Value("${fcrepo.db.connection.checkout.timeout:30000}")
073    private Integer checkoutTimeout;
074
075    private static final Map<String, String> DB_DRIVER_MAP = Map.of(
076            "h2", "org.h2.Driver",
077            "postgresql", "org.postgresql.Driver",
078            "mariadb", "org.mariadb.jdbc.Driver",
079            "mysql", "com.mysql.cj.jdbc.Driver"
080    );
081
082    @PostConstruct
083    public void setup() {
084        ((ConverterRegistry) DefaultConversionService.getSharedInstance())
085                // Adds a converter for mapping local datetimes to instants. This is dubious and not supported
086                // by default because you must make an assumption about the timezone
087                .addConverter(new Converter<LocalDateTime, Instant>() {
088                    @Override
089                    public Instant convert(final LocalDateTime source) {
090                        return source.toInstant(ZoneOffset.UTC);
091                    }
092                });
093    }
094
095    @Bean
096    public DataSource dataSource() throws Exception {
097        final var driver = identifyDbDriver();
098
099        LOGGER.info("JDBC URL: {}", dbUrl);
100        LOGGER.info("JDBC User: {}", dbUser);
101        LOGGER.info("JDBC Password length: {}", dbPassword == null ? 0 : dbPassword.length());
102        LOGGER.info("Using database driver: {}", driver);
103
104        final var dataSource = new HikariDataSource();
105        dataSource.setDriverClassName(driver);
106        dataSource.setJdbcUrl(dbUrl);
107        dataSource.setUsername(dbUser);
108        dataSource.setPassword(dbPassword);
109        dataSource.setConnectionTimeout(checkoutTimeout);
110        dataSource.setMaximumPoolSize(maxPoolSize);
111
112        flyway(dataSource);
113
114        return dataSource;
115    }
116
117    /**
118     * Get the database type in use
119     * @return database type from the connect url.
120     */
121    private String getDbType() {
122        final var parts = dbUrl.split(":");
123
124        if (parts.length < 2) {
125            throw new IllegalArgumentException("Invalid DB url: " + dbUrl);
126        }
127        return parts[1].toLowerCase();
128    }
129
130    private String identifyDbDriver() {
131        final var driver = DB_DRIVER_MAP.get(getDbType());
132
133        if (driver == null) {
134            throw new IllegalStateException("No database driver found for: " + dbUrl);
135        }
136
137        return driver;
138    }
139
140    @Bean
141    public DataSourceTransactionManager txManager(final DataSource dataSource) {
142        final var txManager = new DataSourceTransactionManager();
143        txManager.setDataSource(dataSource);
144        return txManager;
145    }
146
147    @Bean
148    public TransactionTemplate txTemplate(final PlatformTransactionManager txManager) {
149        final var txDefinition = new DefaultTransactionDefinition();
150        txDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
151        return new TransactionTemplate(txManager, txDefinition);
152    }
153
154    @Bean
155    public Flyway flyway(final DataSource source) throws Exception {
156        LOGGER.debug("Instantiating a new flyway bean");
157        return FlywayFactory.create().setDataSource(source).setDatabaseType(getDbType()).getObject();
158    }
159
160}