All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.builder.api.ProvidedUtil Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023 Oracle and/or its affiliates.
 *
 * 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 io.helidon.builder.api;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.config.Config;
import io.helidon.common.config.ConfigException;
import io.helidon.common.config.ConfiguredProvider;
import io.helidon.common.config.NamedService;

final class ProvidedUtil {
    private static final System.Logger PROVIDER_LOGGER = System.getLogger(Prototype.class.getName() + ".provider");
    /**
     * Special configuration key that can be defined on provided options (loaded through ServiceLoader) that defines
     * the mapping to a provider.
     * Type of service, used to map to {@link io.helidon.common.config.ConfiguredProvider#configKey()}, which is the
     * default "type" of a configured provider. It is then used in {@link io.helidon.common.config.NamedService#type()},
     * to allow multiple instances of the same "type" with different "name".
     */
    private static final String KEY_SERVICE_TYPE = "type";
    /**
     * Special configuration key that can be defined on provided options (loaded through ServiceLoader) that defines
     * the name of an instance.
     * It is then used in {@link io.helidon.common.config.NamedService#name()}
     * to allow multiple instances of the same "type" with different "name".
     * In case of object type configurations, name is taken from the configuration node name.
     */
    private static final String KEY_SERVICE_NAME = "name";
    /**
     * Special configuration key that can be defined on provided options (loaded through ServiceLoader) that defines
     * whether a service provider of the type is enabled.
     * Each service from a {@link io.helidon.common.config.ConfiguredProvider} can be enabled/disabled through
     * configuration. If marked as {@code enabled = false}, the service will be ignored and not added.
     */
    private static final String KEY_SERVICE_ENABLED = "enabled";

    private ProvidedUtil() {
    }

    /**
     * Discover service from configuration.
     * This method looks for a single provider only.
     *
     * @param config           configuration located at the parent node of the service providers
     * @param configKey        configuration key of the provider list
     *                         (either a list node, or object, where each child is one service)
     * @param serviceLoader    helidon service loader for the expected type
     * @param providerType     service provider interface type
     * @param configType       configured service type
     * @param discoverServices whether all services from service loader should be used, or only the ones with configured
     *                         node
     * @param existingValue    value already configured, if the name is same as discovered from configuration
     * @param               type of the expected service
     * @return first service by {@link io.helidon.common.Weight}, or empty optional
     */
    @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
    static  Optional
    discoverService(Config config,
                    String configKey,
                    HelidonServiceLoader> serviceLoader,
                    Class> providerType,
                    Class configType,
                    boolean discoverServices,
                    Optional existingValue) {

        // there is an explicit configuration for this service, ignore configuration
        if (existingValue.isPresent()) {
            return Optional.empty();
        }

        // all child nodes of the current node
        List serviceConfigList = config.get(configKey).asNodeList()
                .orElseGet(List::of);

        // if more than one is configured in config, fail
        // if more than one exists in service loader, use the first one
        if (serviceConfigList.size() > 1) {
            throw new ConfigException("There can only be one provider configured for " + config.key());
        }

        List services = discoverServices(config,
                                            configKey,
                                            serviceLoader,
                                            providerType,
                                            configType,
                                            discoverServices,
                                            List.of());

        return services.isEmpty() ? Optional.empty() : Optional.of(services.getFirst());
    }

    /**
     * Discover services from configuration.
     *
     * @param config               configuration located at the parent node of the service providers
     * @param configKey            configuration key of the provider list
     *                             (either a list node, or object, where each child is one service)
     * @param serviceLoader        helidon service loader for the expected type
     * @param providerType         service provider interface type
     * @param configType           configured service type
     * @param allFromServiceLoader whether all services from service loader should be used, or only the ones with configured
     *                             node
     * @param existingInstances    already configured instances
     * @param                   type of the expected service
     * @return list of discovered services, ordered by {@link io.helidon.common.Weight} (highest weight first)
     */
    static  List discoverServices(Config config,
                                                             String configKey,
                                                             HelidonServiceLoader> serviceLoader,
                                                             Class> providerType,
                                                             Class configType,
                                                             boolean allFromServiceLoader,
                                                             List existingInstances) {

        // type and name is a unique identification of a service - for services already defined on the builder
        // do not add them from configuration (as this would duplicate service instances)
        Set ignoredServices = new HashSet<>();
        existingInstances.forEach(it -> ignoredServices.add(new TypeAndName(it.type(), it.name())));

        boolean discoverServices = config.get(configKey + "-discover-services").asBoolean().orElse(allFromServiceLoader);
        Config providersConfig = config.get(configKey);

        List configuredServices = new ArrayList<>();

        // all child nodes of the current node
        List serviceConfigList = providersConfig.asNodeList()
                .orElseGet(List::of);
        boolean isList = providersConfig.isList();

        for (Config serviceConfig : serviceConfigList) {
            configuredServices.add(configuredService(serviceConfig, isList));
        }

        // now we have all service configurations, we can start building up instances
        if (providersConfig.isList()) {
            // driven by order of declaration in config
            return servicesFromList(serviceLoader,
                                    providerType,
                                    configType,
                                    configuredServices,
                                    discoverServices,
                                    ignoredServices);
        } else {
            // driven by service loader order
            return servicesFromObject(providersConfig,
                                      serviceLoader,
                                      providerType,
                                      configType,
                                      configuredServices,
                                      discoverServices,
                                      ignoredServices);
        }
    }

    private static  List
    servicesFromObject(Config providersConfig,
                       HelidonServiceLoader> serviceLoader,
                       Class> providerType,
                       Class configType,
                       List configuredServices,
                       boolean allFromServiceLoader,
                       Set ignoredServices) {
        // order is determined by service loader
        Set availableProviders = new HashSet<>();
        Map allConfigs = new HashMap<>();
        configuredServices.forEach(it -> allConfigs.put(it.typeAndName().type, it));
        Set unusedConfigs = new HashSet<>(allConfigs.keySet());

        List result = new ArrayList<>();

        serviceLoader.forEach(provider -> {
            ConfiguredService configuredService = allConfigs.get(provider.configKey());
            availableProviders.add(provider.configKey());
            unusedConfigs.remove(provider.configKey());
            if (configuredService == null) {
                if (allFromServiceLoader) {
                    // even though the specific key does not exist, we want to have the real config tree, so we can get to the
                    // root of it
                    // when there is no configuration, the name defaults to the type
                    String type = provider.configKey();
                    if (ignoredServices.add(new TypeAndName(type, type))) {
                        result.add(provider.create(providersConfig.get(type), type));
                    } else {
                        if (PROVIDER_LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                            PROVIDER_LOGGER.log(System.Logger.Level.DEBUG, "Service: " + new TypeAndName(type, type)
                                    + " is already added in builder, ignoring configured one.");
                        }
                    }
                }
            } else {
                if (configuredService.enabled()) {
                    if (ignoredServices.add(configuredService.typeAndName())) {
                        result.add(provider.create(configuredService.serviceConfig(),
                                                   configuredService.typeAndName().name()));
                    } else {
                        if (PROVIDER_LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                            PROVIDER_LOGGER.log(System.Logger.Level.DEBUG, "Service: " + configuredService.typeAndName()
                                    + " is already added in builder, ignoring configured one.");
                        }
                    }
                }
            }
        });
        if (!unusedConfigs.isEmpty()) {
            throw new ConfigException("Unknown provider configured. Expected providers with types: " + unusedConfigs
                                              + ", but only the following providers are supported: " + availableProviders
                                              + ", provider interface: " + providerType.getName()
                                              + ", configured service: " + configType.getName());
        }
        return result;
    }

    private static  List
    servicesFromList(HelidonServiceLoader> serviceLoader,
                     Class> providerType,
                     Class configType,
                     List configuredServices,
                     boolean allFromServiceLoader,
                     Set ignoredServices) {
        Map> allProvidersByType = new HashMap<>();
        Map> unusedProvidersByType = new LinkedHashMap<>();
        serviceLoader.forEach(it -> {
            allProvidersByType.put(it.configKey(), it);
            unusedProvidersByType.put(it.configKey(), it);
        });

        List result = new ArrayList<>();

        // first add all configured
        for (ConfiguredService service : configuredServices) {
            TypeAndName typeAndName = service.typeAndName();
            if (!ignoredServices.add(typeAndName)) {
                unusedProvidersByType.remove(typeAndName.type());

                if (PROVIDER_LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                    PROVIDER_LOGGER.log(System.Logger.Level.DEBUG, "Service: " + typeAndName
                            + " is already added in builder, ignoring configured one.");
                }

                continue;
            }
            ConfiguredProvider provider = allProvidersByType.get(typeAndName.type());
            if (provider == null) {
                throw new ConfigException("Unknown provider configured. Expecting a provider with type \"" + typeAndName.type()
                                                  + "\", but only the following providers are supported: "
                                                  + allProvidersByType.keySet() + ", "
                                                  + "provider interface: " + providerType.getName()
                                                  + ", configured service: " + configType.getName());
            }
            unusedProvidersByType.remove(typeAndName.type());
            if (service.enabled()) {
                result.add(provider.create(service.serviceConfig(), typeAndName.name()));
            }
        }

        // then (if desired) add the rest
        if (allFromServiceLoader) {
            unusedProvidersByType.forEach((type, provider) -> {
                if (ignoredServices.add(new TypeAndName(type, type))) {
                    result.add(provider.create(Config.empty(), type));
                }
            });
        }

        return result;
    }

    private static ConfiguredService configuredService(Config serviceConfig, boolean isList) {
        if (isList) {
            // order is significant
            String type = serviceConfig.get(KEY_SERVICE_TYPE).asString().orElse(null);
            String name = serviceConfig.get(KEY_SERVICE_NAME).asString().orElse(type);
            boolean enabled = serviceConfig.get(KEY_SERVICE_ENABLED).asBoolean().orElse(true);

            Config usedConfig = serviceConfig;
            if (type == null) {
                // nested approach (we are on the first node of the list, we need to go deeper)
                List configs = serviceConfig.asNodeList().orElseGet(List::of);
                if (configs.size() != 1) {
                    throw new ConfigException(
                            "Service provider configuration defined as a list must have a single node that is the type, "
                                    + "with children containing the provider configuration. Failed on: " + serviceConfig.key());
                }
                usedConfig = configs.get(0);
                name = usedConfig.name();
                type = usedConfig.get(KEY_SERVICE_TYPE).asString().orElse(name);
                enabled = usedConfig.get(KEY_SERVICE_ENABLED).asBoolean().orElse(enabled);
            }
            return new ConfiguredService(new TypeAndName(type, name), usedConfig, enabled);
        }
        // just collect each node, order will be determined by weight

        String name = serviceConfig.name(); // name is the config node name for object types
        String type = serviceConfig.get(KEY_SERVICE_TYPE).asString().orElse(name);
        boolean enabled = serviceConfig.get(KEY_SERVICE_ENABLED).asBoolean().orElse(true);

        return new ConfiguredService(new TypeAndName(type, name), serviceConfig, enabled);
    }

    private record TypeAndName(String type, String name) {
    }

    private record ConfiguredService(TypeAndName typeAndName, Config serviceConfig, boolean enabled) {
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy