All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kie.dmn.validation.dtanalysis.model.DTAnalysis Maven / Gradle / Ivy

The newest version!
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.kie.dmn.validation.dtanalysis.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.kie.dmn.api.core.DMNMessage;
import org.kie.dmn.api.core.DMNMessage.Severity;
import org.kie.dmn.core.util.Msg;
import org.kie.dmn.core.util.MsgUtil;
import org.kie.dmn.feel.lang.ast.DashNode;
import org.kie.dmn.feel.runtime.Range.RangeBoundary;
import org.kie.dmn.model.api.DecisionTable;
import org.kie.dmn.model.api.HitPolicy;
import org.kie.dmn.model.api.InputClause;
import org.kie.dmn.model.api.LiteralExpression;
import org.kie.dmn.validation.ValidatorUtil;
import org.kie.dmn.validation.dtanalysis.DMNDTAnalysisMessage;
import org.kie.dmn.validation.dtanalysis.mcdc.MCDCAnalyser.PosNegBlock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DTAnalysis {

    private static final Logger LOG = LoggerFactory.getLogger(DTAnalysis.class);

    private final List gaps = new ArrayList<>();
    private final List overlaps = new ArrayList<>();
    private final List maskedRules = new ArrayList<>();
    private final Set misleadingRules = new HashSet<>();
    private final List subsumptions = new ArrayList<>();
    private final List contractions = new ArrayList<>();
    private final Map> cacheNonContractingRules = new HashMap<>();
    private boolean c1stNFViolation = false;
    private Collection> cOfDuplicateRules = Collections.emptyList();
    private Collection contractionsViolating2ndNF = new ArrayList<>();
    private Collection cellsViolating2ndNF = new ArrayList<>();
    private final DecisionTable sourceDT;
    private final Throwable error;
    private final DDTATable ddtaTable;
    private final Collection passThruMessages = new ArrayList<>();
    private List selectedBlocks;

    public DTAnalysis(DecisionTable sourceDT, DDTATable ddtaTable) {
        this.sourceDT = sourceDT;
        this.error = null;
        this.ddtaTable = ddtaTable;
    }

    private DTAnalysis(DecisionTable sourceDT, Throwable error) {
        this.sourceDT = sourceDT;
        this.error = error;
        this.ddtaTable = null;
    }

    public static DTAnalysis ofError(DecisionTable sourceDT, Throwable error) {
        return new DTAnalysis(sourceDT, error);
    }

    public boolean isError() {
        return error != null;
    }

    public DDTATable getDdtaTable() {
        return ddtaTable;
    }

    public Collection getGaps() {
        return Collections.unmodifiableList(gaps);
    }

    public void addGap(Hyperrectangle gap) {
        this.gaps.add(gap);
    }

    public DecisionTable getSource() {
        return sourceDT;
    }

    public List getOverlaps() {
        return Collections.unmodifiableList(overlaps);
    }

    public void addOverlap(Overlap overlap) {
        this.overlaps.add(overlap);
    }

    public void normalize() {
        int prevSize = this.overlaps.size();
        internalNormalize();
        int curSize = this.overlaps.size();
        if (curSize != prevSize) {
            normalize();
        }
    }

    private void internalNormalize() {
        List newOverlaps = new ArrayList<>();
        List overlapsProcessing = new ArrayList<>();
        overlapsProcessing.addAll(overlaps);
        while (!overlapsProcessing.isEmpty()) {
            List toBeRemoved = new ArrayList<>();
            List toBeAdded = new ArrayList<>();
            Overlap curOverlap = overlapsProcessing.remove(0);
            for (Overlap otherOverlap : overlapsProcessing) {
                if (curOverlap == null) {
                    break;
                }
                int x = curOverlap.contigousOnDimension(otherOverlap);
                if (x > 0) {
                    Overlap mergedOverlap = Overlap.newByMergeOnDimension(curOverlap, otherOverlap, x);
                    curOverlap = null;
                    toBeRemoved.add(otherOverlap);
                    toBeAdded.add(mergedOverlap);
                }
            }
            for (Overlap x : toBeRemoved) {
                overlapsProcessing.remove(x);
            }
            for (Overlap x : toBeAdded) {
                overlapsProcessing.add(0, x);
            }
            if (curOverlap != null) {
                newOverlaps.add(curOverlap);
            }
        }
        this.overlaps.clear();
        this.overlaps.addAll(newOverlaps);
    }

    public List asDMNMessages() {
        List results = new ArrayList<>();
        if (isError()) {
            DMNMessage m = new DMNDTAnalysisMessage(this,
                                                    Severity.WARN,
                                                    MsgUtil.createMessage(Msg.DTANALYSIS_ERROR_ANALYSIS_SKIPPED,
                                                                          nameOrIDOfTable(),
                                                                          error.getMessage()),
                                                    Msg.DTANALYSIS_ERROR_ANALYSIS_SKIPPED.getType());
            results.add(m);
            return results;
        }
        results.addAll(passThruMessages());
        results.addAll(gapsAsMessages());
        results.addAll(overlapsAsMessages());
        results.addAll(maskedAndMisleadingRulesAsMessagesIfPriority());
        results.addAll(subsumptionsAsMessages());
        results.addAll(contractionsAsMessages());
        results.addAll(check1stNFViolationAsMessages());
        results.addAll(check2ndNFViolationAsMessages());

        // keep last.
        if (results.isEmpty()) {
            DMNMessage m = new DMNDTAnalysisMessage(this,
                                                    Severity.INFO,
                                                    MsgUtil.createMessage(Msg.DTANALYSIS_EMPTY,
                                                                          nameOrIDOfTable()),
                                                    Msg.DTANALYSIS_EMPTY.getType());
            results.add(m);
            return results;
        }
        return results;
    }

    private Collection maskedAndMisleadingRulesAsMessagesIfPriority() {
        if (sourceDT.getHitPolicy() != HitPolicy.PRIORITY) {
            return Collections.emptyList();
        }
        List results = new ArrayList<>();
        for (MaskedRule masked : maskedRules) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.ERROR,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_HITPOLICY_PRIORITY_MASKED_RULE,
                                                                       masked.maskedRule,
                                                                       masked.maskedBy),
                                                 Msg.DTANALYSIS_HITPOLICY_PRIORITY_MASKED_RULE.getType(), Collections.singletonList(masked.maskedRule)));
        }
        for (MisleadingRule misleading : misleadingRules) {
            boolean duplicatesAMasked = maskedRules.stream().anyMatch(masked -> masked.maskedBy == misleading.misleadingRule && masked.maskedRule == misleading.misleadRule);
            if (!duplicatesAMasked) {
                results.add(new DMNDTAnalysisMessage(this,
                                                     Severity.WARN,
                                                     MsgUtil.createMessage(Msg.DTANALYSIS_HITPOLICY_PRIORITY_MISLEADING_RULE,
                                                                           misleading.misleadingRule,
                                                                           misleading.misleadRule),
                                                     Msg.DTANALYSIS_HITPOLICY_PRIORITY_MISLEADING_RULE.getType(), Collections.singletonList(misleading.misleadingRule)));
            } else {
                LOG.debug("Misleading record is not displayed as message because it is redundant to a Masked rule message: {}", misleading);
            }
        }
        return results;
    }

    private Collection subsumptionsAsMessages() {
        List results = new ArrayList<>();
        for (Subsumption s : subsumptions) {
            List inNaturalOrder = Arrays.asList(s.rule, s.includedRule);
            List inReversedOrder = Arrays.asList(s.includedRule, s.rule);
            boolean subsumptionIsA1NFdup = getDuplicateRulesTuples().stream().anyMatch(tuple -> tuple.equals(inNaturalOrder) || tuple.equals(inReversedOrder));
            if (!subsumptionIsA1NFdup) {
                results.add(new DMNDTAnalysisMessage(this,
                                                     Severity.WARN,
                                                     MsgUtil.createMessage(Msg.DTANALYSIS_SUBSUMPTION_RULE,
                                                                           s.rule,
                                                                           s.includedRule,
                                                                           s.rule,
                                                                           s.includedRule),
                                                     Msg.DTANALYSIS_SUBSUMPTION_RULE.getType(), Collections.singletonList(s.rule)));
            } else {
                LOG.debug("skipping Subsumption message because it is actually redundant to the 1st NF duplicate rule ERROR: {}", s);
            }
        }
        return results;
    }

    private Collection contractionsAsMessages() {
        List results = new ArrayList<>();
        for (Contraction x : contractions) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.WARN,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_CONTRACTION_RULE,
                                                                       x.impactedRules(),
                                                                       x.adjacentDimension),
                                                 Msg.DTANALYSIS_CONTRACTION_RULE.getType(), x.impactedRules()));
        }
        return results;
    }

    private Collection check1stNFViolationAsMessages() {
        if (!is1stNFViolation()) {
            return Collections.emptyList();
        }
        List results = new ArrayList<>();
        if (sourceDT.getHitPolicy() == HitPolicy.FIRST) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.WARN,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_1STNFVIOLATION_FIRST),
                                                 Msg.DTANALYSIS_1STNFVIOLATION_FIRST.getType()));
        }
        if (sourceDT.getHitPolicy() == HitPolicy.RULE_ORDER) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.WARN,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_1STNFVIOLATION_RULE_ORDER),
                                                 Msg.DTANALYSIS_1STNFVIOLATION_RULE_ORDER.getType()));
        }
        for (Collection duplicateRulesTuple : getDuplicateRulesTuples()) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 sourceDT.getHitPolicy() == HitPolicy.COLLECT ? Severity.WARN : Severity.ERROR,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_1STNFVIOLATION_DUPLICATE_RULES,
                                                                       duplicateRulesTuple),
                                                 Msg.DTANALYSIS_1STNFVIOLATION_DUPLICATE_RULES.getType(), duplicateRulesTuple));
        }
        return results;
    }

    private Collection check2ndNFViolationAsMessages() {
        if (!is2ndNFViolation()) {
            return Collections.emptyList();
        }
        List results = new ArrayList<>();
        for (Contraction c : getContractionsViolating2ndNF()) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.WARN,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_2NDNFVIOLATION,
                                                                       c.adjacentDimension,
                                                                       c.impactedRules()),
                                                 Msg.DTANALYSIS_2NDNFVIOLATION.getType(), c.impactedRules()));
        }
        for (RuleColumnCoordinate c : getCellsViolating2ndNF()) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.WARN,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_2NDNFVIOLATION_WAS_DASH,
                                                                       c.feelText,
                                                                       c.rule,
                                                                       c.column),
                                                 Msg.DTANALYSIS_2NDNFVIOLATION.getType(), List.of(c.rule)));
        }
        return results;
    }

    private Collection passThruMessages() {
        return passThruMessages;
    }

    private Collection overlapsAsMessages() {
        List results = new ArrayList<>();
        for (Overlap overlap : overlaps) {
            switch (sourceDT.getHitPolicy()) {
                case UNIQUE:
                    results.add(new DMNDTAnalysisMessage(this,
                                                         Severity.ERROR,
                                                         MsgUtil.createMessage(Msg.DTANALYSIS_OVERLAP_HITPOLICY_UNIQUE,
                                                                               overlap.asHumanFriendly(ddtaTable)),
                                                         Msg.DTANALYSIS_OVERLAP_HITPOLICY_UNIQUE.getType(), overlap.getRules()));
                    break;
                case ANY:
                    List> prevValue = ddtaTable.getRule().get(overlap.getRules().get(0) - 1).getOutputEntry();
                    for (int i = 1; i < overlap.getRules().size(); i++) { // deliberately start index 1 for 2nd overlapping rule number.
                        int curIndex = overlap.getRules().get(i) - 1;
                        List> curValue = ddtaTable.getRule().get(curIndex).getOutputEntry();
                        if (!prevValue.equals(curValue)) {
                            results.add(new DMNDTAnalysisMessage(this,
                                                                 Severity.ERROR,
                                                                 MsgUtil.createMessage(Msg.DTANALYSIS_OVERLAP_HITPOLICY_ANY,
                                                                                       overlap.asHumanFriendly(ddtaTable)),
                                                                 Msg.DTANALYSIS_OVERLAP_HITPOLICY_ANY.getType(), overlap.getRules()));
                            break;
                        } else {
                            prevValue = curValue;
                        }
                    }
                    break;
                case PRIORITY:
                case FIRST:
                default:
                    LOG.debug("In case of any other HitPolicy no overalps is reported, DROOLS-5363: {}", overlap);
                    break;
            }
        }
        return results;
    }

    private Collection gapsAsMessages() {
        List results = new ArrayList<>();
        if (!ddtaTable.getColIDsStringWithoutEnum().isEmpty()) {
            List names = ddtaTable.getColIDsStringWithoutEnum()
                                          .stream()
                                          .map(id -> sourceDT.getInput().get(id - 1))
                                          .map(InputClause::getInputExpression)
                                          .map(LiteralExpression::getText)
                                          .collect(Collectors.toList());
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.WARN,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_GAP_SKIPPED_BECAUSE_FREE_STRING,
                                                                       names),
                                                 Msg.DTANALYSIS_GAP_SKIPPED_BECAUSE_FREE_STRING.getType()));
            return results;
        }
        for (Hyperrectangle gap : gaps) {
            results.add(new DMNDTAnalysisMessage(this,
                                                 Severity.WARN,
                                                 MsgUtil.createMessage(Msg.DTANALYSIS_GAP,
                                                                       gap.asHumanFriendly(ddtaTable)),
                                                 Msg.DTANALYSIS_GAP.getType()));
        }
        return results;
    }

    public void computeMaskedRules() {
        if (sourceDT.getHitPolicy() != HitPolicy.PRIORITY) {
            return;
        }
        for (Overlap overlap : overlaps) {
            analyseOverlapForMaskedRules(overlap);
        }
    }

    private void analyseOverlapForMaskedRules(Overlap overlap) {
        for (Integer ruleId : overlap.getRules()) {
            List> curValues = ddtaTable.getRule().get(ruleId - 1).getOutputEntry();

            for (int jOutputIdx = 0; jOutputIdx < ddtaTable.outputCols(); jOutputIdx++) {
                DDTAOutputClause curOutputClause = ddtaTable.getOutputs().get(jOutputIdx);
                if (curOutputClause.isDiscreteDomain()) {
                    int curOutputIdx = curOutputClause.getOutputOrder().indexOf(curValues.get(jOutputIdx));

                    List otherRules = listWithoutElement(overlap.getRules(), ruleId);

                    for (Integer otherRuleID : otherRules) {
                        List> otherRuleValues = ddtaTable.getRule().get(otherRuleID - 1).getOutputEntry();
                        int otherOutputIdx = curOutputClause.getOutputOrder().indexOf(otherRuleValues.get(jOutputIdx));
                        if (curOutputIdx > otherOutputIdx) {
                            try {
                                boolean isOtherRuleWider = comparingRulesIsRightWider(ruleId, otherRuleID);
                                if (isOtherRuleWider) {
                                    maskedRules.add(new MaskedRule(ruleId, otherRuleID));
                                }
                            } catch (ComparingRulesWithMultipleInputEntries e) {
                                passThruMessages.add(new DMNDTAnalysisMessage(this,
                                                                              Severity.WARN,
                                                                              MsgUtil.createMessage(Msg.DTANALYSIS_HITPOLICY_PRIORITY_ANALYSIS_SKIPPED,
                                                                                                    sourceDT.getOutputLabel(),
                                                                                                    ruleId, otherRuleID),
                                                                              Msg.DTANALYSIS_HITPOLICY_PRIORITY_ANALYSIS_SKIPPED.getType(), Collections.singletonList(ruleId)));
                            }
                        }
                    }
                }
            }
        }
    }

    public boolean comparingRulesIsRightWider(int ruleId, int isWiderRuleId) throws ComparingRulesWithMultipleInputEntries {
        boolean isOtherRuleWider = true;
        for (int jInputIdx = 0; isOtherRuleWider && jInputIdx < ddtaTable.inputCols(); jInputIdx++) {
            DDTAInputEntry ruleIdInputAtIdx = ddtaTable.getRule().get(ruleId - 1).getInputEntry().get(jInputIdx);
            DDTAInputEntry otherRuleInputAtIdx = ddtaTable.getRule().get(isWiderRuleId - 1).getInputEntry().get(jInputIdx);
            if (ruleIdInputAtIdx.getIntervals().size() != 1 || otherRuleInputAtIdx.getIntervals().size() != 1) {
                throw new ComparingRulesWithMultipleInputEntries("Multiple entries not supported");
            } else {
                Interval ruleIdInterval = ruleIdInputAtIdx.getIntervals().get(0);
                Interval otherRuleInterval = otherRuleInputAtIdx.getIntervals().get(0);
                isOtherRuleWider = isOtherRuleWider && otherRuleInterval.includes(ruleIdInterval);
            }
        }
        return isOtherRuleWider;
    }

    public class ComparingRulesWithMultipleInputEntries extends Exception {

        public ComparingRulesWithMultipleInputEntries(String message) {
            super(message);
        }

    }

    public List getMaskedRules() {
        return Collections.unmodifiableList(maskedRules);
    }

    public void computeMisleadingRules() {
        if (sourceDT.getHitPolicy() != HitPolicy.PRIORITY) {
            return;
        }
        for (Overlap overlap : overlaps) {
            analyseOverlapForMisleadingRules(overlap);
        }
    }

    private void analyseOverlapForMisleadingRules(Overlap overlap) {
        for (Integer ruleId : overlap.getRules()) {
            List> curValues = ddtaTable.getRule().get(ruleId - 1).getOutputEntry();

            for (int jOutputIdx = 0; jOutputIdx < ddtaTable.outputCols(); jOutputIdx++) {
                DDTAOutputClause curOutputClause = ddtaTable.getOutputs().get(jOutputIdx);
                if (curOutputClause.isDiscreteDomain()) {
                    int curOutputIdx = curOutputClause.getOutputOrder().indexOf(curValues.get(jOutputIdx));
                    boolean isOutputLowestPriority = curOutputIdx == curOutputClause.getOutputOrder().size() - 1;
                    if (!isOutputLowestPriority) {
                        List inputEntry = ddtaTable.getRule().get(ruleId - 1).getInputEntry();
                        for (int col = 0; col < inputEntry.size(); col++) {
                            boolean thisColIsHypen = inputEntry.get(col).getUts().stream().anyMatch(DashNode.class::isInstance);
                            if (thisColIsHypen) {
                                List otherRules = listWithoutElement(overlap.getRules(), ruleId);
                                for (Integer otherRuleID : otherRules) {
                                    List> otherRuleValues = ddtaTable.getRule().get(otherRuleID - 1).getOutputEntry();
                                    int otherOutputIdx = curOutputClause.getOutputOrder().indexOf(otherRuleValues.get(jOutputIdx));
                                    List otherRuleInputEntry = ddtaTable.getRule().get(otherRuleID - 1).getInputEntry();
                                    boolean thatColIsHypen = otherRuleInputEntry.get(col).getUts().stream().anyMatch(DashNode.class::isInstance);
                                    if (otherOutputIdx > curOutputIdx && !thatColIsHypen) {
                                        misleadingRules.add(new MisleadingRule(ruleId, otherRuleID));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private static  List listWithoutElement(Collection coll, T elem) {
        List others = new ArrayList<>(coll);
        others.remove(elem);
        return others;
    }

    public Collection getMisleadingRules() {
        return Collections.unmodifiableSet(misleadingRules);
    }

    public void computeSubsumptions() {
        for (Overlap overlap : overlaps) {
            analyseOverlapForSubsumptions(overlap);
        }
    }

    private void analyseOverlapForSubsumptions(Overlap overlap) {
        Set>> outputEntries = new HashSet<>();
        for (Integer ruleId : overlap.getRules()) {
            List> curValues = ddtaTable.getRule().get(ruleId - 1).getOutputEntry();
            outputEntries.add(curValues);
        }
        for (List> curOutputEntry : outputEntries) {
            Set rulesWithGivenOutputEntry = new LinkedHashSet<>();
            for (Integer ruleId : overlap.getRules()) {
                List> curValues = ddtaTable.getRule().get(ruleId - 1).getOutputEntry();
                if (curValues.equals(curOutputEntry)) {
                    rulesWithGivenOutputEntry.add(ruleId);
                }
            }
            for (Integer ruleId : rulesWithGivenOutputEntry) {
                List curInputEntries = ddtaTable.getRule().get(ruleId - 1).getInputEntry();
                List otherRules = listWithoutElement(rulesWithGivenOutputEntry, ruleId);
                for (Integer otherRuleId : otherRules) {
                    List otherInputEntries = ddtaTable.getRule().get(otherRuleId - 1).getInputEntry();
                    boolean inputEntriesIncludeAll = DDTARule.inputEntriesIncludeAll(curInputEntries, otherInputEntries);
                    if (inputEntriesIncludeAll) {
                        subsumptions.add(new Subsumption(ruleId, otherRuleId));
                    }
                }
            }
        }
    }

    public List getSubsumptions() {
        return Collections.unmodifiableList(subsumptions);
    }

    private boolean areRulesSubsumption(Integer a, Integer b) {
        return subsumptions.stream().filter(s -> (s.rule == a && s.includedRule == b) || (s.rule == b && s.includedRule == a)).findAny().isPresent();
    }

    private boolean areRulesContraction(Integer a, Integer b) {
        return contractions.stream().filter(s -> (s.rule == b && s.pairedRules.contains(a)) || (s.rule == a && s.pairedRules.contains(b))).findAny().isPresent();
    }

    private boolean areRulesInNonContractionCache(Integer a, Integer b) {
        return cacheNonContractingRules.getOrDefault(a, Collections.emptySet()).contains(b) || cacheNonContractingRules.getOrDefault(b, Collections.emptySet()).contains(a);
    }

    public void computeContractions() {
        Set>> outputEntries = ddtaTable.outputEntries();
        for (List> curOutputEntry : outputEntries) {
            List rulesWithGivenOutputEntry = ddtaTable.ruleIDsByOutputEntry(curOutputEntry);
            for (Integer ruleId : rulesWithGivenOutputEntry) {
                List curInputEntries = ddtaTable.getRule().get(ruleId - 1).getInputEntry();
                List otherRules = listWithoutElement(rulesWithGivenOutputEntry, ruleId);
                for (Integer otherRuleId : otherRules) {
                    if (areRulesSubsumption(ruleId, otherRuleId) || areRulesContraction(ruleId, otherRuleId) || areRulesInNonContractionCache(ruleId, otherRuleId)) {
                        continue;
                    }
                    LOG.debug("computeContractions ruleId {} otherRuleId {}", ruleId, otherRuleId);
                    List otherInputEntries = ddtaTable.getRule().get(otherRuleId - 1).getInputEntry();
                    int detectedAdjacentOrOverlap = 0;
                    boolean allEqualsAllowingOneAdjOverlap = true;
                    for (int i = 0; i < curInputEntries.size(); i++) {
                        DDTAInputEntry curIE = curInputEntries.get(i);
                        DDTAInputEntry otherIE = otherInputEntries.get(i);
                        boolean intervalsAreEqual = curIE.getIntervals().equals(otherIE.getIntervals());
                        if (intervalsAreEqual) {
                            continue;
                        }
                        boolean canOverlapThisDimention = detectedAdjacentOrOverlap == 0 && curIE.adjOrOverlap(otherIE);
                        if (canOverlapThisDimention) {
                            detectedAdjacentOrOverlap = i + 1;
                            continue;
                        }
                        allEqualsAllowingOneAdjOverlap = false;
                    }
                    if (allEqualsAllowingOneAdjOverlap) {
                        List allIntervals = new ArrayList<>();
                        allIntervals.addAll(curInputEntries.get(detectedAdjacentOrOverlap - 1).getIntervals());
                        allIntervals.addAll(otherInputEntries.get(detectedAdjacentOrOverlap - 1).getIntervals());
                        List flatten = Interval.flatten(allIntervals);
                        DDTAInputClause ddtaInputClause = ddtaTable.getInputs().get(detectedAdjacentOrOverlap - 1);
                        if (ddtaInputClause.isDiscreteDomain()) {
                            flatten = Interval.normalizeDiscrete(flatten, ddtaInputClause.getDiscreteValues());
                        }
                        Contraction contraction = new Contraction(ruleId, List.of(otherRuleId), detectedAdjacentOrOverlap, flatten);
                        LOG.debug("NEW CONTRACTION: {}", contraction);
                        contractions.add(contraction);
                    } else {
                        cacheNonContractingRules.computeIfAbsent(otherRuleId, x -> new HashSet<>()).add(ruleId);
                        cacheNonContractingRules.computeIfAbsent(ruleId, x -> new HashSet<>()).add(otherRuleId);
                    }
                }
            }
        }
        if (!this.contractions.isEmpty()) {
            normalizeContractions(); // early normalization call to suite for consistent 2NF computations.
        }
    }

    private void normalizeContractions() {
        int prevSize = this.overlaps.size();
        internalNormalizeContractions();
        int curSize = this.overlaps.size();
        if (curSize != prevSize) {
            normalizeContractions();
        }
    }

    private void internalNormalizeContractions() {
        List newCollection = new ArrayList<>();
        List collectionProcessing = new ArrayList<>();
        collectionProcessing.addAll(this.contractions);
        while (!collectionProcessing.isEmpty()) {
            List toBeRemoved = new ArrayList<>();
            List toBeAdded = new ArrayList<>();
            Contraction cur = collectionProcessing.remove(0);
            for (Contraction other : collectionProcessing) {
                if (cur == null) {
                    break;
                }
                if (cur.adjacentDimension == other.adjacentDimension && Interval.adjOrOverlap(cur.dimensionAsContracted, other.dimensionAsContracted)) {
                    List intervals = new ArrayList<>();
                    intervals.addAll(cur.dimensionAsContracted);
                    intervals.addAll(other.dimensionAsContracted);
                    List flatten = Interval.flatten(intervals);
                    DDTAInputClause ddtaInputClause = ddtaTable.getInputs().get(cur.adjacentDimension - 1);
                    if (ddtaInputClause.isDiscreteDomain()) {
                        flatten = Interval.normalizeDiscrete(flatten, ddtaInputClause.getDiscreteValues());
                    }
                    Set allRules = new HashSet<>();
                    allRules.add(cur.rule);
                    allRules.add(other.rule);
                    allRules.addAll(cur.pairedRules);
                    allRules.addAll(other.pairedRules);
                    Integer mainRuleId = Collections.min(allRules);
                    allRules.remove(mainRuleId);
                    Contraction merged = new Contraction(mainRuleId, allRules, cur.adjacentDimension, flatten);
                    LOG.debug("MERGED CONTRACTION: {}", merged);
                    cur = null;
                    toBeRemoved.add(other);
                    toBeAdded.add(merged);
                }
            }
            for (Contraction x : toBeRemoved) {
                collectionProcessing.remove(x);
            }
            for (Contraction x : toBeAdded) {
                collectionProcessing.add(0, x);
            }
            if (cur != null) {
                newCollection.add(cur);
            }
        }
        this.contractions.clear();
        this.contractions.addAll(newCollection);
    }

    public List getContractions() {
        return Collections.unmodifiableList(contractions);
    }

    public void compute1stNFViolations() {
        if (sourceDT.getHitPolicy() == HitPolicy.FIRST || sourceDT.getHitPolicy() == HitPolicy.RULE_ORDER) {
            c1stNFViolation = true;
        }
        cOfDuplicateRules = ddtaTable.getCacheByInputEntry().values().stream().filter(c -> c.size() > 1).collect(Collectors.toList());
        if (!cOfDuplicateRules.isEmpty()) {
            c1stNFViolation = true;
        }
        LOG.debug("compute1stNFViolations() c1stNFViolation result: {}", c1stNFViolation);
    }

    public boolean is1stNFViolation() {
        return c1stNFViolation;
    }

    public Collection> getDuplicateRulesTuples() {
        return Collections.unmodifiableCollection(cOfDuplicateRules);
    }

    public void compute2ndNFViolations() {
        if (is1stNFViolation()) {
            LOG.debug("Violated already at 1st NF.");
            return;
        }
        for (Contraction c : contractions) { // is a contraction resulting in a 2NF violation?
            if (c.dimensionAsContracted.size() == 1) {
                Interval domainMinMax = ddtaTable.getInputs().get(c.adjacentDimension - 1).getDomainMinMax();
                if (domainMinMax.equals(c.dimensionAsContracted.get(0))) {
                    LOG.debug("compute2ndNFViolations() Contraction: {} violates 2NF", c);
                    contractionsViolating2ndNF.add(c);
                }
            }
        }
        for (int r = 0; r < ddtaTable.getRule().size(); r++) { // is a cell equivalent to a Dash `-` in DMN decision table?
            DDTARule rule = ddtaTable.getRule().get(r);
            for (int c = 0; c < ddtaTable.getInputs().size(); c++) {
                if (rule.getInputEntry().get(c).getIntervals().size() != 1) {
                    continue;
                }
                Interval int0 = rule.getInputEntry().get(c).getIntervals().get(0);
                if (!(rule.getInputEntry().get(c).getUts().get(0) instanceof DashNode) &&
                    int0.getLowerBound().getBoundaryType() == RangeBoundary.CLOSED &&
                    int0.getUpperBound().getBoundaryType() == RangeBoundary.CLOSED &&
                    !int0.getLowerBound().getValue().equals(int0.getUpperBound().getValue())) { // a normalized closed-interval, but not a `-`: but is it equivalent to it?
                    DDTAInputClause col = ddtaTable.getInputs().get(c);
                    boolean includes = int0.includes(col.getDomainMinMax());
                    if (includes) {
                        RuleColumnCoordinate rc = new RuleColumnCoordinate(r + 1, c + 1, sourceDT.getRule().get(r).getInputEntry().get(c).getText());
                        LOG.debug("compute2ndNFViolations() Cell: {} violates 2NF", rc);
                        cellsViolating2ndNF.add(rc);
                    }
                }
            }
        }
        LOG.debug("compute2ndNFViolations() c2ndNFViolation result: {}", is2ndNFViolation());
    }

    public boolean is2ndNFViolation() {
        return !contractionsViolating2ndNF.isEmpty() || !cellsViolating2ndNF.isEmpty();
    }

    public Collection getContractionsViolating2ndNF() {
        return Collections.unmodifiableCollection(contractionsViolating2ndNF);
    }

    public Collection getCellsViolating2ndNF() {
        return Collections.unmodifiableCollection(cellsViolating2ndNF);
    }

    public void computeHitPolicyRecommender() {
        if (!gaps.isEmpty() || !isHitPolicySingle(sourceDT.getHitPolicy())) {
            return;
        }
        if (overlaps.isEmpty() && sourceDT.getHitPolicy() != HitPolicy.UNIQUE) {
            passThruMessages.add(new DMNDTAnalysisMessage(this,
                                                          Severity.WARN,
                                                          MsgUtil.createMessage(Msg.DTANALYSIS_HITPOLICY_RECOMMENDER_UNIQUE,
                                                                                nameOrIDOfTable()),
                                                          Msg.DTANALYSIS_HITPOLICY_RECOMMENDER_UNIQUE.getType()));
        } else if (!overlaps.isEmpty()) {
            boolean overlapsShareSameOutput = true;
            for (Overlap ol : overlaps) {
                List rules = ol.getRules();
                Set>> outputsForOverlap = new HashSet<>();
                for (Integer ruleID : rules) {
                    outputsForOverlap.add(ddtaTable.getRule().get(ruleID - 1).getOutputEntry());
                }
                overlapsShareSameOutput &= outputsForOverlap.size() == 1;
            }
            if (overlapsShareSameOutput && sourceDT.getHitPolicy() != HitPolicy.ANY) {
                passThruMessages.add(new DMNDTAnalysisMessage(this,
                                                              sourceDT.getHitPolicy() == HitPolicy.UNIQUE ? Severity.ERROR : Severity.WARN,
                                                              MsgUtil.createMessage(Msg.DTANALYSIS_HITPOLICY_RECOMMENDER_ANY,
                                                                                    nameOrIDOfTable()),
                                                              Msg.DTANALYSIS_HITPOLICY_RECOMMENDER_ANY.getType()));
            } else if (!overlapsShareSameOutput && sourceDT.getHitPolicy() != HitPolicy.PRIORITY) {
                passThruMessages.add(new DMNDTAnalysisMessage(this,
                                                              sourceDT.getHitPolicy() == HitPolicy.FIRST ? Severity.WARN : Severity.ERROR,
                                                              MsgUtil.createMessage(Msg.DTANALYSIS_HITPOLICY_RECOMMENDER_PRIORITY,
                                                                                    nameOrIDOfTable()),
                                                              Msg.DTANALYSIS_HITPOLICY_RECOMMENDER_PRIORITY.getType()));
            }
        }
    }

    public boolean isHitPolicySingle(HitPolicy hp) {
        return hp == HitPolicy.UNIQUE || hp == HitPolicy.ANY || hp == HitPolicy.PRIORITY || hp == HitPolicy.FIRST;
    }

    public String nameOrIDOfTable() {
        return ValidatorUtil.nameOrIDOfTable(sourceDT);
    }

    public void computeOutputInLOV() {
        for (int ruleIdx = 0; ruleIdx < ddtaTable.getRule().size(); ruleIdx++) {
            DDTARule rule = ddtaTable.getRule().get(ruleIdx);
            for (int outputIdx = 0; outputIdx < ddtaTable.getOutputs().size(); outputIdx++) {
                Comparable value = rule.getOutputEntry().get(outputIdx);
                if (value instanceof DDTAOutputEntryExpression) {
                    continue; // we can't say _statically_ if the output expression is in the output LoV; skipping
                }
                DDTAOutputClause outputClause = ddtaTable.getOutputs().get(outputIdx);
                if (outputClause.isDiscreteDomain() && !outputClause.getDiscreteValues().contains(value)) {
                    passThruMessages.add(new DMNDTAnalysisMessage(this,
                                                                  Severity.ERROR,
                                                                  MsgUtil.createMessage(Msg.DTANALYSIS_ERROR_RULE_OUTPUT_OUTSIDE_LOV,
                                                                                        ruleIdx + 1,
                                                                                        value,
                                                                                        outputIdx + 1,
                                                                                        outputClause.getDiscreteValues()),
                                                                  Msg.DTANALYSIS_ERROR_RULE_OUTPUT_OUTSIDE_LOV.getType()));
                }
            }
        }
    }

    public void setMCDCSelectedBlocks(List selectedBlocks) {
        this.selectedBlocks = selectedBlocks;
    }

    public List getMCDCSelectedBlocks() {
        return Collections.unmodifiableList(selectedBlocks);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy