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.property;
017
018import static de.cuioss.tools.collect.CollectionLiterals.mutableList;
019import static de.cuioss.tools.string.MoreStrings.requireNotEmptyTrimmed;
020import static java.util.Objects.requireNonNull;
021
022import java.beans.Beans;
023import java.beans.IntrospectionException;
024import java.beans.Introspector;
025import java.beans.PropertyDescriptor;
026import java.lang.reflect.InvocationTargetException;
027import java.lang.reflect.Method;
028import java.util.Objects;
029import java.util.Optional;
030
031import de.cuioss.tools.base.Preconditions;
032import de.cuioss.tools.logging.CuiLogger;
033import de.cuioss.tools.reflect.MoreReflection;
034import de.cuioss.tools.string.MoreStrings;
035import lombok.Builder;
036import lombok.NonNull;
037import lombok.Value;
038
039/**
040 * <h2>Overview</h2> An instance of {@link PropertyHolder} provides runtime
041 * information for a specific BeanProperty. Under the hood it uses {@link Beans}
042 * tooling provided by the JDK and the utilities {@link PropertyUtil} an and
043 * {@link MoreReflection}. Compared to the standard tooling it is more flexible
044 * regarding fluent api style of bean / DTOs.
045 * <h3>Usage</h3>
046 * <p>
047 * The usual entry-point is {@link #from(Class, String)}. In case you want to
048 * create your own instance you can use the contained builder directly using
049 * {@link #builder()}
050 * </p>
051 * <p>
052 * Now you can access the metadata for that property, see
053 * {@link #getMemberInfo()}, {@link #getName()}, {@link #getType()},
054 * {@link #getReadMethod()}
055 * </p>
056 * <p>
057 * Reading and writing of properties should be done by {@link #readFrom(Object)}
058 * and {@link #writeTo(Object, Object)}. Directly using {@link #getReadMethod()}
059 * and {@link #getWriteMethod()} is more error-prone and less versatile.
060 * </p>
061 *
062 * <h2>Caution:</h2>
063 * <p>
064 * Use reflection only if there is no other way. Even if some of the problems
065 * are minimized by using this type. It should be used either in test-code, what
066 * we actually do, and not production code. An other reason could be framework
067 * code. as for that you should exactly know what you do.
068 * </p>
069 *
070 * @author Oliver Wolff
071 *
072 */
073@Value
074@Builder
075public class PropertyHolder {
076
077    private static final String UNABLE_TO_LOAD_PROPERTY_DESCRIPTOR = "Unable to load property-descriptor for attribute '%s' on type '%s'";
078
079    private static final CuiLogger log = new CuiLogger(PropertyHolder.class);
080
081    /** The name of the property. */
082    @NonNull
083    private final String name;
084
085    /** The actual type of the property. */
086    @NonNull
087    private final Class<?> type;
088
089    /**
090     * Provides additional runtime information for the property, see
091     * {@link PropertyMemberInfo}
092     */
093    @NonNull
094    private final PropertyMemberInfo memberInfo;
095
096    /** Provides additional Runtime-information, see {@link PropertyReadWrite} */
097    @NonNull
098    private final PropertyReadWrite readWrite;
099
100    /** Derived by {@link PropertyDescriptor}, may be null */
101    private final Method readMethod;
102
103    /** Derived by {@link PropertyDescriptor}, may be null */
104    private final Method writeMethod;
105
106    /**
107     * Reads the property on the given bean identified by the concrete
108     * {@link PropertyHolder} and the given bean. First it tries to access the
109     * readMethod derived by the {@link PropertyDescriptor}. If this can not be
110     * achieved, e.g. for types that do not match exactly Java-Bean-Specification it
111     * tries to read the property by using
112     * {@link PropertyUtil#readProperty(Object, String)}
113     *
114     * @param bean instance to be read from, must not be null
115     * @return the object read from the property
116     * @throws IllegalStateException in case the property can not be read, see
117     *                               {@link PropertyReadWrite#isReadable()}
118     * @throws IllegalStateException in case some Exception occurred while reading
119     */
120    public Object readFrom(Object bean) {
121        log.debug("Reading property '%s' from %s", name, bean);
122        requireNonNull(bean);
123        Preconditions.checkState(readWrite.isReadable(), "Property '%s' on bean '%s' can not be read", name, bean);
124        if (null != readMethod) {
125            try {
126                return readMethod.invoke(bean);
127            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
128                throw new IllegalStateException(
129                        MoreStrings.lenientFormat(PropertyUtil.UNABLE_TO_READ_PROPERTY, name, bean.getClass()), e);
130            }
131        }
132        return PropertyUtil.readProperty(bean, name);
133    }
134
135    /**
136     * @param bean          instance to be read from, must not be null
137     * @param propertyValue to be set
138     * @return In case the property set method is void the given bean will be
139     *         returned. Otherwise, the return value of the method invocation,
140     *         assuming the setMethods is a builder / fluent-api type.
141     * @throws IllegalStateException in case the property can not be read, see
142     *                               {@link PropertyReadWrite#isWriteable()}
143     * @throws IllegalStateException in case some Exception occurred while writing
144     */
145    public Object writeTo(Object bean, Object propertyValue) {
146        log.debug("Writing %s to property '%s' on %s", propertyValue, name, bean);
147        requireNonNull(bean);
148        Preconditions.checkState(readWrite.isWriteable(), "Property '%s' on bean '%s' can not be written", name, bean);
149        if (null != writeMethod) {
150            try {
151                var result = writeMethod.invoke(bean, propertyValue);
152                return Objects.requireNonNullElse(result, bean);
153            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
154                throw new IllegalStateException(
155                        MoreStrings.lenientFormat(PropertyUtil.UNABLE_TO_WRITE_PROPERTY_RUNTIME, name, bean.getClass()),
156                        e);
157            }
158        }
159        return PropertyUtil.writeProperty(bean, name, propertyValue);
160    }
161
162    /**
163     * Factory Method for creating a concrete {@link PropertyHolder}
164     *
165     * @param beanType      must not be null
166     * @param attributeName must not be null nor empty
167     * @return the concrete {@link PropertyHolder} for the given parameter if
168     *         applicable
169     * @throws IllegalArgumentException for cases where {@link Introspector} is not
170     *                                  capable of resolving a
171     *                                  {@link PropertyDescriptor}. This is usually
172     *                                  the case if it is not a valid bean.
173     */
174    public static Optional<PropertyHolder> from(Class<?> beanType, String attributeName) {
175        requireNonNull(beanType);
176        requireNotEmptyTrimmed(attributeName);
177        try {
178            var info = Introspector.getBeanInfo(beanType);
179            var descriptor = mutableList(info.getPropertyDescriptors()).stream()
180                    .filter(desc -> attributeName.equalsIgnoreCase(desc.getName())).findFirst();
181            if (descriptor.isEmpty()) {
182                log.debug(UNABLE_TO_LOAD_PROPERTY_DESCRIPTOR, attributeName, beanType);
183                return buildByReflection(beanType, attributeName);
184            }
185            return doBuild(descriptor.get(), beanType, attributeName);
186        } catch (IntrospectionException e) {
187            throw new IllegalArgumentException(
188                    MoreStrings.lenientFormat(UNABLE_TO_LOAD_PROPERTY_DESCRIPTOR, attributeName, beanType), e);
189        }
190    }
191
192    private static Optional<PropertyHolder> doBuild(PropertyDescriptor propertyDescriptor, Class<?> type,
193            String attributeName) {
194        var builder = builder();
195        builder.name(attributeName);
196        builder.readWrite(PropertyReadWrite.fromPropertyDescriptor(propertyDescriptor, type, attributeName));
197        builder.readMethod(propertyDescriptor.getReadMethod());
198        builder.writeMethod(propertyDescriptor.getWriteMethod());
199        builder.memberInfo(PropertyMemberInfo.resolveForBean(type, attributeName));
200        builder.type(propertyDescriptor.getPropertyType());
201        return Optional.of(builder.build());
202    }
203
204    static Optional<PropertyHolder> buildByReflection(Class<?> beanType, String attributeName) {
205        log.trace("Trying reflection for determining attribute '%s' on type '%s'", attributeName, beanType);
206        var field = MoreReflection.accessField(beanType, attributeName);
207        if (field.isEmpty()) {
208            return Optional.empty();
209        }
210        var builder = builder();
211        builder.name(attributeName);
212        builder.readWrite(PropertyReadWrite.resolveForBean(beanType, attributeName));
213        builder.memberInfo(PropertyMemberInfo.resolveForBean(beanType, attributeName));
214        builder.type(PropertyUtil.resolvePropertyType(beanType, attributeName).orElse(Object.class));
215        return Optional.of(builder.build());
216    }
217
218}