Skip to content

Commit

Permalink
Merge pull request #311 from focus-shift/310-service-loader
Browse files Browse the repository at this point in the history
Introduce service loader to retrieve different configuration services
  • Loading branch information
derTobsch committed Sep 20, 2023
2 parents 810078a + c89e672 commit dcfe659
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

public interface ManagerParameter {

String CONFIGURATION_DATASOURCE_IMPL_CLASS = "configuration.datasource.impl";
String MANAGER_IMPL_CLASS_PREFIX = "manager.impl";

void mergeProperties(Properties properties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.focus_shift.jollyday.core.ManagerParameter;
import de.focus_shift.jollyday.core.datasource.ConfigurationServiceManager;
import de.focus_shift.jollyday.core.spi.ConfigurationService;
import de.focus_shift.jollyday.core.support.LazyServiceLoaderCache;
import de.focus_shift.jollyday.core.util.ClassLoadingUtil;

/**
Expand All @@ -18,7 +19,8 @@ public class HolidayManagerValueHandler implements Cache.ValueHandler<HolidayMan
* Manager for providing configuration data sources which return the holiday
* data.
*/
private final ConfigurationServiceManager configurationServiceManager = new ConfigurationServiceManager();
private final ConfigurationServiceManager configurationServiceManager =
new ConfigurationServiceManager(new LazyServiceLoaderCache<>(ConfigurationService.class));

/**
* Utility to load classes.
Expand All @@ -38,7 +40,7 @@ public String getKey() {
@Override
public HolidayManager createValue() {
final HolidayManager manager = instantiateManagerImpl(managerImplClassName);
final ConfigurationService configurationService = configurationServiceManager.getConfigurationService(parameter);
final ConfigurationService configurationService = configurationServiceManager.getConfigurationService();
manager.setConfigurationService(configurationService);
manager.init(parameter);
return manager;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
package de.focus_shift.jollyday.core.datasource;

import de.focus_shift.jollyday.core.ManagerParameter;
import de.focus_shift.jollyday.core.spi.ConfigurationService;
import de.focus_shift.jollyday.core.util.ClassLoadingUtil;
import de.focus_shift.jollyday.core.support.LazyServiceLoaderCache;

import java.util.List;

/**
* This manager is responsible for instantiating the configured configuration datasource
* This manager is responsible for instantiating
* the provided implementation of the {@link ConfigurationService}
* which is used to access the holiday data.
*/
public class ConfigurationServiceManager {

private final ClassLoadingUtil classLoadingUtil = new ClassLoadingUtil();
private final LazyServiceLoaderCache<ConfigurationService> configurationServiceCache;

public ConfigurationService getConfigurationService(ManagerParameter parameter) {
validateConfiguration(parameter);
final String dataSourceClassName = parameter.getProperty(ManagerParameter.CONFIGURATION_DATASOURCE_IMPL_CLASS);
return instantiateDataSource(dataSourceClassName);
public ConfigurationServiceManager(LazyServiceLoaderCache<ConfigurationService> configurationServiceCache) {
this.configurationServiceCache = configurationServiceCache;
}

private ConfigurationService instantiateDataSource(String dataSourceClassName) {
try {
final Class<?> dataSourceClass = classLoadingUtil.loadClass(dataSourceClassName);
return (ConfigurationService) dataSourceClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new IllegalStateException("Cannot instantiate datasource instance of " + dataSourceClassName, e);
}
public ConfigurationService getConfigurationService() {
return instantiateDataSource();
}

private void validateConfiguration(ManagerParameter parameter) {
if (parameter.getProperty(ManagerParameter.CONFIGURATION_DATASOURCE_IMPL_CLASS) == null) {
throw new IllegalStateException("Missing holiday configuration datasource implementation class under config key " + ManagerParameter.CONFIGURATION_DATASOURCE_IMPL_CLASS);
private ConfigurationService instantiateDataSource() {

final List<ConfigurationService> services = configurationServiceCache.getServices();

if (services.size() > 1) {
throw new IllegalStateException("Cannot instantiate datasource instance because there are two or more implementations available " + services);
}
if (services.isEmpty()) {
throw new IllegalStateException("Cannot instantiate datasource instance because there is no implementations");
}

return services.get(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.focus_shift.jollyday.core.support;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.concurrent.CopyOnWriteArrayList;

import static java.lang.String.format;

public class LazyServiceLoaderCache<S> {
private static final Logger LOG = LoggerFactory.getLogger(LazyServiceLoaderCache.class.getName());

private final Class<S> clz;
private List<S> services;

public LazyServiceLoaderCache(Class<S> clz) {
this.clz = clz;
}

public List<S> getServices() {
if (services == null) {
loadServices();
}
return services;
}

private synchronized void loadServices() {
services = new CopyOnWriteArrayList<>();
try {
for (S s : ServiceLoader.load(clz)) {
services.add(s);
}
} catch (ServiceConfigurationError serviceConfigurationError) {
final String message = format("Cannot load services of type [%s].%n %s",
clz.getName(),
serviceConfigurationError.getMessage()
);
LOG.warn(message);
}
}
}
3 changes: 2 additions & 1 deletion jollyday-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module de.focus_shift.jollyday.core {

opens de.focus_shift.jollyday.core.datasource;
uses de.focus_shift.jollyday.core.spi.ConfigurationService;

opens de.focus_shift.jollyday.core.util;
opens holidays;
opens descriptions;
Expand Down
3 changes: 0 additions & 3 deletions jollyday-core/src/main/resources/jollyday.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ manager.impl=de.focus_shift.jollyday.core.impl.DefaultHolidayManager
# Holiday manager for Japan implements some specific Japanese holiday rule.
manager.impl.jp=de.focus_shift.jollyday.core.impl.JapaneseHolidayManager

# Implementation class for holiday configurations
configuration.datasource.impl=de.focus_shift.jollyday.jaxb.JaxbConfigurationService

# Configure the parsers to be used for each individual configuration type
parser.impl.de.focus_shift.jollyday.core.spi.Fixed=de.focus_shift.jollyday.core.parser.impl.FixedParser
parser.impl.de.focus_shift.jollyday.core.spi.FixedWeekdayInMonth=de.focus_shift.jollyday.core.parser.impl.FixedWeekdayInMonthParser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class DefaultConfigurationProviderContentTest {
private static final Set<String> KEYS_DEFAULT_CONFIG = Set.of(
"manager.impl",
"manager.impl.jp",
"configuration.datasource.impl",
"parser.impl.de.focus_shift.jollyday.core.spi.Fixed",
"parser.impl.de.focus_shift.jollyday.core.spi.FixedWeekdayInMonth",
"parser.impl.de.focus_shift.jollyday.core.spi.IslamicHoliday",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package de.focus_shift.jollyday.core.datasource;

import de.focus_shift.jollyday.core.ManagerParameter;
import de.focus_shift.jollyday.core.spi.Configuration;
import de.focus_shift.jollyday.core.spi.ConfigurationService;
import de.focus_shift.jollyday.core.spi.Holidays;
import de.focus_shift.jollyday.core.support.LazyServiceLoaderCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class ConfigurationServiceManagerTest {

@Mock
private LazyServiceLoaderCache<ConfigurationService> configurationServiceCache;

private ConfigurationServiceManager sut;

@BeforeEach
void setUp() {
sut = new ConfigurationServiceManager(configurationServiceCache);
}

@Test
void ensuresToThrowExceptionIfNoImplementationIsAvailable() {

when(configurationServiceCache.getServices()).thenReturn(List.of());

final IllegalStateException exception = assertThrows(IllegalStateException.class, () -> sut.getConfigurationService());
assertThat(exception.getMessage()).contains("Cannot instantiate datasource instance because there is no implementations");
}

@Test
void ensuresToProvideConfigurationServiceIfExactlyOneIsAvailable() {
when(configurationServiceCache.getServices()).thenReturn(List.of(new MockConfigurationService()));

final ConfigurationService configurationService = sut.getConfigurationService();
assertThat(configurationService).isInstanceOf(MockConfigurationService.class);
}

@Test
void ensuresToThrowExceptionIfMultipleImplementationsAreAvailable() {

when(configurationServiceCache.getServices()).thenReturn(List.of(new MockConfigurationService(), new MockConfigurationService()));

final IllegalStateException exception = assertThrows(IllegalStateException.class, () -> sut.getConfigurationService());
assertThat(exception.getMessage()).contains("Cannot instantiate datasource instance because there are two or more implementations available");
}

private static class MockConfigurationService implements ConfigurationService {

@Override
public Configuration getConfiguration(ManagerParameter parameter) {
return new Configuration() {
@Override
public Holidays holidays() {
return null;
}

@Override
public Stream<Configuration> subConfigurations() {
return null;
}

@Override
public String hierarchy() {
return null;
}

@Override
public String description() {
return null;
}
};
}
}
}
11 changes: 10 additions & 1 deletion jollyday-jaxb/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import de.focus_shift.jollyday.core.spi.ConfigurationService;
import de.focus_shift.jollyday.jaxb.JaxbConfigurationService;

module de.focus_shift.jollyday.jaxb {

opens de.focus_shift.jollyday.jaxb.mapping to jakarta.xml.bind;

requires java.xml;
requires jakarta.xml.bind;
requires org.slf4j;
requires org.threeten.extra;

requires de.focus_shift.jollyday.core;

exports de.focus_shift.jollyday.jaxb to de.focus_shift.jollyday.core;
provides ConfigurationService
with JaxbConfigurationService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
de.focus_shift.jollyday.jaxb.JaxbConfigurationService

This file was deleted.

0 comments on commit dcfe659

Please sign in to comment.