Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/921 read hibernate config from application properties #968

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading