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 private final Class<T> type; 072 private final ResourceType<?> containedType; 073 074 /// Initializes a new resource type. 075 /// 076 /// @param type the type 077 /// @param containedType the contained type 078 /// 079 @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) 080 public ResourceType(Class<? extends Resource> type, 081 ResourceType<?> containedType) { 082 this.type = (Class<T>) type; 083 this.containedType = containedType; 084 } 085 086 /// Creates a new resource type from the given container type 087 /// and contained type. The common usage pattern is to import 088 /// this method statically. 089 /// 090 /// @param <C> the generic type 091 /// @param <T> the generic type 092 /// @param type the type 093 /// @param containedType the contained type 094 /// @return the resource type 095 /// 096 public static <C extends Resources<T>, T extends Resource> 097 ResourceType<C> 098 resourceType(Class<C> type, Class<T> containedType) { 099 return new ResourceType<>(type, resourceType(containedType)); 100 } 101 102 /// Creates a new resource type from the given type. The common 103 /// usage pattern is to import this method statically. 104 /// 105 /// @param <T> the generic type 106 /// @param type the type 107 /// @return the resource type 108 /// 109 public static <T extends Resource> ResourceType<T> 110 resourceType(Class<T> type) { 111 return new ResourceType<>(type, null); 112 } 113 114 @SuppressWarnings("unchecked") 115 private ResourceType(Type type) { 116 if (type instanceof WildcardType wType) { 117 type = wType.getUpperBounds()[0]; 118 if (Object.class.equals(type)) { 119 type = Resource.class; 120 } 121 } 122 if (type instanceof ParameterizedType pType && Resources.class 123 .isAssignableFrom((Class<?>) pType.getRawType())) { 124 this.type = (Class<T>) pType.getRawType(); 125 var argType = pType.getActualTypeArguments()[0]; 126 if (argType instanceof ParameterizedType pArgType) { 127 containedType = new ResourceType<>(pArgType); 128 } else { 129 var subType = pType.getActualTypeArguments()[0]; 130 containedType = new ResourceType<>(subType); 131 } 132 return; 133 } 134 135 // If type is not a parameterized type, its super or one of its 136 // interfaces may be. 137 this.type = (Class<T>) type; 138 this.containedType = Stream.concat( 139 Optional.ofNullable(((Class<?>) type).getGenericSuperclass()) 140 .stream(), 141 getAllInterfaces((Class<?>) type).map(Class::getGenericInterfaces) 142 .map(Arrays::stream).flatMap(s -> s)) 143 .filter(t -> t instanceof ParameterizedType pType && Resources.class 144 .isAssignableFrom((Class<?>) pType.getRawType())) 145 .map(t -> (ParameterizedType) t).findFirst() 146 .map(t -> new ResourceType<>(Resources.class, 147 new ResourceType<>(t).containedType())) 148 .orElseGet(() -> new ResourceType<>(Resources.class, null)) 149 .containedType(); 150 } 151 152 /// Gets all interfaces that the given class implements, 153 /// including the class itself. 154 /// 155 /// @param clazz the clazz 156 /// @return all interfaces 157 /// 158 public static Stream<Class<?>> getAllInterfaces(Class<?> clazz) { 159 return Stream.concat(Stream.of(clazz), 160 Arrays.stream(clazz.getInterfaces()) 161 .map(ResourceType::getAllInterfaces).flatMap(s -> s)); 162 } 163 164 /// Instantiates a new resource type, using the information from a 165 /// derived class. 166 /// 167 @SuppressWarnings({ "unchecked", "PMD.AvoidCatchingGenericException", 168 "rawtypes" }) 169 protected ResourceType() { 170 Type resourceType = getClass().getGenericSuperclass(); 171 try { 172 Type theResource = ((ParameterizedType) resourceType) 173 .getActualTypeArguments()[0]; 174 var tempType = new ResourceType(theResource); 175 type = tempType.rawType(); 176 containedType = tempType.containedType(); 177 } catch (Exception e) { 178 throw new UnsupportedOperationException( 179 "Could not derive resource type for " + resourceType, e); 180 } 181 } 182 183 /// Return the type. 184 /// 185 /// @return the class 186 /// 187 public Class<T> rawType() { 188 return type; 189 } 190 191 /// Return the contained type or `null`, if the resource is not 192 /// a container. 193 /// 194 /// @return the type 195 /// 196 public ResourceType<?> containedType() { 197 return containedType; 198 } 199 200 /// Checks if this is assignable from the other resource type. 201 /// 202 /// @param other the other 203 /// @return true, if is assignable from 204 /// 205 @SuppressWarnings("PMD.SimplifyBooleanReturns") 206 public boolean isAssignableFrom(ResourceType<?> other) { 207 if (!type.isAssignableFrom(other.type)) { 208 return false; 209 } 210 if (Objects.isNull(containedType)) { 211 // If this is not a container but assignable, we're okay. 212 return true; 213 } 214 if (Objects.isNull(other.containedType)) { 215 // If this is a container but other is not, this should 216 // have failed before. 217 return false; 218 } 219 return containedType.isAssignableFrom(other.containedType); 220 } 221 222 /// Returns a new [ResourceType] with the type (`this.type()`) 223 /// widened to the given type. While this method may be invoked 224 /// for any [ResourceType], it is intended to be used for 225 /// containers (`ResourceType<Resources<?>>`) only. 226 /// 227 /// @param <R> the new raw type 228 /// @param type the desired super type. This should actually be 229 /// declared as `Class <R>`, but there is no way to specify a 230 /// parameterized type as actual parameter. 231 /// @return the new resource type 232 /// 233 public <R extends Resource> ResourceType<R> widened( 234 Class<? extends Resource> type) { 235 if (!type.isAssignableFrom(this.type)) { 236 throw new IllegalArgumentException("Cannot replace " 237 + this.type + " with " + type + " because it is not a " 238 + "super class"); 239 } 240 if (Resources.class.isAssignableFrom(this.type) 241 && !Resources.class.isAssignableFrom(type)) { 242 throw new IllegalArgumentException("Cannot replace container" 243 + " type " + this.type + " with non-container type " + type); 244 } 245 @SuppressWarnings("unchecked") 246 var result = new ResourceType<R>((Class<R>) type, containedType); 247 return result; 248 } 249 250 @Override 251 public int hashCode() { 252 return Objects.hash(containedType, type); 253 } 254 255 @Override 256 public boolean equals(Object obj) { 257 if (this == obj) { 258 return true; 259 } 260 if (obj == null) { 261 return false; 262 } 263 if (!ResourceType.class.isAssignableFrom(obj.getClass())) { 264 return false; 265 } 266 ResourceType<?> other = (ResourceType<?>) obj; 267 return Objects.equals(containedType, other.containedType) 268 && Objects.equals(type, other.type); 269 } 270 271 @Override 272 public String toString() { 273 return type.getSimpleName() + (containedType == null ? "" 274 : "(" + containedType + ")"); 275 } 276 277}