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}