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