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<String> 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<String> 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<String> 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}