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