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.contract;
017
018import static java.util.Objects.requireNonNull;
019import static org.junit.jupiter.api.Assertions.assertEquals;
020import static org.junit.jupiter.api.Assertions.assertNotNull;
021import static org.junit.jupiter.api.Assertions.assertTrue;
022
023import java.io.ByteArrayInputStream;
024import java.io.ByteArrayOutputStream;
025import java.io.ObjectInputStream;
026import java.io.ObjectOutputStream;
027import java.io.Serializable;
028import java.util.Arrays;
029import java.util.List;
030import java.util.SortedSet;
031import java.util.TreeSet;
032
033import de.cuioss.test.valueobjects.api.object.ObjectTestConfig;
034import de.cuioss.test.valueobjects.api.object.ObjectTestContract;
035import de.cuioss.test.valueobjects.objects.ParameterizedInstantiator;
036import de.cuioss.test.valueobjects.objects.impl.ExceptionHelper;
037import de.cuioss.test.valueobjects.property.PropertyMetadata;
038import de.cuioss.tools.logging.CuiLogger;
039import lombok.RequiredArgsConstructor;
040
041/**
042 * Tests whether the object in hand implements {@link Serializable} and than
043 * serializes / deserializes the object, and compares the newly created object
044 * with the original by using {@link Object#equals(Object)}
045 *
046 * @author Oliver Wolff
047 */
048@RequiredArgsConstructor
049public class SerializableContractImpl implements ObjectTestContract {
050
051    private static final CuiLogger log = new CuiLogger(SerializableContractImpl.class);
052
053    @Override
054    public void assertContract(final ParameterizedInstantiator<?> instantiator,
055            final ObjectTestConfig objectTestConfig) {
056        requireNonNull(instantiator);
057
058        final var builder = new StringBuilder("Verifying ");
059        builder.append(getClass().getName()).append("\nWith configuration: ").append(instantiator.toString());
060        log.info(builder.toString());
061
062        var shouldUseEquals = checkForEqualsComparison(objectTestConfig);
063
064        Object template = instantiator.newInstanceMinimal();
065
066        assertTrue(template instanceof Serializable,
067                template.getClass().getName() + " does not implement java.io.Serializable");
068
069        final var serializationFailedMessage = template.getClass().getName() + " is not equal after serialization";
070        var serializeAndDeserialize = serializeAndDeserialize(template);
071        if (shouldUseEquals) {
072            assertEquals(template, serializeAndDeserialize, serializationFailedMessage);
073        }
074        if (!checkTestBasicOnly(objectTestConfig)
075                && !instantiator.getRuntimeProperties().getWritableProperties().isEmpty()) {
076            var properties = filterProperties(instantiator.getRuntimeProperties().getWritableProperties(),
077                    objectTestConfig);
078            template = instantiator.newInstance(properties);
079            serializeAndDeserialize = serializeAndDeserialize(template);
080            if (shouldUseEquals) {
081                assertEquals(template, serializeAndDeserialize, serializationFailedMessage);
082            }
083        }
084    }
085
086    static List<PropertyMetadata> filterProperties(final List<PropertyMetadata> allProperties,
087            final ObjectTestConfig objectTestConfig) {
088        if (null == objectTestConfig) {
089            return allProperties;
090        }
091        final SortedSet<String> consideredAttributes = new TreeSet<>();
092        allProperties.forEach(p -> consideredAttributes.add(p.getName()));
093        // Whitelist takes precedence
094        if (objectTestConfig.serializableOf().length > 0) {
095            consideredAttributes.clear();
096            consideredAttributes.addAll(Arrays.asList(objectTestConfig.serializableOf()));
097        } else {
098            consideredAttributes.removeAll(Arrays.asList(objectTestConfig.serializableExclude()));
099        }
100        return allProperties.stream().filter(p -> consideredAttributes.contains(p.getName())).toList();
101    }
102
103    static boolean checkForEqualsComparison(final ObjectTestConfig objectTestConfig) {
104        return null == objectTestConfig || objectTestConfig.serializableCompareUsingEquals();
105    }
106
107    static boolean checkTestBasicOnly(final ObjectTestConfig objectTestConfig) {
108        return null != objectTestConfig && objectTestConfig.serializableBasicOnly();
109    }
110
111    /**
112     * Shorthand combining the calls {@link #serializeObject(Object)}
113     * {@link #deserializeObject(byte[])}
114     *
115     * @param object to be serialized, must not be null
116     * @return the deserialized object.
117     */
118    public static final Object serializeAndDeserialize(final Object object) {
119        assertNotNull(object, "Given Object must not be null");
120        final var serialized = serializeObject(object);
121        return deserializeObject(serialized);
122    }
123
124    /**
125     * Serializes an object into a newly created byteArray
126     *
127     * @param object to be serialized
128     * @return the resulting byte array
129     */
130    public static final byte[] serializeObject(final Object object) {
131        assertNotNull(object, "Given Object must not be null");
132        final var baos = new ByteArrayOutputStream(1024);
133        try (var oas = new ObjectOutputStream(baos)) {
134            oas.writeObject(object);
135            oas.flush();
136        } catch (final Exception e) {
137            throw new AssertionError(
138                    "Unable to serialize, due to " + ExceptionHelper.extractCauseMessageFromThrowable(e));
139        }
140        return baos.toByteArray();
141    }
142
143    /**
144     * Deserializes an object from a given byte-array
145     *
146     * @param bytes to be deserialized
147     * @return the deserialized object
148     */
149    public static final Object deserializeObject(final byte[] bytes) {
150        assertNotNull(bytes, "Given byte-array must not be null");
151        final var bais = new ByteArrayInputStream(bytes);
152        try (var ois = new ObjectInputStream(bais)) {
153            return ois.readObject();
154        } catch (final Exception e) {
155            throw new AssertionError(
156                    "Unable to deserialize, due to " + ExceptionHelper.extractCauseMessageFromThrowable(e));
157        }
158    }
159
160}