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.core;
020
021import java.lang.reflect.Method;
022import java.lang.reflect.Proxy;
023import java.nio.file.Path;
024import java.util.Arrays;
025import java.util.Optional;
026import static java.util.function.Predicate.not;
027import java.util.function.Supplier;
028import java.util.stream.Collectors;
029import org.jdrupes.builder.api.ExecResult;
030import org.jdrupes.builder.api.FileResource;
031import org.jdrupes.builder.api.FileTree;
032import org.jdrupes.builder.api.Project;
033import org.jdrupes.builder.api.Proxyable;
034import org.jdrupes.builder.api.Resource;
035import org.jdrupes.builder.api.ResourceFactory;
036import org.jdrupes.builder.api.ResourceProvider;
037import org.jdrupes.builder.api.ResourceType;
038import org.jdrupes.builder.api.Resources;
039import org.jdrupes.builder.api.TestResult;
040
041/// A factory for creating the Core resource objects.
042///
043public class CoreResourceFactory implements ResourceFactory {
044
045    /// Instantiates a new core resource factory.
046    ///
047    public CoreResourceFactory() {
048        // Make javadoc happy.
049    }
050
051    /// Creates a narrowed resource. Given a wanted interface type, an
052    /// implemented interface type and a supplier that returns an
053    /// instance of the implemented type, returns an instance of the
054    /// wanted type if possible.
055    /// 
056    /// Returning an implementation of the wanted type is possible if
057    /// the following conditions are met:
058    /// 
059    ///  1. The wanted type has no superclass (i.e. is an interface).
060    /// 
061    ///  2. The wanted type is a subclass of the implemented type.
062    /// 
063    ///  3. The wanted type does not add any methods to the
064    ///     implemented type.
065    /// 
066    /// The implementation uses a dynamic proxy to wrap the
067    /// implemented instance together with a [ForwardingHandler],
068    /// that simply forwards all invocations to the proxied object
069    /// (hence the requirement that the wanted type does not add
070    /// any methods to the implemented type).
071    ///
072    /// @param <T> the wanted type
073    /// @param <I> the implemented (available) type
074    /// @param wanted the wanted
075    /// @param implemented the implemented interface
076    /// @param supplier the supplier of a class that implements I
077    /// @return an instance if possible
078    ///
079    @SuppressWarnings("unchecked")
080    public static <T extends Resource, I extends Resource> Optional<T>
081            createNarrowed(ResourceType<T> wanted, Class<I> implemented,
082                    Supplier<? extends I> supplier) {
083        if (implemented.isAssignableFrom(wanted.rawType())
084            // we now know that T extends I
085            && wanted.rawType().getSuperclass() == null
086            && !addsMethod(implemented,
087                (Class<? extends I>) wanted.rawType())) {
088            return Optional.of(narrow(wanted, supplier.get()));
089        }
090        return Optional.empty();
091    }
092
093    /// Checks if the derived interface adds any methods to the
094    /// base interface.
095    ///
096    /// @param <T> the generic type
097    /// @param base the base
098    /// @param derived the derived
099    /// @return true, if successful
100    ///
101    public static <T> boolean addsMethod(
102            Class<T> base, Class<? extends T> derived) {
103        var baseItfs = ResourceType.getAllInterfaces(base)
104            .collect(Collectors.toSet());
105        return ResourceType.getAllInterfaces(derived)
106            .filter(not(baseItfs::contains))
107            .filter(itf -> Arrays.stream(itf.getDeclaredMethods())
108                .filter(not(Method::isDefault)).findAny().isPresent())
109            .findAny().isPresent();
110    }
111
112    @SuppressWarnings({ "unchecked" })
113    private static <T extends Resource> T narrow(ResourceType<T> type,
114            Resource instance) {
115        return (T) Proxy.newProxyInstance(type.rawType().getClassLoader(),
116            new Class<?>[] { type.rawType(), Proxyable.class },
117            new ForwardingHandler(instance));
118    }
119
120    /// New resource.
121    ///
122    /// @param <T> the generic type
123    /// @param type the type
124    /// @param project the project
125    /// @param args the args
126    /// @return the optional
127    ///
128    @Override
129    @SuppressWarnings({ "unchecked" })
130    public <T extends Resource> Optional<T> newResource(ResourceType<T> type,
131            Project project, Object... args) {
132        // ? extends FileResource
133        var candidate = createNarrowed(type, FileResource.class,
134            () -> new DefaultFileResource(
135                (ResourceType<? extends FileResource>) type, (Path) args[0]));
136        if (candidate.isPresent()) {
137            return candidate;
138        }
139
140        // ? extends TestResult
141        candidate = createNarrowed(type, TestResult.class,
142            () -> new DefaultTestResult(project, (ResourceProvider) args[0],
143                (String) args[1], (long) args[2], (long) args[3]));
144        if (candidate.isPresent()) {
145            return candidate;
146        }
147
148        // ? extends ExecResult
149        candidate = createNarrowed(type, ExecResult.class,
150            () -> new DefaultExecResult((ResourceProvider) args[0],
151                (String) args[1], (int) args[2]));
152        if (candidate.isPresent()) {
153            return candidate;
154        }
155
156        // ? extends Resources
157        candidate = createNarrowed(type, Resources.class,
158            () -> new DefaultResources<>(
159                (ResourceType<? extends Resources<?>>) type));
160        if (candidate.isPresent()) {
161            return candidate;
162        }
163
164        // ? extends FileTree
165        candidate = createNarrowed(type, FileTree.class,
166            () -> new DefaultFileTree<>(
167                (ResourceType<? extends FileTree<?>>) type,
168                project, (Path) args[0], (String) args[1]));
169        if (candidate.isPresent()) {
170            return candidate;
171        }
172
173        // Finally, try resource
174        return createNarrowed(type, Resource.class,
175            () -> new ResourceObject((ResourceType<?>) type) {});
176    }
177
178}