Skip to content

Commit

Permalink
Merge pull request #968 from puzzle/feature/921_read_hibernate_config…
Browse files Browse the repository at this point in the history
…_from_application_properties

Feature/921 read hibernate config from application properties
  • Loading branch information
janikEndtner authored Jul 10, 2024
2 parents ba9bfcf + 6a507ce commit c3ece77
Show file tree
Hide file tree
Showing 17 changed files with 155 additions and 136 deletions.
9 changes: 6 additions & 3 deletions backend/src/main/java/ch/puzzle/okr/OkrApplication.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package ch.puzzle.okr;

import ch.puzzle.okr.service.clientconfig.ClientCustomizationProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(ClientCustomizationProperties.class)
public class OkrApplication {
public static void main(String[] args) {
SpringApplication.run(OkrApplication.class, args);

new SpringApplicationBuilder(OkrApplication.class) //
.initializers(new OkrApplicationContextInitializer()) //
.run(args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ch.puzzle.okr;

import ch.puzzle.okr.multitenancy.listener.HibernateContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OkrApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

private static final Logger logger = LoggerFactory.getLogger(OkrApplicationContextInitializer.class);

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
logger.info("Loading hibernate configuration from application properties");
HibernateContext.cacheHibernateProperties(applicationContext.getEnvironment());
}

}
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
package ch.puzzle.okr.multitenancy;

import ch.puzzle.okr.exception.ConnectionProviderException;
import ch.puzzle.okr.multitenancy.listener.HibernateContext;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl;
import org.hibernate.engine.jdbc.connections.spi.AbstractMultiTenantConnectionProvider;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.*;

import static ch.puzzle.okr.multitenancy.TenantContext.DEFAULT_TENANT_ID;

public abstract class AbstractSchemaMultiTenantConnectionProvider
extends AbstractMultiTenantConnectionProvider<String> {
private static final Logger logger = LoggerFactory.getLogger(AbstractSchemaMultiTenantConnectionProvider.class);
public class SchemaMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider<String> {

private static final Logger logger = LoggerFactory.getLogger(SchemaMultiTenantConnectionProvider.class);

final Map<String, ConnectionProvider> connectionProviderMap;

public AbstractSchemaMultiTenantConnectionProvider() {
public SchemaMultiTenantConnectionProvider() {
this.connectionProviderMap = new HashMap<>();
}

Expand Down Expand Up @@ -80,21 +80,13 @@ private ConnectionProvider createConnectionProvider(String tenantIdentifier) {
}

protected Properties getHibernatePropertiesForTenantIdentifier(String tenantIdentifier) {
try {
Properties properties = getPropertiesFromFilePaths();
if (!Objects.equals(tenantIdentifier, DEFAULT_TENANT_ID)) {
properties.put(AvailableSettings.DEFAULT_SCHEMA, MessageFormat.format("okr_{0}", tenantIdentifier));
}
return properties;
} catch (IOException e) {
throw new RuntimeException(
String.format("Cannot open hibernate properties: %s)", this.getHibernatePropertiesFilePaths()));
Properties properties = getHibernateProperties();
if (properties == null || properties.isEmpty()) {
throw new RuntimeException("Cannot load hibernate properties from application.properties)");
}
if (!Objects.equals(tenantIdentifier, DEFAULT_TENANT_ID)) {
properties.put(AvailableSettings.DEFAULT_SCHEMA, MessageFormat.format("okr_{0}", tenantIdentifier));
}
}

protected Properties getPropertiesFromFilePaths() throws IOException {
Properties properties = new Properties();
properties.load(getClass().getResourceAsStream(this.getHibernatePropertiesFilePaths()));
return properties;
}

Expand All @@ -118,5 +110,7 @@ private Map<String, Object> convertPropertiesToMap(Properties properties) {
return configProperties;
}

protected abstract String getHibernatePropertiesFilePaths();
protected Properties getHibernateProperties() {
return HibernateContext.getHibernateConfig();
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ch.puzzle.okr.multitenancy.listener;

import org.springframework.core.env.ConfigurableEnvironment;

import java.util.Properties;

public class HibernateContext {
public static final String HIBERNATE_CONNECTION_URL = "hibernate.connection.url";
public static final String HIBERNATE_CONNECTION_USERNAME = "hibernate.connection.username";
public static final String HIBERNATE_CONNECTION_PASSWORD = "hibernate.connection.password";
public static final String HIBERNATE_MULTITENANCY = "hibernate.multiTenancy";

public static String SPRING_DATASOURCE_URL = "spring.datasource.url";
public static String SPRING_DATASOURCE_USERNAME = "spring.datasource.username";
public static String SPRING_DATASOURCE_PASSWORD = "spring.datasource.password";

public record DbConfig(String url, String username, String password, String multiTenancy) {
}

private static DbConfig cachedHibernateConfig;

public static void setHibernateConfig(DbConfig dbConfig) {
cachedHibernateConfig = dbConfig;
}

public static Properties getHibernateConfig() {
return getConfigAsProperties(cachedHibernateConfig);
}

public static Properties getConfigAsProperties(DbConfig dbConfig) {
Properties properties = new Properties();
properties.put(HibernateContext.HIBERNATE_CONNECTION_URL, dbConfig.url());
properties.put(HibernateContext.HIBERNATE_CONNECTION_USERNAME, dbConfig.username());
properties.put(HibernateContext.HIBERNATE_CONNECTION_PASSWORD, dbConfig.password());
properties.put(HibernateContext.HIBERNATE_MULTITENANCY, dbConfig.multiTenancy());
properties.put(HibernateContext.SPRING_DATASOURCE_URL, dbConfig.url());
properties.put(HibernateContext.SPRING_DATASOURCE_USERNAME, dbConfig.username());
properties.put(HibernateContext.SPRING_DATASOURCE_PASSWORD, dbConfig.password());
return properties;
}

public static void cacheHibernateProperties(ConfigurableEnvironment environment) {
String url = environment.getProperty(HibernateContext.HIBERNATE_CONNECTION_URL);
String username = environment.getProperty(HibernateContext.HIBERNATE_CONNECTION_USERNAME);
String password = environment.getProperty(HibernateContext.HIBERNATE_CONNECTION_PASSWORD);
String multiTenancy = environment.getProperty(HibernateContext.HIBERNATE_MULTITENANCY);

HibernateContext.DbConfig h2DbConfig = new HibernateContext.DbConfig(url, username, password, multiTenancy);
HibernateContext.setHibernateConfig(h2DbConfig);
}

}
6 changes: 6 additions & 0 deletions backend/src/main/resources/application-dev.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ spring.flyway.locations=classpath:db/migration,classpath:db/data-migration,class
okr.tenant-ids=pitc,acme
okr.datasource.driver-class-name=org.postgresql.Driver

# hibernate
hibernate.connection.url=jdbc:postgresql://localhost:5432/okr
hibernate.connection.username=user
hibernate.connection.password=pwd
hibernate.multiTenancy=SCHEMA

# pitc
okr.tenants.pitc.datasource.url=jdbc:postgresql://localhost:5432/okr
okr.tenants.pitc.datasource.username=pitc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ spring.flyway.locations=classpath:db/h2-db/database-h2-schema,classpath:db/h2-db
okr.tenant-ids=pitc,acme
okr.datasource.driver-class-name=org.h2.Driver

# hibernate
hibernate.connection.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;
hibernate.connection.username=user
hibernate.connection.password=sa
hibernate.multiTenancy=SCHEMA

# pitc
okr.tenants.pitc.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS okr_pitc
okr.tenants.pitc.datasource.username=user
Expand All @@ -35,5 +41,3 @@ okr.tenants.acme.user.champion.emails=peggimann@puzzle.ch,wunderland@puzzle.ch
okr.tenants.acme.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8544/realms/pitc/protocol/openid-connect/certs
okr.tenants.acme.security.oauth2.frontend.issuer-url=http://localhost:8544/realms/pitc
okr.tenants.acme.security.oauth2.frontend.client-id=acme_okr_staging

spring.jpa.properties.hibernate.multi_tenant_connection_provider=ch.puzzle.okr.multitenancy.SchemaMultiTenantConnectionProviderH2
2 changes: 1 addition & 1 deletion backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ okr.jwt.claim.email=email

spring.jpa.properties.hibernate.multiTenancy=SCHEMA
spring.jpa.properties.hibernate.tenant_identifier_resolver=ch.puzzle.okr.multitenancy.CurrentTenantIdentifierResolverImpl
spring.jpa.properties.hibernate.multi_tenant_connection_provider=ch.puzzle.okr.multitenancy.SchemaMultiTenantConnectionProviderPGSQL
spring.jpa.properties.hibernate.multi_tenant_connection_provider=ch.puzzle.okr.multitenancy.SchemaMultiTenantConnectionProvider

okr.clientcustomization.favicon=assets/favicon.png
okr.clientcustomization.logo=assets/images/okr-logo.svg
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package ch.puzzle.okr.multitenancy;

import ch.puzzle.okr.exception.ConnectionProviderException;
import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.Properties;

import static org.mockito.Mockito.mock;
Expand All @@ -16,25 +14,11 @@ public class SchemaMultiTenantConnectionProviderInternalsTest {

private static final String TENANT_ID = "pitc";

private static class BasicConnectionProviderMock extends AbstractSchemaMultiTenantConnectionProvider {
@Override
protected String getHibernatePropertiesFilePaths() {
return null;
}
private static class ConfigurableConnectionProviderMock extends SchemaMultiTenantConnectionProvider {

@Override
protected DriverManagerConnectionProviderImpl getDriverManagerConnectionProviderImpl() {
return mock(DriverManagerConnectionProviderImpl.class);
}
}

private static class ConfigurableConnectionProviderMock extends BasicConnectionProviderMock {

@Override
protected Properties getPropertiesFromFilePaths() {
Properties properties = new Properties();
properties.put("hibernate.connection.url", "no_value");
return properties;
protected Properties getHibernateProperties() {
return new Properties();
}

public void registerProvider(String tenantIdentifier, ConnectionProvider connectionProvider) {
Expand All @@ -56,19 +40,6 @@ void getConnectionProviderReturnConnectionProviderIfTenantIdIsRegistered() {
Assertions.assertNotNull(foundConnectionProvider);
}

@DisplayName("getConnectionProvider() creates ConnectionProvider if TenantId is not registered")
@Test
void getConnectionProviderCreatesConnectionProviderIfTenantIdIsNotRegistered() {
// arrange
ConfigurableConnectionProviderMock mockProvider = new ConfigurableConnectionProviderMock();

// act
ConnectionProvider notFoundConnectionProvider = mockProvider.getConnectionProvider(TENANT_ID);

// assert
Assertions.assertNotNull(notFoundConnectionProvider);
}

@DisplayName("getConnectionProvider() throws Exception when lookup TenantId is null")
@Test
void getConnectionProviderThrowsExceptionWhenLookupTenantIdIsNull() {
Expand Down Expand Up @@ -107,17 +78,11 @@ void getAnyConnectionProviderReturnConnectionProviderForTenantIdPublic() {
Assertions.assertNotNull(foundConnectionProvider);
}

@DisplayName("throws a RuntimeException when getPropertiesFromFilePaths() throws an IOException")
@DisplayName("getConnectionProviderShouldThrowRuntimeExceptionWhenNoPropertiesAreFound")
@Test
void throwsRuntimeExceptionWhenGetPropertiesFromFilePathsThrowsIOException() {
BasicConnectionProviderMock mockProviderWhichThrowsIOException = new BasicConnectionProviderMock() {
@Override
protected Properties getPropertiesFromFilePaths() throws IOException {
throw new IOException("no properties found");
}
};

Assertions.assertThrows(RuntimeException.class,
() -> mockProviderWhichThrowsIOException.getConnectionProvider(TENANT_ID));
void getConnectionProviderShouldThrowRuntimeExceptionWhenNoPropertiesAreFound() {
ConfigurableConnectionProviderMock mockProvider = new ConfigurableConnectionProviderMock();

Assertions.assertThrows(RuntimeException.class, () -> mockProvider.getConnectionProvider(TENANT_ID));
}
}

This file was deleted.

Loading

0 comments on commit c3ece77

Please sign in to comment.