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]&lt;[FileResource]&gt;.
072    @SuppressWarnings("PMD.FieldNamingConventions")
073    public static final ResourceType<FileTree<FileResource>> BaseFileTreeType
074        = new ResourceType<>() {};
075
076    /// The resource type for [InputTree]&lt;[InputResource]&gt;.
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}