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}