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