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/// is 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 superclasses 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 /// The resource type for [ZipFile]. 102 @SuppressWarnings("PMD.FieldNamingConventions") 103 public static final ResourceType<ZipFile> ZipFileType 104 = new ResourceType<>() {}; 105 106 /// The resource type for [TarFile]. 107 @SuppressWarnings("PMD.FieldNamingConventions") 108 public static final ResourceType<TarFile> TarFileType 109 = new ResourceType<>() {}; 110 111 /// The resource type for [ZipFile]. 112 @SuppressWarnings("PMD.FieldNamingConventions") 113 public static final ResourceType<TarGzFile> TarGzFileType 114 = new ResourceType<>() {}; 115 116 private final Class<T> type; 117 private final ResourceType<?> containedType; 118 119 /// Creates a new resource type from the given type. The common 120 /// usage pattern is to import this method statically. 121 /// 122 /// @param <T> the generic type 123 /// @param type the type 124 /// @return the resource type 125 /// 126 public static <T extends Resource> ResourceType<T> 127 resourceType(Class<T> type) { 128 if (Resources.class.isAssignableFrom(type)) { 129 throw new IllegalArgumentException("Method resourceType may" 130 + " not be called with container type " + type); 131 } 132 return new ResourceType<>(type); 133 } 134 135 /// Creates a new [Resources] type from the given values. The common 136 /// usage pattern is to import this method statically. 137 /// 138 /// @param <T> the generic type 139 /// @param type the type 140 /// @param containedType the contained type 141 /// @return the resource type 142 /// 143 public static <T extends Resource> ResourceType<T> resourceType( 144 @SuppressWarnings("rawtypes") Class<? extends Resources> type, 145 ResourceType<?> containedType) { 146 return new ResourceType<>(type, containedType); 147 } 148 149 @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) 150 private ResourceType(Class<? extends Resource> type, 151 ResourceType<?> containedType) { 152 if (Resources.class.isAssignableFrom(type) && containedType == null) { 153 logger.atWarning().withStackTrace(MEDIUM).log("Creating resource" 154 + " type for %s without information about contained type", 155 type); 156 } 157 this.type = (Class<T>) type; 158 this.containedType = containedType; 159 } 160 161 /// Creates a new resource type from the given container type 162 /// and contained type. The common usage pattern is to import 163 /// this method statically. 164 /// 165 /// @param <C> the container type 166 /// @param <E> the element type 167 /// @param type the type 168 /// @param elementType the element type 169 /// @return the resource type 170 /// 171 public static <C extends Resources<E>, E extends Resource> ResourceType<C> 172 create(Class<C> type, Class<E> elementType) { 173 return new ResourceType<>(type, resourceType(elementType)); 174 } 175 176 @SuppressWarnings("unchecked") 177 private ResourceType(Type type) { 178 if (type instanceof WildcardType wType) { 179 type = wType.getUpperBounds()[0]; 180 if (Object.class.equals(type)) { 181 type = Resource.class; 182 } 183 } 184 if (type instanceof ParameterizedType pType && Resources.class 185 .isAssignableFrom((Class<?>) pType.getRawType())) { 186 this.type = (Class<T>) pType.getRawType(); 187 var argType = pType.getActualTypeArguments()[0]; 188 if (argType instanceof ParameterizedType pArgType) { 189 containedType = new ResourceType<>(pArgType); 190 } else { 191 var subType = pType.getActualTypeArguments()[0]; 192 if (subType instanceof TypeVariable) { 193 logger.atWarning().withStackTrace(MEDIUM).log( 194 "Type contained in %s is unknown", type); 195 containedType = BaseResourceType; 196 return; 197 } 198 containedType = new ResourceType<>(subType); 199 } 200 return; 201 } 202 203 // If this is a parameterized type, but not resources, 204 // ignore the parameter(s). 205 if (type instanceof ParameterizedType pType) { 206 type = pType.getRawType(); 207 } 208 209 this.type = (Class<T>) type; 210 if (!Resources.class.isAssignableFrom(this.type)) { 211 this.containedType = null; 212 return; 213 } 214 215 // If type is not a parameterized type, its super or one of its 216 // interfaces may be. 217 @SuppressWarnings("rawtypes") 218 final var rawBaseResourceType = (ResourceType) BaseResourceType; 219 this.containedType = Stream.concat( 220 Optional.ofNullable(((Class<?>) type).getGenericSuperclass()) 221 .stream(), 222 getAllInterfaces((Class<?>) type).map(Class::getGenericInterfaces) 223 .map(Arrays::stream).flatMap(s -> s)) 224 .filter(t -> t instanceof ParameterizedType pType && Resources.class 225 .isAssignableFrom((Class<?>) pType.getRawType())) 226 .map(t -> (ParameterizedType) t).findFirst() 227 .map(t -> new ResourceType<>(t).containedType()) 228 .orElse(rawBaseResourceType); 229 } 230 231 /// Gets all interfaces that the given class implements, 232 /// including the class itself. 233 /// 234 /// @param clazz the clazz 235 /// @return all interfaces 236 /// 237 public static Stream<Class<?>> getAllInterfaces(Class<?> clazz) { 238 return Stream.concat(Stream.of(clazz), 239 Arrays.stream(clazz.getInterfaces()) 240 .map(ResourceType::getAllInterfaces).flatMap(s -> s)); 241 } 242 243 /// Instantiates a new resource type, using the information from a 244 /// derived class. 245 /// 246 @SuppressWarnings({ "unchecked", "PMD.AvoidCatchingGenericException", 247 "rawtypes" }) 248 protected ResourceType() { 249 Type resourceType = getClass().getGenericSuperclass(); 250 try { 251 Type theResource = ((ParameterizedType) resourceType) 252 .getActualTypeArguments()[0]; 253 var tempType = new ResourceType(theResource); 254 type = tempType.rawType(); 255 containedType = tempType.containedType(); 256 } catch (Exception e) { 257 throw new UnsupportedOperationException( 258 "Could not derive resource type for " + resourceType, e); 259 } 260 } 261 262 /// Return the type. 263 /// 264 /// @return the class 265 /// 266 public Class<T> rawType() { 267 return type; 268 } 269 270 /// Return the contained type or `null`, if the resource is not 271 /// a container. 272 /// 273 /// @return the type 274 /// 275 public ResourceType<?> containedType() { 276 return containedType; 277 } 278 279 /// Checks if this is assignable from the other resource type. 280 /// 281 /// @param other the other 282 /// @return true, if is assignable from 283 /// 284 @SuppressWarnings("PMD.SimplifyBooleanReturns") 285 public boolean isAssignableFrom(ResourceType<?> other) { 286 if (!type.isAssignableFrom(other.type)) { 287 return false; 288 } 289 if (Objects.isNull(containedType)) { 290 // If this is not a container but assignable, we're okay. 291 return true; 292 } 293 if (Objects.isNull(other.containedType)) { 294 // If this is a container but other is not, this should 295 // have failed before. 296 return false; 297 } 298 return containedType.isAssignableFrom(other.containedType); 299 } 300 301 /// Returns a new [ResourceType] with the type (`this.type()`) 302 /// widened to the given type. While this method may be invoked 303 /// for any [ResourceType], it is intended to be used for 304 /// containers (`ResourceType<Resources<?>>`) only. 305 /// 306 /// @param <R> the new raw type 307 /// @param type the desired super type. This should actually be 308 /// declared as `Class <R>`, but there is no way to specify a 309 /// parameterized type as actual parameter. 310 /// @return the new resource type 311 /// 312 public <R extends Resource> ResourceType<R> widened( 313 Class<? extends Resource> type) { 314 if (!type.isAssignableFrom(this.type)) { 315 throw new IllegalArgumentException("Cannot replace " 316 + this.type + " with " + type + " because it is not a " 317 + "super class"); 318 } 319 if (Resources.class.isAssignableFrom(this.type) 320 && !Resources.class.isAssignableFrom(type)) { 321 throw new IllegalArgumentException("Cannot replace container" 322 + " type " + this.type + " with non-container type " + type); 323 } 324 @SuppressWarnings("unchecked") 325 var result = new ResourceType<R>((Class<R>) type, containedType); 326 return result; 327 } 328 329 @Override 330 public int hashCode() { 331 return Objects.hash(containedType, type); 332 } 333 334 @Override 335 public boolean equals(Object obj) { 336 if (this == obj) { 337 return true; 338 } 339 if (obj == null) { 340 return false; 341 } 342 if (!ResourceType.class.isAssignableFrom(obj.getClass())) { 343 return false; 344 } 345 ResourceType<?> other = (ResourceType<?>) obj; 346 return Objects.equals(containedType, other.containedType) 347 && Objects.equals(type, other.type); 348 } 349 350 @Override 351 public String toString() { 352 return type.getSimpleName() + (containedType == null ? "" 353 : "<" + containedType + ">"); 354 } 355 356}