001// Generated by delombok at Sat Feb 25 17:58:34 UTC 2023
002package io.cui.tools.property;
003
004import static io.cui.tools.string.MoreStrings.requireNotEmptyTrimmed;
005import static java.util.Objects.requireNonNull;
006import java.lang.reflect.InvocationTargetException;
007import java.lang.reflect.Method;
008import java.util.Objects;
009import java.util.Optional;
010import io.cui.tools.base.Preconditions;
011import io.cui.tools.logging.CuiLogger;
012import io.cui.tools.reflect.MoreReflection;
013import io.cui.tools.string.MoreStrings;
014
015/**
016 * Helper class providing convenient methods for reading from / writing to java beans.
017 * <h2>Caution:</h2>
018 * <p>
019 * Use reflection only if there is no other way. Even if some of the problems are
020 * minimized by using this type. It should be used either in test-code, what is we actually do, and
021 * not production code. An other reason could be framework code. as for that you should exactly know
022 * what you do.
023 * </p>
024 *
025 * @author Oliver Wolff
026 */
027public final class PropertyUtil {
028    private static final CuiLogger log = new CuiLogger(PropertyUtil.class);
029    static final String UNABLE_TO_WRITE_PROPERTY = "Unable to write property \'%s\' to beanType \'%s\': no suitable write method found. Needed property-type \'%s\'";
030    static final String UNABLE_TO_WRITE_PROPERTY_RUNTIME = "Unable to write property \'%s\' to beanType \'%s\'. Needed property-type \'%s\'";
031    static final String UNABLE_TO_READ_PROPERTY = "Unable to read property \'%s\' from beanType \'%s\'.";
032
033    // owolff: False Positive, Optional#isPresent is checked
034    /**
035     * @param bean instance to be read from, must not be null
036     * @param propertyName to be read, must not be null nor empty nor blank
037     * @return the object read from the property
038     * @throws IllegalArgumentException in case the property does not exist (determined by a read
039     *             method)
040     * @throws IllegalStateException in case some Exception occurred while reading
041     */
042    @SuppressWarnings("squid:S3655")
043    public static Object readProperty(Object bean, String propertyName) {
044        log.debug("Reading property \'%s\' from %s", propertyName, bean);
045        requireNonNull(bean);
046        requireNotEmptyTrimmed(propertyName);
047        var reader = MoreReflection.retrieveAccessMethod(bean.getClass(), propertyName);
048        Preconditions.checkArgument(reader.isPresent(), UNABLE_TO_READ_PROPERTY, propertyName, bean.getClass());
049        try {
050            return reader.get().invoke(bean);
051        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
052            throw new IllegalStateException(MoreStrings.lenientFormat(UNABLE_TO_READ_PROPERTY, propertyName, bean.getClass()), e);
053        }
054    }
055
056    /**
057     * @param bean instance to be read from, must not be null
058     * @param propertyName to be read, must not be null nor empty nor blank
059     * @param propertyValue to be set
060     * @return In case the property set method is void the given bean will be returned. Otherwise,
061     *         the return value of the method invocation, assuming the setMethods is a builder type.
062     * @throws IllegalArgumentException in case the property does not exist (determined by a write
063     *             method)
064     * @throws IllegalStateException in case some Exception occurred while writing
065     */
066    public static Object writeProperty(Object bean, String propertyName, Object propertyValue) {
067        log.debug("Writing \'%s\' to property \'%s\' on \'%s\'", propertyValue, propertyName, bean);
068        requireNonNull(bean);
069        requireNotEmptyTrimmed(propertyName);
070        var writeMethod = determineWriteMethod(bean, propertyName, propertyValue);
071        try {
072            var result = writeMethod.invoke(bean, propertyValue);
073            return Objects.requireNonNullElse(result, bean);
074        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
075            var target = propertyValue != null ? propertyValue.getClass().getName() : "Undefined";
076            throw new IllegalStateException(MoreStrings.lenientFormat(UNABLE_TO_WRITE_PROPERTY_RUNTIME, propertyName, bean.getClass(), target), e);
077        }
078    }
079
080    /**
081     * Tries to determine the type of given property
082     *
083     * @param beanType to be checked, must not be null
084     * @param propertyName to be checked, must not be null
085     * @return an {@link Optional} on the actual type of the property. First it checks an
086     *         access-methods, then it tries to directly access the field, and if that fails it uses
087     *         the first read method found, {@link Optional#empty()} otherwise.
088     */
089    public static Optional<Class<?>> resolvePropertyType(Class<?> beanType, String propertyName) {
090        var retrieveAccessMethod = MoreReflection.retrieveAccessMethod(beanType, propertyName);
091        if (retrieveAccessMethod.isPresent()) {
092            log.trace("Found read-method on class \'%s\' for property-name \'%s\'", beanType, propertyName);
093            return Optional.of(retrieveAccessMethod.get().getReturnType());
094        }
095        var field = MoreReflection.accessField(beanType, propertyName);
096        if (field.isPresent()) {
097            log.trace("Found field on class \'%s\' with name \'%s\'", beanType, propertyName);
098            return Optional.of(field.get().getType());
099        }
100        log.debug("Neither read-method nor field found on class \'%s\' for property-name \'%s\', checking write methods, returning first type found", beanType, propertyName);
101        var candidates = MoreReflection.retrieveWriteMethodCandidates(beanType, propertyName);
102        if (!candidates.isEmpty()) {
103            return Optional.of(candidates.iterator().next().getParameterTypes()[0]);
104        }
105        log.debug("Unable to detect property-type on class \'%s\' for property-name \'%s\'", beanType, propertyName);
106        return Optional.empty();
107    }
108
109    static Method determineWriteMethod(Object bean, String propertyName, Object propertyValue) {
110        var candidates = MoreReflection.retrieveWriteMethodCandidates(bean.getClass(), propertyName);
111        var target = propertyValue != null ? propertyValue.getClass().getName() : "Undefined";
112        Preconditions.checkArgument(!candidates.isEmpty(), UNABLE_TO_WRITE_PROPERTY, propertyName, bean.getClass(), target);
113        if (null == propertyValue) {
114            log.trace("No / Null propertyValue given, so any method should suffice to write property \'%s\' to %s", propertyName, bean);
115            return candidates.iterator().next();
116        }
117        for (Method candidate : candidates) {
118            if (MoreReflection.checkWhetherParameterIsAssignable(candidate.getParameterTypes()[0], propertyValue.getClass())) {
119                log.trace("Found method %s to write property \'%s\' to %s", candidate, propertyName, bean);
120                return candidate;
121            }
122        }
123        throw new IllegalArgumentException(MoreStrings.lenientFormat(UNABLE_TO_WRITE_PROPERTY, propertyName, bean.getClass(), target));
124    }
125
126    @java.lang.SuppressWarnings("all")
127    @lombok.Generated
128    private PropertyUtil() {
129        throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
130    }
131}