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.util;
017
018import static de.cuioss.tools.collect.CollectionLiterals.mutableList;
019import static org.junit.jupiter.api.Assertions.assertNotNull;
020
021import java.lang.reflect.InvocationTargetException;
022import java.lang.reflect.Method;
023import java.util.Collection;
024import java.util.List;
025
026import de.cuioss.test.valueobjects.objects.impl.ExceptionHelper;
027import de.cuioss.test.valueobjects.property.PropertyMetadata;
028import de.cuioss.test.valueobjects.property.impl.BuilderMetadata;
029import de.cuioss.test.valueobjects.property.impl.BuilderMetadata.BuilderMetadataBuilder;
030import de.cuioss.tools.logging.CuiLogger;
031import de.cuioss.tools.property.PropertyUtil;
032import de.cuioss.tools.reflect.MoreReflection;
033
034/**
035 * Defines different ways for reading / writing properties.
036 *
037 * @author Oliver Wolff
038 */
039
040public enum PropertyAccessStrategy {
041
042    /**
043     * Reads and writes property according to the JavaBean-Spec. It uses
044     * {@link PropertyUtil} to do so. It acts as part of JUnit testing, therefore it
045     * will translate many of the more technical Exceptions to corresponding
046     * {@link AssertionError}
047     *
048     * @author Oliver Wolff
049     */
050    BEAN_PROPERTY {
051
052        @Override
053        public Object writeProperty(final Object target, final PropertyMetadata propertyMetadata,
054                final Object propertyValue) {
055            assertNotNull(target, TARGET_MUST_NOT_BE_NULL);
056            assertNotNull(target, PROPERTY_METADATA_MUST_NOT_BE_NULL);
057            try {
058                PropertyUtil.writeProperty(target, propertyMetadata.getName(), propertyValue);
059                return target;
060            } catch (IllegalArgumentException | IllegalStateException e) {
061                throw new AssertionError(UNABLE_TO_SET_PROPERTY.formatted(propertyMetadata.getName(),
062                        ExceptionHelper.extractCauseMessageFromThrowable(e)), e);
063            }
064
065        }
066
067        @Override
068        public Object readProperty(final Object target, final PropertyMetadata propertyMetadata) {
069            assertNotNull(target, TARGET_MUST_NOT_BE_NULL);
070            assertNotNull(target, PROPERTY_METADATA_MUST_NOT_BE_NULL);
071            try {
072                return PropertyUtil.readProperty(target, propertyMetadata.getName());
073            } catch (IllegalArgumentException | IllegalStateException e) {
074                throw new AssertionError(UNABLE_TO_READ_PROPERTY.formatted(propertyMetadata.getName(),
075                        ExceptionHelper.extractCauseMessageFromThrowable(e)), e);
076            }
077        }
078    },
079    /**
080     * In some cases the builder supports multiple ways to fill {@link Collection}
081     * based elements, e.g. there is a field with the structure
082     *
083     * <pre>
084     * <code>
085     *     private List&lt;String&gt; name;
086     * </code>
087     * </pre>
088     *
089     * There can be two methods to fill this elements in the builder:
090     *
091     * <pre>
092     * <code>
093     *  public Builder names(List&lt;String&gt; names);
094     *  public Builder names(String name);
095     * </code>
096     * </pre>
097     *
098     * This strategy writes the property using <em>both</em> methods. Therefore it
099     * uses {@link BuilderMetadata#getBuilderSingleAddMethodName()} in order to find
100     * the single addMethod. The plural add method is supposed to be the name of the
101     * property itself therefore derived by
102     * {@link BuilderMetadata#getBuilderAddMethodName()}. In case there is different
103     * methodName for adding, e.g.
104     *
105     * <pre>
106     * <code>
107     *  public Builder names(List&lt;String&gt; names);
108     *  public Builder name(String name);
109     * </code>
110     * </pre>
111     *
112     * you can specify the method-name explicitly by using
113     * {@link BuilderMetadataBuilder#builderSingleAddMethodName(String)}
114     * <p>
115     * The read method delegates to {@link PropertyAccessStrategy#BEAN_PROPERTY}
116     * because it can not be read from an actual builder but from the later created
117     * bean.
118     * </p>
119     *
120     * @author Oliver Wolff
121     */
122    BUILDER_COLLECTION_AND_SINGLE_ELEMENT {
123
124        @Override
125        public Object writeProperty(final Object target, final PropertyMetadata propertyMetadata,
126                final Object propertyValue) {
127            if (!(propertyValue instanceof Iterable)) {
128                throw new AssertionError(
129                        "Invalid valueType given, must be at least Iterable, but was " + propertyValue);
130            }
131            final Iterable<?> iterable = (Iterable<?>) propertyValue;
132            final List<?> elements = mutableList(iterable);
133            BuilderMetadata builderMetadata;
134            if (!(propertyMetadata instanceof BuilderMetadata metadata)) {
135                builderMetadata = BuilderMetadata.wrapFromMetadata(propertyMetadata);
136            } else {
137                builderMetadata = metadata;
138            }
139            try {
140                if (!elements.isEmpty()) {
141                    final Object singleElement = elements.iterator().next();
142
143                    final var writeAddMethod = target.getClass().getMethod(
144                            builderMetadata.getBuilderSingleAddMethodName(), propertyMetadata.getPropertyClass());
145                    writeAddMethod.invoke(target, singleElement);
146
147                    // Remove the element from the elements list
148                    elements.remove(singleElement);
149                }
150                final var writeCollectionMethod = determineCollectionWriteMethod(target, propertyMetadata,
151                        builderMetadata);
152
153                // Now write the remaining elements
154                return writeCollectionMethod.invoke(target,
155                        propertyMetadata.getCollectionType().wrapToIterable(elements));
156            } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
157                    | InvocationTargetException e) {
158                throw new AssertionError(UNABLE_TO_SET_PROPERTY.formatted(propertyMetadata.getName(),
159                        ExceptionHelper.extractCauseMessageFromThrowable(e)), e);
160            }
161
162        }
163
164        private Method determineCollectionWriteMethod(final Object target, final PropertyMetadata propertyMetadata,
165                BuilderMetadata builderMetadata) throws NoSuchMethodException {
166            try {
167                return target.getClass().getMethod(builderMetadata.getBuilderAddMethodName(),
168                        propertyMetadata.getCollectionType().getIterableType());
169            } catch (NoSuchMethodException e) {
170                // Noop -> Assuming collection-parameter
171            }
172            return target.getClass().getMethod(builderMetadata.getBuilderAddMethodName(),
173                    CollectionType.COLLECTION.getIterableType());
174        }
175
176        @Override
177        public Object readProperty(final Object target, final PropertyMetadata propertyMetadata) {
178            return PropertyAccessStrategy.BEAN_PROPERTY.readProperty(target, propertyMetadata);
179        }
180    },
181    /**
182     * Writes a property in a builder using
183     * {@link BuilderMetadata#getBuilderAddMethodName()} to determine the correct
184     * write method. The parameter type is exactly the same as defined at
185     * {@link PropertyMetadata#getPropertyClass()}. The read method delegates to
186     * {@link PropertyAccessStrategy#BEAN_PROPERTY} because it can not be read from
187     * an actual builder but from the later created bean.
188     *
189     * @author Oliver Wolff
190     */
191    BUILDER_DIRECT {
192
193        @Override
194        public Object writeProperty(final Object target, final PropertyMetadata propertyMetadata,
195                final Object propertyValue) {
196            BuilderMetadata builderMetadata;
197            if (!(propertyMetadata instanceof BuilderMetadata metadata)) {
198                builderMetadata = BuilderMetadata.wrapFromMetadata(propertyMetadata);
199            } else {
200                builderMetadata = metadata;
201            }
202            try {
203                final var writeMethod = target.getClass().getMethod(builderMetadata.getBuilderAddMethodName(),
204                        propertyMetadata.resolveActualClass());
205                return writeMethod.invoke(target, propertyValue);
206            } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
207                    | InvocationTargetException e) {
208                var message = UNABLE_TO_SET_PROPERTY.formatted(propertyMetadata.getName(),
209                        ExceptionHelper.extractCauseMessageFromThrowable(e));
210                new CuiLogger(getClass()).error(message);
211                throw new AssertionError(message, e);
212            }
213        }
214
215        @Override
216        public Object readProperty(final Object target, final PropertyMetadata propertyMetadata) {
217            return PropertyAccessStrategy.BEAN_PROPERTY.readProperty(target, propertyMetadata);
218        }
219    },
220    /**
221     * This strategy is for cases where {@link PropertyUtil} is not capable of
222     * writing an attribute. This is usually the case for elements that defined a
223     * fluent-api (not void as return value). The read method delegates to
224     * {@link PropertyAccessStrategy#BEAN_PROPERTY}
225     */
226    FLUENT_WRITER {
227
228        @Override
229        public Object writeProperty(Object target, PropertyMetadata propertyMetadata, Object propertyValue) {
230            var writeMethod = MoreReflection.retrieveWriteMethod(target.getClass(), propertyMetadata.getName(),
231                    propertyMetadata.resolveActualClass());
232            if (writeMethod.isEmpty()) {
233                throw new AssertionError(
234                        UNABLE_TO_SET_PROPERTY.formatted(propertyMetadata.getName(), "No write-method could be found"));
235            }
236            try {
237                return writeMethod.get().invoke(target, propertyValue);
238            } catch (SecurityException | IllegalAccessException | IllegalArgumentException
239                    | InvocationTargetException e) {
240                throw new AssertionError(UNABLE_TO_SET_PROPERTY.formatted(propertyMetadata.getName(),
241                        ExceptionHelper.extractCauseMessageFromThrowable(e)), e);
242            }
243        }
244
245        @Override
246        public Object readProperty(final Object target, final PropertyMetadata propertyMetadata) {
247            return PropertyAccessStrategy.BEAN_PROPERTY.readProperty(target, propertyMetadata);
248        }
249
250    };
251
252    private static final String UNABLE_TO_READ_PROPERTY = "Unable to read property '%s' because of '%s'";
253    private static final String TARGET_MUST_NOT_BE_NULL = "target must not be null";
254    private static final String PROPERTY_METADATA_MUST_NOT_BE_NULL = "propertyMetadata must not be null";
255    private static final String UNABLE_TO_SET_PROPERTY = "Unable to set property '%s' because of '%s'";
256
257    /**
258     * Writes the property into the given target;
259     *
260     * @param target           to be written to.
261     * @param propertyMetadata identifying the concrete property, must not be null
262     * @param propertyValue    to be set, may be null
263     * @return the modified object
264     * @throws AssertionError in case the property can not be written.
265     */
266    public abstract Object writeProperty(final Object target, final PropertyMetadata propertyMetadata,
267            final Object propertyValue);
268
269    /**
270     * Reads the property from the given target;
271     *
272     * @param target           to be written to.
273     * @param propertyMetadata identifying the concrete property, must not be null
274     * @return the read property, may be null
275     * @throws AssertionError in case the property can not be read.
276     */
277    public abstract Object readProperty(final Object target, final PropertyMetadata propertyMetadata);
278
279}