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