001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.tools.reflect;
017
018import static de.cuioss.tools.collect.MoreCollections.requireNotEmpty;
019import static java.util.Objects.requireNonNull;
020
021import java.lang.annotation.Annotation;
022import java.lang.reflect.Field;
023import java.lang.reflect.InvocationHandler;
024import java.lang.reflect.Method;
025import java.lang.reflect.Modifier;
026import java.lang.reflect.ParameterizedType;
027import java.lang.reflect.Proxy;
028import java.lang.reflect.Type;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Optional;
037import java.util.WeakHashMap;
038
039import de.cuioss.tools.base.Preconditions;
040import de.cuioss.tools.collect.CollectionBuilder;
041import de.cuioss.tools.logging.CuiLogger;
042import lombok.Synchronized;
043import lombok.experimental.UtilityClass;
044
045/**
046 * Provides a number of methods simplifying the usage of Reflection-based
047 * access.
048 * <h2>Caution</h2>
049 * <p>
050 * Use reflection only if there is no other way. Even if some of the problems
051 * are minimized by using this type. It should be used either in test-code, what
052 * is we actually do, and not production code. An other reason could be
053 * framework code. as for that you should exactly know what you do.
054 * </p>
055 *
056 * @author Oliver Wolff
057 */
058@UtilityClass
059public final class MoreReflection {
060
061    private static final String IGNORING_METHOD_ON_CLASS = "Ignoring method '{}' on class '{}'";
062
063    private static final CuiLogger log = new CuiLogger(MoreReflection.class);
064
065    /**
066     * We use {@link WeakHashMap} in order to allow the garbage collector to do its
067     * job
068     */
069    private static final Map<Class<?>, List<Method>> publicObjectMethodCache = new WeakHashMap<>();
070
071    private static final Map<Class<?>, Map<String, Field>> fieldCache = new WeakHashMap<>();
072
073    /**
074     * Tries to access a field on a given type. If none can be found it recursively
075     * calls itself with the corresponding parent until {@link Object}.
076     * <em>Caution:</em>
077     * <p>
078     * The field elements are shared between requests (cached), therefore you must
079     * ensure that changes to the instance, like
080     * {@link Field#setAccessible(boolean)} are reseted by the client. This can be
081     * simplified by using {@link FieldWrapper}
082     * </p>
083     *
084     * @param type      to be checked, must not be null
085     * @param fieldName to be checked, must not be null
086     * @return an {@link Optional} {@link Field} if it can be found
087     */
088    @Synchronized
089    @SuppressWarnings("java:S3824") // owolff: computeIfAbsent is not an option because we add null
090                                    // to the field
091    public static Optional<Field> accessField(final Class<?> type, final String fieldName) {
092        requireNonNull(type);
093        requireNonNull(fieldName);
094        if (!fieldCache.containsKey(type)) {
095            fieldCache.put(type, new HashMap<>());
096        }
097        final Map<String, Field> typeMap = fieldCache.get(type);
098        if (!typeMap.containsKey(fieldName)) {
099            typeMap.put(fieldName, resolveField(type, fieldName).orElse(null));
100        }
101        return Optional.ofNullable(typeMap.get(fieldName));
102    }
103
104    private static Optional<Field> resolveField(final Class<?> type, final String fieldName) {
105        try {
106            return Optional.of(type.getDeclaredField(fieldName));
107        } catch (final NoSuchFieldException | SecurityException e) {
108            log.trace("Error while trying to read field {} on type {}", type, fieldName, e);
109            if (Object.class.equals(type.getClass()) || null == type.getSuperclass()) {
110                return Optional.empty();
111            }
112            return resolveField(type.getSuperclass(), fieldName);
113        }
114    }
115
116    /**
117     * Determines the public not static methods of a given {@link Class}.
118     * {@link Object#getClass()} will implicitly ignore
119     *
120     * @param clazz to be checked
121     * @return the found public-methods.
122     */
123    @Synchronized
124    public static List<Method> retrievePublicObjectMethods(final Class<?> clazz) {
125        requireNonNull(clazz);
126
127        if (!publicObjectMethodCache.containsKey(clazz)) {
128            final List<Method> found = new ArrayList<>();
129            for (final Method method : clazz.getMethods()) {
130                final int modifiers = method.getModifiers();
131                if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)
132                        && !"getClass".equals(method.getName())) {
133                    found.add(method);
134                }
135            }
136            publicObjectMethodCache.put(clazz, found);
137            return found;
138        }
139        return publicObjectMethodCache.get(clazz);
140    }
141
142    /**
143     * Determines the access methods of a given class. An access method is defined
144     * as being a public not static zero-argument method that is prefixed with
145     * either "get" or "is". The Method "getClass" is explicitly filtered
146     *
147     * @param clazz to be checked
148     * @return the found access-methods.
149     */
150    public static List<Method> retrieveAccessMethods(final Class<?> clazz) {
151        final List<Method> found = new ArrayList<>();
152        for (final Method method : retrievePublicObjectMethods(clazz)) {
153            if (0 == method.getParameterCount()) {
154                final var name = method.getName();
155                if (name.startsWith("get") || name.startsWith("is")) {
156                    log.debug("Adding found method '{}' on class '{}'", name, clazz);
157                    found.add(method);
158                }
159            } else {
160                log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz);
161            }
162        }
163        return found;
164    }
165
166    /**
167     * Determines the access methods of a given class. An access method is defined
168     * as being a public not static zero-argument method that is prefixed with
169     * either "get" or "is". The Method "getClass" is explicitly filtered
170     *
171     * @param clazz            to be checked
172     * @param ignoreProperties identifies the property by name that must be filtered
173     *                         from the result
174     * @return the found access-methods.
175     */
176    public static List<Method> retrieveAccessMethods(final Class<?> clazz, final Collection<String> ignoreProperties) {
177        final List<Method> found = new ArrayList<>();
178        for (final Method method : retrieveAccessMethods(clazz)) {
179            final var propertyName = computePropertyNameFromMethodName(method.getName());
180            if (!ignoreProperties.contains(propertyName)) {
181                found.add(method);
182            }
183        }
184        return found;
185    }
186
187    /**
188     * Determines the modifier methods of a given class. A modifier method is
189     * defined as being a public not static single-argument method that is prefixed
190     * with either "set" or consists of the propertyName only.
191     *
192     * @param clazz         to be checked
193     * @param propertyName  to be checked, must not be null
194     * @param parameterType identifying the parameter to be passed to the given
195     *                      method, must not be null
196     * @return the found modifier-method or {@link Optional#empty()} if none could
197     *         be found
198     */
199    public static Optional<Method> retrieveWriteMethod(final Class<?> clazz, final String propertyName,
200            final Class<?> parameterType) {
201        requireNonNull(parameterType);
202
203        for (final Method method : retrieveWriteMethodCandidates(clazz, propertyName)) {
204            if (checkWhetherParameterIsAssignable(method.getParameterTypes()[0], parameterType)) {
205                return Optional.of(method);
206            }
207            log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz);
208        }
209        return Optional.empty();
210    }
211
212    /**
213     * @param assignableSource the type to be checked
214     * @param queryType        to be checked for
215     * @return boolean indicating whether the given parameter, identified by their
216     *         class attributes match
217     */
218    public static boolean checkWhetherParameterIsAssignable(final Class<?> assignableSource, final Class<?> queryType) {
219        requireNonNull(assignableSource);
220        requireNonNull(queryType);
221        if (assignableSource.equals(queryType)) {
222            log.trace("Parameter-type matches exactly '%s'", assignableSource);
223            return true;
224        }
225        if (assignableSource.isAssignableFrom(queryType)) {
226            log.trace("Parameter '%s' is assignable from '%s'", assignableSource, queryType);
227            return true;
228        }
229        final Class<?> boxedSource = resolveWrapperTypeForPrimitive(assignableSource);
230        final Class<?> boxedQuery = resolveWrapperTypeForPrimitive(queryType);
231        if (boxedSource.equals(boxedQuery)) {
232            log.trace("Parameter-type matches exactly after autoboxing '%s'", assignableSource);
233            return true;
234        }
235        return boxedSource.isAssignableFrom(boxedQuery);
236    }
237
238    /**
239     * Helper class for converting a primitive to a wrapper type.
240     *
241     * @param check must not be null
242     * @return the wrapper type if the given type represents a primitive, the given
243     *         type otherwise.
244     */
245    static Class<?> resolveWrapperTypeForPrimitive(final Class<?> check) {
246        if (!check.isPrimitive()) {
247            return check;
248        }
249        return switch (check.getName()) {
250        case "boolean" -> Boolean.class;
251        case "byte" -> Byte.class;
252        case "char" -> Character.class;
253        case "short" -> Short.class;
254        case "int" -> Integer.class;
255        case "long" -> Long.class;
256        case "double" -> Double.class;
257        case "float" -> Float.class;
258        default -> {
259            log.warn("Unable to determine wrapper type for '{}', ", check);
260            yield check;
261        }
262        };
263    }
264
265    /**
266     * Determines the modifier methods of a given class for a property. A modifier
267     * method is defined as being a public not static single-argument method that is
268     * prefixed with either "set" or consists of the ropertyName only. This will
269     * implicitly return all possible setter or builder methods, e.g.
270     * {@code setPropertyName(String name)}, {@code propertyName(String name)} and
271     * {@code setPropertyName(Collection<String> name)} will all be part of the
272     * result.
273     *
274     * @param clazz        to be checked
275     * @param propertyName to be checked, must not be null
276     * @return the found modifier-methods or an empty {@link Collection} if none
277     *         could be found
278     */
279    public static Collection<Method> retrieveWriteMethodCandidates(final Class<?> clazz, final String propertyName) {
280        requireNotEmpty(propertyName);
281        final var builder = new CollectionBuilder<Method>();
282        for (final Method method : retrievePublicObjectMethods(clazz)) {
283            if (1 == method.getParameterCount()) {
284                final var name = method.getName();
285                if (propertyName.equals(name)) {
286                    log.debug("Returning found method '{}' on class '{}'", name, clazz);
287                    builder.add(method);
288                }
289                if (name.startsWith("set") && computePropertyNameFromMethodName(name).equalsIgnoreCase(propertyName)) {
290                    log.debug("Returning found method '{}' on class '{}'", name, clazz);
291                    builder.add(method);
292                }
293            } else {
294                log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz);
295            }
296        }
297        return builder.toImmutableList();
298    }
299
300    /**
301     * Retrieves the access-method for a given property Name. See
302     * {@link #retrieveAccessMethods(Class)} for the definition of an access-method
303     *
304     * @param clazz        must not be null
305     * @param propertyName to be accessed
306     * @return {@link Optional#empty()} in case no method could be found, an
307     *         {@link Optional} with the found method otherwise.
308     */
309    public static Optional<Method> retrieveAccessMethod(final Class<?> clazz, final String propertyName) {
310        requireNotEmpty(propertyName);
311        for (final Method method : retrieveAccessMethods(clazz)) {
312            if (computePropertyNameFromMethodName(method.getName()).equalsIgnoreCase(propertyName)) {
313                return Optional.of(method);
314            }
315        }
316        return Optional.empty();
317    }
318
319    /**
320     * Helper method that extract the property-name from a given accessor-method
321     * name.
322     *
323     * @param methodName must not be null nor empty
324     * @return the possible attribute name of a given method-name, e.g. it return
325     *         'name' for getName/setName/isName. If none of the prefixes 'get',
326     *         'set', 'is' is found it returns the passed String.
327     */
328    public static String computePropertyNameFromMethodName(final String methodName) {
329        requireNotEmpty(methodName);
330
331        if (methodName.startsWith("get") || methodName.startsWith("set")) {
332            if (methodName.length() > 3) {
333                return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
334            }
335            log.debug("Name to short for extracting attributeName '{}'", methodName);
336        }
337        if (methodName.startsWith("is")) {
338            if (methodName.length() > 2) {
339                return methodName.substring(2, 3).toLowerCase() + methodName.substring(3);
340            }
341            log.debug("Name to short for extracting attributeName '{}'", methodName);
342        }
343        return methodName;
344    }
345
346    /**
347     * Helper class for extracting <em>all</em> annotations of a given class
348     * including from their ancestors.
349     *
350     * @param <A>           the concrete annotation type
351     * @param annotatedType the (possibly) annotated type. If it is null or
352     *                      {@link Object#getClass()} it will return an empty list
353     * @param annotation    the annotation to be extracted, must not be null
354     * @return an immutable List with all annotations found at the given object or
355     *         one of its ancestors. May be empty but never null
356     */
357    public static <A extends Annotation> List<A> extractAllAnnotations(final Class<?> annotatedType,
358            final Class<A> annotation) {
359        if (null == annotatedType || Object.class.equals(annotatedType.getClass())) {
360            return Collections.emptyList();
361        }
362
363        final var builder = new CollectionBuilder<A>();
364        builder.add(annotatedType.getAnnotationsByType(annotation));
365        builder.add(extractAllAnnotations(annotatedType.getSuperclass(), annotation));
366        return builder.toImmutableList();
367    }
368
369    /**
370     * Helper class for extracting an annotation of a given class including from
371     * their ancestors.
372     *
373     * @param <A>           the concrete annotation type
374     * @param annotatedType the (possibly) annotated type. If it is null or
375     *                      {@link Object#getClass()} {@link Optional#empty()}
376     * @param annotation    the annotation to be extracted, must not be null
377     * @return an {@link Optional} on the annotated Object if the annotation can be
378     *         found. In case the annotation is found multiple times the first
379     *         element will be returned.
380     */
381    public static <A extends Annotation> Optional<A> extractAnnotation(final Class<?> annotatedType,
382            final Class<A> annotation) {
383        requireNonNull(annotation);
384        final List<A> extracted = extractAllAnnotations(annotatedType, annotation);
385        if (extracted.isEmpty()) {
386            return Optional.empty();
387        }
388        return Optional.of(extracted.iterator().next());
389    }
390
391    /**
392     * Extracts the first generic type argument for the given type.
393     *
394     * @param <T>                   identifying the type to be looked for
395     * @param typeToBeExtractedFrom must not be null
396     * @return an {@link Optional} of the KeyStoreType-Argument of the given class.
397     * @throws IllegalArgumentException in case the given type does not represent a
398     *                                  generic.
399     */
400    @SuppressWarnings("unchecked") // owolff: The unchecked casting is necessary
401    public static <T> Class<T> extractFirstGenericTypeArgument(final Class<?> typeToBeExtractedFrom) {
402        final var parameterizedType = extractParameterizedType(typeToBeExtractedFrom)
403                .orElseThrow(() -> new IllegalArgumentException(
404                        "Given type defines no generic KeyStoreType: " + typeToBeExtractedFrom));
405
406        requireNotEmpty(parameterizedType.getActualTypeArguments(),
407                "No type argument found for " + typeToBeExtractedFrom.getName());
408
409        final Class<?> firstType = extractGenericTypeCovariantly(parameterizedType.getActualTypeArguments()[0])
410                .orElseThrow(() -> new IllegalArgumentException(
411                        "Unable to determine genric type for " + typeToBeExtractedFrom));
412
413        try {
414            return (Class<T>) firstType;
415        } catch (final ClassCastException e) {
416            throw new IllegalArgumentException(
417                    "No type argument can be extracted from " + typeToBeExtractedFrom.getName(), e);
418        }
419    }
420
421    /**
422     * @param type to be extracted from
423     * @return if applicable the actual type argument for the given type. If the
424     *         type represents already a {@link Class} it will be returned directly.
425     *         Otherwise, the super-type will be checked by calling the superclass
426     */
427    public static Optional<Class<?>> extractGenericTypeCovariantly(final Type type) {
428        if (null == type) {
429            log.trace("No KeyStoreType given, returning empty");
430            return Optional.empty();
431        }
432        if (type instanceof Class<?> class1) {
433            log.debug("Found actual class returning as result {}", type);
434            return Optional.of(class1);
435        }
436        if (type instanceof ParameterizedType parameterizedType) {
437            log.debug("found Parameterized type, for {}, calling recursively", type);
438            return extractGenericTypeCovariantly(parameterizedType.getRawType());
439        }
440        log.warn("Unable to determines generic-type for {}", type);
441        return Optional.empty();
442    }
443
444    /**
445     * Extracts a {@link ParameterizedType} view for the given type
446     *
447     * @param typeToBeExtractedFrom must not be null
448     * @return an {@link Optional} of the {@link ParameterizedType} view of the
449     *         given class.
450     */
451    public static Optional<ParameterizedType> extractParameterizedType(final Class<?> typeToBeExtractedFrom) {
452        log.debug("Extracting ParameterizedType from {}", typeToBeExtractedFrom);
453        if (null == typeToBeExtractedFrom) {
454            return Optional.empty();
455        }
456        if (Object.class.equals(typeToBeExtractedFrom)) {
457            log.debug("java.lang.Object is not a ParameterizedType");
458            return Optional.empty();
459        }
460        final var genericSuperclass = typeToBeExtractedFrom.getGenericSuperclass();
461        if (genericSuperclass instanceof ParameterizedType type) {
462            return Optional.of(type);
463        }
464        // Check the tree
465        return extractParameterizedType(typeToBeExtractedFrom.getSuperclass());
466    }
467
468    /**
469     * Returns a proxy instance that implements {@code interfaceType} by dispatching
470     * method invocations to {@code handler}. The class loader of
471     * {@code interfaceType} will be used to define the proxy class. To implement
472     * multiple interfaces or specify a class loader, use
473     * Proxy#newProxyInstance(Class, Constructor, InvocationHandler).
474     *
475     * @param interfaceType must not be null
476     * @param handler       the invocation handler
477     * @param <T>           the target type of the proxy
478     * @return the created Proxy-instance
479     * @throws IllegalArgumentException if {@code interfaceType} does not specify
480     *                                  the type of Java interface
481     * @author https://github.com/google/guava/blob/master/guava/src/com/google/common/reflect/Reflection.java
482     */
483    public static <T> T newProxy(final Class<T> interfaceType, final InvocationHandler handler) {
484        requireNonNull(handler);
485        Preconditions.checkArgument(interfaceType.isInterface(), "%s is not an interface", interfaceType);
486        final var object = Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class<?>[] { interfaceType },
487                handler);
488        return interfaceType.cast(object);
489    }
490
491    /**
492     * Try to detect class from call stack which was the previous, before the marker
493     * class name
494     *
495     * @param markerClasses class names which could be used as marker before the
496     *                      real caller name. Collection must not be {@code null}.
497     * @return option of detected caller class name
498     */
499    public static Optional<String> findCaller(final Collection<String> markerClasses) {
500        final var callerElement = findCallerElement(null, markerClasses);
501        return callerElement.map(StackTraceElement::getClassName);
502    }
503
504    /**
505     * Tries to detect class from call stack which was the previous, before the
506     * marker class name
507     *
508     * @param throwable     is an optional parameter, will be used to access the
509     *                      stack
510     * @param markerClasses class names which could be used as marker before the
511     *                      real caller name. Collection must not be {@code null}.
512     * @return option of detected caller class name
513     */
514    public static Optional<StackTraceElement> findCallerElement(final Throwable throwable,
515            final Collection<String> markerClasses) {
516
517        Objects.requireNonNull(markerClasses, "Marker class names are missing");
518
519        final StackTraceElement[] stackTraceElements;
520        if (null == throwable) {
521            stackTraceElements = Thread.currentThread().getStackTrace();
522        } else {
523            stackTraceElements = throwable.getStackTrace();
524        }
525        if (null == stackTraceElements || stackTraceElements.length < 5) {
526            return Optional.empty();
527        }
528        for (var index = 2; index < stackTraceElements.length; index++) {
529            final var element = stackTraceElements[index];
530            if (markerClasses.contains(element.getClassName())) {
531                if (stackTraceElements.length > index + 1) {
532                    return Optional.of(stackTraceElements[index + 1]);
533                }
534                return Optional.empty();
535            }
536        }
537        return Optional.empty();
538    }
539
540}