Skip to content

Commit

Permalink
feat: Add basic tenant details APIs
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Vitale <[email protected]>
  • Loading branch information
ThomasVitale committed Mar 24, 2024
1 parent e6f4682 commit fdf878e
Show file tree
Hide file tree
Showing 15 changed files with 426 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.arconia.core.multitenancy.tenantdetails;

import java.util.HashMap;
import java.util.Map;

import org.springframework.util.Assert;

/**
* Default implementation to hold tenant details.
*/
public class Tenant implements TenantDetails {

private final String identifier;

private final boolean enabled;

private final Map<String, Object> attributes;

public Tenant(String identifier, boolean enabled, Map<String, Object> attributes) {
Assert.hasText(identifier, "identifier cannot be null or empty");
Assert.notNull(attributes, "attributes cannot be null");

this.identifier = identifier;
this.enabled = enabled;
this.attributes = attributes;
}

@Override
public String getIdentifier() {
return identifier;
}

@Override
public boolean isEnabled() {
return enabled;
}

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

public static Builder create() {
return new Builder();
}

public static class Builder {

private String identifier;

private boolean enabled;

private Map<String, Object> attributes = new HashMap<>();

public Builder identifier(String identifier) {
this.identifier = identifier;
return this;
}

public Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}

public Builder attributes(Map<String, Object> attributes) {
this.attributes = attributes;
return this;
}

public Builder addAttribute(String key, Object value) {
attributes.put(key, value);
return this;
}

public Tenant build() {
return new Tenant(identifier, enabled, attributes);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.arconia.core.multitenancy.tenantdetails;

import java.io.Serializable;
import java.util.Map;

/**
* Provides core tenant information.
*/
public interface TenantDetails extends Serializable {

/**
* Identifier for the tenant.
*/
String getIdentifier();

/**
* Whether the tenant is enabled.
*/
boolean isEnabled();

/**
* Additional information about the tenant.
*/
default Map<String, Object> getAttributes() {
return Map.of();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.arconia.core.multitenancy.tenantdetails;

import java.util.List;

import org.springframework.lang.Nullable;

/**
* Loads tenant-specific data. It is used throughout the framework as a tenant DAO.
*/
public interface TenantDetailsService {

List<? extends TenantDetails> loadAllTenants();

@Nullable
TenantDetails loadTenantByIdentifier(String identifier);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@NonNullApi
@NonNullFields
package io.arconia.core.multitenancy.tenantdetails;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.arconia.core.multitenancy.tenantdetails;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Unit tests for {@link Tenant}.
*/
class TenantTests {

@Test
void whenIdentifierIsNullThenThrow() {
assertThatThrownBy(() -> Tenant.create().build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("identifier cannot be null or empty");
}

@Test
void whenIdentifierIsEmptyThenThrow() {
assertThatThrownBy(() -> Tenant.create().identifier("").build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("identifier cannot be null or empty");
}

@Test
void whenAttributesIsNullThenThrow() {
assertThatThrownBy(() -> Tenant.create().identifier("acme").attributes(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("attributes cannot be null");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;

import io.arconia.autoconfigure.multitenancy.core.tenantdetails.PropertiesTenantDetailsService;
import io.arconia.autoconfigure.multitenancy.core.tenantdetails.TenantDetailsProperties;
import io.arconia.core.multitenancy.cache.DefaultTenantKeyGenerator;
import io.arconia.core.multitenancy.cache.TenantKeyGenerator;
import io.arconia.core.multitenancy.context.events.HolderTenantContextEventListener;
Expand All @@ -15,12 +17,14 @@
import io.arconia.core.multitenancy.context.resolvers.FixedTenantResolver;
import io.arconia.core.multitenancy.events.DefaultTenantEventPublisher;
import io.arconia.core.multitenancy.events.TenantEventPublisher;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

/**
* Auto-configuration for core multitenancy.
*/
@AutoConfiguration
@EnableConfigurationProperties({ FixedTenantResolutionProperties.class, TenantManagementProperties.class })
@EnableConfigurationProperties({ FixedTenantResolutionProperties.class, TenantDetailsProperties.class,
TenantManagementProperties.class })
public class MultitenancyCoreAutoConfiguration {

@Bean
Expand Down Expand Up @@ -67,4 +71,12 @@ TenantEventPublisher tenantEventPublisher(ApplicationEventPublisher applicationE
return new DefaultTenantEventPublisher(applicationEventPublisher);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = TenantDetailsProperties.CONFIG_PREFIX, name = "source", havingValue = "properties",
matchIfMissing = true)
TenantDetailsService tenantDetailsService(TenantDetailsProperties tenantDetailsProperties) {
return new PropertiesTenantDetailsService(tenantDetailsProperties);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.arconia.autoconfigure.multitenancy.core.tenantdetails;

import java.util.List;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import io.arconia.core.multitenancy.tenantdetails.TenantDetails;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

/**
* An implementation of {@link TenantDetailsService} that uses application properties as
* the source for the tenant details.
*/
public class PropertiesTenantDetailsService implements TenantDetailsService {

private final TenantDetailsProperties tenantDetailsProperties;

public PropertiesTenantDetailsService(TenantDetailsProperties tenantDetailsProperties) {
this.tenantDetailsProperties = tenantDetailsProperties;
}

@Override
public List<? extends TenantDetails> loadAllTenants() {
return tenantDetailsProperties.getTenants();
}

@Nullable
@Override
public TenantDetails loadTenantByIdentifier(String identifier) {
Assert.hasText(identifier, "identifier cannot be null or empty");
return tenantDetailsProperties.getTenants()
.stream()
.filter(tenant -> tenant.getIdentifier().equals(identifier))
.findFirst()
.orElse(null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.arconia.autoconfigure.multitenancy.core.tenantdetails;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

import io.arconia.core.multitenancy.tenantdetails.Tenant;

/**
* Configuration properties for tenant details.
*/
@ConfigurationProperties(prefix = TenantDetailsProperties.CONFIG_PREFIX)
public class TenantDetailsProperties {

public static final String CONFIG_PREFIX = "arconia.multitenancy.details";

/**
* The source of tenant details.
*/
private Source source = Source.PROPERTIES;

/**
* List of tenant details.
*/
private List<Tenant> tenants = new ArrayList<>();

public Source getSource() {
return source;
}

public void setSource(Source source) {
this.source = source;
}

public List<Tenant> getTenants() {
return tenants;
}

public void setTenants(List<Tenant> tenants) {
this.tenants = tenants;
}

public enum Source {

HTTP, JDBC, PROPERTIES

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@NonNullApi
@NonNullFields
package io.arconia.autoconfigure.multitenancy.core.tenantdetails;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.arconia.autoconfigure.multitenancy.core.FixedTenantResolutionProperties;
import io.arconia.core.multitenancy.context.resolvers.FixedTenantResolver;
import io.arconia.core.multitenancy.events.TenantEventPublisher;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;
import io.arconia.web.multitenancy.context.filters.TenantContextFilter;
import io.arconia.web.multitenancy.context.filters.TenantContextIgnorePathMatcher;
import io.arconia.web.multitenancy.context.resolvers.CookieTenantResolver;
Expand Down Expand Up @@ -58,9 +59,9 @@ static class HttpTenantFilterConfiguration {
@ConditionalOnMissingBean
TenantContextFilter tenantContextFilter(HttpRequestTenantResolver httpRequestTenantResolver,
TenantContextIgnorePathMatcher tenantContextIgnorePathMatcher,
TenantEventPublisher tenantEventPublisher) {
TenantDetailsService tenantDetailsService, TenantEventPublisher tenantEventPublisher) {
return new TenantContextFilter(httpRequestTenantResolver, tenantContextIgnorePathMatcher,
tenantEventPublisher);
tenantDetailsService, tenantEventPublisher);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"properties": [
{
"name": "arconia.multitenancy.details.source",
"defaultValue": "properties"
},
{
"name": "arconia.multitenancy.management.observations.cardinality",
"defaultValue": "high"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.arconia.core.multitenancy.context.events.ObservationTenantContextEventListener;
import io.arconia.core.multitenancy.context.resolvers.FixedTenantResolver;
import io.arconia.core.multitenancy.events.TenantEventPublisher;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -84,4 +85,18 @@ void tenantEventPublisher() {
});
}

@Test
void tenantDetailsServiceWhenDefault() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(TenantDetailsService.class);
});
}

@Test
void tenantDetailsServiceWhenDisabled() {
contextRunner.withPropertyValues("arconia.multitenancy.details.source=http").run(context -> {
assertThat(context).doesNotHaveBean(TenantDetailsService.class);
});
}

}
Loading

0 comments on commit fdf878e

Please sign in to comment.