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.support;
017
018import static org.junit.jupiter.api.Assertions.assertTrue;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Set;
028import java.util.stream.Collectors;
029
030import de.cuioss.test.valueobjects.api.VerifyMapperConfiguration;
031import de.cuioss.test.valueobjects.objects.RuntimeProperties;
032import de.cuioss.tools.logging.CuiLogger;
033import lombok.EqualsAndHashCode;
034import lombok.ToString;
035
036/**
037 * Helper class for asserting single-attribute mappings for
038 * {@link VerifyMapperConfiguration}
039 *
040 * @author Oliver Wolff
041 *
042 */
043@EqualsAndHashCode
044@ToString
045public class MapperAttributesAsserts {
046
047    private static final CuiLogger log = new CuiLogger(MapperAttributesAsserts.class);
048
049    private static final String PROPERTY_MAPPING_INCOMPLETE = """
050            Caution: you have unmapped {}-properties: {} you can adapt this behavior by either:\
051
052            - @VerifyMapperConfiguration(equals("name:firstName"))\
053
054            - Make use of the provided property controls like @VerifyMapperConfiguration(of("name"))""";
055
056    private final List<AssertTuple> sourceAsserts;
057
058    /**
059     * @param config
060     * @param targetProperties
061     * @param sourceProperties
062     */
063    public MapperAttributesAsserts(VerifyMapperConfiguration config, RuntimeProperties targetProperties,
064            RuntimeProperties sourceProperties) {
065        var targetPropertyMap = targetProperties.asMapView(false);
066        var sourcePropertyMap = sourceProperties.asMapView(false);
067        List<MappingTuple> mapping = new ArrayList<>(MappingAssertStrategy.EQUALS.readConfiguration(config));
068
069        mapping.addAll(MappingAssertStrategy.NOT_NULL.readConfiguration(config));
070
071        for (MappingTuple tuple : mapping) {
072            assertTrue(targetPropertyMap.containsKey(tuple.getTarget()),
073                    "Invalid (unmapped) attribute-name for target: " + tuple.toString());
074            assertTrue(sourcePropertyMap.containsKey(tuple.getSource()),
075                    "Invalid (unmapped) attribute-name for source: " + tuple.toString());
076        }
077
078        sourceAsserts = new ArrayList<>();
079        for (MappingTuple tuple : mapping) {
080            sourceAsserts.add(new AssertTuple(sourcePropertyMap.get(tuple.getSource()),
081                    targetPropertyMap.get(tuple.getTarget()), tuple));
082        }
083        logConfigurationStatus(targetProperties, sourceProperties);
084    }
085
086    private void logConfigurationStatus(RuntimeProperties targetProperties, RuntimeProperties sourceProperties) {
087        if (sourceAsserts.isEmpty()) {
088            log.warn(
089                    "No attribute specific mapping found. use @VerifyMapperConfiguration(equals(\"name:firstName\")) or @VerifyMapperConfiguration(notNull(\"name:lastName\")) in order to activate");
090        }
091        Set<String> sourceMappingNames = sourceAsserts.stream().map(a -> a.getMappingTuple().getSource())
092                .collect(Collectors.toSet());
093        Set<String> targetMappingNames = sourceAsserts.stream().map(a -> a.getMappingTuple().getTarget())
094                .collect(Collectors.toSet());
095        var sourceTypeProperties = RuntimeProperties.extractNames(sourceProperties.getAllProperties());
096        var targetTypeProperties = RuntimeProperties.extractNames(targetProperties.getAllProperties());
097
098        sourceTypeProperties.removeAll(sourceMappingNames);
099        targetTypeProperties.removeAll(targetMappingNames);
100        if (sourceTypeProperties.isEmpty()) {
101            log.info("All source-properties are covered.");
102        } else {
103            log.warn(PROPERTY_MAPPING_INCOMPLETE, "source", sourceTypeProperties);
104        }
105        if (targetTypeProperties.isEmpty()) {
106            log.info("All target-properties are covered.");
107        } else {
108            log.warn(PROPERTY_MAPPING_INCOMPLETE, "target", targetTypeProperties);
109        }
110    }
111
112    /**
113     * @param sourceAttributes to be tested
114     * @param source
115     * @param target
116     */
117    public void assertMappingForSourceAttributes(Collection<String> sourceAttributes, Object source, Object target) {
118        if (sourceAsserts.isEmpty()) {
119            return;
120        }
121        Map<String, List<AssertTuple>> asserts = new HashMap<>();
122        for (String name : sourceAttributes) {
123            var concreteAsserts = sourceAsserts.stream().filter(a -> a.isResponsibleForSource(name)).toList();
124            if (concreteAsserts.isEmpty()) {
125                log.info("Checked property '{}' is not configured to be asserted, ist this intentional?", name);
126            } else {
127                asserts.put(name, concreteAsserts);
128            }
129        }
130        if (asserts.isEmpty()) {
131            return;
132        }
133        // Defines the elements that should not be affected in the correct instance and
134        // should
135        // therefore not be changed
136        Set<AssertTuple> activeAsserts = new HashSet<>();
137        asserts.values().forEach(activeAsserts::addAll);
138        List<AssertTuple> nullAsserts = new ArrayList<>(sourceAsserts);
139        nullAsserts.removeAll(activeAsserts);
140
141        // Now do the actual checks
142        for (Entry<String, List<AssertTuple>> entry : asserts.entrySet()) {
143            log.debug("Asserting attribute {}", entry.getKey());
144            entry.getValue().forEach(a -> a.assertContract(source, target));
145        }
146        // All not affected elements that provide no default should be null / empty
147        for (AssertTuple nullAssert : nullAsserts) {
148            log.debug("Asserting attribute to be null / not set {}", nullAssert.getTargetSupport().getName());
149            MappingAssertStrategy.NULL_OR_DEFAULT.assertMapping(nullAssert.getSourceSupport(), source,
150                    nullAssert.getTargetSupport(), target);
151        }
152
153    }
154
155}