001/*
002 * JDrupes Builder
003 * Copyright (C) 2025 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.builder.api;
020
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.ServiceLoader;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.stream.StreamSupport;
027
028/// Defines both an interface for factories that create [Resource]s and
029/// factory methods for invoking an appropriate factory.
030///
031@SuppressWarnings("PMD.ImplicitFunctionalInterface")
032public interface ResourceFactory {
033
034    /// The factories as found by the [ServiceLoader].
035    Map<ClassLoader, List<ResourceFactory>> FACTORIES
036        = new ConcurrentHashMap<>();
037
038    /// Returns a new resource with the given type, passing the given
039    /// arguments to the constructor of the resource. The implementation
040    /// uses [ServiceLoader] to find a [ResourceFactory] that creates the
041    /// resource, i.e. that does not return `Optional.empty()` when
042    /// [newResource] is called.
043    /// 
044    /// The implementation uses [ServiceLoader#load(Class, ClassLoader)]
045    /// with the class loader of the [Project] if provided, or the
046    /// class loader of the current thread otherwise.
047    ///
048    /// @param <T> the generic resource type
049    /// @param type the resource type
050    /// @param project the project
051    /// @param args the additional arguments
052    /// @return the resource
053    ///
054    static <T extends Resource> T create(ResourceType<T> type,
055            Project project, Object... args) {
056        var clsLdr = Optional.ofNullable(project).map(Project::context)
057            .map(BuildContext::classLoader)
058            .orElseGet(() -> Thread.currentThread().getContextClassLoader());
059        return FACTORIES.computeIfAbsent(clsLdr,
060            cl -> StreamSupport.stream(ServiceLoader.load(
061                ResourceFactory.class, cl).spliterator(), false).toList())
062            .stream().map(f -> f.newResource(type, project, args))
063            .filter(Optional::isPresent).map(Optional::get).findFirst()
064            .orElseThrow(() -> new ConfigurationException()
065                .message("No resource factory for %s", type));
066    }
067
068    /// Short for `create(type, null, args)`.
069    ///
070    /// @param <T> the generic resource type
071    /// @param type the resource type
072    /// @param args the additional arguments
073    /// @return the resource
074    ///
075    static <T extends Resource> T create(ResourceType<T> type,
076            Object... args) {
077        return create(type, null, args);
078    }
079
080    /// Returns a new resource of the given type if the factory instance
081    /// can create it.
082    ///
083    /// @param <T> the generic resource type
084    /// @param type the resource type
085    /// @param project the project
086    /// @param args the additional arguments
087    /// @return the result. `Optional.empty()` if the resource cannot
088    /// be created by the factory instance.
089    ///
090    <T extends Resource> Optional<T> newResource(ResourceType<T> type,
091            Project project, Object... args);
092
093}