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.annotation.EnableTransactionManagement;
040
041import com.mchange.v2.c3p0.ComboPooledDataSource;
042
043/**
044 * @author pwinckles
045 */
046@EnableTransactionManagement
047@Configuration
048public class DatabaseConfig extends BasePropsConfig {
049
050    private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseConfig.class);
051
052    private static final String H2_FILE = "fcrepo-h2";
053
054    @Value("${fcrepo.db.url:#{'jdbc:h2:'" +
055            " + fedoraPropsConfig.fedoraData.resolve('" + H2_FILE + "').toAbsolutePath().toString()" +
056            " + ';FILE_LOCK=SOCKET'}}")
057    private String dbUrl;
058
059    @Value("${fcrepo.db.user:}")
060    private String dbUser;
061
062    @Value("${fcrepo.db.password:}")
063    private String dbPassword;
064
065    @Value("${fcrepo.db.max.pool.size:15}")
066    private Integer maxPoolSize;
067
068    @Value("${fcrepo.db.connection.checkout.timeout:10000}")
069    private Integer checkoutTimeout;
070
071    @Value("${fcrepo.db.connection.idle.test.period:300}")
072    private Integer idleConnectionTestPeriod;
073
074    @Value("${fcrepo.db.connection.test.on.checkout:true}")
075    private boolean testConnectionOnCheckout;
076
077    private static final Map<String, String> DB_DRIVER_MAP = Map.of(
078            "h2", "org.h2.Driver",
079            "postgresql", "org.postgresql.Driver",
080            "mariadb", "org.mariadb.jdbc.Driver",
081            "mysql", "com.mysql.cj.jdbc.Driver"
082    );
083
084    @PostConstruct
085    public void setup() {
086        ((ConverterRegistry) DefaultConversionService.getSharedInstance())
087                // Adds a converter for mapping local datetimes to instants. This is dubious and not supported
088                // by default because you must make an assumption about the timezone
089                .addConverter(new Converter<LocalDateTime, Instant>() {
090                    @Override
091                    public Instant convert(final LocalDateTime source) {
092                        return source.toInstant(ZoneOffset.UTC);
093                    }
094                });
095    }
096
097    @Bean
098    public DataSource dataSource() throws Exception {
099        final var driver = identifyDbDriver();
100
101        LOGGER.debug("JDBC URL: {}", dbUrl);
102        LOGGER.debug("Using database driver: {}", driver);
103
104        final var dataSource = new ComboPooledDataSource();
105        dataSource.setDriverClass(driver);
106        dataSource.setJdbcUrl(dbUrl);
107        dataSource.setUser(dbUser);
108        dataSource.setPassword(dbPassword);
109        dataSource.setCheckoutTimeout(checkoutTimeout);
110        dataSource.setMaxPoolSize(maxPoolSize);
111        dataSource.setIdleConnectionTestPeriod(idleConnectionTestPeriod);
112        dataSource.setTestConnectionOnCheckout(testConnectionOnCheckout);
113
114        flyway(dataSource);
115
116        return dataSource;
117    }
118
119    /**
120     * Get the database type in use
121     * @return database type from the connect url.
122     */
123    private String getDbType() {
124        final var parts = dbUrl.split(":");
125
126        if (parts.length < 2) {
127            throw new IllegalArgumentException("Invalid DB url: " + dbUrl);
128        }
129        return parts[1].toLowerCase();
130    }
131
132    private String identifyDbDriver() {
133        final var driver = DB_DRIVER_MAP.get(getDbType());
134
135        if (driver == null) {
136            throw new IllegalStateException("No database driver found for: " + dbUrl);
137        }
138
139        return driver;
140    }
141
142    @Bean
143    public DataSourceTransactionManager txManager(final DataSource dataSource) {
144        final var txManager = new DataSourceTransactionManager();
145        txManager.setDataSource(dataSource);
146        return txManager;
147    }
148
149    @Bean
150    public Flyway flyway(final DataSource source) throws Exception {
151        LOGGER.debug("Instantiating a new flyway bean");
152        return FlywayFactory.create().setDataSource(source).setDatabaseType(getDbType()).getObject();
153    }
154
155}