DefinitionContentInspector.java

  1. /*
  2.  * Copyright © 2017, Salesforce.com, Inc
  3.  * All rights reserved.
  4.  *
  5.  * Redistribution and use in source and binary forms, with or without
  6.  * modification, are permitted provided that the following conditions are met:
  7.  *     * Redistributions of source code must retain the above copyright
  8.  *       notice, this list of conditions and the following disclaimer.
  9.  *     * Redistributions in binary form must reproduce the above copyright
  10.  *       notice, this list of conditions and the following disclaimer in the
  11.  *       documentation and/or other materials provided with the distribution.
  12.  *     * Neither the name of the <organization> nor the
  13.  *       names of its contributors may be used to endorse or promote products
  14.  *       derived from this software without specific prior written permission.
  15.  *
  16.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17.  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18.  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19.  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  20.  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21.  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23.  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24.  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25.  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26.  */
  27. package com.salesforce.apt.graph.processing;

  28. import java.util.AbstractMap.SimpleEntry;
  29. import java.util.ArrayList;
  30. import java.util.Arrays;
  31. import java.util.Collection;
  32. import java.util.HashMap;
  33. import java.util.List;
  34. import java.util.Map;
  35. import java.util.Map.Entry;
  36. import java.util.Set;
  37. import java.util.function.Consumer;
  38. import java.util.stream.Collectors;
  39. import java.util.stream.Stream;

  40. import org.jgrapht.Graph;
  41. import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles;
  42. import org.jgrapht.graph.DefaultDirectedGraph;
  43. import org.jgrapht.graph.DefaultEdge;

  44. import com.salesforce.apt.graph.model.AbstractModel;
  45. import com.salesforce.apt.graph.model.BaseInstanceModel;
  46. import com.salesforce.apt.graph.model.DefinitionModel;
  47. import com.salesforce.apt.graph.model.ExpectedModel;
  48. import com.salesforce.apt.graph.model.InstanceDependencyModel;
  49. import com.salesforce.apt.graph.model.InstanceModel;
  50. import com.salesforce.apt.graph.model.errors.ErrorModel;
  51. import com.salesforce.apt.graph.model.errors.ErrorType;
  52. import com.salesforce.apt.graph.model.storage.DefinitionModelStore;
  53. import com.salesforce.apt.graph.types.AssignabilityUtils;

  54. public class DefinitionContentInspector {

  55.   public void inspectDefinitionGraph(Set<DefinitionModel> definitionGraphHeads,
  56.       Consumer<ErrorModel> errorListener, AssignabilityUtils assignabilityUtils, DefinitionModelStore store) {
  57.     for (DefinitionModel definition : definitionGraphHeads) {
  58.       depthFirstExpectedsInspector(definition, errorListener, assignabilityUtils, store);
  59.     }
  60.   }
  61.  
  62.   public InstanceModel getOneWithSourceElementElseAny(final Collection<InstanceModel> possibilities) {
  63.     return possibilities.stream()
  64.          .filter(im -> im.getSourceElement().isPresent())
  65.          .findAny().orElseGet(() -> possibilities.iterator().next());
  66.   }
  67.  
  68.   /**
  69.    * The the sha 256 of dependencies against the stored data.
  70.    *
  71.    * @param model model who's dependencies we will inspect
  72.    * @param store the store of all the model data (abstraction, will likely be in memory or class files)
  73.    * @param errorListener if any shas mismatch will report here.
  74.    * @return true if all shas match.
  75.    */
  76.   public boolean verifiedShas(DefinitionModel model, DefinitionModelStore store, Consumer<ErrorModel> errorListener) {
  77.     boolean verified = true;
  78.     for (DefinitionModel dep : model.getDependencies()) {
  79.       if (!dep.getSourceElement().isPresent()  //not recompiling
  80.           && model.getDependencyNameToSha256().containsKey(dep.getIdentity())) { //model already has a sha256 of it
  81.         if (!model.getDependencyNameToSha256().get(dep.getIdentity()).equals(dep.getSha256())) {
  82.           errorListener.accept(new ErrorModel(ErrorType.DEPENDENCY_SHA_MISMATCH,
  83.                 Arrays.asList(model, dep),  Arrays.asList(model)));
  84.           verified = false;
  85.         }
  86.       }
  87.     }
  88.     return verified;
  89.   }
  90.  
  91.  
  92.   /**
  93.    * Verify that all expected entities are marked expected.
  94.    * Verify that the types of provided entities satisfy all expected types....
  95.    * <p/>
  96.    * if a child record was freshly process, so too must all parent ( if the md5 has changed ).
  97.    *  
  98.    * @param definition the head of current tree of definitions linked by imports
  99.    * @param errorListener registers all errors found
  100.    */
  101.   private boolean depthFirstExpectedsInspector(DefinitionModel definition, Consumer<ErrorModel> errorListener,
  102.       AssignabilityUtils assignabilityUtils, DefinitionModelStore store) {
  103.     boolean errored = false;
  104.     for (DefinitionModel dependency : definition.getDependencies()) {
  105.       if (!dependency.isLockedAnalyzed()) {
  106.         errored = depthFirstExpectedsInspector(dependency, errorListener, assignabilityUtils, store) || errored;
  107.         if (errored) {
  108.           return true; // no need to continue.
  109.         }
  110.       } else {
  111.         if (!verifiedShas(dependency, store, errorListener)) {
  112.           errored = true;
  113.         }
  114.       }
  115.     }
  116.    
  117.     if (errored) {
  118.       return errored;
  119.     }
  120.    
  121.     //check that each object has only one source ( could be the imported from a dependency, in a diamond pattern )
  122.     //validate the expected beans are correct.
  123.     Map<String, InstanceModel> resolvedInstances = ensureSingleInstanceOfEachName(definition, errorListener);
  124.    
  125.     //short circuit if dependencies couldn't be resolved.
  126.     if (resolvedInstances == null) {
  127.       return false;
  128.     }
  129.    
  130.     //looks for cycles and unexpected missing entities.
  131.     errored = detectCyclesInEntityGraph(definition, resolvedInstances, errorListener);

  132.     //check types of non-expected dependencies
  133.     errored = checkInstancesTypesInDefinition(definition, resolvedInstances, errorListener, assignabilityUtils) || errored;

  134.     //check all definitions with expected, that each instance expecting a definition can use the supplied.
  135.     errored = checkProvidedSupplyCorrectTypes(definition, resolvedInstances, errorListener, assignabilityUtils) || errored;

  136.     //prune all edged from graph that don't end in an expected

  137.     //store computed expects with all types that must satisfied.
  138.    
  139.    
  140.    
  141.     if (!errored) {
  142.       //store as provided dependencies
  143.       definition.addAllProvidedInstances(resolvedInstances.values());
  144.       for (DefinitionModel dep : definition.getDependencies()) {
  145.         definition.addDependencyNameToSha256(dep.getIdentity(), dep.getSha256());
  146.       }
  147.       //storing will lock a the definition, as will reading.
  148.       if (!store.store(definition)) {
  149.         errorListener.accept(
  150.             new ErrorModel(ErrorType.COULD_NOT_STORE, Arrays.asList(definition), Arrays.asList(definition)));
  151.       }
  152.     }
  153.    
  154.     return errored;
  155.   }

  156.   private ErrorModel errorForMismatchedExpected(final DefinitionModel definition, ExpectedModel computedExpected,
  157.       final Map<String, InstanceModel> nameToEntity, AssignabilityUtils assignabilityUtils) {
  158.     //that which is to fill all the expectedInstance references.
  159.     InstanceModel providedInstance = nameToEntity.get(computedExpected.getIdentity());
  160.    
  161.     //the top level definition model should have declared this an expectedBean - maybe move that error here?
  162.     if (providedInstance == null) {
  163.       return null;
  164.     }
  165.    
  166.     //Instances which expect to use the provided instance
  167.     List<InstanceModel> mistmatchedExpectantEntities = computedExpected.getDefinitionReferenceToType().keySet().stream()
  168.         .map(name -> nameToEntity.get(name))
  169.         .filter(expectantInstance -> !assignabilityUtils.isAssignableFrom(providedInstance, expectantInstance))
  170.         .collect(Collectors.toList());
  171.    
  172.     if (mistmatchedExpectantEntities.size() == 0) {
  173.       return null;
  174.     } else {
  175.       List<InstanceModel> involved = new ArrayList<>();
  176.       involved.add(providedInstance);
  177.       involved.addAll(mistmatchedExpectantEntities);
  178.       return new ErrorModel(ErrorType.UNMATCHED_TYPES, involved, Arrays.asList(definition, providedInstance));
  179.     }
  180.   }
  181.  
  182.   private boolean checkProvidedSupplyCorrectTypes(final DefinitionModel definition, final Map<String, InstanceModel> nameToEntity,
  183.       final Consumer<ErrorModel> errorListner, AssignabilityUtils assignabilityUtils) {
  184.     List<ErrorModel> errors = definition.getDependencies().stream()
  185.         .flatMap(dependency -> dependency.getComputedExpected().stream())
  186.         .filter(expectedModel -> !definition.getExpectedDefinitions().stream()
  187.             .anyMatch(ed -> ed.getIdentity().equals(expectedModel.getIdentity())))
  188.         .map(em -> errorForMismatchedExpected(definition, em, nameToEntity, assignabilityUtils))
  189.         .filter(errorModel -> errorModel != null)
  190.         .collect(Collectors.toList());
  191.     errors.stream().forEach(errorListner);
  192.     return errors.size() > 0;
  193.   }
  194.  
  195.  
  196.   private List<Entry<String, InstanceModel>> getEntryListForNameAndAlias(InstanceModel instanceModel) {
  197.     List<Entry<String, InstanceModel>> output = new ArrayList<>();
  198.     output.add(new SimpleEntry<>(instanceModel.getIdentity(), instanceModel));
  199.     for (String alias : instanceModel.getAliases()) {
  200.       if (!alias.equals(instanceModel.getIdentity())) {
  201.         output.add(new SimpleEntry<>(alias, instanceModel));
  202.       }
  203.     }
  204.     return output;
  205.   }
  206.  
  207.   /**
  208.    * Verify that a single named instance model is correctly identifiable composed of owningDefinition/ElementLocation/Identity.
  209.    *
  210.    *
  211.    * @param definition the definition of
  212.    * @param errorListener accepts and displays all errors produced by analyzing the models.
  213.    * @return returns a map of each instance name to the single model that the has been resolved to.
  214.    */
  215.   private Map<String, InstanceModel> ensureSingleInstanceOfEachName(DefinitionModel definition,
  216.       Consumer<ErrorModel> errorListener) {
  217.     Map<String, Map<String, InstanceModel>> instancesByNameAndLocationDedupped =
  218.         definitionToAllInstancesByNameAndSourceLocation(definition);
  219.    
  220.     final Map<String, InstanceModel> resolvedDependencies = new HashMap<>();
  221.     boolean errored = false;    
  222.     for (Entry<String, Map<String, InstanceModel>> entry : instancesByNameAndLocationDedupped.entrySet()) {
  223.       if (entry.getValue().size() == 1) {
  224.         resolvedDependencies.put(entry.getKey(), entry.getValue().values().iterator().next()); //get only InstanceModel.
  225.       } else {
  226.         errored = true;
  227.         errorListener.accept(errorForDuplicateInstanceModels(definition, entry.getValue().values().stream()
  228.             .sorted((i1, i2) -> i1.getElementLocation().compareTo(i2.getElementLocation()))
  229.             .collect(Collectors.toList())));
  230.       }
  231.     }
  232.     return errored ? null : resolvedDependencies;
  233.   }
  234.  
  235.   /**
  236.    * Give a definition model, extract all instances from this definition, and all imported definitions.
  237.    * Group the instances by name, and then map them from source location to InstanceModel.
  238.    *  
  239.    * @param definition model to extract all instance information from, including imported definitions.
  240.    * @return a map of maps, [name -> [sourceLocation -> instanceModel]]
  241.    */
  242.   private Map<String, Map<String, InstanceModel>> definitionToAllInstancesByNameAndSourceLocation(
  243.       DefinitionModel definition) {
  244.     //create a stream of all imported Definition's InstanceModels
  245.     Stream<InstanceModel> imported = definition.getDependencies().stream()
  246.         .map(d -> d.getProvidedInstances()).flatMap(x -> x.stream());
  247.     //merge the stream with all local instance models.
  248.     Stream<InstanceModel> instanceModelStream = Stream.concat(definition.getObjectDefinitions().stream(), imported);

  249.     //map all instances by name and then by source location (due to diamond dependencies in definition imports.
  250.     Map<String, Map<String, InstanceModel>> instancesByNameAndLocationDedupped = instanceModelStream
  251.         .flatMap(instance -> getEntryListForNameAndAlias(instance).stream()) //flat map all alias to entries
  252.         .collect(Collectors.groupingBy(entry -> entry.getKey(), //group by name
  253.             Collectors.mapping(entry -> entry.getValue(),
  254.                 Collectors.toMap(im -> im.getElementLocation(), //by location
  255.                     im -> im, //identity function for first insert
  256.                     //choose the one with source element on merge, if any
  257.                     (im1, im2) -> im1.getSourceElement().isPresent() ? im1 : im2))));
  258.     return instancesByNameAndLocationDedupped;
  259.   }

  260.   /**
  261.    * Given a list of duplicate instance models (same identifier, different source elements) produce a
  262.    * well formed ErrorModel.
  263.    *
  264.    * @param definition where the error was first detected.
  265.    * @param causes the list of all instances models from the definition or any of it's imported definitions.
  266.    * @return an well formed ErrorModel of the duplicated beans.
  267.    */
  268.   private ErrorModel errorForDuplicateInstanceModels(DefinitionModel definition, List<InstanceModel> causes) {
  269.     ArrayList<AbstractModel> involved = new ArrayList<>();
  270.     involved.add(definition);
  271.     involved.addAll(causes.stream()
  272.         .filter(instanceModel -> instanceModel.getSourceElement().isPresent())
  273.         .collect(Collectors.toList()));
  274.     return new ErrorModel(ErrorType.DUPLICATE_OBJECT_DEFINITIONS, causes, involved);
  275.   }
  276.  
  277.   /**
  278.    * Inspects the instance graph for cycles, any cycle is printed as an error.   The nameToEntity parameter doesn't list expected
  279.    * instances, any instances that are not found in the nameToInstances map (they are looked for because they are referenced as a
  280.    * dependency by an instance in the map) and are not found by name in the definition's expectedInstances are treated as errors
  281.    * as well.
  282.    *
  283.    * @param definition definition being processed.  Will uses it's expected list, any instances references as dependencies but
  284.    *     not found, not listed as expected in this DefinitionModel, will be treated as errors.
  285.    * @param nameToEntity name to unique instanceModels, verified before call.
  286.    * @param errorListner accepts and displays all errors produced by analyzing the models
  287.    * @return true if an error occurred, false otherwise
  288.    */
  289.   private boolean detectCyclesInEntityGraph(final DefinitionModel definition, final Map<String, InstanceModel> nameToEntity,
  290.       final Consumer<ErrorModel> errorListener) {
  291.     final Map<String, ExpectedModel> missing = new HashMap<>();
  292.     final Graph<BaseInstanceModel, DefaultEdge> entityGraph = new DefaultDirectedGraph<>(DefaultEdge.class);
  293.     for (BaseInstanceModel entity : nameToEntity.values()) {
  294.       if (!entityGraph.containsVertex(entity)) {
  295.         entityGraph.addVertex(entity);
  296.       }
  297.       if (InstanceModel.class.isAssignableFrom(entity.getClass())) {
  298.         InstanceModel instanceModel = (InstanceModel) entity;
  299.         for (InstanceDependencyModel instanceDependency : instanceModel.getDependencies()) {
  300.           BaseInstanceModel dependency = nameToEntity.get(instanceDependency.getIdentity());
  301.           if (dependency == null) {
  302.             dependency = missing.computeIfAbsent(instanceDependency.getIdentity(), s -> new ExpectedModel(s));
  303.             missing.get(instanceDependency.getIdentity())
  304.               .addDefinitionReferenceToType(instanceModel.getIdentity(), instanceDependency.getType());
  305.           }
  306.           if (!entityGraph.containsVertex(dependency)) {
  307.             entityGraph.addVertex(dependency);
  308.           }
  309.           entityGraph.addEdge(entity, dependency);
  310.         }
  311.       }
  312.     }
  313.    
  314.     boolean errored = errorsForCycles(errorListener, entityGraph);
  315.     errored = testAllMissingEntitiesAreExpected(definition, errorListener, missing, entityGraph) || errored;
  316.     errored = errorUnusedExpectedsOnDefinition(definition, errorListener, missing) || errored;
  317.     return errored;
  318.   }

  319.   private boolean checkInstancesTypesInDefinition(final DefinitionModel definition, final Map<String, InstanceModel> nameToEntity,
  320.       final Consumer<ErrorModel> errorListner, AssignabilityUtils assignabilityUtils) {
  321.     boolean errored = false;
  322.     List<InstanceModel> instances = definition.getObjectDefinitions();
  323.     for (InstanceModel instance : instances) {
  324.       for (InstanceDependencyModel instanceDependency : instance.getDependencies()) {
  325.         InstanceModel dependency = nameToEntity.get(instanceDependency.getIdentity());
  326.         if (dependency != null && !assignabilityUtils.isAssignableFrom(dependency, instance)) {
  327.           errored = true;
  328.           errorListner.accept(new ErrorModel(ErrorType.UNMATCHED_TYPES,
  329.               Arrays.asList(instance, dependency), Arrays.asList(instance, dependency)));
  330.         }
  331.       }
  332.     }
  333.     return errored;
  334.   }

  335.   private boolean errorUnusedExpectedsOnDefinition(final DefinitionModel definition, final Consumer<ErrorModel> errorListner,
  336.       final Map<String, ExpectedModel> missing) {
  337.     boolean errored = false;
  338.     for (ExpectedModel expected : definition.getExpectedDefinitions()) {
  339.       if (!missing.keySet().contains(expected.getIdentity())) {
  340.         errorListner.accept(new ErrorModel(ErrorType.UNUSED_EXPECTED, Arrays.asList(expected), Arrays.asList(definition)));
  341.         errored = true;
  342.       }
  343.     }
  344.     return errored;
  345.   }

  346.   private boolean errorsForCycles(final Consumer<ErrorModel> errorListner,
  347.       final Graph<BaseInstanceModel, DefaultEdge> entityGraph) {
  348.     SzwarcfiterLauerSimpleCycles<BaseInstanceModel, DefaultEdge> cycleFind = new SzwarcfiterLauerSimpleCycles<>();
  349.     boolean errored = false;
  350.     cycleFind.setGraph(entityGraph);
  351.     List<List<BaseInstanceModel>> cycles = cycleFind.findSimpleCycles();
  352.     for (List<BaseInstanceModel> cycle : cycles) {
  353.       errored = true;
  354.       errorListner.accept(new ErrorModel(ErrorType.CYCLE_IN_DEFINITION_SOURCES, cycle, cycle));
  355.     }
  356.     return errored;
  357.   }

  358.   private boolean testAllMissingEntitiesAreExpected(final DefinitionModel definition, final Consumer<ErrorModel> errorListner,
  359.       final Map<String, ExpectedModel> missing, final Graph<BaseInstanceModel, DefaultEdge> entityGraph) {
  360.     //check computed expected are actually expected
  361.     boolean errored = false;
  362.     List<String> expectedMissing = definition.getExpectedDefinitions().stream().map(em -> em.getIdentity())
  363.         .collect(Collectors.toList());
  364.     for (ExpectedModel expected : missing.values()) {
  365.       if (!expectedMissing.contains(expected.getIdentity())) {
  366.         List<AbstractModel> dependsOnMissing = Stream.concat(
  367.             Stream.of(definition),
  368.             entityGraph.incomingEdgesOf(expected).stream().map(edge -> entityGraph.getEdgeSource(edge))
  369.               .filter(m -> m.getSourceElement().isPresent()))
  370.             .collect(Collectors.toList());
  371.         errored = true;
  372.         errorListner.accept(new ErrorModel(ErrorType.MISSING_BEAN_DEFINITIONS, Arrays.asList(expected), dependsOnMissing));
  373.       } else {
  374.         definition.addComputedExpected(expected);
  375.       }
  376.     }
  377.     return errored;
  378.   }
  379. }