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