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.reflect; 017 018import java.lang.reflect.Field; 019import java.lang.reflect.Member; 020import java.util.Optional; 021 022import de.cuioss.tools.logging.CuiLogger; 023import de.cuioss.tools.string.MoreStrings; 024import lombok.Getter; 025import lombok.NonNull; 026 027/** 028 * Wrapper around a {@link Field} that handles implicitly the accessible flag 029 * for access. 030 * 031 * @author Oliver Wolff 032 * 033 */ 034@SuppressWarnings("java:S3011") // owolff: The warning is "Reflection should not be used to 035 // increase accessibility of classes, methods, or fields " 036 // What is actually the use-case of this type, therefore there 037 // is nothing we can do 038public class FieldWrapper { 039 040 private static final CuiLogger log = new CuiLogger(FieldWrapper.class); 041 042 @Getter 043 @NonNull 044 private final Field field; 045 046 private final Class<?> declaringClass; 047 048 /** 049 * @param field must not be null 050 */ 051 public FieldWrapper(Field field) { 052 this.field = field; 053 declaringClass = ((Member) field).getDeclaringClass(); 054 } 055 056 /** 057 * Reads from the field determined by {@link #getField()}. It implicitly sets 058 * and resets the {@link Field#isAccessible()} flag. 059 * 060 * @param object to be read from 061 * @return an {@link Optional} on the given field value if applicable. May 062 * return {@link Optional#empty()} for cases where: 063 * <ul> 064 * <li>Field value is {@code null}</li> 065 * <li>Given Object is {@code null}</li> 066 * <li>Given Object is improper type</li> 067 * <li>an {@link IllegalAccessException} occurred while accessing</li> 068 * </ul> 069 */ 070 public Optional<Object> readValue(Object object) { 071 if (null == object) { 072 log.trace("No Object given, returning Optional#empty()"); 073 return Optional.empty(); 074 } 075 if (!declaringClass.isAssignableFrom(object.getClass())) { 076 log.trace("Given Object is improper type, returning Optional#empty()"); 077 return Optional.empty(); 078 } 079 var initialAccessible = field.canAccess(object); 080 log.trace("Reading from field '{}' with accessibleFlag='{}' ", field, initialAccessible); 081 synchronized (field) { 082 if (!initialAccessible) { 083 log.trace("Explicitly setting accessible flag"); 084 field.setAccessible(true); 085 } 086 try { 087 return Optional.ofNullable(field.get(object)); 088 } catch (IllegalArgumentException | IllegalAccessException e) { 089 log.warn(e, "Reading from field '{}' with accessible='{}' and parameter ='{}' could not complete", 090 field, initialAccessible, object); 091 return Optional.empty(); 092 } finally { 093 if (!initialAccessible) { 094 log.trace("Resetting accessible flag"); 095 field.setAccessible(false); 096 } 097 } 098 } 099 } 100 101 /** 102 * Reads the value from the field in the given object. It implicitly sets and 103 * resets the {@link Field#isAccessible()} flag. 104 * 105 * @param fieldName to be read 106 * @param object to be read from 107 * 108 * @return the field value. {@link Optional#empty()} if the field cannot be 109 * read. 110 */ 111 public static final Optional<Object> readValue(final String fieldName, final Object object) { 112 final var fieldProvider = from(object.getClass(), fieldName); 113 log.trace("FieldWrapper: {}", fieldProvider); 114 if (fieldProvider.isPresent()) { 115 var fieldWrapper = fieldProvider.get(); 116 return fieldWrapper.readValue(object); 117 } 118 return Optional.empty(); 119 } 120 121 /** 122 * Writes to the field determined by {@link #getField()}. It implicitly sets and 123 * resets the {@link Field#isAccessible()} flag. 124 * 125 * @param object to be written to, must not be null 126 * @param value to be written, may be null 127 * 128 * @throws NullPointerException in case object is {@code null} 129 * @throws IllegalArgumentException in case the value is not applicable to the 130 * field 131 * @throws IllegalStateException wrapping an {@link IllegalAccessException} 132 */ 133 public void writeValue(@NonNull Object object, Object value) { 134 var initialAccessible = field.canAccess(object); 135 log.trace("Writing to field '{}' with accessibleFlag='{}' ", field, initialAccessible); 136 synchronized (field) { 137 if (!initialAccessible) { 138 log.trace("Explicitly setting accessible flag"); 139 field.setAccessible(true); 140 } 141 try { 142 field.set(object, value); 143 } catch (IllegalAccessException e) { 144 var message = MoreStrings.lenientFormat( 145 "Writing to field '{}' with accessible='{}' and parameter ='{}' could not complete", field, 146 initialAccessible, object); 147 throw new IllegalStateException(message, e); 148 } finally { 149 if (!initialAccessible) { 150 log.trace("Resetting accessible flag"); 151 field.setAccessible(false); 152 } 153 } 154 } 155 } 156 157 /** 158 * Factory Method for creating an {@link FieldWrapper} instance 159 * 160 * @param type must not be null 161 * @param fieldName must not be null 162 * @return a {@link FieldWrapper} if the {@link Field} can be determined, 163 * {@link Optional#empty()} otherwise 164 */ 165 public static final Optional<FieldWrapper> from(final Class<?> type, final String fieldName) { 166 var loaded = MoreReflection.accessField(type, fieldName); 167 if (loaded.isPresent()) { 168 return Optional.of(new FieldWrapper(loaded.get())); 169 } 170 return Optional.empty(); 171 } 172}