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}