/* * Copyright 2017 Red Hat, Inc. and/or its affiliates. * * Licensed 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.drools.workbench.services.verifier.core.checks; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.IntStream; import java.util.stream.Stream; import org.drools.workbench.services.verifier.api.client.configuration.AnalyzerConfiguration; import org.drools.workbench.services.verifier.api.client.index.FieldCondition; import org.drools.workbench.services.verifier.api.client.index.ObjectField; import org.drools.workbench.services.verifier.api.client.relations.Operator; import org.drools.workbench.services.verifier.api.client.reporting.CheckType; import org.drools.workbench.services.verifier.api.client.reporting.Issue; import org.drools.workbench.services.verifier.api.client.reporting.Severity; import org.drools.workbench.services.verifier.core.cache.inspectors.RuleInspector; import org.drools.workbench.services.verifier.core.cache.inspectors.condition.ConditionInspector; import org.drools.workbench.services.verifier.core.checks.base.CheckBase; import static java.util.stream.Collectors.*; import static org.drools.workbench.services.verifier.api.client.relations.Operator.resolve; public class SingleRangeCheck extends CheckBase { private List<RangeError> errors = new ArrayList<>(); private final Collection<RuleInspector> ruleInspectors; public SingleRangeCheck( AnalyzerConfiguration configuration, Collection<RuleInspector> ruleInspectors ) { super( configuration ); this.ruleInspectors = ruleInspectors; } @Override public boolean check() { if (ruleInspectors == null || ruleInspectors.isEmpty()) { return hasIssues = false; } errors.clear(); int conditionNr = ruleInspectors.iterator().next().getConditionsInspectors().size(); for (int i = 0; i < conditionNr; i++) { checkCondition(i); } return hasIssues = !errors.isEmpty(); } private void checkCondition(int conditionIndex) { Map<OperatorType, Set<ObjectField>> fields = ruleInspectors.stream() .map( r -> r.getConditionsInspectors().get( conditionIndex ) ) .flatMap( cond -> cond.keySet().stream() ) .collect( groupingBy( f -> getFieldOperatorType(f, conditionIndex), toSet() ) ); Set<ObjectField> rangeFields = fields.get(OperatorType.RANGE); if (rangeFields != null && !rangeFields.isEmpty()) { checkRanges( rangeFields, partition(fields.get(OperatorType.PARTITION), ruleInspectors, conditionIndex), conditionIndex ); } } private void checkRanges( Collection<ObjectField> rangeFields, Map<PartitionKey, List<RuleInspector>> partitions, int conditionIndex ) { for (Map.Entry<PartitionKey, List<RuleInspector>> partition : partitions.entrySet()) { List<List<? extends Range<?>>> dimensions = new ArrayList<>(); for ( ObjectField field : rangeFields ) { if ( "Integer".equals( field.getFieldType() ) ) { checkMonodimensionalRange( partition, dimensions, field, conditionIndex, IntegerRange::new, Integer.MIN_VALUE, Integer.MAX_VALUE ); } else { checkMonodimensionalRange( partition, dimensions, field, conditionIndex, NumericRange::new, Double.MIN_VALUE, Double.MAX_VALUE ); } } checkBidimensionalRanges( partition, dimensions ); } } private <T extends Comparable> void checkMonodimensionalRange( Entry<PartitionKey, List<RuleInspector>> partition, List<List<? extends Range<?>>> dimensions, ObjectField field, int conditionIndex, Function<List<ConditionInspector>, Range<T>> rangeSupplier, T min, T max ) { List<Range<T>> ranges = partition.getValue().stream() .map( r -> r.getConditionsInspectors().get( conditionIndex ) ) .map(c -> c.get(field)) .map( rangeSupplier ) .collect( toList() ); T upper = getCoverageUpperBound( min, ranges ); if ( upper.equals( max ) ) { dimensions.add( ranges ); } else { errors.add( new RangeError( partition.getValue(), partition.getKey(), upper ) ); } } private void checkBidimensionalRanges( Entry<PartitionKey, List<RuleInspector>> partition, List<List<? extends Range<?>>> dimensions ) { if (errors.isEmpty() && dimensions.size() >= 2) { for (int i = 0; i < dimensions.size()-1; i++) { for ( int j = i + 1; j < dimensions.size(); j++ ) { if ( !checkBidimensionalRanges( dimensions.get( i ), dimensions.get( j ) ) ) { errors.add( new RangeError( partition.getValue(), partition.getKey(), null ) ); } } } } } private <H extends Comparable, V extends Comparable> boolean checkBidimensionalRanges( List<? extends Range<? extends H>> hRanges, List<? extends Range<? extends V>> vRanges ) { List<BidimensionalRange<H,V>> bidiRanges = IntStream.range( 0, hRanges.size() ) .mapToObj( i -> new BidimensionalRange<H,V>( hRanges.get(i), vRanges.get(i) ) ) .sorted( Comparator.comparing( r -> r.horizontal ) ) .collect( toList() ); SortedSet<H> hBreakPoints = new TreeSet<>(); Map<H, List<BidimensionalRange<H,V>>> lowerHBounds = new HashMap<>(); Map<H, List<BidimensionalRange<H,V>>> upperHBounds = new HashMap<>(); for ( BidimensionalRange<H,V> bidiRange : bidiRanges ) { Range<? extends H> hRange = bidiRange.horizontal; hBreakPoints.add( hRange.lowerBound ); lowerHBounds.computeIfAbsent( hRange.lowerBound, x -> new ArrayList<>() ).add( bidiRange ); if ( !hRange.upperBound.equals( hRange.maxValue() ) ) { hBreakPoints.add( hRange.upperBound ); upperHBounds.computeIfAbsent( hRange.upperBound, x -> new ArrayList<>() ).add( bidiRange ); } } V minV = vRanges.get(0).minValue(); V maxV = vRanges.get(0).maxValue(); boolean first = true; List<BidimensionalRange<H,V>> parsedRanges = new ArrayList<>(); for ( H sweep : hBreakPoints ) { List<BidimensionalRange<H,V>> enteringRanges = lowerHBounds.get(sweep); if (enteringRanges != null) { parsedRanges.addAll( enteringRanges ); } List<BidimensionalRange<H,V>> exitingRanges = upperHBounds.get(sweep); if (exitingRanges != null) { parsedRanges.removeAll( exitingRanges ); } if ( ( first || exitingRanges != null ) && !maxV.equals( getCoverageUpperBound( minV, parsedRanges.stream().map( br -> br.vertical ) ) )) { return false; } first = false; } return true; } private OperatorType getFieldOperatorType(ObjectField field, int conditionIndex) { return ruleInspectors.stream() .flatMap( r -> getConditionStream( r, field, conditionIndex ) ) .map( c -> resolve( (( FieldCondition ) c.getCondition()).getOperator() ) ) .map( OperatorType::decode ) .reduce( OperatorType.PARTITION, OperatorType::combine ); } enum OperatorType { PARTITION, RANGE, UNKNOWN; static OperatorType decode(Operator op) { return op == Operator.EQUALS ? PARTITION : op.isRangeOperator() ? RANGE : UNKNOWN; } OperatorType combine(OperatorType other) { if (this == UNKNOWN || other == UNKNOWN) { return UNKNOWN; } if (this == RANGE || other == RANGE) { return RANGE; } return PARTITION; } } private Map<PartitionKey, List<RuleInspector>> partition(Collection<ObjectField> partitionFields, Collection<RuleInspector> rules, int conditionIndex) { List<PartitionKey> keysWithNull = new ArrayList<>(); Map<PartitionKey, List<RuleInspector>> partitions = new HashMap<>(); for (RuleInspector rule : rules) { PartitionKey key = getPartitionKey(partitionFields, rule, conditionIndex); partitions.computeIfAbsent( key, k -> { if (k.hasNulls()) { keysWithNull.add(k); } return new ArrayList<>(); } ).add(rule); } for (PartitionKey key : keysWithNull) { for (Map.Entry<PartitionKey, List<RuleInspector>> partition : partitions.entrySet()) { if (key.subsumes(partition.getKey())) { partition.getValue().addAll(partitions.get(key)); } } } keysWithNull.forEach( partitions::remove ); return partitions; } private PartitionKey getPartitionKey(Collection<ObjectField> partitionFields, RuleInspector rule, int conditionIndex) { return partitionFields == null || partitionFields.isEmpty() ? PartitionKey.EMPTY_KEY : new PartitionKey( partitionFields.stream().map( f -> getValue(rule, f, conditionIndex) ).toArray() ); } private Object getValue(RuleInspector rule, ObjectField field, int conditionIndex) { List<ConditionInspector> conditions = getConditions( rule, field, conditionIndex ); return conditions != null ? conditions.get(0).getCondition().getValues().iterator().next() : null; } private Stream<ConditionInspector> getConditionStream( RuleInspector rule, ObjectField field, int conditionIndex ) { List<ConditionInspector> conditionInspectors = getConditions( rule, field, conditionIndex ); return conditionInspectors != null ? conditionInspectors.stream() : Stream.empty(); } private List<ConditionInspector> getConditions( RuleInspector rule, ObjectField field, int conditionIndex ) { return rule.getConditionsInspectors() != null ? rule.getConditionsInspectors().get( conditionIndex ).get( field ) : null; } private static class PartitionKey { private static PartitionKey EMPTY_KEY = new PartitionKey( new Object[0] ); private final Object[] keys; private PartitionKey( Object[] keys ) { this.keys = keys; } @Override public boolean equals( Object obj ) { return Arrays.equals( keys, ((PartitionKey)obj).keys ); } @Override public int hashCode() { return Arrays.hashCode( keys ); } @Override public String toString() { return Arrays.toString( keys ); } boolean hasNulls() { return Stream.of( keys ).anyMatch( Objects::isNull ); } public boolean subsumes( PartitionKey other ) { return IntStream.range( 0, keys.length ).allMatch( i -> keys[i] == null || keys[i].equals( other.keys[i] ) ); } } private <T extends Comparable> T getCoverageUpperBound( T lowerBound, List<? extends Range<? extends T>> ranges ) { return getCoverageUpperBound( lowerBound, ranges.stream() ); } private <T extends Comparable> T getCoverageUpperBound( T lowerBound, Stream<? extends Range<? extends T>> ranges ) { T limit = lowerBound; Iterator<? extends Range<? extends T>> i = ranges.sorted().iterator(); while (i.hasNext()) { Range<? extends T> range = i.next(); if (range.lowerBound.compareTo( limit ) > 0) { return limit; } limit = range.upperBound.compareTo( limit ) > 0 ? range.upperBound : limit; } return limit; } private abstract static class Range<T extends Comparable> implements Comparable<Range<T>> { protected T lowerBound = minValue(); protected T upperBound = maxValue(); public Range(List<ConditionInspector> conditionInspectors) { if (conditionInspectors != null) { conditionInspectors.forEach( getConditionParser() ); } } protected abstract Consumer<ConditionInspector> getConditionParser(); @Override public String toString() { return lowerBound + " < x < " + upperBound; } @Override public int compareTo( Range<T> o ) { return lowerBound.compareTo( o.lowerBound ); } protected abstract T minValue(); protected abstract T maxValue(); } private static class IntegerRange extends Range<Integer> implements Comparable<Range<Integer>> { IntegerRange( List<ConditionInspector> conditionInspectors ) { super(conditionInspectors); } @Override protected Consumer<ConditionInspector> getConditionParser() { return c -> { FieldCondition cond = ( FieldCondition ) c.getCondition(); Operator op = resolve( cond.getOperator() ); switch (op) { case LESS_OR_EQUAL: upperBound = (Integer) cond.getValues().iterator().next() + 1; break; case LESS_THAN: upperBound = (Integer) cond.getValues().iterator().next(); break; case GREATER_OR_EQUAL: lowerBound = (Integer) cond.getValues().iterator().next() - 1; break; case GREATER_THAN: lowerBound = (Integer) cond.getValues().iterator().next(); break; case EQUALS: lowerBound = (Integer) cond.getValues().iterator().next(); upperBound = (Integer) cond.getValues().iterator().next(); break; } }; } @Override protected Integer minValue() { return Integer.MIN_VALUE; } @Override protected Integer maxValue() { return Integer.MAX_VALUE; } } private static class NumericRange extends Range<Double> implements Comparable<Range<Double>> { NumericRange( List<ConditionInspector> conditionInspectors ) { super(conditionInspectors); } @Override protected Consumer<ConditionInspector> getConditionParser() { return c -> { FieldCondition cond = ( FieldCondition ) c.getCondition(); Operator op = resolve( cond.getOperator() ); switch (op) { case LESS_OR_EQUAL: case LESS_THAN: upperBound = ((Number) cond.getValues().iterator().next()).doubleValue(); break; case GREATER_THAN: case GREATER_OR_EQUAL: lowerBound = ((Number) cond.getValues().iterator().next()).doubleValue(); break; } }; } @Override protected Double minValue() { return Double.MIN_VALUE; } @Override protected Double maxValue() { return Double.MAX_VALUE; } } private static class BidimensionalRange<H extends Comparable, V extends Comparable> { private final Range<? extends H> horizontal; private final Range<? extends V> vertical; private BidimensionalRange( Range<? extends H> horizontal, Range<? extends V> vertical ) { this.horizontal = horizontal; this.vertical = vertical; } @Override public String toString() { return "[" + horizontal + "][" + vertical + "]"; } } @Override protected Issue makeIssue( Severity severity, CheckType checkType ) { return errors.get(0).toIssue( severity, checkType ); } private static class RangeError { private final Collection<RuleInspector> ruleInspectors; private final PartitionKey partitionKey; private final Object uncoveredValue; private RangeError( Collection<RuleInspector> ruleInspectors, PartitionKey partitionKey, Object uncoveredValue ) { this.ruleInspectors = ruleInspectors; this.partitionKey = partitionKey; this.uncoveredValue = uncoveredValue; } private Issue toIssue( Severity severity, CheckType checkType ) { return new Issue( severity, checkType, new HashSet<>( ruleInspectors.stream().map( r -> r.getRowIndex() + 1 ).collect( toSet() ) ) ).setDebugMessage( getMessage() ); } private String getMessage() { return "Uncovered range" + (uncoveredValue != null ? " starting from value " + uncoveredValue : "") + (partitionKey != PartitionKey.EMPTY_KEY ? " in partition " + partitionKey : ""); } } @Override protected CheckType getCheckType() { return CheckType.MISSING_RANGE; } @Override protected Severity getDefaultSeverity() { return Severity.NOTE; } }