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