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 generic type 101 /// @param <T> the generic type 102 /// @param type the type 103 /// @param containedType the contained type 104 /// @return the resource type 105 /// 106 public static <C extends Resources<?>, T extends Resource> ResourceType<C> 107 resourceType(Class<C> type, Class<T> containedType) { 108 return new ResourceType<>(type, resourceType(containedType)); 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 type is not a parameterized type, its super or one of its 145 // interfaces may be. 146 this.type = (Class<T>) type; 147 this.containedType = Stream.concat( 148 Optional.ofNullable(((Class<?>) type).getGenericSuperclass()) 149 .stream(), 150 getAllInterfaces((Class<?>) type).map(Class::getGenericInterfaces) 151 .map(Arrays::stream).flatMap(s -> s)) 152 .filter(t -> t instanceof ParameterizedType pType && Resources.class 153 .isAssignableFrom((Class<?>) pType.getRawType())) 154 .map(t -> (ParameterizedType) t).findFirst() 155 .map(t -> new ResourceType<>(Resources.class, 156 new ResourceType<>(t).containedType())) 157 .orElseGet(() -> new ResourceType<>(Resources.class, null)) 158 .containedType(); 159 } 160 161 /// Gets all interfaces that the given class implements, 162 /// including the class itself. 163 /// 164 /// @param clazz the clazz 165 /// @return all interfaces 166 /// 167 public static Stream<Class<?>> getAllInterfaces(Class<?> clazz) { 168 return Stream.concat(Stream.of(clazz), 169 Arrays.stream(clazz.getInterfaces()) 170 .map(ResourceType::getAllInterfaces).flatMap(s -> s)); 171 } 172 173 /// Instantiates a new resource type, using the information from a 174 /// derived class. 175 /// 176 @SuppressWarnings({ "unchecked", "PMD.AvoidCatchingGenericException", 177 "rawtypes" }) 178 protected ResourceType() { 179 Type resourceType = getClass().getGenericSuperclass(); 180 try { 181 Type theResource = ((ParameterizedType) resourceType) 182 .getActualTypeArguments()[0]; 183 var tempType = new ResourceType(theResource); 184 type = tempType.rawType(); 185 containedType = tempType.containedType(); 186 } catch (Exception e) { 187 throw new UnsupportedOperationException( 188 "Could not derive resource type for " + resourceType, e); 189 } 190 } 191 192 /// Return the type. 193 /// 194 /// @return the class 195 /// 196 public Class<T> rawType() { 197 return type; 198 } 199 200 /// Return the contained type or `null`, if the resource is not 201 /// a container. 202 /// 203 /// @return the type 204 /// 205 public ResourceType<?> containedType() { 206 return containedType; 207 } 208 209 /// Checks if this is assignable from the other resource type. 210 /// 211 /// @param other the other 212 /// @return true, if is assignable from 213 /// 214 @SuppressWarnings("PMD.SimplifyBooleanReturns") 215 public boolean isAssignableFrom(ResourceType<?> other) { 216 if (!type.isAssignableFrom(other.type)) { 217 return false; 218 } 219 if (Objects.isNull(containedType)) { 220 // If this is not a container but assignable, we're okay. 221 return true; 222 } 223 if (Objects.isNull(other.containedType)) { 224 // If this is a container but other is not, this should 225 // have failed before. 226 return false; 227 } 228 return containedType.isAssignableFrom(other.containedType); 229 } 230 231 /// Returns a new [ResourceType] with the type (`this.type()`) 232 /// widened to the given type. While this method may be invoked 233 /// for any [ResourceType], it is intended to be used for 234 /// containers (`ResourceType<Resources<?>>`) only. 235 /// 236 /// @param <R> the new raw type 237 /// @param type the desired super type. This should actually be 238 /// declared as `Class <R>`, but there is no way to specify a 239 /// parameterized type as actual parameter. 240 /// @return the new resource type 241 /// 242 public <R extends Resource> ResourceType<R> widened( 243 Class<? extends Resource> type) { 244 if (!type.isAssignableFrom(this.type)) { 245 throw new IllegalArgumentException("Cannot replace " 246 + this.type + " with " + type + " because it is not a " 247 + "super class"); 248 } 249 if (Resources.class.isAssignableFrom(this.type) 250 && !Resources.class.isAssignableFrom(type)) { 251 throw new IllegalArgumentException("Cannot replace container" 252 + " type " + this.type + " with non-container type " + type); 253 } 254 @SuppressWarnings("unchecked") 255 var result = new ResourceType<R>((Class<R>) type, containedType); 256 return result; 257 } 258 259 @Override 260 public int hashCode() { 261 return Objects.hash(containedType, type); 262 } 263 264 @Override 265 public boolean equals(Object obj) { 266 if (this == obj) { 267 return true; 268 } 269 if (obj == null) { 270 return false; 271 } 272 if (!ResourceType.class.isAssignableFrom(obj.getClass())) { 273 return false; 274 } 275 ResourceType<?> other = (ResourceType<?>) obj; 276 return Objects.equals(containedType, other.containedType) 277 && Objects.equals(type, other.type); 278 } 279 280 @Override 281 public String toString() { 282 return type.getSimpleName() + (containedType == null ? "" 283 : "<" + containedType + ">"); 284 } 285 286}