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