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