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}