001// Generated by delombok at Mon Jan 02 16:27:27 UTC 2023
002package io.cui.tools.property;
003
004import static io.cui.tools.collect.CollectionLiterals.mutableList;
005import static io.cui.tools.string.MoreStrings.requireNotEmptyTrimmed;
006import static java.util.Objects.requireNonNull;
007import java.beans.BeanInfo;
008import java.beans.Beans;
009import java.beans.IntrospectionException;
010import java.beans.Introspector;
011import java.beans.PropertyDescriptor;
012import java.lang.reflect.Field;
013import java.lang.reflect.InvocationTargetException;
014import java.lang.reflect.Method;
015import java.util.Objects;
016import java.util.Optional;
017import io.cui.tools.base.Preconditions;
018import io.cui.tools.logging.CuiLogger;
019import io.cui.tools.reflect.MoreReflection;
020import io.cui.tools.string.MoreStrings;
021import lombok.NonNull;
022
023/**
024 * <h2>Overview</h2>
025 * An instance of {@link PropertyHolder} provides runtime information for a specific BeanProperty.
026 * Under the hood it uses {@link Beans} tooling provided by the JDK and the utilities
027 * {@link PropertyUtil} an and {@link MoreReflection}. Compared to the standard tooling it is
028 * more flexible regarding fluent api style of bean / DTOs.
029 * <h3>Usage</h3>
030 * <p>
031 * The usual entry-point is {@link #from(Class, String)}. In case you want to create your own
032 * instance you can use the contained builder directly using {@link #builder()}
033 * </p>
034 * <p>
035 * Now you can access the metadata for that property, see {@link #getMemberInfo()},
036 * {@link #getName()}, {@link #getType()}, {@link #getReadMethod()}
037 * </p>
038 * <p>
039 * Reading and writing of properties should be done by {@link #readFrom(Object)}
040 * and {@link #writeTo(Object, Object)}. Directly using
041 * {@link #getReadMethod()} and {@link #getWriteMethod()} is more error-prone and less versatile.
042 * </p>
043 *
044 * <h2>Caution:</h2>
045 * <p>
046 * Use reflection only if there is no other way. Even if some of the problems are
047 * minimized by using this type. It should be used either in test-code, what we actually do, and
048 * not production code. An other reason could be framework code. as for that you should exactly know
049 * what you do.
050 * </p>
051 *
052 * @author Oliver Wolff
053 */
054public final class PropertyHolder {
055    private static final String UNABLE_TO_LOAD_PROPERTY_DESCRIPTOR = "Unable to load property-descriptor for attribute \'%s\' on type \'%s\'";
056    private static final CuiLogger log = new CuiLogger(PropertyHolder.class);
057    /**
058     * The name of the property.
059     */
060    @NonNull
061    private final String name;
062    /**
063     * The actual type of the property.
064     */
065    @NonNull
066    private final Class<?> type;
067    /**
068     * Provides additional runtime information for the property, see {@link PropertyMemberInfo}
069     */
070    @NonNull
071    private final PropertyMemberInfo memberInfo;
072    /**
073     * Provides additional Runtime-information, see {@link PropertyReadWrite}
074     */
075    @NonNull
076    private final PropertyReadWrite readWrite;
077    /**
078     * Derived by {@link PropertyDescriptor}, may be null
079     */
080    private final Method readMethod;
081    /**
082     * Derived by {@link PropertyDescriptor}, may be null
083     */
084    private final Method writeMethod;
085
086    /**
087     * Reads the property on the given bean identified by the concrete {@link PropertyHolder} and
088     * the given bean. First it tries to access the readMethod derived by the
089     * {@link PropertyDescriptor}. If this can not be achieved, e.g. for types that do not match
090     * exactly Java-Bean-Specification it tries to read the property by using
091     * {@link PropertyUtil#readProperty(Object, String)}
092     *
093     * @param bean instance to be read from, must not be null
094     * @return the object read from the property
095     * @throws IllegalStateException in case the property can not be read, see
096     *             {@link PropertyReadWrite#isReadable()}
097     * @throws IllegalStateException in case some Exception occurred while reading
098     */
099    public Object readFrom(Object bean) {
100        log.debug("Reading property \'%s\' from %s", name, bean);
101        requireNonNull(bean);
102        Preconditions.checkState(readWrite.isReadable(), "Property \'%s\' on bean \'%s\' can not be read", name, bean);
103        if (null != readMethod) {
104            try {
105                return readMethod.invoke(bean);
106            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
107                throw new IllegalStateException(MoreStrings.lenientFormat(PropertyUtil.UNABLE_TO_READ_PROPERTY, name, bean.getClass()), e);
108            }
109        }
110        return PropertyUtil.readProperty(bean, name);
111    }
112
113    /**
114     * @param bean instance to be read from, must not be null
115     * @param propertyValue to be set
116     * @return In case the property set method is void the given bean will be returned. Otherwise,
117     *         the return value of the method invocation, assuming the setMethods is a builder /
118     *         fluent-api type.
119     * @throws IllegalStateException in case the property can not be read, see
120     *             {@link PropertyReadWrite#isWriteable()}
121     * @throws IllegalStateException in case some Exception occurred while writing
122     */
123    public Object writeTo(Object bean, Object propertyValue) {
124        log.debug("Writing %s to property \'%s\' on %s", propertyValue, name, bean);
125        requireNonNull(bean);
126        Preconditions.checkState(readWrite.isWriteable(), "Property \'%s\' on bean \'%s\' can not be written", name, bean);
127        if (null != writeMethod) {
128            try {
129                Object result = writeMethod.invoke(bean, propertyValue);
130                return Objects.requireNonNullElse(result, bean);
131            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
132                throw new IllegalStateException(MoreStrings.lenientFormat(PropertyUtil.UNABLE_TO_WRITE_PROPERTY_RUNTIME, name, bean.getClass()), e);
133            }
134        }
135        return PropertyUtil.writeProperty(bean, name, propertyValue);
136    }
137
138    /**
139     * Factory Method for creating a concrete {@link PropertyHolder}
140     *
141     * @param beanType must not be null
142     * @param attributeName must not be null nor empty
143     * @return the concrete {@link PropertyHolder} for the given parameter if applicable
144     * @throws IllegalArgumentException for cases where {@link Introspector} is not capable of
145     *             resolving a {@link PropertyDescriptor}. This is usually the case if it is not a
146     *             valid bean.
147     */
148    public static Optional<PropertyHolder> from(Class<?> beanType, String attributeName) {
149        requireNonNull(beanType);
150        requireNotEmptyTrimmed(attributeName);
151        try {
152            BeanInfo info = Introspector.getBeanInfo(beanType);
153            Optional<PropertyDescriptor> descriptor = mutableList(info.getPropertyDescriptors()).stream().filter(desc -> attributeName.equalsIgnoreCase(desc.getName())).findFirst();
154            if (!descriptor.isPresent()) {
155                log.debug(UNABLE_TO_LOAD_PROPERTY_DESCRIPTOR, attributeName, beanType);
156                return buildByReflection(beanType, attributeName);
157            }
158            return doBuild(descriptor.get(), beanType, attributeName);
159        } catch (IntrospectionException e) {
160            throw new IllegalArgumentException(MoreStrings.lenientFormat(UNABLE_TO_LOAD_PROPERTY_DESCRIPTOR, attributeName, beanType), e);
161        }
162    }
163
164    private static Optional<PropertyHolder> doBuild(PropertyDescriptor propertyDescriptor, Class<?> type, String attributeName) {
165        PropertyHolderBuilder builder = builder();
166        builder.name(attributeName);
167        builder.readWrite(PropertyReadWrite.fromPropertyDescriptor(propertyDescriptor, type, attributeName));
168        builder.readMethod(propertyDescriptor.getReadMethod());
169        builder.writeMethod(propertyDescriptor.getWriteMethod());
170        builder.memberInfo(PropertyMemberInfo.resolveForBean(type, attributeName));
171        builder.type(propertyDescriptor.getPropertyType());
172        return Optional.of(builder.build());
173    }
174
175    static Optional<PropertyHolder> buildByReflection(Class<?> beanType, String attributeName) {
176        log.trace("Trying reflection for determining attribute \'%s\' on type \'%s\'", attributeName, beanType);
177        Optional<Field> field = MoreReflection.accessField(beanType, attributeName);
178        if (!field.isPresent()) {
179            return Optional.empty();
180        }
181        PropertyHolderBuilder builder = builder();
182        builder.name(attributeName);
183        builder.readWrite(PropertyReadWrite.resolveForBean(beanType, attributeName));
184        builder.memberInfo(PropertyMemberInfo.resolveForBean(beanType, attributeName));
185        builder.type(PropertyUtil.resolvePropertyType(beanType, attributeName).orElse(Object.class));
186        return Optional.of(builder.build());
187    }
188
189    @java.lang.SuppressWarnings("all")
190    @lombok.Generated
191    PropertyHolder(@NonNull final String name, @NonNull final Class<?> type, @NonNull final PropertyMemberInfo memberInfo, @NonNull final PropertyReadWrite readWrite, final Method readMethod, final Method writeMethod) {
192        if (name == null) {
193            throw new java.lang.NullPointerException("name is marked non-null but is null");
194        }
195        if (type == null) {
196            throw new java.lang.NullPointerException("type is marked non-null but is null");
197        }
198        if (memberInfo == null) {
199            throw new java.lang.NullPointerException("memberInfo is marked non-null but is null");
200        }
201        if (readWrite == null) {
202            throw new java.lang.NullPointerException("readWrite is marked non-null but is null");
203        }
204        this.name = name;
205        this.type = type;
206        this.memberInfo = memberInfo;
207        this.readWrite = readWrite;
208        this.readMethod = readMethod;
209        this.writeMethod = writeMethod;
210    }
211
212
213    @java.lang.SuppressWarnings("all")
214    @lombok.Generated
215    public static class PropertyHolderBuilder {
216        @java.lang.SuppressWarnings("all")
217        @lombok.Generated
218        private String name;
219        @java.lang.SuppressWarnings("all")
220        @lombok.Generated
221        private Class<?> type;
222        @java.lang.SuppressWarnings("all")
223        @lombok.Generated
224        private PropertyMemberInfo memberInfo;
225        @java.lang.SuppressWarnings("all")
226        @lombok.Generated
227        private PropertyReadWrite readWrite;
228        @java.lang.SuppressWarnings("all")
229        @lombok.Generated
230        private Method readMethod;
231        @java.lang.SuppressWarnings("all")
232        @lombok.Generated
233        private Method writeMethod;
234
235        @java.lang.SuppressWarnings("all")
236        @lombok.Generated
237        PropertyHolderBuilder() {
238        }
239
240        /**
241         * The name of the property.
242         * @return {@code this}.
243         */
244        @java.lang.SuppressWarnings("all")
245        @lombok.Generated
246        public PropertyHolder.PropertyHolderBuilder name(@NonNull final String name) {
247            if (name == null) {
248                throw new java.lang.NullPointerException("name is marked non-null but is null");
249            }
250            this.name = name;
251            return this;
252        }
253
254        /**
255         * The actual type of the property.
256         * @return {@code this}.
257         */
258        @java.lang.SuppressWarnings("all")
259        @lombok.Generated
260        public PropertyHolder.PropertyHolderBuilder type(@NonNull final Class<?> type) {
261            if (type == null) {
262                throw new java.lang.NullPointerException("type is marked non-null but is null");
263            }
264            this.type = type;
265            return this;
266        }
267
268        /**
269         * Provides additional runtime information for the property, see {@link PropertyMemberInfo}
270         * @return {@code this}.
271         */
272        @java.lang.SuppressWarnings("all")
273        @lombok.Generated
274        public PropertyHolder.PropertyHolderBuilder memberInfo(@NonNull final PropertyMemberInfo memberInfo) {
275            if (memberInfo == null) {
276                throw new java.lang.NullPointerException("memberInfo is marked non-null but is null");
277            }
278            this.memberInfo = memberInfo;
279            return this;
280        }
281
282        /**
283         * Provides additional Runtime-information, see {@link PropertyReadWrite}
284         * @return {@code this}.
285         */
286        @java.lang.SuppressWarnings("all")
287        @lombok.Generated
288        public PropertyHolder.PropertyHolderBuilder readWrite(@NonNull final PropertyReadWrite readWrite) {
289            if (readWrite == null) {
290                throw new java.lang.NullPointerException("readWrite is marked non-null but is null");
291            }
292            this.readWrite = readWrite;
293            return this;
294        }
295
296        /**
297         * Derived by {@link PropertyDescriptor}, may be null
298         * @return {@code this}.
299         */
300        @java.lang.SuppressWarnings("all")
301        @lombok.Generated
302        public PropertyHolder.PropertyHolderBuilder readMethod(final Method readMethod) {
303            this.readMethod = readMethod;
304            return this;
305        }
306
307        /**
308         * Derived by {@link PropertyDescriptor}, may be null
309         * @return {@code this}.
310         */
311        @java.lang.SuppressWarnings("all")
312        @lombok.Generated
313        public PropertyHolder.PropertyHolderBuilder writeMethod(final Method writeMethod) {
314            this.writeMethod = writeMethod;
315            return this;
316        }
317
318        @java.lang.SuppressWarnings("all")
319        @lombok.Generated
320        public PropertyHolder build() {
321            return new PropertyHolder(this.name, this.type, this.memberInfo, this.readWrite, this.readMethod, this.writeMethod);
322        }
323
324        @java.lang.Override
325        @java.lang.SuppressWarnings("all")
326        @lombok.Generated
327        public java.lang.String toString() {
328            return "PropertyHolder.PropertyHolderBuilder(name=" + this.name + ", type=" + this.type + ", memberInfo=" + this.memberInfo + ", readWrite=" + this.readWrite + ", readMethod=" + this.readMethod + ", writeMethod=" + this.writeMethod + ")";
329        }
330    }
331
332    @java.lang.SuppressWarnings("all")
333    @lombok.Generated
334    public static PropertyHolder.PropertyHolderBuilder builder() {
335        return new PropertyHolder.PropertyHolderBuilder();
336    }
337
338    /**
339     * The name of the property.
340     */
341    @NonNull
342    @java.lang.SuppressWarnings("all")
343    @lombok.Generated
344    public String getName() {
345        return this.name;
346    }
347
348    /**
349     * The actual type of the property.
350     */
351    @NonNull
352    @java.lang.SuppressWarnings("all")
353    @lombok.Generated
354    public Class<?> getType() {
355        return this.type;
356    }
357
358    /**
359     * Provides additional runtime information for the property, see {@link PropertyMemberInfo}
360     */
361    @NonNull
362    @java.lang.SuppressWarnings("all")
363    @lombok.Generated
364    public PropertyMemberInfo getMemberInfo() {
365        return this.memberInfo;
366    }
367
368    /**
369     * Provides additional Runtime-information, see {@link PropertyReadWrite}
370     */
371    @NonNull
372    @java.lang.SuppressWarnings("all")
373    @lombok.Generated
374    public PropertyReadWrite getReadWrite() {
375        return this.readWrite;
376    }
377
378    /**
379     * Derived by {@link PropertyDescriptor}, may be null
380     */
381    @java.lang.SuppressWarnings("all")
382    @lombok.Generated
383    public Method getReadMethod() {
384        return this.readMethod;
385    }
386
387    /**
388     * Derived by {@link PropertyDescriptor}, may be null
389     */
390    @java.lang.SuppressWarnings("all")
391    @lombok.Generated
392    public Method getWriteMethod() {
393        return this.writeMethod;
394    }
395
396    @java.lang.Override
397    @java.lang.SuppressWarnings("all")
398    @lombok.Generated
399    public boolean equals(final java.lang.Object o) {
400        if (o == this) return true;
401        if (!(o instanceof PropertyHolder)) return false;
402        final PropertyHolder other = (PropertyHolder) o;
403        final java.lang.Object this$name = this.getName();
404        final java.lang.Object other$name = other.getName();
405        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
406        final java.lang.Object this$type = this.getType();
407        final java.lang.Object other$type = other.getType();
408        if (this$type == null ? other$type != null : !this$type.equals(other$type)) return false;
409        final java.lang.Object this$memberInfo = this.getMemberInfo();
410        final java.lang.Object other$memberInfo = other.getMemberInfo();
411        if (this$memberInfo == null ? other$memberInfo != null : !this$memberInfo.equals(other$memberInfo)) return false;
412        final java.lang.Object this$readWrite = this.getReadWrite();
413        final java.lang.Object other$readWrite = other.getReadWrite();
414        if (this$readWrite == null ? other$readWrite != null : !this$readWrite.equals(other$readWrite)) return false;
415        final java.lang.Object this$readMethod = this.getReadMethod();
416        final java.lang.Object other$readMethod = other.getReadMethod();
417        if (this$readMethod == null ? other$readMethod != null : !this$readMethod.equals(other$readMethod)) return false;
418        final java.lang.Object this$writeMethod = this.getWriteMethod();
419        final java.lang.Object other$writeMethod = other.getWriteMethod();
420        if (this$writeMethod == null ? other$writeMethod != null : !this$writeMethod.equals(other$writeMethod)) return false;
421        return true;
422    }
423
424    @java.lang.Override
425    @java.lang.SuppressWarnings("all")
426    @lombok.Generated
427    public int hashCode() {
428        final int PRIME = 59;
429        int result = 1;
430        final java.lang.Object $name = this.getName();
431        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
432        final java.lang.Object $type = this.getType();
433        result = result * PRIME + ($type == null ? 43 : $type.hashCode());
434        final java.lang.Object $memberInfo = this.getMemberInfo();
435        result = result * PRIME + ($memberInfo == null ? 43 : $memberInfo.hashCode());
436        final java.lang.Object $readWrite = this.getReadWrite();
437        result = result * PRIME + ($readWrite == null ? 43 : $readWrite.hashCode());
438        final java.lang.Object $readMethod = this.getReadMethod();
439        result = result * PRIME + ($readMethod == null ? 43 : $readMethod.hashCode());
440        final java.lang.Object $writeMethod = this.getWriteMethod();
441        result = result * PRIME + ($writeMethod == null ? 43 : $writeMethod.hashCode());
442        return result;
443    }
444
445    @java.lang.Override
446    @java.lang.SuppressWarnings("all")
447    @lombok.Generated
448    public java.lang.String toString() {
449        return "PropertyHolder(name=" + this.getName() + ", type=" + this.getType() + ", memberInfo=" + this.getMemberInfo() + ", readWrite=" + this.getReadWrite() + ", readMethod=" + this.getReadMethod() + ", writeMethod=" + this.getWriteMethod() + ")";
450    }
451}