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}