diff --git a/README.md b/README.md
index ef1622af5..50b2f050c 100644
--- a/README.md
+++ b/README.md
@@ -511,6 +511,11 @@ dependencies {
## Release notes
+### Unreleased
+#### Feature:
++ Added common microservice entry point
++ Added configuration provider to common factory
+
### 5.15.0-dev
+ Updated:
diff --git a/build.gradle b/build.gradle
index 04a4aa55b..ebf1381cb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -123,6 +123,7 @@ dependencies {
implementation libs.kotlin.logging
+ testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation libs.junit.jupiter
testImplementation libs.mockito.kotlin
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
diff --git a/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java b/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java
index cff1833aa..b1d1a057e 100644
--- a/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java
+++ b/src/main/java/com/exactpro/th2/common/schema/factory/AbstractCommonFactory.java
@@ -34,13 +34,16 @@
import com.exactpro.th2.common.metrics.PrometheusConfiguration;
import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration;
import com.exactpro.th2.common.schema.configuration.ConfigurationManager;
+import com.exactpro.th2.common.schema.configuration.impl.JsonConfigurationProvider;
import com.exactpro.th2.common.schema.cradle.CradleConfidentialConfiguration;
import com.exactpro.th2.common.schema.cradle.CradleNonConfidentialConfiguration;
import com.exactpro.th2.common.schema.dictionary.DictionaryType;
+import com.exactpro.th2.common.schema.event.EventBatchRouter;
import com.exactpro.th2.common.schema.exception.CommonFactoryException;
import com.exactpro.th2.common.schema.grpc.configuration.GrpcConfiguration;
import com.exactpro.th2.common.schema.grpc.configuration.GrpcRouterConfiguration;
import com.exactpro.th2.common.schema.grpc.router.GrpcRouter;
+import com.exactpro.th2.common.schema.grpc.router.impl.DefaultGrpcRouter;
import com.exactpro.th2.common.schema.message.MessageRouter;
import com.exactpro.th2.common.schema.message.MessageRouterContext;
import com.exactpro.th2.common.schema.message.MessageRouterMonitor;
@@ -57,9 +60,12 @@
import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConsumeConnectionManager;
import com.exactpro.th2.common.schema.message.impl.rabbitmq.custom.MessageConverter;
import com.exactpro.th2.common.schema.message.impl.rabbitmq.custom.RabbitCustomRouter;
+import com.exactpro.th2.common.schema.message.impl.rabbitmq.group.RabbitMessageGroupBatchRouter;
+import com.exactpro.th2.common.schema.message.impl.rabbitmq.notification.NotificationEventBatchRouter;
+import com.exactpro.th2.common.schema.message.impl.rabbitmq.parsed.RabbitParsedBatchRouter;
+import com.exactpro.th2.common.schema.message.impl.rabbitmq.raw.RabbitRawBatchRouter;
import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.GroupBatch;
import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.TransportGroupBatchRouter;
-import com.exactpro.th2.common.schema.strategy.route.json.RoutingStrategyModule;
import com.exactpro.th2.common.schema.util.Log4jConfigUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@@ -69,13 +75,11 @@
import io.prometheus.client.exporter.HTTPServer;
import io.prometheus.client.hotspot.DefaultExports;
import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.text.StringSubstitutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
@@ -102,7 +106,6 @@
import static com.exactpro.th2.common.schema.factory.LazyProvider.lazyAutocloseable;
import static com.exactpro.th2.common.schema.factory.LazyProvider.lazyCloseable;
import static java.util.Objects.requireNonNull;
-import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
/**
* Class for load JSON schema configuration and create {@link GrpcRouter} and {@link MessageRouter}
@@ -121,25 +124,9 @@ public abstract class AbstractCommonFactory implements AutoCloseable {
private static final String SHARED_EXECUTOR_NAME = "rabbit-shared";
private static final String CHANNEL_CHECKER_EXECUTOR_NAME = "channel-checker-executor";
- public static final ObjectMapper MAPPER = new ObjectMapper();
-
- static {
- MAPPER.registerModules(
- new KotlinModule.Builder()
- .withReflectionCacheSize(512)
- .configure(KotlinFeature.NullToEmptyCollection, false)
- .configure(KotlinFeature.NullToEmptyMap, false)
- .configure(KotlinFeature.NullIsSameAsDefault, false)
- .configure(KotlinFeature.SingletonSupport, false)
- .configure(KotlinFeature.StrictNullChecks, false)
- .build(),
- new RoutingStrategyModule(MAPPER),
- new JavaTimeModule()
- );
- }
-
+ static final String CUSTOM_CFG_ALIAS = "custom";
+ public static final ObjectMapper MAPPER = JsonConfigurationProvider.MAPPER;
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCommonFactory.class);
- private final StringSubstitutor stringSubstitutor;
private final Class extends MessageRouter> messageRouterParsedBatchClass;
private final Class extends MessageRouter> messageRouterRawBatchClass;
@@ -201,6 +188,15 @@ public abstract class AbstractCommonFactory implements AutoCloseable {
configureLogger();
}
+ protected AbstractCommonFactory() {
+ messageRouterParsedBatchClass = RabbitParsedBatchRouter.class ;
+ messageRouterRawBatchClass = RabbitRawBatchRouter.class;
+ messageRouterMessageGroupBatchClass = RabbitMessageGroupBatchRouter.class;
+ eventBatchRouterClass = EventBatchRouter.class;
+ grpcRouterClass = DefaultGrpcRouter.class;
+ notificationEventBatchRouterClass = NotificationEventBatchRouter.class;
+ }
+
/**
* Create factory with non-default implementations schema classes
*
@@ -213,7 +209,6 @@ public AbstractCommonFactory(FactorySettings settings) {
eventBatchRouterClass = settings.getEventBatchRouterClass();
grpcRouterClass = settings.getGrpcRouterClass();
notificationEventBatchRouterClass = settings.getNotificationEventBatchRouterClass();
- stringSubstitutor = new StringSubstitutor(key -> defaultIfBlank(settings.getVariables().get(key), System.getenv(key)));
}
public void start() {
@@ -354,11 +349,11 @@ public MessageRouter getCustomMessageRouter(Class messageClass) {
}
/**
- * @return Configuration by specified path
+ * @return Configuration by specified alias
* @throws IllegalStateException if can not read configuration
*/
- public T getConfiguration(Path configPath, Class configClass, ObjectMapper customObjectMapper) {
- return getConfigurationManager().loadConfiguration(customObjectMapper, stringSubstitutor, configClass, configPath, false);
+ public T getConfiguration(String configAlias, Class configClass, ObjectMapper customObjectMapper) {
+ return getConfigurationManager().loadConfiguration(configClass, configAlias, false);
}
/**
@@ -369,7 +364,7 @@ public T getConfiguration(Path configPath, Class configClass, ObjectMappe
* @return configuration object
*/
protected T getConfigurationOrLoad(Class configClass, boolean optional) {
- return getConfigurationManager().getConfigurationOrLoad(MAPPER, stringSubstitutor, configClass, optional);
+ return getConfigurationManager().getConfigurationOrLoad(configClass, optional);
}
public RabbitMQConfiguration getRabbitMqConfiguration() {
@@ -385,7 +380,7 @@ public MessageRouterConfiguration getMessageRouterConfiguration() {
}
public GrpcConfiguration getGrpcConfiguration() {
- return getConfigurationManager().getConfigurationOrLoad(MAPPER, stringSubstitutor, GrpcConfiguration.class, false);
+ return getConfigurationManager().getConfigurationOrLoad(GrpcConfiguration.class, false);
}
public GrpcRouterConfiguration getGrpcRouterConfiguration() {
@@ -543,17 +538,7 @@ private CradleManager createCradleManager() {
* @throws IllegalStateException if can not read configuration
*/
public T getCustomConfiguration(Class confClass, ObjectMapper customObjectMapper) {
- File configFile = getPathToCustomConfiguration().toFile();
- if (!configFile.exists()) {
- try {
- return confClass.getConstructor().newInstance();
- } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
- NoSuchMethodException e) {
- return null;
- }
- }
-
- return getConfiguration(getPathToCustomConfiguration(), confClass, customObjectMapper);
+ return getConfiguration(CUSTOM_CFG_ALIAS, confClass, customObjectMapper);
}
/**
@@ -602,7 +587,8 @@ public T getCustomConfiguration(Class confClass) {
* @param dictionaryType desired type of dictionary
* @return Dictionary as {@link InputStream}
* @throws IllegalStateException if can not read dictionary
- * @deprecated Dictionary types will be removed in future releases of infra, use alias instead
+ * @deprecated Dictionary types will be removed in future releases of infra, use alias instead.
+ * Please use {@link #loadDictionary(String)}
*/
@Deprecated(since = "3.33.0", forRemoval = true)
public abstract InputStream readDictionary(DictionaryType dictionaryType);
@@ -639,25 +625,6 @@ private EventID createRootEventID() throws IOException {
protected abstract ConfigurationManager getConfigurationManager();
- /**
- * @return Path to custom configuration
- */
- protected abstract Path getPathToCustomConfiguration();
-
- /**
- * @return Path to dictionaries with type dir
- */
- @Deprecated(since = "3.33.0", forRemoval = true)
- protected abstract Path getPathToDictionaryTypesDir();
-
- /**
- * @return Path to dictionaries with alias dir
- */
- protected abstract Path getPathToDictionaryAliasesDir();
-
- @Deprecated(since = "3.33.0", forRemoval = true)
- protected abstract Path getOldPathToDictionariesDir();
-
/**
* @return Context for all routers except event router
*/
diff --git a/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java b/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java
index b48d26e92..2f9a8051f 100644
--- a/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java
+++ b/src/main/java/com/exactpro/th2/common/schema/factory/CommonFactory.java
@@ -19,6 +19,11 @@
import com.exactpro.th2.common.metrics.PrometheusConfiguration;
import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration;
import com.exactpro.th2.common.schema.configuration.ConfigurationManager;
+import com.exactpro.th2.common.schema.configuration.IConfigurationProvider;
+import com.exactpro.th2.common.schema.configuration.IDictionaryProvider;
+import com.exactpro.th2.common.schema.configuration.impl.DictionaryKind;
+import com.exactpro.th2.common.schema.configuration.impl.DictionaryProvider;
+import com.exactpro.th2.common.schema.configuration.impl.JsonConfigurationProvider;
import com.exactpro.th2.common.schema.cradle.CradleConfidentialConfiguration;
import com.exactpro.th2.common.schema.cradle.CradleNonConfidentialConfiguration;
import com.exactpro.th2.common.schema.dictionary.DictionaryType;
@@ -49,7 +54,6 @@
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
-import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -59,20 +63,14 @@
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Base64;
+import java.util.EnumMap;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import static com.exactpro.th2.common.schema.util.ArchiveUtils.getGzipBase64StringDecoder;
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;
-import static java.util.Objects.requireNonNullElseGet;
-import static org.apache.commons.io.FilenameUtils.removeExtension;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
/**
@@ -84,16 +82,15 @@ public class CommonFactory extends AbstractCommonFactory {
public static final String TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY = TH2_COMMON_SYSTEM_PROPERTY + '.' + "configuration-directory";
static final Path CONFIG_DEFAULT_PATH = Path.of("/var/th2/config/");
- static final String RABBIT_MQ_FILE_NAME = "rabbitMQ.json";
- static final String ROUTER_MQ_FILE_NAME = "mq.json";
- static final String GRPC_FILE_NAME = "grpc.json";
- static final String ROUTER_GRPC_FILE_NAME = "grpc_router.json";
- static final String CRADLE_CONFIDENTIAL_FILE_NAME = "cradle.json";
- static final String PROMETHEUS_FILE_NAME = "prometheus.json";
- static final String CUSTOM_FILE_NAME = "custom.json";
- static final String BOX_FILE_NAME = "box.json";
- static final String CONNECTION_MANAGER_CONF_FILE_NAME = "mq_router.json";
- static final String CRADLE_NON_CONFIDENTIAL_FILE_NAME = "cradle_manager.json";
+ static final String RABBIT_MQ_CFG_ALIAS = "rabbitMQ";
+ static final String ROUTER_MQ_CFG_ALIAS = "mq";
+ static final String GRPC_CFG_ALIAS = "grpc";
+ static final String ROUTER_GRPC_CFG_ALIAS = "grpc_router";
+ static final String CRADLE_CONFIDENTIAL_CFG_ALIAS = "cradle";
+ static final String PROMETHEUS_CFG_ALIAS = "prometheus";
+ static final String BOX_CFG_ALIAS = "box";
+ static final String CONNECTION_MANAGER_CFG_ALIAS = "mq_router";
+ static final String CRADLE_NON_CONFIDENTIAL_CFG_ALIAS = "cradle_manager";
/** @deprecated please use {@link #DICTIONARY_ALIAS_DIR_NAME} */
@Deprecated
@@ -114,52 +111,43 @@ public class CommonFactory extends AbstractCommonFactory {
private static final String CRADLE_MANAGER_CONFIG_MAP = "cradle-manager";
private static final String LOGGING_CONFIG_MAP = "logging-config";
- private final Path custom;
- private final Path dictionaryTypesDir;
- private final Path dictionaryAliasesDir;
- private final Path oldDictionariesDir;
- final ConfigurationManager configurationManager;
+ private final IConfigurationProvider configurationProvider;
+ private final IDictionaryProvider dictionaryProvider;
+ private final ConfigurationManager configurationManager;
private static final Logger LOGGER = LoggerFactory.getLogger(CommonFactory.class.getName());
- public CommonFactory(FactorySettings settings) {
- super(settings);
- custom = defaultPathIfNull(settings.getCustom(), CUSTOM_FILE_NAME);
- dictionaryTypesDir = defaultPathIfNull(settings.getDictionaryTypesDir(), DICTIONARY_TYPE_DIR_NAME);
- dictionaryAliasesDir = defaultPathIfNull(settings.getDictionaryAliasesDir(), DICTIONARY_ALIAS_DIR_NAME);
- oldDictionariesDir = requireNonNullElseGet(settings.getOldDictionariesDir(), CommonFactory::getConfigPath);
- configurationManager = createConfigurationManager(settings);
+ public CommonFactory(@NotNull IConfigurationProvider configurationProvider, @NotNull IDictionaryProvider dictionaryProvider) {
+ super();
+ this.dictionaryProvider = requireNonNull(dictionaryProvider, "Dictionary provider can't be null");
+ this.configurationProvider = requireNonNull(configurationProvider, "Configuration provider can't be null");
+ configurationManager = createConfigurationManager(configurationProvider);
start();
}
- public CommonFactory() {
- this(new FactorySettings());
+ @Deprecated(since = "6", forRemoval = true)
+ public CommonFactory(FactorySettings settings) {
+ this(createConfigurationProvider(settings), createDictionaryProvider(settings));
}
- @Override
- protected Path getPathToCustomConfiguration() {
- return custom;
+ public CommonFactory() {
+ this(new FactorySettings());
}
@Override
- protected Path getPathToDictionaryTypesDir() {
- return dictionaryTypesDir;
+ protected ConfigurationManager getConfigurationManager() {
+ return configurationManager;
}
- @Override
- protected Path getPathToDictionaryAliasesDir() {
- return dictionaryAliasesDir;
+ public IConfigurationProvider getConfigurationProvider() {
+ return configurationProvider;
}
- @Override
- protected Path getOldPathToDictionariesDir() {
- return oldDictionariesDir;
+ public static CommonFactory createFromProvider(@NotNull IConfigurationProvider configurationProvider,
+ @NotNull IDictionaryProvider dictionaryProvider) {
+ return new CommonFactory(configurationProvider, dictionaryProvider);
}
- @Override
- protected ConfigurationManager getConfigurationManager() {
- return configurationManager;
- }
/**
* Create {@link CommonFactory} from command line arguments
*
@@ -237,7 +225,7 @@ public static CommonFactory createFromArguments(String... args) {
try {
CommandLine cmd = new DefaultParser().parse(options, args);
- Path configs = getConfigPath(cmd.getOptionValue(configOption.getLongOpt()));
+ Path configs = toPath(cmd.getOptionValue(configOption.getLongOpt()));
if (cmd.hasOption(namespaceOption.getLongOpt()) && cmd.hasOption(boxNameOption.getLongOpt())) {
String namespace = cmd.getOptionValue(namespaceOption.getLongOpt());
@@ -268,24 +256,26 @@ public static CommonFactory createFromArguments(String... args) {
return createFromKubernetes(namespace, boxName, contextName, dictionaries);
}
- if (!CONFIG_DEFAULT_PATH.equals(configs)) {
- configureLogger(configs);
- }
FactorySettings settings = new FactorySettings();
- settings.setRabbitMQ(calculatePath(cmd, rabbitConfigurationOption, configs, RABBIT_MQ_FILE_NAME));
- settings.setRouterMQ(calculatePath(cmd, messageRouterConfigurationOption, configs, ROUTER_MQ_FILE_NAME));
- settings.setConnectionManagerSettings(calculatePath(cmd, connectionManagerConfigurationOption, configs, CONNECTION_MANAGER_CONF_FILE_NAME));
- settings.setGrpc(calculatePath(cmd, grpcConfigurationOption, grpcRouterConfigurationOption, configs, GRPC_FILE_NAME));
- settings.setRouterGRPC(calculatePath(cmd, grpcRouterConfigOption, configs, ROUTER_GRPC_FILE_NAME));
- settings.setCradleConfidential(calculatePath(cmd, cradleConfidentialConfigurationOption, cradleConfigurationOption, configs, CRADLE_CONFIDENTIAL_FILE_NAME));
- settings.setCradleNonConfidential(calculatePath(cmd, cradleManagerConfigurationOption, configs, CRADLE_NON_CONFIDENTIAL_FILE_NAME));
- settings.setPrometheus(calculatePath(cmd, prometheusConfigurationOption, configs, PROMETHEUS_FILE_NAME));
- settings.setBoxConfiguration(calculatePath(cmd, boxConfigurationOption, configs, BOX_FILE_NAME));
- settings.setCustom(calculatePath(cmd, customConfigurationOption, configs, CUSTOM_FILE_NAME));
- settings.setDictionaryTypesDir(calculatePath(cmd, dictionariesDirOption, configs, DICTIONARY_TYPE_DIR_NAME));
- settings.setDictionaryAliasesDir(calculatePath(cmd, dictionariesDirOption, configs, DICTIONARY_ALIAS_DIR_NAME));
- String oldDictionariesDir = cmd.getOptionValue(dictionariesDirOption.getLongOpt());
- settings.setOldDictionariesDir(oldDictionariesDir == null ? configs : Path.of(oldDictionariesDir));
+ if (configs != null) {
+ settings.setBaseConfigDir(configs);
+ if (!CONFIG_DEFAULT_PATH.equals(configs)) {
+ configureLogger(configs);
+ }
+ }
+ settings.setRabbitMQ(toPath(cmd.getOptionValue(rabbitConfigurationOption.getLongOpt())));
+ settings.setRouterMQ(toPath(cmd.getOptionValue(messageRouterConfigurationOption.getLongOpt())));
+ settings.setConnectionManagerSettings(toPath(cmd.getOptionValue(connectionManagerConfigurationOption.getLongOpt())));
+ settings.setGrpc(toPath(defaultIfNull(cmd.getOptionValue(grpcConfigurationOption.getLongOpt()), cmd.getOptionValue(grpcRouterConfigurationOption.getLongOpt()))));
+ settings.setRouterGRPC(toPath(cmd.getOptionValue(grpcRouterConfigOption.getLongOpt())));
+ settings.setCradleConfidential(toPath(cmd.getOptionValue(cradleConfidentialConfigurationOption.getLongOpt())));
+ settings.setCradleNonConfidential(toPath(defaultIfNull(cmd.getOptionValue(cradleManagerConfigurationOption.getLongOpt()), cmd.getOptionValue(cradleConfigurationOption.getLongOpt()))));
+ settings.setPrometheus(toPath(cmd.getOptionValue(prometheusConfigurationOption.getLongOpt())));
+ settings.setBoxConfiguration(toPath(cmd.getOptionValue(boxConfigurationOption.getLongOpt())));
+ settings.setCustom(toPath(cmd.getOptionValue(customConfigurationOption.getLongOpt())));
+ settings.setDictionaryTypesDir(toPath(cmd.getOptionValue(dictionariesDirOption.getLongOpt())));
+ settings.setDictionaryAliasesDir(toPath(cmd.getOptionValue(dictionariesDirOption.getLongOpt())));
+ settings.setOldDictionariesDir(toPath(cmd.getOptionValue(dictionariesDirOption.getLongOpt())));
return new CommonFactory(settings);
} catch (ParseException e) {
@@ -300,6 +290,7 @@ public static CommonFactory createFromArguments(String... args) {
* @param boxName - the name of the target th2 box placed in the specified namespace in Kubernetes
* @return CommonFactory with set path
*/
+ @Deprecated(since = "6", forRemoval = true)
public static CommonFactory createFromKubernetes(String namespace, String boxName) {
return createFromKubernetes(namespace, boxName, null);
}
@@ -312,6 +303,7 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam
* @param contextName - context name to choose the context from Kube config
* @return CommonFactory with set path
*/
+ @Deprecated(since = "6", forRemoval = true)
public static CommonFactory createFromKubernetes(String namespace, String boxName, @Nullable String contextName) {
return createFromKubernetes(namespace, boxName, contextName, emptyMap());
}
@@ -332,7 +324,7 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam
Path dictionaryTypePath = configPath.resolve(DICTIONARY_TYPE_DIR_NAME);
Path dictionaryAliasPath = configPath.resolve(DICTIONARY_ALIAS_DIR_NAME);
- Path boxConfigurationPath = configPath.resolve(BOX_FILE_NAME);
+ Path boxConfigurationPath = configPath.resolve(BOX_CFG_ALIAS);
FactorySettings settings = new FactorySettings();
@@ -399,21 +391,21 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam
configureLogger(configPath);
}
- settings.setRabbitMQ(writeFile(configPath, RABBIT_MQ_FILE_NAME, rabbitMqData));
- settings.setRouterMQ(writeFile(configPath, ROUTER_MQ_FILE_NAME, boxData));
- settings.setConnectionManagerSettings(writeFile(configPath, CONNECTION_MANAGER_CONF_FILE_NAME, boxData));
- settings.setGrpc(writeFile(configPath, GRPC_FILE_NAME, boxData));
- settings.setRouterGRPC(writeFile(configPath, ROUTER_GRPC_FILE_NAME, boxData));
- settings.setCradleConfidential(writeFile(configPath, CRADLE_CONFIDENTIAL_FILE_NAME, cradleConfidential));
- settings.setCradleNonConfidential(writeFile(configPath, CRADLE_NON_CONFIDENTIAL_FILE_NAME, cradleNonConfidential));
- settings.setPrometheus(writeFile(configPath, PROMETHEUS_FILE_NAME, boxData));
- settings.setCustom(writeFile(configPath, CUSTOM_FILE_NAME, boxData));
+ settings.setRabbitMQ(writeFile(configPath, RABBIT_MQ_CFG_ALIAS, rabbitMqData));
+ settings.setRouterMQ(writeFile(configPath, ROUTER_MQ_CFG_ALIAS, boxData));
+ settings.setConnectionManagerSettings(writeFile(configPath, CONNECTION_MANAGER_CFG_ALIAS, boxData));
+ settings.setGrpc(writeFile(configPath, GRPC_CFG_ALIAS, boxData));
+ settings.setRouterGRPC(writeFile(configPath, ROUTER_GRPC_CFG_ALIAS, boxData));
+ settings.setCradleConfidential(writeFile(configPath, CRADLE_CONFIDENTIAL_CFG_ALIAS, cradleConfidential));
+ settings.setCradleNonConfidential(writeFile(configPath, CRADLE_NON_CONFIDENTIAL_CFG_ALIAS, cradleNonConfidential));
+ settings.setPrometheus(writeFile(configPath, PROMETHEUS_CFG_ALIAS, boxData));
+ settings.setCustom(writeFile(configPath, CUSTOM_CFG_ALIAS, boxData));
settings.setBoxConfiguration(boxConfigurationPath);
settings.setDictionaryTypesDir(dictionaryTypePath);
settings.setDictionaryAliasesDir(dictionaryAliasPath);
- String boxConfig = boxData.get(BOX_FILE_NAME);
+ String boxConfig = boxData.get(BOX_CFG_ALIAS);
if (boxConfig == null) {
writeToJson(boxConfigurationPath, box);
@@ -433,136 +425,49 @@ public static CommonFactory createFromKubernetes(String namespace, String boxNam
@Override
public InputStream loadSingleDictionary() {
- Path dictionaryFolder = getPathToDictionaryAliasesDir();
- try {
- LOGGER.debug("Loading dictionary from folder: {}", dictionaryFolder);
- List dictionaries = null;
- if (Files.isDirectory(dictionaryFolder)) {
- try (Stream files = Files.list(dictionaryFolder)) {
- dictionaries = files.filter(Files::isRegularFile).collect(Collectors.toList());
- }
- }
-
- if (dictionaries==null || dictionaries.isEmpty()) {
- throw new IllegalStateException("No dictionary at path: " + dictionaryFolder.toAbsolutePath());
- } else if (dictionaries.size() > 1) {
- throw new IllegalStateException("Found several dictionaries at path: " + dictionaryFolder.toAbsolutePath());
- }
-
- var targetDictionary = dictionaries.get(0);
-
- return new ByteArrayInputStream(getGzipBase64StringDecoder().decode(Files.readString(targetDictionary)));
- } catch (IOException e) {
- throw new IllegalStateException("Can not read dictionary from path: " + dictionaryFolder.toAbsolutePath(), e);
- }
+ return dictionaryProvider.load();
}
@Override
public Set getDictionaryAliases() {
- Path dictionaryFolder = getPathToDictionaryAliasesDir();
- try {
- if (!Files.isDirectory(dictionaryFolder)) {
- return Set.of();
- }
-
- try (Stream files = Files.list(dictionaryFolder)) {
- Function getAlias = path -> removeExtension(path.getFileName().toString()).toLowerCase();
-
- Map> filesByAlias = files.filter(Files::isRegularFile)
- .collect(Collectors.groupingBy(getAlias, Collectors.toSet()));
- Map> duplicates = filesByAlias.entrySet().stream()
- .filter(entry -> entry.getValue().size() > 1)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- if (!duplicates.isEmpty()) {
- throw new IllegalStateException(
- "Dictionary directory contains files with the same name in different cases, " +
- "files by dictionary alias: " + duplicates + ", " +
- "path: " + dictionaryFolder.toAbsolutePath());
- }
- return Set.copyOf(filesByAlias.keySet());
- }
- } catch (IOException e) {
- throw new IllegalStateException("Can not get dictionaries aliases from path: " + dictionaryFolder.toAbsolutePath(), e);
- }
+ return dictionaryProvider.aliases();
}
@Override
public InputStream loadDictionary(String alias) {
- Path dictionaryFolder = getPathToDictionaryAliasesDir();
- try {
- LOGGER.debug("Loading dictionary by alias ({}) from folder: {}", alias, dictionaryFolder);
- List dictionaries = null;
-
- if (Files.isDirectory(dictionaryFolder)) {
- try (Stream files = Files.list(dictionaryFolder)) {
- dictionaries = files
- .filter(Files::isRegularFile)
- .filter(path -> removeExtension(path.getFileName().toString()).equalsIgnoreCase(alias))
- .collect(Collectors.toList());
- }
- }
-
- if (dictionaries==null || dictionaries.isEmpty()) {
- throw new IllegalStateException("No dictionary was found by alias '" + alias + "' at path: " + dictionaryFolder.toAbsolutePath());
- } else if (dictionaries.size() > 1) {
- throw new IllegalStateException("Found several dictionaries by alias '" + alias + "' at path: " + dictionaryFolder.toAbsolutePath());
- }
-
- return new ByteArrayInputStream(getGzipBase64StringDecoder().decode(Files.readString(dictionaries.get(0))));
- } catch (IOException e) {
- throw new IllegalStateException("Can not read dictionary '" + alias + "' from path: " + dictionaryFolder.toAbsolutePath(), e);
- }
+ return dictionaryProvider.load(alias);
}
@Override
public InputStream readDictionary() {
- return readDictionary(DictionaryType.MAIN);
+ return dictionaryProvider.load(DictionaryType.MAIN);
}
@Override
public InputStream readDictionary(DictionaryType dictionaryType) {
- try {
- List dictionaries = null;
- Path typeFolder = dictionaryType.getDictionary(getPathToDictionaryTypesDir());
- if (Files.isDirectory(typeFolder)) {
- try (Stream files = Files.list(typeFolder)) {
- dictionaries = files.filter(Files::isRegularFile)
- .collect(Collectors.toList());
- }
- }
-
- // Find with old format
- Path oldFolder = getOldPathToDictionariesDir();
- if ((dictionaries == null || dictionaries.isEmpty()) && Files.isDirectory(oldFolder)) {
- try (Stream files = Files.list(oldFolder)) {
- dictionaries = files.filter(path -> Files.isRegularFile(path) && path.getFileName().toString().contains(dictionaryType.name()))
- .collect(Collectors.toList());
- }
- }
+ return dictionaryProvider.load(dictionaryType);
+ }
- Path dictionaryAliasFolder = getPathToDictionaryAliasesDir();
- if ((dictionaries == null || dictionaries.isEmpty()) && Files.isDirectory(dictionaryAliasFolder)) {
- try (Stream files = Files.list(dictionaryAliasFolder)) {
- dictionaries = files.filter(Files::isRegularFile).filter(path -> removeExtension(path.getFileName().toString()).equalsIgnoreCase(dictionaryType.name())).collect(Collectors.toList());
- }
- }
+ static @NotNull Path getConfigPath() {
+ return getConfigPath(null);
+ }
- if (dictionaries == null || dictionaries.isEmpty()) {
- throw new IllegalStateException("No dictionary found with type '" + dictionaryType + "'");
- } else if (dictionaries.size() > 1) {
- throw new IllegalStateException("Found several dictionaries satisfying the '" + dictionaryType + "' type");
+ /**
+ * Priority:
+ * 1. passed via commandline arguments
+ * 2. {@value #TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY} system property
+ * 3. {@link #CONFIG_DEFAULT_PATH} default value
+ */
+ static @NotNull Path getConfigPath(@Nullable Path basePath) {
+ if (basePath != null) {
+ if (Files.exists(basePath) && Files.isDirectory(basePath)) {
+ return basePath;
}
-
- var targetDictionary = dictionaries.get(0);
-
- return new ByteArrayInputStream(getGzipBase64StringDecoder().decode(Files.readString(targetDictionary)));
- } catch (IOException e) {
- throw new IllegalStateException("Can not read dictionary", e);
+ LOGGER.warn("'{}' config directory passed via CMD doesn't exist or it is not a directory", basePath);
+ } else {
+ LOGGER.debug("Skipped blank CMD path for configs directory");
}
- }
- static @NotNull Path getConfigPath() {
String pathString = System.getProperty(TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY);
if (pathString != null) {
Path path = Paths.get(pathString);
@@ -582,21 +487,6 @@ public InputStream readDictionary(DictionaryType dictionaryType) {
return CONFIG_DEFAULT_PATH;
}
- static @NotNull Path getConfigPath(@Nullable String cmdPath) {
- String pathString = StringUtils.trim(cmdPath);
- if (pathString != null) {
- Path path = Paths.get(pathString);
- if (Files.exists(path) && Files.isDirectory(path)) {
- return path;
- }
- LOGGER.warn("'{}' config directory passed via CMD doesn't exist or it is not a directory", cmdPath);
- } else {
- LOGGER.debug("Skipped blank CMD path for configs directory");
- }
-
- return getConfigPath();
- }
-
private static Path writeFile(Path configPath, String fileName, Map configMap) throws IOException {
Path file = configPath.resolve(fileName);
writeFile(file, configMap.get(fileName));
@@ -661,23 +551,49 @@ private static void createDirectory(Path dir) throws IOException {
}
}
- private static ConfigurationManager createConfigurationManager(FactorySettings settings) {
- Map, Path> paths = new HashMap<>();
- paths.put(RabbitMQConfiguration.class, defaultPathIfNull(settings.getRabbitMQ(), RABBIT_MQ_FILE_NAME));
- paths.put(MessageRouterConfiguration.class, defaultPathIfNull(settings.getRouterMQ(), ROUTER_MQ_FILE_NAME));
- paths.put(ConnectionManagerConfiguration.class, defaultPathIfNull(settings.getConnectionManagerSettings(), CONNECTION_MANAGER_CONF_FILE_NAME));
- paths.put(GrpcConfiguration.class, defaultPathIfNull(settings.getGrpc(), GRPC_FILE_NAME));
- paths.put(GrpcRouterConfiguration.class, defaultPathIfNull(settings.getRouterGRPC(), ROUTER_GRPC_FILE_NAME));
- paths.put(CradleConfidentialConfiguration.class, defaultPathIfNull(settings.getCradleConfidential(), CRADLE_CONFIDENTIAL_FILE_NAME));
- paths.put(CradleNonConfidentialConfiguration.class, defaultPathIfNull(settings.getCradleNonConfidential(), CRADLE_NON_CONFIDENTIAL_FILE_NAME));
- paths.put(CassandraStorageSettings.class, defaultPathIfNull(settings.getCradleNonConfidential(), CRADLE_NON_CONFIDENTIAL_FILE_NAME));
- paths.put(PrometheusConfiguration.class, defaultPathIfNull(settings.getPrometheus(), PROMETHEUS_FILE_NAME));
- paths.put(BoxConfiguration.class, defaultPathIfNull(settings.getBoxConfiguration(), BOX_FILE_NAME));
- return new ConfigurationManager(paths);
+ private static IDictionaryProvider createDictionaryProvider(FactorySettings settings) {
+ Map paths = new EnumMap<>(DictionaryKind.class);
+ putIfNotNull(paths, DictionaryKind.OLD, settings.getOldDictionariesDir());
+ putIfNotNull(paths, DictionaryKind.TYPE, settings.getDictionaryTypesDir());
+ putIfNotNull(paths, DictionaryKind.ALIAS, settings.getDictionaryAliasesDir());
+ return DictionaryProvider.create(defaultIfNull(settings.getBaseConfigDir(), getConfigPath()), paths);
+ }
+
+ private static IConfigurationProvider createConfigurationProvider(FactorySettings settings) {
+ Map paths = new HashMap<>();
+ putIfNotNull(paths, CUSTOM_CFG_ALIAS, settings.getCustom());
+ putIfNotNull(paths, RABBIT_MQ_CFG_ALIAS, settings.getRabbitMQ());
+ putIfNotNull(paths, ROUTER_MQ_CFG_ALIAS, settings.getRouterMQ());
+ putIfNotNull(paths, CONNECTION_MANAGER_CFG_ALIAS, settings.getConnectionManagerSettings());
+ putIfNotNull(paths, GRPC_CFG_ALIAS, settings.getGrpc());
+ putIfNotNull(paths, ROUTER_GRPC_CFG_ALIAS, settings.getRouterGRPC());
+ putIfNotNull(paths, CRADLE_CONFIDENTIAL_CFG_ALIAS, settings.getCradleConfidential());
+ putIfNotNull(paths, CRADLE_NON_CONFIDENTIAL_CFG_ALIAS, settings.getCradleNonConfidential());
+ putIfNotNull(paths, PROMETHEUS_CFG_ALIAS, settings.getPrometheus());
+ putIfNotNull(paths, BOX_CFG_ALIAS, settings.getBoxConfiguration());
+ return JsonConfigurationProvider.create(defaultIfNull(settings.getBaseConfigDir(), getConfigPath()), paths);
+ }
+ private static ConfigurationManager createConfigurationManager(IConfigurationProvider configurationProvider) {
+ Map, String> paths = new HashMap<>();
+ paths.put(RabbitMQConfiguration.class, RABBIT_MQ_CFG_ALIAS);
+ paths.put(MessageRouterConfiguration.class, ROUTER_MQ_CFG_ALIAS);
+ paths.put(ConnectionManagerConfiguration.class, CONNECTION_MANAGER_CFG_ALIAS);
+ paths.put(GrpcConfiguration.class, GRPC_CFG_ALIAS);
+ paths.put(GrpcRouterConfiguration.class, ROUTER_GRPC_CFG_ALIAS);
+ paths.put(CradleConfidentialConfiguration.class, CRADLE_CONFIDENTIAL_CFG_ALIAS);
+ paths.put(CradleNonConfidentialConfiguration.class, CRADLE_NON_CONFIDENTIAL_CFG_ALIAS);
+ paths.put(CassandraStorageSettings.class, CRADLE_NON_CONFIDENTIAL_CFG_ALIAS);
+ paths.put(PrometheusConfiguration.class, PROMETHEUS_CFG_ALIAS);
+ paths.put(BoxConfiguration.class, BOX_CFG_ALIAS);
+ return new ConfigurationManager(configurationProvider, paths);
}
- private static Path defaultPathIfNull(Path path, String name) {
- return path == null ? getConfigPath().resolve(name) : path;
+ private static void putIfNotNull(@NotNull Map paths, @NotNull T key, @Nullable Path path) {
+ requireNonNull(paths, "'Paths' can't be null");
+ requireNonNull(key, "'Key' can't be null");
+ if (path != null) {
+ paths.put(key, path);
+ }
}
private static void writeFile(Path path, String data) throws IOException {
@@ -699,15 +615,8 @@ private static Option createLongOption(Options options, String optionName) {
return option;
}
- private static Path calculatePath(String path, @NotNull Path configsPath, String fileName) {
- return path != null ? Path.of(path) : configsPath.resolve(fileName);
+ private static @Nullable Path toPath(@Nullable String path) {
+ return path == null ? null : Path.of(path);
}
- private static Path calculatePath(CommandLine cmd, Option option, @NotNull Path configs, String fileName) {
- return calculatePath(cmd.getOptionValue(option.getLongOpt()), configs, fileName);
- }
-
- private static Path calculatePath(CommandLine cmd, Option current, Option deprecated, @NotNull Path configs, String fileName) {
- return calculatePath(defaultIfNull(cmd.getOptionValue(current.getLongOpt()), cmd.getOptionValue(deprecated.getLongOpt())), configs, fileName);
- }
}
diff --git a/src/main/kotlin/com/exactpro/th2/common/microservice/ApplicationContext.kt b/src/main/kotlin/com/exactpro/th2/common/microservice/ApplicationContext.kt
new file mode 100644
index 000000000..dd192a223
--- /dev/null
+++ b/src/main/kotlin/com/exactpro/th2/common/microservice/ApplicationContext.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 Exactpro (Exactpro Systems Limited)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.exactpro.th2.common.microservice
+
+import com.exactpro.th2.common.schema.factory.AbstractCommonFactory
+import java.util.function.BiConsumer
+import java.util.function.Consumer
+
+/**
+ * @param registerResource: register all resources which must be closed in reverse registration order.
+ * @param onPanic: call this method when application can not correct work anymore.
+ */
+class ApplicationContext(
+ val commonFactory: AbstractCommonFactory,
+ val registerResource: BiConsumer,
+ val onPanic: Consumer,
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/exactpro/th2/common/microservice/IApplication.kt b/src/main/kotlin/com/exactpro/th2/common/microservice/IApplication.kt
new file mode 100644
index 000000000..2b5a5eb20
--- /dev/null
+++ b/src/main/kotlin/com/exactpro/th2/common/microservice/IApplication.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Exactpro (Exactpro Systems Limited)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.exactpro.th2.common.microservice
+
+/**
+ * Instance of one time application which works from the [IApplication.start] to [IApplication.close] method.
+ */
+interface IApplication: AutoCloseable {
+ /**
+ * Starts one time application.
+ * This method can be called only once.
+ * @exception IllegalStateException can be thrown when:
+ * * the method is called the two or more time
+ * * close method has been called before
+ * * other reasons
+ */
+ fun start()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/exactpro/th2/common/microservice/IApplicationFactory.kt b/src/main/kotlin/com/exactpro/th2/common/microservice/IApplicationFactory.kt
new file mode 100644
index 000000000..730e70b88
--- /dev/null
+++ b/src/main/kotlin/com/exactpro/th2/common/microservice/IApplicationFactory.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2023 Exactpro (Exactpro Systems Limited)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.exactpro.th2.common.microservice
+
+import com.exactpro.th2.common.metrics.MetricMonitor
+import com.exactpro.th2.common.metrics.registerLiveness
+import com.exactpro.th2.common.metrics.registerReadiness
+import com.exactpro.th2.common.schema.factory.CommonFactory
+import io.github.oshai.kotlinlogging.KotlinLogging
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedDeque
+import java.util.concurrent.locks.Condition
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.thread
+import kotlin.concurrent.withLock
+import kotlin.system.exitProcess
+
+/**
+ * Factory can share closable resources between application. They should be closed on [IApplicationFactory.close] method call
+ */
+interface IApplicationFactory : AutoCloseable {
+ /**
+ * Creates onetime application.
+ * If you need restart of reconfigure application, close old instance and create new one.
+ */
+ fun createApplication(context: ApplicationContext): IApplication
+
+ /**
+ * Close resources shared between created application.
+ */
+ override fun close() {}
+
+ companion object {
+ private val K_LOGGER = KotlinLogging.logger {}
+ private const val APPLICATION_FACTORY_SYSTEM_PROPERTY = "th2.microservice.application-factory"
+ private const val MONITOR_NAME = "microservice_main"
+ @JvmStatic
+ @JvmOverloads
+ fun run(factory: IApplicationFactory? = null, vararg args: String) {
+ val liveness: MetricMonitor = registerLiveness(MONITOR_NAME)
+ val readiness: MetricMonitor = registerReadiness(MONITOR_NAME)
+
+ val resources: Deque = ConcurrentLinkedDeque()
+ val lock = ReentrantLock()
+ val condition: Condition = lock.newCondition()
+
+ configureShutdownHook(resources, lock, condition, liveness, readiness)
+
+ K_LOGGER.info { "Starting a component" }
+ liveness.enable()
+
+ K_LOGGER.info { "Preparing application by ${ factory?.javaClass ?: "internal factory" }" }
+
+ val applicationFactory = factory ?: loadApplicationFactory().also {
+ // add only loaded application factory to resources as internal resource
+ resources.add(Resource("application-factory", it))
+ K_LOGGER.info { "Loaded the ${it.javaClass} factory" }
+ }
+
+ val commonFactory = CommonFactory.createFromArguments(*args)
+ resources.add(Resource("common-factory", commonFactory))
+
+ val application = applicationFactory.createApplication(
+ ApplicationContext(
+ commonFactory,
+ { name: String, closeable: AutoCloseable -> resources.add(Resource(name, closeable)) },
+ ::panic
+ )
+ )
+ resources.add(Resource("application", application))
+
+ K_LOGGER.info { "Starting the ${application.javaClass} application of the '${commonFactory.boxConfiguration.boxName}' component" }
+
+ application.start()
+
+ K_LOGGER.info { "Started the '${commonFactory.boxConfiguration.boxName}' component" }
+ readiness.enable()
+
+ awaitShutdown(lock, condition)
+ }
+
+ private fun panic(ex: Throwable?) {
+ K_LOGGER.error(ex) { "Component panic exception" }
+ exitProcess(2)
+ }
+ private fun configureShutdownHook(
+ resources: Deque,
+ lock: ReentrantLock,
+ condition: Condition,
+ liveness: MetricMonitor,
+ readiness: MetricMonitor,
+ ) {
+ Runtime.getRuntime().addShutdownHook(thread(
+ start = false,
+ name = "Shutdown-hook"
+ ) {
+ K_LOGGER.info { "Shutdown start" }
+ readiness.disable()
+ lock.withLock { condition.signalAll() }
+ resources.descendingIterator().forEachRemaining { resource ->
+ runCatching {
+ resource.closable.close()
+ }.onFailure { e ->
+ K_LOGGER.error(e) { "Cannot close the ${resource.name} resource ${resource.closable::class}" }
+ }
+ }
+ liveness.disable()
+ K_LOGGER.info { "Shutdown end" }
+ })
+ }
+ @Throws(InterruptedException::class)
+ fun awaitShutdown(lock: ReentrantLock, condition: Condition) {
+ lock.withLock {
+ K_LOGGER.info { "Wait shutdown" }
+ condition.await()
+ K_LOGGER.info { "App shutdown" }
+ }
+ }
+
+ private fun loadApplicationFactory(): IApplicationFactory {
+ val instances = ServiceLoader.load(IApplicationFactory::class.java).toList()
+ return when (instances.size) {
+ 0 -> error("No instances of ${IApplicationFactory::class.simpleName}")
+ 1 -> instances.single().also { single ->
+ System.getProperty(APPLICATION_FACTORY_SYSTEM_PROPERTY)?.let { value ->
+ check(value == single::class.qualifiedName) {
+ "Found instance of ${IApplicationFactory::class.simpleName} mismatches the class specified by $APPLICATION_FACTORY_SYSTEM_PROPERTY system property," +
+ "configured: $value, found: ${single::class.qualifiedName}"
+ }
+ }
+ }
+ else -> {
+ System.getProperty(APPLICATION_FACTORY_SYSTEM_PROPERTY)?.let { value ->
+ instances.find { value == it::class.qualifiedName }
+ ?: error(
+ "Found instances of ${IApplicationFactory::class.simpleName} mismatches the class specified by $APPLICATION_FACTORY_SYSTEM_PROPERTY system property," +
+ "configured: $value, found: ${instances.map { Object::class.qualifiedName }}"
+ )
+ } ?: error(
+ "More than 1 instance of ${IApplicationFactory::class.simpleName} has been found " +
+ "and $APPLICATION_FACTORY_SYSTEM_PROPERTY system property isn't specified," +
+ "instances: $instances"
+ )
+ }
+ }
+ }
+
+ private class Resource(val name: String, val closable: AutoCloseable)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt
index 90d2b99b6..0259cf377 100644
--- a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt
+++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/ConfigurationManager.kt
@@ -15,45 +15,36 @@
package com.exactpro.th2.common.schema.configuration
-import com.fasterxml.jackson.databind.ObjectMapper
import io.github.oshai.kotlinlogging.KotlinLogging
-import org.apache.commons.text.StringSubstitutor
import java.io.IOException
-import java.nio.file.Files
-import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
-class ConfigurationManager(private val configurationPath: Map, Path>) {
+class ConfigurationManager(
+ private val configurationProvider: IConfigurationProvider,
+ private val configurationPath: Map, String>
+) {
private val configurations: MutableMap, Any?> = ConcurrentHashMap()
- operator fun get(clazz: Class<*>): Path? = configurationPath[clazz]
+ operator fun get(clazz: Class<*>): String? = configurationPath[clazz]
- fun loadConfiguration(
- objectMapper: ObjectMapper,
- stringSubstitutor: StringSubstitutor,
+ fun loadConfiguration(
configClass: Class,
- configPath: Path,
+ configAlias: String,
optional: Boolean
): T {
try {
- if (optional && !(Files.exists(configPath) && Files.size(configPath) > 0)) {
- LOGGER.warn { "Can not read configuration for ${configClass.name}. Use default configuration" }
- return configClass.getDeclaredConstructor().newInstance()
+ return configurationProvider.load(configAlias, configClass) {
+ if (!optional) {
+ throw IllegalStateException("The '$configAlias' is required")
+ }
+ configClass.getDeclaredConstructor().newInstance()
}
-
- val sourceContent = String(Files.readAllBytes(configPath))
- LOGGER.info { "Configuration path $configClass source content $sourceContent" }
- val content: String = stringSubstitutor.replace(sourceContent)
- return objectMapper.readerFor(configClass).readValue(content)
} catch (e: IOException) {
- throw IllegalStateException("Cannot read ${configClass.name} configuration from path '${configPath}'", e)
+ throw IllegalStateException("Cannot read ${configClass.name} configuration for config alias '$configAlias'", e)
}
}
- @Suppress("UNCHECKED_CAST")
- fun getConfigurationOrLoad(
- objectMapper: ObjectMapper,
- stringSubstitutor: StringSubstitutor,
+ fun getConfigurationOrLoad(
configClass: Class,
optional: Boolean
): T {
@@ -61,9 +52,9 @@ class ConfigurationManager(private val configurationPath: Map, Path>) {
checkNotNull(configurationPath[configClass]) {
"Unknown class $configClass"
}.let {
- loadConfiguration(objectMapper, stringSubstitutor, configClass, it, optional)
+ loadConfiguration(configClass, it, optional)
}
- } as T
+ }.let(configClass::cast)
}
companion object {
diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/IConfigurationProvider.kt b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/IConfigurationProvider.kt
new file mode 100644
index 000000000..c6da5c209
--- /dev/null
+++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/IConfigurationProvider.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 Exactpro (Exactpro Systems Limited)
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.exactpro.th2.common.schema.configuration
+
+import java.io.InputStream
+import java.util.function.Function
+import java.util.function.Supplier
+
+interface IConfigurationProvider {
+ /**
+ * Loads instance of [configClass] from a resource related to [alias].
+ * Configuration provider parses resource using internal parser.
+ * You can load the same resources using different classes.
+ * @param configClass - target class of loading config
+ * @param alias - alias of related resources
+ * @param default - supplier of default instance of configuration class. Pass `null` if configuration must be loaded
+ */
+ fun load(alias: String, configClass: Class, default: Supplier?): T
+ /**
+ * Loads instance of [configClass] from a resource related to [alias].
+ * Configuration provider parses resource using internal parser.
+ * You can load the same resources using different classes.
+ * @param configClass - target class of loading config
+ * @param alias - alias of related resources
+ */
+ fun load(alias: String, configClass: Class): T = load(alias, configClass, null)
+ /**
+ * Loads instance using [parser] from a resource related to [alias].
+ * You can load the same resources by different classes.
+ * @param alias - alias of related resources
+ * @param parser - function to parse [InputStream] to config instance
+ * @param default - supplier of default instance of configuration class. Pass `null` if configuration must be loaded
+ */
+ fun load(alias: String, parser: Function, default: Supplier?): T
+ /**
+ * Loads instance using [parser] from a resource related to [alias].
+ * You can load the same resources by different classes.
+ * @param alias - alias of related resources
+ * @param parser - function to parse [InputStream] to config instance
+ */
+ fun load(alias: String, parser: Function): T = load(alias, parser, null as Supplier?)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/IDictionaryProvider.kt b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/IDictionaryProvider.kt
new file mode 100644
index 000000000..e17de4d28
--- /dev/null
+++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/IDictionaryProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024-2025 Exactpro (Exactpro Systems Limited)
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.exactpro.th2.common.schema.configuration
+
+import com.exactpro.th2.common.schema.dictionary.DictionaryType
+import java.io.InputStream
+
+interface IDictionaryProvider {
+ fun aliases(): Set
+ fun load(alias: String): InputStream
+ @Deprecated("Load dictionary by type is deprecated, please use load by alias")
+ fun load(type: DictionaryType): InputStream
+ fun load(): InputStream
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/DictionaryProvider.kt b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/DictionaryProvider.kt
new file mode 100644
index 000000000..22be51ebc
--- /dev/null
+++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/DictionaryProvider.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2024 Exactpro (Exactpro Systems Limited)
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.exactpro.th2.common.schema.configuration.impl
+
+import com.exactpro.th2.common.schema.configuration.IDictionaryProvider
+import com.exactpro.th2.common.schema.configuration.impl.DictionaryKind.ALIAS
+import com.exactpro.th2.common.schema.configuration.impl.DictionaryKind.OLD
+import com.exactpro.th2.common.schema.configuration.impl.DictionaryKind.TYPE
+import com.exactpro.th2.common.schema.dictionary.DictionaryType
+import com.exactpro.th2.common.schema.util.ArchiveUtils
+import org.apache.commons.io.FilenameUtils
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.io.InputStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.Locale
+import kotlin.io.path.isDirectory
+import kotlin.io.path.isRegularFile
+import kotlin.streams.asSequence
+
+class DictionaryProvider private constructor(
+ baseDir: Path,
+ paths: Map = emptyMap()
+) : IDictionaryProvider {
+
+ private val directoryPaths = DictionaryKind.createMapping(baseDir)
+ .plus(paths.mapValues { (_, value) -> value.toAbsolutePath() })
+
+ private val dictionaryOldPath: Path
+ get() = requireNotNull(directoryPaths[OLD]) {
+ "$OLD dictionary kind isn't present in $directoryPaths"
+ }
+ private val dictionaryTypePath: Path
+ get() = requireNotNull(directoryPaths[TYPE]) {
+ "$TYPE dictionary kind isn't present in $directoryPaths"
+ }
+
+ private val dictionaryAliasPath: Path
+ get() = requireNotNull(directoryPaths[ALIAS]) {
+ "$ALIAS dictionary kind isn't present in $directoryPaths"
+ }
+ private val directories = directoryPaths.values
+
+ override fun aliases(): Set {
+ try {
+ if (!dictionaryAliasPath.isDirectory()) {
+ return emptySet()
+ }
+
+ val fileList = Files.walk(dictionaryAliasPath, 1).asSequence()
+ .filter(Path::isRegularFile)
+ .toList()
+ val aliasSet: MutableSet = mutableSetOf()
+ val duplicates: MutableMap> = mutableMapOf()
+ for (path in fileList) {
+ val alias = FilenameUtils.removeExtension(path.fileName.toString())
+ .lowercase(Locale.getDefault())
+ if (!aliasSet.add(alias)) {
+ duplicates.getOrPut(alias, ::mutableSetOf).add(path.fileName.toString())
+ }
+ }
+ check(duplicates.isEmpty()) {
+ "Dictionary directory contains files with the same name in different cases, files: $duplicates, path: $dictionaryAliasPath"
+ }
+ return aliasSet
+ } catch (e: IOException) {
+ throw IllegalStateException(
+ "Can not get dictionaries aliases from path: ${dictionaryAliasPath.toAbsolutePath()}",
+ e
+ )
+ }
+ }
+
+ override fun load(alias: String): InputStream {
+ try {
+ require(alias.isNotBlank()) {
+ "Dictionary is blank"
+ }
+
+ check(dictionaryAliasPath.isDirectory()) {
+ "Dictionary dir doesn't exist or isn't directory, path ${dictionaryAliasPath.toAbsolutePath()}"
+ }
+
+ val files = searchInAliasDir(alias)
+ val file = single(listOf(dictionaryAliasPath), files, alias)
+
+ return open(file)
+ } catch (e: IOException) {
+ throw IllegalStateException(
+ "Can not load dictionary by '$alias' alias from path: ${dictionaryAliasPath.toAbsolutePath()}",
+ e
+ )
+ }
+ }
+
+ @Deprecated("Load dictionary by type is deprecated, please use load by alias")
+ override fun load(type: DictionaryType): InputStream {
+ try {
+ var files = searchInAliasDir(type.name)
+
+ if (files.isEmpty()) {
+ files = searchInTypeDir(type)
+ }
+
+ if (files.isEmpty()) {
+ files = searchInOldDir(type.name)
+ }
+
+ val file = single(directories, files, type.name)
+ return open(file)
+ } catch (e: IOException) {
+ throw IllegalStateException("Can not load dictionary by '$type' type from paths: $directories", e)
+ }
+ }
+
+ @Deprecated("Load single dictionary is deprecated, please use load by alias")
+ override fun load(): InputStream {
+ val dirs = listOf(dictionaryAliasPath, dictionaryTypePath)
+ try {
+ var files: List = if (dictionaryAliasPath.isDirectory()) {
+ Files.walk(dictionaryAliasPath, 1).asSequence()
+ .filter(Path::isRegularFile)
+ .toList()
+ } else {
+ emptyList()
+ }
+
+ if (files.isEmpty()) {
+ if (dictionaryTypePath.isDirectory()) {
+ files = Files.walk(dictionaryTypePath, 1).asSequence()
+ .filter(Path::isDirectory)
+ .flatMap { dir ->
+ Files.walk(dir, 1).asSequence()
+ .filter(Path::isRegularFile)
+ }.toList()
+ }
+ }
+
+ check(files.isNotEmpty()) {
+ "No dictionary at path(s): $dirs"
+ }
+ check(files.size == 1) {
+ "Found several dictionaries at paths: $dirs"
+ }
+ val file = files.single()
+ return open(file)
+ } catch (e: IOException) {
+ throw IllegalStateException("Can not read dictionary from from paths: $directories", e)
+ }
+ }
+
+ private fun open(file: Path) = ByteArrayInputStream(
+ ArchiveUtils.getGzipBase64StringDecoder().decode(Files.readString(file))
+ )
+
+ private fun single(dirs: Collection, files: List, alias: String): Path {
+ check(files.isNotEmpty()) {
+ "No dictionary was found by '$alias' name at path(s): $dirs"
+ }
+ check(files.size == 1) {
+ "Found several dictionaries by '$alias' name at path(s): $dirs"
+ }
+ return files.single()
+ }
+
+ private fun searchInOldDir(name: String): List {
+ if (dictionaryOldPath.isDirectory()) {
+ return Files.walk(dictionaryOldPath, 1).asSequence()
+ .filter(Path::isRegularFile)
+ .filter { file -> file.fileName.toString().contains(name) }
+ .toList()
+ }
+ return emptyList()
+ }
+
+ private fun searchInTypeDir(type: DictionaryType): List {
+ val path = type.getDictionary(dictionaryTypePath)
+ if (path.isDirectory()) {
+ return Files.walk(path, 1).asSequence()
+ .filter(Path::isRegularFile)
+ .toList()
+ }
+ return emptyList()
+ }
+
+ private fun searchInAliasDir(alias: String): List {
+ if (dictionaryAliasPath.isDirectory()) {
+ return Files.walk(dictionaryAliasPath, 1).asSequence()
+ .filter(Path::isRegularFile)
+ .filter { file -> alias.equals(toAlias(file), true) }
+ .toList()
+ }
+ return emptyList()
+ }
+
+ companion object {
+ private fun toAlias(path: Path) = FilenameUtils.removeExtension(path.fileName.toString())
+
+ @JvmStatic
+ @JvmOverloads
+ fun create(
+ baseDir: Path,
+ paths: Map = emptyMap()
+ ): DictionaryProvider = DictionaryProvider(baseDir, paths)
+ }
+}
+
+enum class DictionaryKind(
+ val directoryName: String
+) {
+ OLD(""),
+ TYPE("dictionary"),
+ ALIAS("dictionaries");
+
+ companion object {
+ fun createMapping(baseDir: Path): Map {
+ return buildMap {
+ DictionaryKind.values().forEach {
+ put(it, baseDir.resolve(it.directoryName).toAbsolutePath())
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/JsonConfigurationProvider.kt b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/JsonConfigurationProvider.kt
new file mode 100644
index 000000000..831daac64
--- /dev/null
+++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/JsonConfigurationProvider.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023-2025 Exactpro (Exactpro Systems Limited)
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.exactpro.th2.common.schema.configuration.impl
+
+import com.exactpro.th2.common.schema.configuration.IConfigurationProvider
+import com.exactpro.th2.common.schema.strategy.route.json.RoutingStrategyModule
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.kotlin.KotlinFeature
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import io.github.oshai.kotlinlogging.KotlinLogging
+import org.apache.commons.text.StringSubstitutor
+import java.io.InputStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.concurrent.ConcurrentHashMap
+import java.util.function.Function
+import java.util.function.Supplier
+import kotlin.io.path.exists
+
+
+class JsonConfigurationProvider private constructor(
+ internal val baseDir: Path,
+ internal val customPaths: Map = emptyMap(),
+) : IConfigurationProvider {
+
+ private val cache = ConcurrentHashMap()
+
+ private fun getPathFor(alias: String): Path = customPaths[alias] ?: baseDir.resolve("${alias}.${EXTENSION}")
+
+ override fun load(alias: String, configClass: Class, default: Supplier?): T =
+ requireNotNull(cache.compute(alias) { _, value ->
+ when {
+ value == null -> loadFromFile(alias, default) { MAPPER.readValue(it, configClass) }
+ !configClass.isInstance(value) -> loadFromFile(alias, default) {
+ MAPPER.readValue(it, configClass)
+ }.also {
+ K_LOGGER.info { "Parsed value for '$alias' alias has been updated from: ${value::class.java} to ${it::class.java}" }
+ }
+ else -> value
+ }
+ }).also { value ->
+ check(configClass.isInstance(value)) {
+ "Stored configuration instance of $alias config alias mismatches, " +
+ "expected: ${configClass.canonicalName}, actual: ${value::class.java.canonicalName}"
+ }
+ }.let(configClass::cast)
+
+ override fun load(alias: String, parser: Function, default: Supplier?): T =
+ loadFromFile(alias, default, parser).apply {
+ cache.put(alias, this)?.let {
+ K_LOGGER.info { "Parsed value for '$alias' alias has been updated from: ${this@JsonConfigurationProvider::class.java} to ${it::class.java}" }
+ }
+ }
+
+ private fun loadFromFile(alias: String, default: Supplier?, parser: Function): T {
+ val configPath = this.getPathFor(alias)
+ if (!configPath.exists()) {
+ K_LOGGER.warn { "'$configPath' file related to the '$alias' config alias doesn't exist" }
+ return default?.get()
+ ?: error("Configuration loading failure, '$configPath' file related to the '$alias' config alias doesn't exist")
+ }
+ if (Files.size(configPath) == 0L) {
+ K_LOGGER.warn { "'$configPath' file related to the '$alias' config alias has 0 size" }
+ return default?.get()
+ ?: error("Configuration loading failure, '$configPath' file related to the '$alias' config alias has 0 size")
+ }
+
+ val sourceContent = String(Files.readAllBytes(configPath))
+ K_LOGGER.info { "'$configPath' file related to the '$alias' config alias has source content $sourceContent" }
+ val content = SUBSTITUTOR.get().replace(sourceContent)
+ return requireNotNull(parser.apply(content.byteInputStream())) {
+ "Parsed format of config content can't be null, alias: '$alias'"
+ }
+ }
+
+ companion object {
+ private const val EXTENSION = "json"
+
+ private val K_LOGGER = KotlinLogging.logger {}
+
+ @Suppress("SpellCheckingInspection")
+ private val SUBSTITUTOR: ThreadLocal = object : ThreadLocal() {
+ override fun initialValue(): StringSubstitutor = StringSubstitutor(System.getenv())
+ }
+
+ @JvmField
+ val MAPPER = ObjectMapper().apply {
+ registerModules(
+ KotlinModule.Builder()
+ .withReflectionCacheSize(512)
+ .configure(KotlinFeature.NullToEmptyCollection, false)
+ .configure(KotlinFeature.NullToEmptyMap, false)
+ .configure(KotlinFeature.NullIsSameAsDefault, false)
+ .configure(KotlinFeature.SingletonSupport, true)
+ .configure(KotlinFeature.StrictNullChecks, false)
+ .build(),
+ RoutingStrategyModule(this),
+ JavaTimeModule()
+ )
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun create(
+ baseDir: Path,
+ customPaths: Map = emptyMap()
+ ): JsonConfigurationProvider = JsonConfigurationProvider(baseDir, customPaths)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt b/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt
index 47c0c6566..d1eeee354 100644
--- a/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt
+++ b/src/main/kotlin/com/exactpro/th2/common/schema/factory/FactorySettings.kt
@@ -31,11 +31,17 @@ import com.exactpro.th2.common.schema.message.impl.rabbitmq.raw.RabbitRawBatchRo
import java.nio.file.Path
data class FactorySettings @JvmOverloads constructor(
+ @Deprecated("Will be removed in future releases")
var messageRouterParsedBatchClass: Class> = RabbitParsedBatchRouter::class.java,
+ @Deprecated("Will be removed in future releases")
var messageRouterRawBatchClass: Class> = RabbitRawBatchRouter::class.java,
+ @Deprecated("Will be removed in future releases")
var messageRouterMessageGroupBatchClass: Class> = RabbitMessageGroupBatchRouter::class.java,
+ @Deprecated("Will be removed in future releases")
var eventBatchRouterClass: Class> = EventBatchRouter::class.java,
+ @Deprecated("Will be removed in future releases")
var grpcRouterClass: Class = DefaultGrpcRouter::class.java,
+ @Deprecated("Will be removed in future releases")
var notificationEventBatchRouterClass: Class> = NotificationEventBatchRouter::class.java,
var rabbitMQ: Path? = null,
var routerMQ: Path? = null,
@@ -47,9 +53,12 @@ data class FactorySettings @JvmOverloads constructor(
var prometheus: Path? = null,
var boxConfiguration: Path? = null,
var custom: Path? = null,
- @Deprecated("Will be removed in future releases") var dictionaryTypesDir: Path? = null,
+ var baseConfigDir: Path? = null,
+ @Deprecated("Will be removed in future releases")
+ var dictionaryTypesDir: Path? = null,
var dictionaryAliasesDir: Path? = null,
- @Deprecated("Will be removed in future releases") var oldDictionariesDir: Path? = null,
+ @Deprecated("Will be removed in future releases")
+ var oldDictionariesDir: Path? = null,
var variables: MutableMap = HashMap()
) {
fun messageRouterParsedBatchClass(messageRouterParsedBatchClass: Class>): FactorySettings {
@@ -143,7 +152,7 @@ data class FactorySettings @JvmOverloads constructor(
}
fun oldDictionariesDir(oldDictionariesDir: Path?): FactorySettings {
- this.dictionaryTypesDir = oldDictionariesDir
+ this.oldDictionariesDir = oldDictionariesDir
return this
}
diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/TestDictionaryLoad.kt b/src/test/kotlin/com/exactpro/th2/common/schema/DictionaryLoadTest.kt
similarity index 98%
rename from src/test/kotlin/com/exactpro/th2/common/schema/TestDictionaryLoad.kt
rename to src/test/kotlin/com/exactpro/th2/common/schema/DictionaryLoadTest.kt
index 172299b9a..d8c0b9bca 100644
--- a/src/test/kotlin/com/exactpro/th2/common/schema/TestDictionaryLoad.kt
+++ b/src/test/kotlin/com/exactpro/th2/common/schema/DictionaryLoadTest.kt
@@ -32,7 +32,8 @@ import kotlin.io.path.writeBytes
import kotlin.io.path.writeText
import kotlin.test.assertEquals
-class TestDictionaryLoad {
+@Suppress("DEPRECATION")
+class DictionaryLoadTest {
@TempDir
lateinit var tempDir: Path
@@ -64,7 +65,7 @@ class TestDictionaryLoad {
@ParameterizedTest
@ValueSource(strings = ["MAIN", "main", "test-dictionary"])
- fun `test read dictionary from type dictionary dir`(fileName: String, ) {
+ fun `test read dictionary from type dictionary dir`(fileName: String) {
val content = writeDictionary(tempDir.resolve(Path.of("dictionary", "main", fileName)))
CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory ->
assertEquals(content, commonFactory.readDictionary().use { String(it.readAllBytes()) })
@@ -87,7 +88,7 @@ class TestDictionaryLoad {
@ParameterizedTest
@ValueSource(strings = ["MAIN", "main", "MAIN.xml", "main.json"])
- fun `test read dictionary from alias dictionary dir`(fileName: String, ) {
+ fun `test read dictionary from alias dictionary dir`(fileName: String) {
val content = writeDictionary(tempDir.resolve(Path.of("dictionaries", fileName)))
CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory ->
assertEquals(content, commonFactory.readDictionary().use { String(it.readAllBytes()) })
@@ -120,7 +121,7 @@ class TestDictionaryLoad {
@ParameterizedTest
@ValueSource(strings = ["INCOMING", "incoming", "test-dictionary"])
- fun `test read dictionary by type from type dictionary dir`(fileName: String, ) {
+ fun `test read dictionary by type from type dictionary dir`(fileName: String) {
val content = writeDictionary(tempDir.resolve(Path.of("dictionary", "incoming", fileName)))
writeDictionary(tempDir.resolve(Path.of("dictionary", "main", "MAIN")))
CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory ->
@@ -147,7 +148,7 @@ class TestDictionaryLoad {
@ParameterizedTest
@ValueSource(strings = ["INCOMING", "incoming", "INCOMING.xml", "incoming.json"])
- fun `test read dictionary by type from alias dictionary dir`(fileName: String, ) {
+ fun `test read dictionary by type from alias dictionary dir`(fileName: String) {
val content = writeDictionary(tempDir.resolve(Path.of("dictionaries", fileName)))
writeDictionary(tempDir.resolve(Path.of("dictionaries", "MAIN")))
CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory ->
@@ -193,7 +194,7 @@ class TestDictionaryLoad {
@ParameterizedTest
@ValueSource(strings = ["TEST-ALIAS", "test-alias"])
- fun `test load dictionary by alias from alias dictionary dir`(fileName: String, ) {
+ fun `test load dictionary by alias from alias dictionary dir`(fileName: String) {
val content = writeDictionary(tempDir.resolve(Path.of("dictionaries", fileName)))
writeDictionary(tempDir.resolve(Path.of("dictionaries", "test-dictionary")))
CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory ->
@@ -255,7 +256,7 @@ class TestDictionaryLoad {
cfgPath.resolve("prometheus.json").writeText("{\"enabled\":false}")
}
- private fun writeDictionary(path: Path): String? {
+ private fun writeDictionary(path: Path): String {
val content = RandomStringUtils.randomAlphanumeric(10)
path.parent.createDirectories()
path.writeBytes(ArchiveUtils.getGzipBase64StringEncoder().encode(content))
diff --git a/src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt
index c918f8879..9aecfd34f 100644
--- a/src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt
+++ b/src/test/kotlin/com/exactpro/th2/common/schema/factory/CommonFactoryTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Exactpro (Exactpro Systems Limited)
+ * Copyright 2023-2024 Exactpro (Exactpro Systems Limited)
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@@ -14,110 +14,559 @@
*/
package com.exactpro.th2.common.schema.factory
-import com.exactpro.cradle.cassandra.CassandraStorageSettings
import com.exactpro.th2.common.metrics.PrometheusConfiguration
import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration
+import com.exactpro.th2.common.schema.configuration.impl.JsonConfigurationProvider
import com.exactpro.th2.common.schema.cradle.CradleConfidentialConfiguration
import com.exactpro.th2.common.schema.cradle.CradleNonConfidentialConfiguration
+import com.exactpro.th2.common.schema.dictionary.DictionaryType
+import com.exactpro.th2.common.schema.dictionary.DictionaryType.INCOMING
+import com.exactpro.th2.common.schema.dictionary.DictionaryType.MAIN
+import com.exactpro.th2.common.schema.dictionary.DictionaryType.OUTGOING
import com.exactpro.th2.common.schema.factory.CommonFactory.CONFIG_DEFAULT_PATH
-import com.exactpro.th2.common.schema.factory.CommonFactory.CUSTOM_FILE_NAME
-import com.exactpro.th2.common.schema.factory.CommonFactory.DICTIONARY_ALIAS_DIR_NAME
-import com.exactpro.th2.common.schema.factory.CommonFactory.DICTIONARY_TYPE_DIR_NAME
import com.exactpro.th2.common.schema.factory.CommonFactory.TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY
+import com.exactpro.th2.common.schema.factory.CommonFactoryTest.Companion.DictionaryHelper.ALIAS
+import com.exactpro.th2.common.schema.factory.CommonFactoryTest.Companion.DictionaryHelper.Companion.assertDictionary
+import com.exactpro.th2.common.schema.factory.CommonFactoryTest.Companion.DictionaryHelper.OLD
+import com.exactpro.th2.common.schema.factory.CommonFactoryTest.Companion.DictionaryHelper.TYPE
import com.exactpro.th2.common.schema.grpc.configuration.GrpcConfiguration
import com.exactpro.th2.common.schema.grpc.configuration.GrpcRouterConfiguration
import com.exactpro.th2.common.schema.message.configuration.MessageRouterConfiguration
import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.ConnectionManagerConfiguration
import com.exactpro.th2.common.schema.message.impl.rabbitmq.configuration.RabbitMQConfiguration
+import com.exactpro.th2.common.schema.util.ArchiveUtils
+import org.apache.commons.io.file.PathUtils.deleteDirectory
+import org.apache.commons.lang3.RandomStringUtils
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.samePropertyValuesAs
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertAll
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+import org.junitpioneer.jupiter.ClearSystemProperty
import org.junitpioneer.jupiter.SetSystemProperty
import java.nio.file.Path
+import java.util.Locale
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.createDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.writeBytes
+import kotlin.reflect.cast
+import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
+@Suppress("DEPRECATION", "removal")
class CommonFactoryTest {
- @Test
- fun `test load config by default path (default constructor)`() {
- CommonFactory().use { commonFactory ->
- assertConfigs(commonFactory, CONFIG_DEFAULT_PATH)
+ @BeforeEach
+ fun beforeEach() {
+ if (TEMP_DIR.toPath().exists()) {
+ deleteDirectory(TEMP_DIR.toPath())
}
+ CMD_ARG_CFG_DIR_PATH.createDirectories()
+ SYSTEM_PROPERTY_CFG_DIR_PATH.createDirectories()
+ CUSTOM_DIR_PATH.createDirectories()
}
- @Test
- fun `test load config by default path (createFromArguments(empty))`() {
- CommonFactory.createFromArguments().use { commonFactory ->
- assertConfigs(commonFactory, CONFIG_DEFAULT_PATH)
+ @Nested
+ @ClearSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY)
+ inner class CreateByDefaultConstructor {
+ @Test
+ fun `test default`() {
+ CommonFactory().use { commonFactory ->
+ assertThrowsDictionaryDir(commonFactory)
+ assertThrowsConfigs(commonFactory)
+ }
}
- }
- @Test
- fun `test load config by custom path (createFromArguments(not empty))`() {
- CommonFactory.createFromArguments("-c", CONFIG_DIR_IN_RESOURCE).use { commonFactory ->
- assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE))
+ @Test
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ fun `test with system property`() {
+ CommonFactory().use { commonFactory ->
+ assertDictionaryDirs(commonFactory, SYSTEM_PROPERTY_CFG_DIR_PATH)
+ assertConfigs(commonFactory, SYSTEM_PROPERTY_CFG_DIR_PATH)
+ }
}
}
- @Test
- @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = CONFIG_DIR_IN_RESOURCE)
- fun `test load config by environment variable path (default constructor)`() {
- CommonFactory().use { commonFactory ->
- assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE))
+ @Nested
+ @ClearSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY)
+ inner class CreateBySettingsConstructor {
+ @Test
+ fun `test default`() {
+ CommonFactory(FactorySettings()).use { commonFactory ->
+ assertThrowsDictionaryDir(commonFactory)
+ assertThrowsConfigs(commonFactory)
+ }
}
- }
- @Test
- @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = CONFIG_DIR_IN_RESOURCE)
- fun `test load config by environment variable path (createFromArguments(empty))`() {
- CommonFactory.createFromArguments().use { commonFactory ->
- assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE))
+ @ParameterizedTest
+ @EnumSource(value = DictionaryHelper::class)
+ fun `test custom dictionary path`(helper: DictionaryHelper) {
+ CommonFactory(FactorySettings().apply {
+ helper.setOption(this, CUSTOM_DIR_PATH)
+ }).use { commonFactory ->
+ val content = helper.writeDictionaryByCustomPath(CUSTOM_DIR_PATH, DICTIONARY_NAME)
+ assertDictionary(commonFactory, content)
+
+ assertThrowsConfigs(commonFactory)
+ }
}
- }
- @Test
- @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = CONFIG_DIR_IN_RESOURCE)
- fun `test load config by custom path (createFromArguments(not empty) + environment variable)`() {
- CommonFactory.createFromArguments("-c", CONFIG_DIR_IN_RESOURCE).use { commonFactory ->
- assertConfigs(commonFactory, Path.of(CONFIG_DIR_IN_RESOURCE))
+ @ParameterizedTest
+ @EnumSource(ConfigHelper::class)
+ fun `test custom path for config`(helper: ConfigHelper) {
+ val configPath = CUSTOM_DIR_PATH.resolve(helper.alias)
+ CommonFactory(FactorySettings().apply {
+ helper.setOption(this, configPath)
+ }).use { commonFactory ->
+ assertThrowsDictionaryDir(commonFactory)
+
+ val cfgBean = helper.writeConfigByCustomPath(configPath)
+ assertThat(helper.loadConfig(commonFactory), samePropertyValuesAs(cfgBean))
+ }
+ }
+
+ @Test
+ fun `test with custom config path`() {
+ CommonFactory(FactorySettings().apply {
+ baseConfigDir = CMD_ARG_CFG_DIR_PATH
+ }).use { commonFactory ->
+ assertDictionaryDirs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ assertConfigs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = DictionaryHelper::class)
+ fun `test with custom config path and custom dictionary path`(helper: DictionaryHelper) {
+ val settings = FactorySettings().apply {
+ baseConfigDir = CMD_ARG_CFG_DIR_PATH
+ helper.setOption(this, CMD_ARG_CFG_DIR_PATH)
+ }
+ CommonFactory(settings).use { commonFactory ->
+ val content = helper.writeDictionaryByCustomPath(CMD_ARG_CFG_DIR_PATH, DICTIONARY_NAME)
+ assertDictionary(commonFactory, content)
+
+ assertConfigs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(ConfigHelper::class)
+ fun `test with custom config path and custom path for config`(helper: ConfigHelper) {
+ val configPath = CUSTOM_DIR_PATH.resolve(helper.alias)
+ CommonFactory(FactorySettings().apply {
+ baseConfigDir = CMD_ARG_CFG_DIR_PATH
+ helper.setOption(this, configPath)
+ }).use { commonFactory ->
+ assertDictionaryDirs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+
+ val cfgBean = helper.writeConfigByCustomPath(configPath)
+ assertThat(helper.loadConfig(commonFactory), samePropertyValuesAs(cfgBean))
+ }
+ }
+
+ @ParameterizedTest
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ @EnumSource(DictionaryHelper::class)
+ fun `test with system property`(helper: DictionaryHelper) {
+ CommonFactory(FactorySettings()).use { commonFactory ->
+ val content = helper.writeDictionaryByDefaultPath(SYSTEM_PROPERTY_CFG_DIR_PATH, DICTIONARY_NAME)
+ assertDictionary(commonFactory, content)
+
+ assertConfigs(commonFactory, SYSTEM_PROPERTY_CFG_DIR_PATH)
+ }
+ }
+
+ @ParameterizedTest
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ @EnumSource(value = DictionaryHelper::class)
+ fun `test with system property and custom dictionary path`(helper: DictionaryHelper) {
+ val factorySettings = FactorySettings().apply {
+ helper.setOption(this, CUSTOM_DIR_PATH)
+ }
+ CommonFactory(factorySettings).use { commonFactory ->
+ val content = helper.writeDictionaryByCustomPath(CUSTOM_DIR_PATH, DICTIONARY_NAME)
+ assertDictionary(commonFactory, content)
+
+ assertConfigs(commonFactory, SYSTEM_PROPERTY_CFG_DIR_PATH)
+ }
+ }
+
+ @ParameterizedTest
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ @EnumSource(ConfigHelper::class)
+ fun `test with system property and custom path for config`(helper: ConfigHelper) {
+ val configPath = CUSTOM_DIR_PATH.resolve(helper.alias)
+ val factorySettings = FactorySettings().apply {
+ helper.setOption(this, configPath)
+ }
+ CommonFactory(factorySettings).use { commonFactory ->
+ assertDictionaryDirs(commonFactory, SYSTEM_PROPERTY_CFG_DIR_PATH)
+
+ val cfgBean = helper.writeConfigByCustomPath(configPath)
+ assertThat(helper.loadConfig(commonFactory), samePropertyValuesAs(cfgBean))
+ }
+ }
+
+ @Test
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ fun `test with cmd config argument and system property`() {
+ CommonFactory(FactorySettings().apply {
+ baseConfigDir = CMD_ARG_CFG_DIR_PATH
+ }).use { commonFactory ->
+ assertDictionaryDirs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ assertConfigs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ }
}
- }
+ @ParameterizedTest
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ @EnumSource(value = DictionaryHelper::class)
+ fun `test with cmd config argument and system property and custom dictionary path`(helper: DictionaryHelper) {
+ val settings = FactorySettings().apply {
+ baseConfigDir = CMD_ARG_CFG_DIR_PATH
+ helper.setOption(this, CUSTOM_DIR_PATH)
+ }
+ CommonFactory(settings).use { commonFactory ->
+ val content = helper.writeDictionaryByCustomPath(CUSTOM_DIR_PATH, DICTIONARY_NAME)
+ assertDictionary(commonFactory, content)
- private fun assertConfigs(commonFactory: CommonFactory, configPath: Path) {
- CONFIG_NAME_TO_COMMON_FACTORY_SUPPLIER.forEach { (configName, actualPathSupplier) ->
- assertEquals(configPath.resolve(configName), commonFactory.actualPathSupplier(), "Configured config path: $configPath, config name: $configName")
+ assertConfigs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ }
+ }
+
+ @ParameterizedTest
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ @EnumSource(ConfigHelper::class)
+ fun `test with cmd config argument and system property and custom path for config`(helper: ConfigHelper) {
+ val configPath = CUSTOM_DIR_PATH.resolve(helper.alias)
+ CommonFactory(FactorySettings().apply {
+ baseConfigDir = CMD_ARG_CFG_DIR_PATH
+ helper.setOption(this, configPath)
+ }).use { commonFactory ->
+ assertDictionaryDirs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+
+ val cfgBean = helper.writeConfigByCustomPath(configPath)
+ assertThat(helper.loadConfig(commonFactory), samePropertyValuesAs(cfgBean))
+ }
}
- assertConfigurationManager(commonFactory, configPath)
}
- private fun assertConfigurationManager(commonFactory: CommonFactory, configPath: Path) {
- CONFIG_CLASSES.forEach { clazz ->
- assertNotNull(commonFactory.configurationManager[clazz])
- assertEquals(configPath, commonFactory.configurationManager[clazz]?.parent , "Configured config path: $configPath, config class: $clazz")
+ @Nested
+ @ClearSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY)
+ inner class CreateFromArguments {
+ @Test
+ fun `test without parameters`() {
+ CommonFactory.createFromArguments().use { commonFactory ->
+ assertThrowsDictionaryDir(commonFactory)
+ assertThrowsConfigs(commonFactory)
+ }
+ }
+
+ @Test
+ fun `test with cmd config argument`() {
+ CommonFactory.createFromArguments("-c", CMD_ARG_CFG_DIR).use { commonFactory ->
+ assertDictionaryDirs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ assertConfigs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ }
+ }
+
+ @Test
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ fun `test with system property`() {
+ CommonFactory.createFromArguments().use { commonFactory ->
+ assertDictionaryDirs(commonFactory, SYSTEM_PROPERTY_CFG_DIR_PATH)
+ assertConfigs(commonFactory, SYSTEM_PROPERTY_CFG_DIR_PATH)
+ }
+ }
+
+ @Test
+ @SetSystemProperty(key = TH2_COMMON_CONFIGURATION_DIRECTORY_SYSTEM_PROPERTY, value = SYSTEM_PROPERTY_CFG_DIR)
+ fun `test with cmd config argument and system property`() {
+ CommonFactory.createFromArguments("-c", CMD_ARG_CFG_DIR).use { commonFactory ->
+ assertDictionaryDirs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ assertConfigs(commonFactory, CMD_ARG_CFG_DIR_PATH)
+ }
}
}
companion object {
- private const val CONFIG_DIR_IN_RESOURCE = "src/test/resources/test_common_factory_load_configs"
-
- private val CONFIG_NAME_TO_COMMON_FACTORY_SUPPLIER: Map Path> = mapOf(
- CUSTOM_FILE_NAME to { pathToCustomConfiguration },
- DICTIONARY_ALIAS_DIR_NAME to { pathToDictionaryAliasesDir },
- DICTIONARY_TYPE_DIR_NAME to { pathToDictionaryTypesDir },
- )
-
- private val CONFIG_CLASSES: Set> = setOf(
- RabbitMQConfiguration::class.java,
- MessageRouterConfiguration::class.java,
- ConnectionManagerConfiguration::class.java,
- GrpcConfiguration::class.java,
- GrpcRouterConfiguration::class.java,
- CradleConfidentialConfiguration::class.java,
- CradleNonConfidentialConfiguration::class.java,
- CassandraStorageSettings::class.java,
- PrometheusConfiguration::class.java,
- BoxConfiguration::class.java,
- )
+ private const val TEMP_DIR = "build/tmp/test/common-factory"
+ private const val CMD_ARG_CFG_DIR = "$TEMP_DIR/test-cmd-arg-config"
+ private const val SYSTEM_PROPERTY_CFG_DIR = "$TEMP_DIR/test-system-property-config"
+ private const val CUSTOM_DIR = "$TEMP_DIR/test-custom-dictionary-path"
+
+ private val CMD_ARG_CFG_DIR_PATH = CMD_ARG_CFG_DIR.toPath()
+ private val SYSTEM_PROPERTY_CFG_DIR_PATH = SYSTEM_PROPERTY_CFG_DIR.toPath()
+ private val CUSTOM_DIR_PATH = CUSTOM_DIR.toPath()
+
+ private val DICTIONARY_NAME = MAIN.name
+
+ private fun String.toPath() = Path.of(this)
+
+ // FIXME: find another way to check default dictionary path
+ private fun assertThrowsDictionaryDir(commonFactory: CommonFactory) {
+ val exception = assertThrows("Search dictionary by default path") {
+ commonFactory.readDictionary()
+ }
+ val message = assertNotNull(exception.message, "Exception message is null")
+ assertAll(
+ {
+ assertContains(
+ message,
+ other = CONFIG_DEFAULT_PATH.resolve("dictionaries").absolutePathString(),
+ message = "Exception message contains alias dictionaries path"
+ )
+ },
+ {
+ assertContains(
+ message,
+ other = CONFIG_DEFAULT_PATH.resolve("dictionary").absolutePathString(),
+ message = "Exception message contains type dictionaries path"
+ )
+ },
+ {
+ assertContains(
+ message,
+ other = CONFIG_DEFAULT_PATH.absolutePathString(),
+ message = "Exception message contains old dictionaries path"
+ )
+ }
+ )
+ }
+
+ private fun assertDictionaryDirs(commonFactory: CommonFactory, basePath: Path) {
+ assertAll(
+ {
+ val content = ALIAS.writeDictionaryByDefaultPath(basePath, MAIN.name)
+ ALIAS.assertDictionary(commonFactory, MAIN, content)
+ },
+ {
+ val content = TYPE.writeDictionaryByDefaultPath(basePath, INCOMING.name)
+ TYPE.assertDictionary(commonFactory, INCOMING, content)
+ },
+ {
+ val content = OLD.writeDictionaryByDefaultPath(basePath, OUTGOING.name)
+ OLD.assertDictionary(commonFactory, OUTGOING, content)
+ }
+ )
+ }
+
+ // FIXME: find another way to check default config path
+ private fun assertThrowsConfigs(commonFactory: CommonFactory) {
+ assertAll(
+ *ConfigHelper.values().asSequence()
+ .filter { it != ConfigHelper.PROMETHEUS_CFG_ALIAS && it != ConfigHelper.BOX_CFG_ALIAS }
+ .map { helper ->
+ {
+ val exception = assertThrows(
+ "Search config $helper by default path"
+ ) {
+ helper.loadConfig(commonFactory)
+ }
+ val message = assertNotNull(exception.message, "Exception message is null")
+ assertContains(message, other = CONFIG_DEFAULT_PATH.resolve("${helper.alias}.json").absolutePathString(), message = "Exception message contains alias config path")
+ }
+ }.toList().toTypedArray()
+ )
+
+ val provider = assertProvider(commonFactory)
+ assertEquals(CONFIG_DEFAULT_PATH, provider.baseDir)
+ assertEquals(0, provider.customPaths.size)
+ }
+
+ private fun assertConfigs(commonFactory: CommonFactory, configPath: Path) {
+ assertAll(
+ ConfigHelper.values().map { helper ->
+ {
+ val cfgBean = helper.writeConfigByDefaultPath(configPath)
+ assertThat(helper.loadConfig(commonFactory), samePropertyValuesAs(cfgBean))
+ }
+ }
+ )
+ }
+
+ private fun assertProvider(commonFactory: CommonFactory): JsonConfigurationProvider {
+ assertEquals(JsonConfigurationProvider::class, commonFactory.configurationProvider::class)
+ return JsonConfigurationProvider::class.cast(commonFactory.getConfigurationProvider())
+ }
+
+ data class TestCustomConfig(val testField: String = "test-value")
+
+ enum class ConfigHelper(
+ val alias: String,
+ private val configClass: Class<*>
+ ) {
+ RABBIT_MQ_CFG_ALIAS("rabbitMQ", RabbitMQConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.rabbitMQ = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): RabbitMQConfiguration = RabbitMQConfiguration(
+ "test-host",
+ "test-vHost",
+ 1234,
+ "test-username",
+ "test-password",
+ ).also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ ROUTER_MQ_CFG_ALIAS("mq", MessageRouterConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.routerMQ = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): MessageRouterConfiguration = MessageRouterConfiguration()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ CONNECTION_MANAGER_CFG_ALIAS("mq_router", ConnectionManagerConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.connectionManagerSettings = filePath
+ }
+ override fun writeConfigByCustomPath(filePath: Path): ConnectionManagerConfiguration = ConnectionManagerConfiguration()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ GRPC_CFG_ALIAS("grpc", GrpcConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.grpc = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): GrpcConfiguration = GrpcConfiguration()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ ROUTER_GRPC_CFG_ALIAS("grpc_router", GrpcRouterConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.routerGRPC = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): GrpcRouterConfiguration = GrpcRouterConfiguration()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ CRADLE_CONFIDENTIAL_CFG_ALIAS("cradle", CradleConfidentialConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.cradleConfidential = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): CradleConfidentialConfiguration = CradleConfidentialConfiguration(
+ "test-dataCenter",
+ "test-host",
+ "test-keyspace",
+ ).also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ CRADLE_NON_CONFIDENTIAL_CFG_ALIAS("cradle_manager", CradleNonConfidentialConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.cradleNonConfidential = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): CradleNonConfidentialConfiguration = CradleNonConfidentialConfiguration()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ PROMETHEUS_CFG_ALIAS("prometheus", PrometheusConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.prometheus = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): PrometheusConfiguration = PrometheusConfiguration()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ BOX_CFG_ALIAS("box", BoxConfiguration::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.boxConfiguration = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): BoxConfiguration = BoxConfiguration()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ },
+ CUSTOM_CFG_ALIAS("custom", TestCustomConfig::class.java) {
+ override fun setOption(settings: FactorySettings, filePath: Path) {
+ settings.custom = filePath
+ }
+
+ override fun writeConfigByCustomPath(filePath: Path): TestCustomConfig = TestCustomConfig()
+ .also { CommonFactory.MAPPER.writeValue(filePath.toFile(), it) }
+ };
+
+ abstract fun setOption(settings: FactorySettings, filePath: Path)
+
+ abstract fun writeConfigByCustomPath(filePath: Path): Any
+
+ open fun writeConfigByDefaultPath(configPath: Path): Any = writeConfigByCustomPath(configPath.resolve("${alias}.json"))
+
+ fun loadConfig(factory: CommonFactory): Any =
+ factory.getConfigurationProvider().load(alias, configClass)
+ }
+
+ enum class DictionaryHelper {
+ ALIAS {
+ override fun setOption(settings: FactorySettings, path: Path) {
+ settings.dictionaryAliasesDir = path
+ }
+
+ override fun writeDictionaryByDefaultPath(basePath: Path, fileName: String): String {
+ return DictionaryHelper.writeDictionary(basePath.resolve("dictionaries").resolve(fileName))
+ }
+
+ override fun writeDictionaryByCustomPath(basePath: Path, fileName: String): String {
+ return DictionaryHelper.writeDictionary(basePath.resolve(fileName))
+ }
+ },
+ TYPE {
+ override fun setOption(settings: FactorySettings, path: Path) {
+ settings.dictionaryTypesDir = path
+ }
+
+ override fun writeDictionaryByDefaultPath(basePath: Path, fileName: String): String {
+ return DictionaryHelper.writeDictionary(basePath.resolve("dictionary").resolve(fileName.lowercase(
+ Locale.getDefault()
+ )).resolve(fileName))
+ }
+
+ override fun writeDictionaryByCustomPath(basePath: Path, fileName: String): String {
+ return DictionaryHelper.writeDictionary(basePath.resolve(fileName.lowercase(
+ Locale.getDefault()
+ )).resolve(fileName))
+ }
+ },
+ OLD {
+ override fun setOption(settings: FactorySettings, path: Path) {
+ settings.oldDictionariesDir = path
+ }
+
+ override fun writeDictionaryByDefaultPath(basePath: Path, fileName: String): String {
+ return DictionaryHelper.writeDictionary(basePath.resolve(fileName))
+ }
+
+ override fun writeDictionaryByCustomPath(basePath: Path, fileName: String): String {
+ return DictionaryHelper.writeDictionary(basePath.resolve(fileName))
+ }
+ };
+
+ abstract fun setOption(settings: FactorySettings, path: Path)
+ abstract fun writeDictionaryByDefaultPath(basePath: Path, fileName: String): String
+ abstract fun writeDictionaryByCustomPath(basePath: Path, fileName: String): String
+
+ fun assertDictionary(factory: CommonFactory, type: DictionaryType, content: String) {
+ assertEquals(content, factory.readDictionary(type).use { String(it.readAllBytes()) })
+ }
+
+ companion object {
+ fun assertDictionary(factory: CommonFactory, content: String) {
+ assertEquals(content, factory.readDictionary().use { String(it.readAllBytes()) })
+ }
+
+ private fun writeDictionary(path: Path): String {
+ val content = RandomStringUtils.randomAlphanumeric(10)
+ path.parent.createDirectories()
+ path.writeBytes(ArchiveUtils.getGzipBase64StringEncoder().encode(content))
+ return content
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/test/resources/test_common_factory_load_configs/custom.json b/src/test/resources/test_cmd_arg_config/custom.json
similarity index 100%
rename from src/test/resources/test_common_factory_load_configs/custom.json
rename to src/test/resources/test_cmd_arg_config/custom.json
diff --git a/src/test/resources/test_system_property_config/custom.json b/src/test/resources/test_system_property_config/custom.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/src/test/resources/test_system_property_config/custom.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file