diff --git a/README.md b/README.md index 872070a7f..9e23f15c6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2 common library (Java) (5.13.1) +# th2 common library (Java) (5.14.0) ## Usage @@ -511,6 +511,11 @@ dependencies { ## Release notes +### 5.14.0-dev +#### Feature: ++ Added common microservice entry point ++ Added configuration provider to common factory + ### 5.13.1-dev + Provided ability to set either of raw body of several dody data to `Event` builder diff --git a/build.gradle b/build.gradle index 0a7562505..97b1c3a54 100644 --- a/build.gradle +++ b/build.gradle @@ -157,6 +157,7 @@ dependencies { implementation "io.github.microutils:kotlin-logging:3.0.5" + testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'javax.annotation:javax.annotation-api:1.3.2' testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:5.2.1" diff --git a/gradle.properties b/gradle.properties index 2c553bc74..f8a2b4afb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -release_version=5.13.1 +release_version=5.14.0 kotlin_version=1.8.22 description='th2 common library (Java)' vcs_url=https://github.com/th2-net/th2-common-j 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 170920614..8b794f4c4 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 @@ -33,13 +33,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; @@ -55,24 +58,22 @@ import com.exactpro.th2.common.schema.message.impl.rabbitmq.connection.ConnectionManager; 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; -import com.fasterxml.jackson.module.kotlin.KotlinFeature; -import com.fasterxml.jackson.module.kotlin.KotlinModule; 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; @@ -94,7 +95,6 @@ import static com.exactpro.th2.common.schema.factory.LazyProvider.lazy; import static com.exactpro.th2.common.schema.factory.LazyProvider.lazyAutocloseable; 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} @@ -111,25 +111,9 @@ public abstract class AbstractCommonFactory implements AutoCloseable { protected static final Path LOG4J_PROPERTIES_DEFAULT_PATH = Path.of("/var/th2/config"); protected static final String LOG4J2_PROPERTIES_NAME = "log4j2.properties"; - 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> messageRouterParsedBatchClass; private final Class> messageRouterRawBatchClass; @@ -168,6 +152,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 * @@ -180,7 +173,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() { @@ -321,11 +313,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); } /** @@ -336,7 +328,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() { @@ -352,7 +344,7 @@ public MessageRouterConfiguration getMessageRouterConfiguration() { } public GrpcConfiguration getGrpcConfiguration() { - return getConfigurationManager().getConfigurationOrLoad(MAPPER, stringSubstitutor, GrpcConfiguration.class, false); + return getConfigurationManager().getConfigurationOrLoad(GrpcConfiguration.class, false); } public GrpcRouterConfiguration getGrpcRouterConfiguration() { @@ -510,17 +502,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); } /** @@ -569,7 +551,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); @@ -606,25 +589,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 8fe4353c3..e0b025bfe 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 @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-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 @@ -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; @@ -42,7 +47,6 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @@ -50,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; @@ -60,18 +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.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.lang3.ObjectUtils.defaultIfNull; /** @@ -83,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 @@ -113,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 IConfigurationProvider configurationProvider; + final IDictionaryProvider dictionaryProvider; 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); + private 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 * @@ -236,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()); @@ -267,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) { @@ -299,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); } @@ -311,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()); } @@ -331,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(); @@ -398,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); @@ -432,125 +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)) { - return files - .filter(Files::isRegularFile) - .map(dictionary -> FilenameUtils.removeExtension(dictionary.getFileName().toString())) - .collect(Collectors.toSet()); - } - } 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 -> FilenameUtils.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 -> FilenameUtils.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); @@ -570,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)); @@ -649,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 new DictionaryProvider(defaultIfNull(settings.getBaseConfigDir(), getConfigPath()), paths); } - private static Path defaultPathIfNull(Path path, String name) { - return path == null ? getConfigPath().resolve(name) : path; + 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 new JsonConfigurationProvider(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 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 { @@ -687,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 Path calculatePath(CommandLine cmd, Option option, @NotNull Path configs, String fileName) { - return calculatePath(cmd.getOptionValue(option.getLongOpt()), configs, fileName); + private static @Nullable Path toPath(@Nullable String path) { + return path == null ? null : Path.of(path); } - 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..de53c7ad9 --- /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 mu.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 f7eb10d60..3f907d07c 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 mu.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..3121b9c2d --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/IDictionaryProvider.kt @@ -0,0 +1,27 @@ +/* + * 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 + +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..3ccafdfee --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/DictionaryProvider.kt @@ -0,0 +1,231 @@ +/* + * 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 @JvmOverloads 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()) + } +} + +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..110a4c7f1 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/common/schema/configuration/impl/JsonConfigurationProvider.kt @@ -0,0 +1,114 @@ +/* + * 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.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 mu.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 @JvmOverloads 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 {} + 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() + ) + } + } +} \ 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/DictionaryLoadTest.kt b/src/test/kotlin/com/exactpro/th2/common/schema/DictionaryLoadTest.kt new file mode 100644 index 000000000..d8c0b9bca --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/common/schema/DictionaryLoadTest.kt @@ -0,0 +1,266 @@ +/* + * Copyright 2020-2022 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 + +import com.exactpro.th2.common.schema.dictionary.DictionaryType +import com.exactpro.th2.common.schema.factory.CommonFactory +import com.exactpro.th2.common.schema.util.ArchiveUtils +import org.apache.commons.lang3.RandomStringUtils +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectories +import kotlin.io.path.writeBytes +import kotlin.io.path.writeText +import kotlin.test.assertEquals + +@Suppress("DEPRECATION") +class DictionaryLoadTest { + + @TempDir + lateinit var tempDir: Path + + @BeforeEach + fun beforeEach() { + writePrometheus(tempDir) + } + + //--//--//--readDictionary()--//--//--// + + @Test + fun `test read dictionary from old dictionary dir`() { + val content = writeDictionary(tempDir.resolve(Path.of("MAIN"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + assertEquals(content, commonFactory.readDictionary().use { String(it.readAllBytes()) }) + } + } + + @Test + fun `test read dictionary from old dictionary dir - file name mismatch`() { + writeDictionary(tempDir.resolve(Path.of("main"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.readDictionary() + } + } + } + + @ParameterizedTest + @ValueSource(strings = ["MAIN", "main", "test-dictionary"]) + 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()) }) + } + } + + @ParameterizedTest + @ValueSource(strings = ["MAIN", "main", "test-dictionary"]) + fun `test read dictionary from type dictionary dir - dictionary name mismatch`( + fileName: String, + + ) { + writeDictionary(tempDir.resolve(Path.of("dictionary", "MAIN", fileName))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.readDictionary() + } + } + } + + @ParameterizedTest + @ValueSource(strings = ["MAIN", "main", "MAIN.xml", "main.json"]) + 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()) }) + } + } + + //--//--//--readDictionary()--//--//--// + + @Test + fun `test read dictionary by type from old dictionary dir`() { + val content = writeDictionary(tempDir.resolve(Path.of("INCOMING"))) + writeDictionary(tempDir.resolve(Path.of("MAIN"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + assertEquals( + content, + commonFactory.readDictionary(DictionaryType.INCOMING).use { String(it.readAllBytes()) }) + } + } + + @Test + fun `test read dictionary by type from old dictionary dir - file name mismatch`() { + writeDictionary(tempDir.resolve(Path.of("incoming"))) + writeDictionary(tempDir.resolve(Path.of("MAIN"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.readDictionary(DictionaryType.INCOMING) + } + } + } + + @ParameterizedTest + @ValueSource(strings = ["INCOMING", "incoming", "test-dictionary"]) + 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 -> + assertEquals( + content, + commonFactory.readDictionary(DictionaryType.INCOMING).use { String(it.readAllBytes()) }) + } + } + + @ParameterizedTest + @ValueSource(strings = ["INCOMING", "incoming", "test-dictionary"]) + fun `test read dictionary by type from type dictionary dir - dictionary name mismatch`( + fileName: String, + + ) { + writeDictionary(tempDir.resolve(Path.of("dictionary", "INCOMING", fileName))) + writeDictionary(tempDir.resolve(Path.of("dictionary", "main", "MAIN"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.readDictionary(DictionaryType.INCOMING) + } + } + } + + @ParameterizedTest + @ValueSource(strings = ["INCOMING", "incoming", "INCOMING.xml", "incoming.json"]) + 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 -> + assertEquals( + content, + commonFactory.readDictionary(DictionaryType.INCOMING).use { String(it.readAllBytes()) }) + } + } + + //--//--//--loadSingleDictionary()--//--//--// + + @Test + fun `test load single dictionary from alias dictionary dir`() { + val content = writeDictionary(tempDir.resolve(Path.of("dictionaries", "test-dictionary"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + assertEquals(content, commonFactory.loadSingleDictionary().use { String(it.readAllBytes()) }) + } + } + + @Test + fun `test load single dictionary from alias dictionary dir - several dictionaries`() { + writeDictionary(tempDir.resolve(Path.of("dictionaries", "test-dictionary-1"))) + writeDictionary(tempDir.resolve(Path.of("dictionaries", "test-dictionary-2"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.loadSingleDictionary() + } + } + } + + @Test + fun `test load single dictionary from alias dictionary dir - several dictionaries with name in different case`() { + writeDictionary(tempDir.resolve(Path.of("dictionaries", "test-dictionary"))) + writeDictionary(tempDir.resolve(Path.of("dictionaries", "TEST-DICTIONARY"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.loadSingleDictionary() + } + } + } + + //--//--//--loadDictionary()--//--//--// + + @ParameterizedTest + @ValueSource(strings = ["TEST-ALIAS", "test-alias"]) + 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 -> + assertEquals(content, commonFactory.loadDictionary("test-alias").use { String(it.readAllBytes()) }) + } + } + + @ParameterizedTest + @ValueSource(strings = ["TEST-DICTIONARY", "test-dictionary"]) + fun `test load dictionary by alias from alias dictionary dir - several dictionaries with name in different case`( + alias: String, + + ) { + writeDictionary(tempDir.resolve(Path.of("dictionaries", "test-dictionary"))) + writeDictionary(tempDir.resolve(Path.of("dictionaries", "TEST-DICTIONARY"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.loadDictionary(alias) + } + } + } + + //--//--//--loadAliases--//--//--// + + @Test + fun `test dictionary aliases from alias dictionary dir`() { + val alias1 = "test-dictionary-1" + val alias2 = "TEST-DICTIONARY-2" + val file1 = "$alias1.xml" + val file2 = "$alias2.json" + writeDictionary(tempDir.resolve(Path.of("dictionaries", file1))) + writeDictionary(tempDir.resolve(Path.of("dictionaries", file2))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + assertEquals(setOf(alias1, alias2.lowercase()), commonFactory.dictionaryAliases) + } + } + + @Test + fun `test dictionary aliases from alias dictionary dir - empty directory`() { + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + assertEquals(emptySet(), commonFactory.dictionaryAliases) + } + } + + @Test + fun `test dictionary aliases from alias dictionary dir - several dictionaries with name in different case`() { + writeDictionary(tempDir.resolve(Path.of("dictionaries", "test-dictionary"))) + writeDictionary(tempDir.resolve(Path.of("dictionaries", "TEST-DICTIONARY"))) + CommonFactory.createFromArguments("-c", tempDir.absolutePathString()).use { commonFactory -> + Assertions.assertThrows(IllegalStateException::class.java) { + commonFactory.dictionaryAliases + } + } + } + + //--//--//--Others--//--//--// + + private fun writePrometheus(cfgPath: Path) { + cfgPath.resolve("prometheus.json").writeText("{\"enabled\":false}") + } + + 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/kotlin/com/exactpro/th2/common/schema/TestDictionaryLoad.kt b/src/test/kotlin/com/exactpro/th2/common/schema/TestDictionaryLoad.kt deleted file mode 100644 index 76fae29fa..000000000 --- a/src/test/kotlin/com/exactpro/th2/common/schema/TestDictionaryLoad.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2020-2022 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 - -import com.exactpro.th2.common.schema.dictionary.DictionaryType -import com.exactpro.th2.common.schema.factory.CommonFactory -import com.exactpro.th2.common.schema.factory.FactorySettings -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import java.nio.file.Path - -class TestDictionaryLoad { - - @Test - fun `test file load dictionary`() { - val factory = CommonFactory.createFromArguments("-c", "src/test/resources/test_load_dictionaries") - - factory.readDictionary().use { - assert(String(it.readAllBytes()) == "test file") - } - } - - @Test - fun `test folder load dictionary`() { - val factory = CommonFactory.createFromArguments("-c", "src/test/resources/test_load_dictionaries") - - factory.readDictionary(DictionaryType.LEVEL1).use { - assert(String(it.readAllBytes()) == "test file") - } - } - - @Test - fun `test folder load dictionaries by alias`() { - val factory = CommonFactory.createFromArguments("-c", "src/test/resources/test_load_dictionaries") - - Assertions.assertDoesNotThrow { - factory.loadDictionary("test_alias_2").use { - assert(String(it.readAllBytes()) == "test file") - } - } - } - - @Test - fun `test folder load all dictionary aliases`() { - val factory = CommonFactory.createFromArguments("-c", "src/test/resources/test_load_dictionaries") - val expectedNames = listOf("main", "test_alias_1", "test_alias_2", "test_alias_3", "test_alias_4") - val names = factory.dictionaryAliases - Assertions.assertEquals(5, names.size) - Assertions.assertTrue(names.containsAll(expectedNames)) - } - - @Test - fun `test folder load single dictionary from folder with several`() { - val factory = CommonFactory.createFromArguments("-c", "src/test/resources/test_load_dictionaries") - - Assertions.assertThrows(IllegalStateException::class.java) { - factory.loadSingleDictionary() - } - } - - @Test - fun `test folder load single dictionary`() { - val customSettings = FactorySettings().apply { - prometheus = Path.of("src/test/resources/test_load_dictionaries/prometheus.json") - dictionaryAliasesDir = Path.of("src/test/resources/test_load_dictionaries/single_dictionary") - } - val customFactory = CommonFactory(customSettings) - - customFactory.loadSingleDictionary().use { - assert(String(it.readAllBytes()) == "test file") - } - } - - @Test - fun `test folder load single dictionary by type as alias`() { - val customSettings = FactorySettings().apply { - prometheus = Path.of("src/test/resources/test_load_dictionaries/prometheus.json") - dictionaryTypesDir = Path.of("..") - dictionaryAliasesDir = Path.of("src/test/resources/test_load_dictionaries/dictionaries") - } - val customFactory = CommonFactory(customSettings) - - customFactory.readDictionary().use { - assert(String(it.readAllBytes()) == "test file") - } - } - -} \ No newline at end of file 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_load_dictionaries/dictionaries/main b/src/test/resources/test_load_dictionaries/dictionaries/main deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/dictionaries/main +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_1.encoded b/src/test/resources/test_load_dictionaries/dictionaries/test_alias_1.encoded deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_1.encoded +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_2 b/src/test/resources/test_load_dictionaries/dictionaries/test_alias_2 deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_2 +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_3.encoded b/src/test/resources/test_load_dictionaries/dictionaries/test_alias_3.encoded deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_3.encoded +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_4 b/src/test/resources/test_load_dictionaries/dictionaries/test_alias_4 deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/dictionaries/test_alias_4 +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/level1/test_dictionary.encoded b/src/test/resources/test_load_dictionaries/level1/test_dictionary.encoded deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/level1/test_dictionary.encoded +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/prometheus.json b/src/test/resources/test_load_dictionaries/prometheus.json deleted file mode 100644 index c994fef1c..000000000 --- a/src/test/resources/test_load_dictionaries/prometheus.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "enabled": false -} \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/single_dictionary/test_alias_1.encoded b/src/test/resources/test_load_dictionaries/single_dictionary/test_alias_1.encoded deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/single_dictionary/test_alias_1.encoded +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/test_dictionary_LEVEL1.encoded b/src/test/resources/test_load_dictionaries/test_dictionary_LEVEL1.encoded deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/test_dictionary_LEVEL1.encoded +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file diff --git a/src/test/resources/test_load_dictionaries/test_dictionary_MAIN.encoded b/src/test/resources/test_load_dictionaries/test_dictionary_MAIN.encoded deleted file mode 100644 index 0f3ff0936..000000000 --- a/src/test/resources/test_load_dictionaries/test_dictionary_MAIN.encoded +++ /dev/null @@ -1 +0,0 @@ -H4sIAAAAAAAAACtJLS5RSMvMSQUAwWtk8gkAAAA= \ No newline at end of file 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