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