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.api; 020 021import java.lang.reflect.ParameterizedType; 022import java.lang.reflect.Type; 023import java.lang.reflect.WildcardType; 024import java.util.Arrays; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.stream.Stream; 028 029/// A special kind of type token for representing a resource type. 030/// The method [rawType()] returns the type as [Class]. If this class 031/// if derived from [Resources], [containedType()] returns the 032/// [ResourceType] of the contained elements. 033/// 034/// Beware of automatic inference of type arguments. The inferred 035/// type arguments will usually be super classes of what you expect. 036/// 037/// An alternative to using an anonymous class to create a type token 038/// is to statically import the `resourceType` methods. Using these 039/// typically also results in clear code that is sometimes easier to read. 040/// 041/// @param <T> the resource type 042/// 043public class ResourceType<T extends Resource> { 044 045 /// Used to request cleanup. 046 @SuppressWarnings({ "PMD.FieldNamingConventions", 047 "PMD.AvoidDuplicateLiterals" }) 048 public static final ResourceType< 049 Cleanliness> CleanlinessType = new ResourceType<>() {}; 050 051 /// The resource type for [ResourceFile]. 052 @SuppressWarnings("PMD.FieldNamingConventions") 053 public static final ResourceType<ResourceFile> ResourceFileType 054 = new ResourceType<>() {}; 055 056 /// The resource type for [FileResource]. 057 @SuppressWarnings("PMD.FieldNamingConventions") 058 public static final ResourceType<FileResource> FileResourceType 059 = new ResourceType<>() {}; 060 061 /// The resource type for [IOResource]. 062 @SuppressWarnings("PMD.FieldNamingConventions") 063 public static final ResourceType< 064 IOResource> IOResourceType = new ResourceType<>() {}; 065 066 /// The resource type for `Resources[IOResource]`. 067 @SuppressWarnings({ "PMD.FieldNamingConventions" }) 068 public static final ResourceType<Resources<IOResource>> IOResourcesType 069 = new ResourceType<>(Resources.class, IOResourceType) {}; 070 071 /// The resource type for [TestResult]. 072 @SuppressWarnings("PMD.FieldNamingConventions") 073 public static final ResourceType<TestResult> TestResultType 074 = new ResourceType<>() {}; 075 076 /// The resource type for [ExecResult]. 077 @SuppressWarnings("PMD.FieldNamingConventions") 078 public static final ResourceType<ExecResult<?>> ExecResultType 079 = new ResourceType<>() {}; 080 081 private final Class<T> type; 082 private final ResourceType<?> containedType; 083 084 /// Initializes a new resource type. 085 /// 086 /// @param type the type 087 /// @param containedType the contained type 088 /// 089 @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) 090 public ResourceType(Class<? extends Resource> type, 091 ResourceType<?> containedType) { 092 this.type = (Class<T>) type; 093 this.containedType = containedType; 094 } 095 096 /// Creates a new resource type from the given container type 097 /// and contained type. The common usage pattern is to import 098 /// this method statically. 099 /// 100 /// @param <C> the container type 101 /// @param <E> the element type 102 /// @param type the type 103 /// @param elementType the element type 104 /// @return the resource type 105 /// 106 public static <C extends Resources<E>, E extends Resource> ResourceType<C> 107 create(Class<C> type, Class<E> elementType) { 108 return new ResourceType<>(type, resourceType(elementType)); 109 } 110 111 /// Creates a new resource type from the given type. The common 112 /// usage pattern is to import this method statically. 113 /// 114 /// @param <T> the generic type 115 /// @param type the type 116 /// @return the resource type 117 /// 118 public static <T extends Resource> ResourceType<T> 119 resourceType(Class<T> type) { 120 return new ResourceType<>(type, null); 121 } 122 123 @SuppressWarnings("unchecked") 124 private ResourceType(Type type) { 125 if (type instanceof WildcardType wType) { 126 type = wType.getUpperBounds()[0]; 127 if (Object.class.equals(type)) { 128 type = Resource.class; 129 } 130 } 131 if (type instanceof ParameterizedType pType && Resources.class 132 .isAssignableFrom((Class<?>) pType.getRawType())) { 133 this.type = (Class<T>) pType.getRawType(); 134 var argType = pType.getActualTypeArguments()[0]; 135 if (argType instanceof ParameterizedType pArgType) { 136 containedType = new ResourceType<>(pArgType); 137 } else { 138 var subType = pType.getActualTypeArguments()[0]; 139 containedType = new ResourceType<>(subType); 140 } 141 return; 142 } 143 144 // If this is a parameterized type, but not resources, 145 // ignore the parameter(s). 146 if (type instanceof ParameterizedType pType) { 147 type = pType.getRawType(); 148 } 149 150 // If type is not a parameterized type, its super or one of its 151 // interfaces may be. 152 this.type = (Class<T>) type; 153 this.containedType = Stream.concat( 154 Optional.ofNullable(((Class<?>) type).getGenericSuperclass()) 155 .stream(), 156 getAllInterfaces((Class<?>) type).map(Class::getGenericInterfaces) 157 .map(Arrays::stream).flatMap(s -> s)) 158 .filter(t -> t instanceof ParameterizedType pType && Resources.class 159 .isAssignableFrom((Class<?>) pType.getRawType())) 160 .map(t -> (ParameterizedType) t).findFirst() 161 .map(t -> new ResourceType<>(Resources.class, 162 new ResourceType<>(t).containedType())) 163 .orElseGet(() -> new ResourceType<>(Resources.class, null)) 164 .containedType(); 165 } 166 167 /// Gets all interfaces that the given class implements, 168 /// including the class itself. 169 /// 170 /// @param clazz the clazz 171 /// @return all interfaces 172 /// 173 public static Stream<Class<?>> getAllInterfaces(Class<?> clazz) { 174 return Stream.concat(Stream.of(clazz), 175 Arrays.stream(clazz.getInterfaces()) 176 .map(ResourceType::getAllInterfaces).flatMap(s -> s)); 177 } 178 179 /// Instantiates a new resource type, using the information from a 180 /// derived class. 181 /// 182 @SuppressWarnings({ "unchecked", "PMD.AvoidCatchingGenericException", 183 "rawtypes" }) 184 protected ResourceType() { 185 Type resourceType = getClass().getGenericSuperclass(); 186 try { 187 Type theResource = ((ParameterizedType) resourceType) 188 .getActualTypeArguments()[0]; 189 var tempType = new ResourceType(theResource); 190 type = tempType.rawType(); 191 containedType = tempType.containedType(); 192 } catch (Exception e) { 193 throw new UnsupportedOperationException( 194 "Could not derive resource type for " + resourceType, e); 195 } 196 } 197 198 /// Return the type. 199 /// 200 /// @return the class 201 /// 202 public Class<T> rawType() { 203 return type; 204 } 205 206 /// Return the contained type or `null`, if the resource is not 207 /// a container. 208 /// 209 /// @return the type 210 /// 211 public ResourceType<?> containedType() { 212 return containedType; 213 } 214 215 /// Checks if this is assignable from the other resource type. 216 /// 217 /// @param other the other 218 /// @return true, if is assignable from 219 /// 220 @SuppressWarnings("PMD.SimplifyBooleanReturns") 221 public boolean isAssignableFrom(ResourceType<?> other) { 222 if (!type.isAssignableFrom(other.type)) { 223 return false; 224 } 225 if (Objects.isNull(containedType)) { 226 // If this is not a container but assignable, we're okay. 227 return true; 228 } 229 if (Objects.isNull(other.containedType)) { 230 // If this is a container but other is not, this should 231 // have failed before. 232 return false; 233 } 234 return containedType.isAssignableFrom(other.containedType); 235 } 236 237 /// Returns a new [ResourceType] with the type (`this.type()`) 238 /// widened to the given type. While this method may be invoked 239 /// for any [ResourceType], it is intended to be used for 240 /// containers (`ResourceType<Resources<?>>`) only. 241 /// 242 /// @param <R> the new raw type 243 /// @param type the desired super type. This should actually be 244 /// declared as `Class <R>`, but there is no way to specify a 245 /// parameterized type as actual parameter. 246 /// @return the new resource type 247 /// 248 public <R extends Resource> ResourceType<R> widened( 249 Class<? extends Resource> type) { 250 if (!type.isAssignableFrom(this.type)) { 251 throw new IllegalArgumentException("Cannot replace " 252 + this.type + " with " + type + " because it is not a " 253 + "super class"); 254 } 255 if (Resources.class.isAssignableFrom(this.type) 256 && !Resources.class.isAssignableFrom(type)) { 257 throw new IllegalArgumentException("Cannot replace container" 258 + " type " + this.type + " with non-container type " + type); 259 } 260 @SuppressWarnings("unchecked") 261 var result = new ResourceType<R>((Class<R>) type, containedType); 262 return result; 263 } 264 265 @Override 266 public int hashCode() { 267 return Objects.hash(containedType, type); 268 } 269 270 @Override 271 public boolean equals(Object obj) { 272 if (this == obj) { 273 return true; 274 } 275 if (obj == null) { 276 return false; 277 } 278 if (!ResourceType.class.isAssignableFrom(obj.getClass())) { 279 return false; 280 } 281 ResourceType<?> other = (ResourceType<?>) obj; 282 return Objects.equals(containedType, other.containedType) 283 && Objects.equals(type, other.type); 284 } 285 286 @Override 287 public String toString() { 288 return type.getSimpleName() + (containedType == null ? "" 289 : "<" + containedType + ">"); 290 } 291 292}