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}