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