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.util; 017 018import static de.cuioss.tools.string.MoreStrings.isEmpty; 019import static org.junit.jupiter.api.Assertions.assertEquals; 020import static org.junit.jupiter.api.Assertions.assertNotNull; 021import static org.junit.jupiter.api.Assertions.fail; 022 023import java.lang.reflect.InvocationTargetException; 024import java.lang.reflect.Method; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.List; 028 029import org.junit.jupiter.api.Assertions; 030 031import de.cuioss.test.valueobjects.objects.impl.ExceptionHelper; 032import de.cuioss.tools.reflect.MoreReflection; 033import lombok.experimental.UtilityClass; 034 035@UtilityClass 036public class DeepCopyTestHelper { 037 038 /** 039 * To test the result of a deep copy function. 040 * <p> 041 * The main focus is to check if the copy is independent from the source and 042 * does not have any reference to the source. 043 * <p> 044 * To check the equality of the objects the equals method should be implemented 045 * correctly. 046 * 047 * @param source the source object 048 * @param copy the result of the copy function 049 */ 050 public static void verifyDeepCopy(Object source, Object copy) { 051 verifyDeepCopy(source, copy, Collections.emptyList()); 052 } 053 054 /** 055 * To test the result of a deep copy function. 056 * <p> 057 * The main focus is to check if the copy is independent from the source and 058 * does not have any reference to the source. -> Deep Copy, instead of shallow 059 * copy 060 * <p> 061 * To check the equality of the objects the equals method should be implemented 062 * correctly. 063 * 064 * @param source the source object 065 * @param copy the result of the copy function 066 * @param ignoreProperties The top-level attribute names to be ignored 067 */ 068 public static void verifyDeepCopy(Object source, Object copy, Collection<String> ignoreProperties) { 069 testDeepCopy(source, copy, null, ignoreProperties); 070 } 071 072 @SuppressWarnings("java:S2259") // owolff: False positive: assertions are not considered here 073 private static void testDeepCopy(Object source, Object copy, String propertyString, 074 Collection<String> ignoreProperties) { 075 076 assertNotNull(ignoreProperties, "ignore-properties my be empty but never null"); 077 // first check: check equals 078 assertEquals(source, copy); 079 if (null == source) { 080 return; 081 } 082 083 final var currentPropertyString = determinePropertyString(propertyString); 084 085 for (final Method accessMethod : MoreReflection.retrieveAccessMethods(source.getClass(), ignoreProperties)) { 086 var propertyName = MoreReflection.computePropertyNameFromMethodName(accessMethod.getName()); 087 try { 088 var resultSource = accessMethod.invoke(source); 089 var resultCopy = accessMethod.invoke(copy); 090 091 // check for null 092 // No sense in checking Strings, primitives and enums 093 if (!checkNullContract(resultSource, resultCopy, currentPropertyString, propertyName) 094 || resultSource.getClass().isPrimitive() || resultSource.getClass().isEnum() 095 || String.class.equals(resultSource.getClass())) { 096 continue; 097 } 098 if (!checkForList(resultSource, resultCopy, currentPropertyString, propertyName)) { 099 if (MoreReflection.retrieveWriteMethod(source.getClass(), propertyName, resultSource.getClass()) 100 .isEmpty()) { 101 continue; 102 } 103 104 Assertions.assertNotSame(resultSource, resultCopy, "deep copy failed with: " + currentPropertyString 105 + propertyName + " (" + resultSource.toString() + ")"); 106 testDeepCopy(resultSource, resultCopy, currentPropertyString + propertyName, 107 Collections.emptyList()); 108 } 109 } catch (IllegalAccessException | InvocationTargetException e) { 110 fail("invoke method " + accessMethod.getName() + "failed: " 111 + ExceptionHelper.extractCauseMessageFromThrowable(e)); 112 } 113 114 } 115 } 116 117 private boolean checkNullContract(Object resultSource, Object resultCopy, String currentPropertyString, 118 String propertyName) { 119 // check for null 120 if (null != resultSource) { 121 if (null == resultCopy) { 122 fail("property " + currentPropertyString + propertyName + " differs: " + resultSource.toString() 123 + " != null"); 124 } 125 } else if (null == resultCopy) { 126 return false; 127 } else { 128 fail("property " + currentPropertyString + propertyName + " differs: null != " + resultCopy.toString()); 129 } 130 return true; 131 } 132 133 private boolean checkForList(Object resultSource, Object resultCopy, String currentPropertyString, 134 String propertyName) { 135 if (!(resultSource instanceof List)) { 136 return false; 137 } 138 List<?> resultSourceList = (List<?>) resultSource; 139 for (var i = 0; i < resultSourceList.size(); i++) { 140 testDeepCopy(resultSourceList.get(i), ((List<?>) resultCopy).get(i), 141 currentPropertyString + propertyName + "[" + i + "]", Collections.emptyList()); 142 } 143 return true; 144 145 } 146 147 private static String determinePropertyString(String propertyString) { 148 if (isEmpty(propertyString)) { 149 return ""; 150 } 151 return propertyString + "."; 152 } 153}