package ru.nbch.indicsearchservice.util; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; import javax.sql.DataSource; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.event.EventListener; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.stereotype.Component; import ru.nbch.indicsearchservice.config.settings.ApplicationSettings; import ru.nbch.indicsearchservice.config.settings.DataSourceSettings; import ru.nbch.indicsearchservice.event.DataSourceChangedEvent; import ru.nbch.indicsearchservice.exception.InternalException; import ru.nbch.indicsearchservice.repository.MyBatisRepository; import ru.nbch.indicsearchservice.repository.indic.IndicMapper; import ru.nbch.indicsearchservice.service.SettingsService; import ru.nbch.indicsearchservice.util.typehandler.NullableLocalDateTimeTypeHandler; import ru.nbch.indicsearchservice.util.typehandler.NullableLocalDateTypeHandler; @Component public class DataSourceUpdater { private static final Logger log = LoggerFactory.getLogger(DataSourceUpdater.class); private final ApplicationSettings settings; private final Map dataSourceMap = new ConcurrentHashMap<>(); public DataSourceUpdater(ApplicationSettings settings, SettingsService settingsService) { this.settings = settings; buildSettings(DataSourceType.INDIC, settingsService, settings::setIndicDataSource); dataSourceMap.put( DataSourceType.INDIC, new DataSourceData(new AtomicReference<>(null), new AtomicReference<>(null), new AtomicReference<>(null)) ); } @EventListener public void onDatabaseChanged(DataSourceChangedEvent event) { log.info("Connection to datasource {} invalidated", event.dataSourceType()); if (!dataSourceMap.containsKey(event.dataSourceType())) { log.error("Unknown datasource type {}", event.dataSourceType()); return; } DataSource oldDataSource = dataSourceMap.get(event.dataSourceType()).dataSourceRef().getAndSet(null); if (oldDataSource instanceof HikariDataSource ds) { ds.close(); } dataSourceMap.get(event.dataSourceType()).sqlSessionFactoryRef().set(null); dataSourceMap.get(event.dataSourceType()).mapperRef().set(null); } public DataSource getDataSource(DataSourceType dataSourceType) { if (!dataSourceMap.containsKey(dataSourceType)) { log.error("Unknown datasource type {}", dataSourceType); return null; } return getComponent(dataSourceMap.get(dataSourceType).dataSourceRef(), () -> buildDataSource(dataSourceType), "DataSource"); } public SqlSessionFactory getSqlSessionFactory(DataSourceType dataSourceType) { if (!dataSourceMap.containsKey(dataSourceType)) { log.error("Unknown datasource type {}", dataSourceType); return null; } return getComponent(dataSourceMap.get(dataSourceType).sqlSessionFactoryRef(), () -> buildSqlSessionFactory(dataSourceType), "SqlSessionFactory"); } public MyBatisRepository getMapper(DataSourceType dataSourceType) { if (!dataSourceMap.containsKey(dataSourceType)) { log.error("Unknown datasource type {}", dataSourceType); return null; } return getComponent(dataSourceMap.get(dataSourceType).mapperRef(), () -> buildMapper(dataSourceType), "Mapper"); } private T getComponent(AtomicReference ref, Supplier builder, String componentName) { T component = ref.get(); if (component == null) { synchronized (ref) { component = ref.get(); if (component == null) { try { component = builder.get(); ref.set(component); log.info("{} created successfully", componentName); } catch (Exception e) { log.error("Failed to build {}", componentName, e); } } } } return component; } private DataSource buildDataSource(DataSourceType dataSourceType) { DataSourceSettings dataSourceSettings = switch (dataSourceType) { case INDIC -> settings.getIndicDataSource(); }; if (dataSourceSettings == null) { log.error("{} configuration is null", dataSourceType); return null; } HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setDriverClassName(dataSourceSettings.getDriver()); hikariConfig.setJdbcUrl(dataSourceSettings.getUrl()); hikariConfig.setUsername(dataSourceSettings.getUsername()); hikariConfig.setPassword(dataSourceSettings.getPassword()); hikariConfig.setConnectionTimeout(dataSourceSettings.getConnectionTimeout()); hikariConfig.setValidationTimeout(dataSourceSettings.getValidationTimeout()); hikariConfig.setIdleTimeout(dataSourceSettings.getIdleTimeout()); hikariConfig.setMaximumPoolSize(dataSourceSettings.getMaxPoolSize()); hikariConfig.setMaxLifetime(dataSourceSettings.getMaxLifetime()); hikariConfig.setKeepaliveTime(dataSourceSettings.getKeepAliveTime()); hikariConfig.setPoolName(dataSourceType + "-pool"); if (Objects.equals(dataSourceSettings.getDriver(), "net.sourceforge.jtds.jdbc.Driver")) { hikariConfig.setConnectionTestQuery("select 1"); } return new HikariDataSource(hikariConfig); } private SqlSessionFactory buildSqlSessionFactory(DataSourceType dataSourceType) { DataSource dataSource = getDataSource(dataSourceType); try { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); Configuration configuration = new Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setCallSettersOnNulls(true); configuration.getTypeHandlerRegistry() .register(LocalDate.class, JdbcType.DATE, new NullableLocalDateTypeHandler()); configuration.getTypeHandlerRegistry() .register(LocalDateTime.class, JdbcType.TIMESTAMP, new NullableLocalDateTimeTypeHandler()); sqlSessionFactoryBean.setConfiguration(configuration); if (dataSourceType == DataSourceType.INDIC) { sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mappers/indic/*.xml") ); } else { Class mapperClass = getMapperClass(dataSourceType); configuration.addMapper(mapperClass); } SqlSessionFactory sessionFactory = sqlSessionFactoryBean.getObject(); if (sessionFactory == null) { throw new InternalException("Failed to create SqlSessionFactory"); } return sessionFactory; } catch (Exception e) { log.error("Failed to build SqlSessionFactory", e); throw new InternalException("Failed to build SqlSessionFactory " + e); } } private T buildMapper(DataSourceType dataSourceType) { Class mapperClass = getMapperClass(dataSourceType); SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(dataSourceType); SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory); return (T) sqlSessionTemplate.getMapper(mapperClass); } private Class getMapperClass(DataSourceType dataSourceType) { return switch (dataSourceType) { case INDIC -> IndicMapper.class; }; } private void buildSettings(DataSourceType dataSourceType, SettingsService settingsService, Consumer settingsConsumer) { String key = switch (dataSourceType) { case INDIC -> "indic"; }; Map> fetchedSettings = settingsService.getSettingsValuesByNames(List.of( "datasource." + key + ".driver", "datasource." + key + ".url", "datasource." + key + ".username", "datasource." + key + ".password", "datasource." + key + ".connection-timeout", "datasource." + key + ".validation-timeout", "datasource." + key + ".idle-timeout", "datasource." + key + ".max-pool-size", "datasource." + key + ".max-lifetime", "datasource." + key + ".keep-alive-time" )); DataSourceSettings dataSourceSettings = new DataSourceSettings(); dataSourceSettings.setDriver(fetchedSettings.get("datasource." + key + ".driver").orElseThrow(() -> new InternalException("Key datasource." + key + ".driver" + " was not found in SETTINGS!"))); dataSourceSettings.setUrl(fetchedSettings.get("datasource." + key + ".url").orElseThrow(() -> new InternalException("Key datasource." + key + ".url" + " was not found in SETTINGS!"))); dataSourceSettings.setUsername(fetchedSettings.get("datasource." + key + ".username").orElseThrow(() -> new InternalException("Key datasource." + key + ".username" + " was not found in SETTINGS!"))); dataSourceSettings.setPassword(fetchedSettings.get("datasource." + key + ".password").orElseThrow(() -> new InternalException("Key datasource." + key + ".password" + " was not found in SETTINGS!"))); dataSourceSettings.setConnectionTimeout(Long.valueOf(fetchedSettings.get("datasource." + key + ".connection-timeout").orElse("30000"))); dataSourceSettings.setValidationTimeout(Long.valueOf(fetchedSettings.get("datasource." + key + ".validation-timeout").orElse("5000"))); dataSourceSettings.setIdleTimeout(Long.valueOf(fetchedSettings.get("datasource." + key + ".idle-timeout").orElse("600000"))); dataSourceSettings.setMaxPoolSize(Integer.valueOf(fetchedSettings.get("datasource." + key + ".max-pool-size").orElse("100"))); dataSourceSettings.setMaxLifetime(Long.valueOf(fetchedSettings.get("datasource." + key + ".max-lifetime").orElse("1800000"))); dataSourceSettings.setKeepAliveTime(Long.valueOf(fetchedSettings.get("datasource." + key + ".keep-alive-time").orElse("120000"))); settingsConsumer.accept(dataSourceSettings); } private record DataSourceData(AtomicReference dataSourceRef, AtomicReference sqlSessionFactoryRef, AtomicReference mapperRef) { } public enum DataSourceType { INDIC } }