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}