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.test.valueobjects.property;
017
018import static org.junit.jupiter.api.Assertions.assertEquals;
019import static org.junit.jupiter.api.Assertions.assertNotNull;
020import static org.junit.jupiter.api.Assertions.assertTrue;
021
022import de.cuioss.test.generator.TypedGenerator;
023import de.cuioss.test.valueobjects.property.util.AssertionStrategy;
024import de.cuioss.test.valueobjects.property.util.CollectionAsserts;
025import de.cuioss.test.valueobjects.property.util.PropertyAccessStrategy;
026import lombok.Getter;
027import lombok.NonNull;
028import lombok.RequiredArgsConstructor;
029import lombok.Setter;
030import lombok.ToString;
031
032/**
033 * Stateful wrapper around instances of {@link PropertyMetadata} that provides
034 * methods for reading / asserting properties according to the configured
035 * {@link PropertyAccessStrategy}, see {@link #apply(Object)},
036 * {@link #assertValueSet(Object)} and {@link #assertDefaultValue(Object)} . In
037 * addition it can create / store an instant specific generatedValue, see
038 * {@link #generateTestValue()}
039 *
040 * @author Oliver Wolff
041 */
042@RequiredArgsConstructor
043@ToString
044public class PropertySupport {
045
046    /**
047     * Used in conjunction with {@link #createCopyWithNonEqualValue()}. Defines the
048     * upper bound of retries for generating non-equal values from a given
049     * {@link TypedGenerator}
050     */
051    private static final int ENTROPY_GUARD = 50;
052
053    private static final String TARGET_MUST_NOT_BE_NULL = "target must not be null";
054
055    @NonNull
056    @Getter
057    private final PropertyMetadata propertyMetadata;
058
059    @Getter
060    @Setter
061    private Object generatedValue;
062
063    // Shortcuts to PropertyMetadata
064    /**
065     * @return boolean indicating whether the current property is readable
066     */
067    public boolean isReadable() {
068        return propertyMetadata.getPropertyReadWrite().isReadable();
069    }
070
071    /**
072     * @return boolean indicating whether the current property is defaultValued
073     */
074    public boolean isDefaultValue() {
075        return propertyMetadata.isDefaultValue();
076    }
077
078    /**
079     * @return boolean indicating whether the current property is required
080     */
081    public boolean isRequired() {
082        return propertyMetadata.isRequired();
083    }
084
085    /**
086     * @return the name of the property
087     */
088    public String getName() {
089        return propertyMetadata.getName();
090    }
091
092    /**
093     * @return boolean indicating whether the current property is a primitive
094     */
095    public boolean isPrimitive() {
096        return propertyMetadata.resolveActualClass().isPrimitive();
097    }
098
099    /**
100     * Generates a value from the contained generator and sets it to
101     * {@link #setGeneratedValue(Object)}.
102     *
103     * @return the generated test value
104     */
105    public Object generateTestValue() {
106        setGeneratedValue(propertyMetadata.next());
107        return getGeneratedValue();
108    }
109
110    /**
111     * Resets the generated value
112     */
113    public void reset() {
114        setGeneratedValue(null);
115    }
116
117    /**
118     * Writes the property, previously initialized with {@link #generateTestValue()}
119     * to the given target, using the configured {@link PropertyAccessStrategy}
120     *
121     * @param target must not be null
122     */
123    public void apply(final Object target) {
124        writeProperty(target, generatedValue);
125    }
126
127    /**
128     * Asserts that the value previously set by {@link #apply(Object)} is accessible
129     * by the corresponding read-method, e.g getProperty
130     *
131     * @param target must not be null
132     */
133    public void assertValueSet(final Object target) {
134        assertValueSet(target, generatedValue);
135    }
136
137    /**
138     * Asserts that the expected value is accessible by the corresponding
139     * read-method, e.g getProperty
140     *
141     * @param target   must not be null
142     * @param expected the expected content of the attribute
143     */
144    public void assertValueSet(final Object target, Object expected) {
145        final var actual = readProperty(target);
146
147        if (AssertionStrategy.COLLECTION_IGNORE_ORDER.equals(propertyMetadata.getAssertionStrategy())) {
148            CollectionAsserts.assertListsAreEqualIgnoringOrder(propertyMetadata.getName(), expected, actual);
149        } else {
150            assertEquals(expected, actual, "Invalid content found for property " + propertyMetadata.getName()
151                    + ", expected=" + expected + ", actual=" + actual);
152        }
153
154    }
155
156    /**
157     * Reads and returns the read property from the given target
158     *
159     * @param target to be read from, must not be null
160     * @return he read Property, may be {@code null}
161     */
162    public Object readProperty(Object target) {
163        assertNotNull(target, TARGET_MUST_NOT_BE_NULL);
164        return propertyMetadata.getPropertyAccessStrategy().readProperty(target, propertyMetadata);
165    }
166
167    /**
168     * Writes the given property the given target
169     *
170     * @param target        to be written to, must not be null
171     * @param propertyValue to be written
172     * @return he read Property, may be {@code null}
173     */
174    public Object writeProperty(Object target, Object propertyValue) {
175        assertNotNull(target, TARGET_MUST_NOT_BE_NULL);
176        return propertyMetadata.getPropertyAccessStrategy().writeProperty(target, propertyMetadata, propertyValue);
177    }
178
179    /**
180     * Asserts that the given object provides a default value.
181     *
182     * @param target must not be null
183     */
184    public void assertDefaultValue(final Object target) {
185
186        assertNotNull(target, TARGET_MUST_NOT_BE_NULL);
187
188        assertTrue(propertyMetadata.isDefaultValue(),
189                "There is no default value set: Invalid configuration for property " + propertyMetadata.getName());
190
191        assertNotNull(readProperty(target), "No defaultValue found for property " + propertyMetadata.getName());
192    }
193
194    /**
195     * Creates a copy of this instance
196     *
197     * @param withGeneratedValue indicating whether to copy the currently
198     *                           {@link #getGeneratedValue()}
199     * @return the created copy
200     */
201    public PropertySupport createCopy(final boolean withGeneratedValue) {
202        final var support = new PropertySupport(propertyMetadata);
203        if (withGeneratedValue) {
204            support.setGeneratedValue(generatedValue);
205        }
206        return support;
207    }
208
209    /**
210     * Creates a copy of this instance. In addition it tries to create a
211     * generatedValue that is not equal to the contained one. It will try this 50
212     * times and will then throw an {@link AssertionError}
213     *
214     * @return the created copy
215     */
216    public PropertySupport createCopyWithNonEqualValue() {
217        final var support = new PropertySupport(propertyMetadata);
218        if (null == getGeneratedValue()) {
219            support.generateTestValue();
220            return support;
221        }
222        var times = 0;
223        final var initialTestValue = getGeneratedValue();
224        while (true) {
225            final var otherValue = support.generateTestValue();
226            if (!initialTestValue.equals(otherValue)) {
227                break;
228            }
229            times++;
230            if (ENTROPY_GUARD == times) {
231                throw new AssertionError("Unable to create non equal test-value for " + propertyMetadata);
232            }
233        }
234        return support;
235    }
236}