DefinitionContentInspector.java
/*
* Copyright © 2017, Salesforce.com, Inc
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the <organization> nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.salesforce.apt.graph.processing;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jgrapht.Graph;
import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import com.salesforce.apt.graph.model.AbstractModel;
import com.salesforce.apt.graph.model.BaseInstanceModel;
import com.salesforce.apt.graph.model.DefinitionModel;
import com.salesforce.apt.graph.model.ExpectedModel;
import com.salesforce.apt.graph.model.InstanceDependencyModel;
import com.salesforce.apt.graph.model.InstanceModel;
import com.salesforce.apt.graph.model.errors.ErrorModel;
import com.salesforce.apt.graph.model.errors.ErrorType;
import com.salesforce.apt.graph.model.storage.DefinitionModelStore;
import com.salesforce.apt.graph.types.AssignabilityUtils;
public class DefinitionContentInspector {
public void inspectDefinitionGraph(Set<DefinitionModel> definitionGraphHeads,
Consumer<ErrorModel> errorListener, AssignabilityUtils assignabilityUtils, DefinitionModelStore store) {
for (DefinitionModel definition : definitionGraphHeads) {
depthFirstExpectedsInspector(definition, errorListener, assignabilityUtils, store);
}
}
public InstanceModel getOneWithSourceElementElseAny(final Collection<InstanceModel> possibilities) {
return possibilities.stream()
.filter(im -> im.getSourceElement().isPresent())
.findAny().orElseGet(() -> possibilities.iterator().next());
}
/**
* The the sha 256 of dependencies against the stored data.
*
* @param model model who's dependencies we will inspect
* @param store the store of all the model data (abstraction, will likely be in memory or class files)
* @param errorListener if any shas mismatch will report here.
* @return true if all shas match.
*/
public boolean verifiedShas(DefinitionModel model, DefinitionModelStore store, Consumer<ErrorModel> errorListener) {
boolean verified = true;
for (DefinitionModel dep : model.getDependencies()) {
if (!dep.getSourceElement().isPresent() //not recompiling
&& model.getDependencyNameToSha256().containsKey(dep.getIdentity())) { //model already has a sha256 of it
if (!model.getDependencyNameToSha256().get(dep.getIdentity()).equals(dep.getSha256())) {
errorListener.accept(new ErrorModel(ErrorType.DEPENDENCY_SHA_MISMATCH,
Arrays.asList(model, dep), Arrays.asList(model)));
verified = false;
}
}
}
return verified;
}
/**
* Verify that all expected entities are marked expected.
* Verify that the types of provided entities satisfy all expected types....
* <p/>
* if a child record was freshly process, so too must all parent ( if the md5 has changed ).
*
* @param definition the head of current tree of definitions linked by imports
* @param errorListener registers all errors found
*/
private boolean depthFirstExpectedsInspector(DefinitionModel definition, Consumer<ErrorModel> errorListener,
AssignabilityUtils assignabilityUtils, DefinitionModelStore store) {
boolean errored = false;
for (DefinitionModel dependency : definition.getDependencies()) {
if (!dependency.isLockedAnalyzed()) {
errored = depthFirstExpectedsInspector(dependency, errorListener, assignabilityUtils, store) || errored;
if (errored) {
return true; // no need to continue.
}
} else {
if (!verifiedShas(dependency, store, errorListener)) {
errored = true;
}
}
}
if (errored) {
return errored;
}
//check that each object has only one source ( could be the imported from a dependency, in a diamond pattern )
//validate the expected beans are correct.
Map<String, InstanceModel> resolvedInstances = ensureSingleInstanceOfEachName(definition, errorListener);
//short circuit if dependencies couldn't be resolved.
if (resolvedInstances == null) {
return false;
}
//looks for cycles and unexpected missing entities.
errored = detectCyclesInEntityGraph(definition, resolvedInstances, errorListener);
//check types of non-expected dependencies
errored = checkInstancesTypesInDefinition(definition, resolvedInstances, errorListener, assignabilityUtils) || errored;
//check all definitions with expected, that each instance expecting a definition can use the supplied.
errored = checkProvidedSupplyCorrectTypes(definition, resolvedInstances, errorListener, assignabilityUtils) || errored;
//prune all edged from graph that don't end in an expected
//store computed expects with all types that must satisfied.
if (!errored) {
//store as provided dependencies
definition.addAllProvidedInstances(resolvedInstances.values());
for (DefinitionModel dep : definition.getDependencies()) {
definition.addDependencyNameToSha256(dep.getIdentity(), dep.getSha256());
}
//storing will lock a the definition, as will reading.
if (!store.store(definition)) {
errorListener.accept(
new ErrorModel(ErrorType.COULD_NOT_STORE, Arrays.asList(definition), Arrays.asList(definition)));
}
}
return errored;
}
private ErrorModel errorForMismatchedExpected(final DefinitionModel definition, ExpectedModel computedExpected,
final Map<String, InstanceModel> nameToEntity, AssignabilityUtils assignabilityUtils) {
//that which is to fill all the expectedInstance references.
InstanceModel providedInstance = nameToEntity.get(computedExpected.getIdentity());
//the top level definition model should have declared this an expectedBean - maybe move that error here?
if (providedInstance == null) {
return null;
}
//Instances which expect to use the provided instance
List<InstanceModel> mistmatchedExpectantEntities = computedExpected.getDefinitionReferenceToType().keySet().stream()
.map(name -> nameToEntity.get(name))
.filter(expectantInstance -> !assignabilityUtils.isAssignableFrom(providedInstance, expectantInstance))
.collect(Collectors.toList());
if (mistmatchedExpectantEntities.size() == 0) {
return null;
} else {
List<InstanceModel> involved = new ArrayList<>();
involved.add(providedInstance);
involved.addAll(mistmatchedExpectantEntities);
return new ErrorModel(ErrorType.UNMATCHED_TYPES, involved, Arrays.asList(definition, providedInstance));
}
}
private boolean checkProvidedSupplyCorrectTypes(final DefinitionModel definition, final Map<String, InstanceModel> nameToEntity,
final Consumer<ErrorModel> errorListner, AssignabilityUtils assignabilityUtils) {
List<ErrorModel> errors = definition.getDependencies().stream()
.flatMap(dependency -> dependency.getComputedExpected().stream())
.filter(expectedModel -> !definition.getExpectedDefinitions().stream()
.anyMatch(ed -> ed.getIdentity().equals(expectedModel.getIdentity())))
.map(em -> errorForMismatchedExpected(definition, em, nameToEntity, assignabilityUtils))
.filter(errorModel -> errorModel != null)
.collect(Collectors.toList());
errors.stream().forEach(errorListner);
return errors.size() > 0;
}
private List<Entry<String, InstanceModel>> getEntryListForNameAndAlias(InstanceModel instanceModel) {
List<Entry<String, InstanceModel>> output = new ArrayList<>();
output.add(new SimpleEntry<>(instanceModel.getIdentity(), instanceModel));
for (String alias : instanceModel.getAliases()) {
if (!alias.equals(instanceModel.getIdentity())) {
output.add(new SimpleEntry<>(alias, instanceModel));
}
}
return output;
}
/**
* Verify that a single named instance model is correctly identifiable composed of owningDefinition/ElementLocation/Identity.
*
*
* @param definition the definition of
* @param errorListener accepts and displays all errors produced by analyzing the models.
* @return returns a map of each instance name to the single model that the has been resolved to.
*/
private Map<String, InstanceModel> ensureSingleInstanceOfEachName(DefinitionModel definition,
Consumer<ErrorModel> errorListener) {
Map<String, Map<String, InstanceModel>> instancesByNameAndLocationDedupped =
definitionToAllInstancesByNameAndSourceLocation(definition);
final Map<String, InstanceModel> resolvedDependencies = new HashMap<>();
boolean errored = false;
for (Entry<String, Map<String, InstanceModel>> entry : instancesByNameAndLocationDedupped.entrySet()) {
if (entry.getValue().size() == 1) {
resolvedDependencies.put(entry.getKey(), entry.getValue().values().iterator().next()); //get only InstanceModel.
} else {
errored = true;
errorListener.accept(errorForDuplicateInstanceModels(definition, entry.getValue().values().stream()
.sorted((i1, i2) -> i1.getElementLocation().compareTo(i2.getElementLocation()))
.collect(Collectors.toList())));
}
}
return errored ? null : resolvedDependencies;
}
/**
* Give a definition model, extract all instances from this definition, and all imported definitions.
* Group the instances by name, and then map them from source location to InstanceModel.
*
* @param definition model to extract all instance information from, including imported definitions.
* @return a map of maps, [name -> [sourceLocation -> instanceModel]]
*/
private Map<String, Map<String, InstanceModel>> definitionToAllInstancesByNameAndSourceLocation(
DefinitionModel definition) {
//create a stream of all imported Definition's InstanceModels
Stream<InstanceModel> imported = definition.getDependencies().stream()
.map(d -> d.getProvidedInstances()).flatMap(x -> x.stream());
//merge the stream with all local instance models.
Stream<InstanceModel> instanceModelStream = Stream.concat(definition.getObjectDefinitions().stream(), imported);
//map all instances by name and then by source location (due to diamond dependencies in definition imports.
Map<String, Map<String, InstanceModel>> instancesByNameAndLocationDedupped = instanceModelStream
.flatMap(instance -> getEntryListForNameAndAlias(instance).stream()) //flat map all alias to entries
.collect(Collectors.groupingBy(entry -> entry.getKey(), //group by name
Collectors.mapping(entry -> entry.getValue(),
Collectors.toMap(im -> im.getElementLocation(), //by location
im -> im, //identity function for first insert
//choose the one with source element on merge, if any
(im1, im2) -> im1.getSourceElement().isPresent() ? im1 : im2))));
return instancesByNameAndLocationDedupped;
}
/**
* Given a list of duplicate instance models (same identifier, different source elements) produce a
* well formed ErrorModel.
*
* @param definition where the error was first detected.
* @param causes the list of all instances models from the definition or any of it's imported definitions.
* @return an well formed ErrorModel of the duplicated beans.
*/
private ErrorModel errorForDuplicateInstanceModels(DefinitionModel definition, List<InstanceModel> causes) {
ArrayList<AbstractModel> involved = new ArrayList<>();
involved.add(definition);
involved.addAll(causes.stream()
.filter(instanceModel -> instanceModel.getSourceElement().isPresent())
.collect(Collectors.toList()));
return new ErrorModel(ErrorType.DUPLICATE_OBJECT_DEFINITIONS, causes, involved);
}
/**
* Inspects the instance graph for cycles, any cycle is printed as an error. The nameToEntity parameter doesn't list expected
* instances, any instances that are not found in the nameToInstances map (they are looked for because they are referenced as a
* dependency by an instance in the map) and are not found by name in the definition's expectedInstances are treated as errors
* as well.
*
* @param definition definition being processed. Will uses it's expected list, any instances references as dependencies but
* not found, not listed as expected in this DefinitionModel, will be treated as errors.
* @param nameToEntity name to unique instanceModels, verified before call.
* @param errorListner accepts and displays all errors produced by analyzing the models
* @return true if an error occurred, false otherwise
*/
private boolean detectCyclesInEntityGraph(final DefinitionModel definition, final Map<String, InstanceModel> nameToEntity,
final Consumer<ErrorModel> errorListener) {
final Map<String, ExpectedModel> missing = new HashMap<>();
final Graph<BaseInstanceModel, DefaultEdge> entityGraph = new DefaultDirectedGraph<>(DefaultEdge.class);
for (BaseInstanceModel entity : nameToEntity.values()) {
if (!entityGraph.containsVertex(entity)) {
entityGraph.addVertex(entity);
}
if (InstanceModel.class.isAssignableFrom(entity.getClass())) {
InstanceModel instanceModel = (InstanceModel) entity;
for (InstanceDependencyModel instanceDependency : instanceModel.getDependencies()) {
BaseInstanceModel dependency = nameToEntity.get(instanceDependency.getIdentity());
if (dependency == null) {
dependency = missing.computeIfAbsent(instanceDependency.getIdentity(), s -> new ExpectedModel(s));
missing.get(instanceDependency.getIdentity())
.addDefinitionReferenceToType(instanceModel.getIdentity(), instanceDependency.getType());
}
if (!entityGraph.containsVertex(dependency)) {
entityGraph.addVertex(dependency);
}
entityGraph.addEdge(entity, dependency);
}
}
}
boolean errored = errorsForCycles(errorListener, entityGraph);
errored = testAllMissingEntitiesAreExpected(definition, errorListener, missing, entityGraph) || errored;
errored = errorUnusedExpectedsOnDefinition(definition, errorListener, missing) || errored;
return errored;
}
private boolean checkInstancesTypesInDefinition(final DefinitionModel definition, final Map<String, InstanceModel> nameToEntity,
final Consumer<ErrorModel> errorListner, AssignabilityUtils assignabilityUtils) {
boolean errored = false;
List<InstanceModel> instances = definition.getObjectDefinitions();
for (InstanceModel instance : instances) {
for (InstanceDependencyModel instanceDependency : instance.getDependencies()) {
InstanceModel dependency = nameToEntity.get(instanceDependency.getIdentity());
if (dependency != null && !assignabilityUtils.isAssignableFrom(dependency, instance)) {
errored = true;
errorListner.accept(new ErrorModel(ErrorType.UNMATCHED_TYPES,
Arrays.asList(instance, dependency), Arrays.asList(instance, dependency)));
}
}
}
return errored;
}
private boolean errorUnusedExpectedsOnDefinition(final DefinitionModel definition, final Consumer<ErrorModel> errorListner,
final Map<String, ExpectedModel> missing) {
boolean errored = false;
for (ExpectedModel expected : definition.getExpectedDefinitions()) {
if (!missing.keySet().contains(expected.getIdentity())) {
errorListner.accept(new ErrorModel(ErrorType.UNUSED_EXPECTED, Arrays.asList(expected), Arrays.asList(definition)));
errored = true;
}
}
return errored;
}
private boolean errorsForCycles(final Consumer<ErrorModel> errorListner,
final Graph<BaseInstanceModel, DefaultEdge> entityGraph) {
SzwarcfiterLauerSimpleCycles<BaseInstanceModel, DefaultEdge> cycleFind = new SzwarcfiterLauerSimpleCycles<>();
boolean errored = false;
cycleFind.setGraph(entityGraph);
List<List<BaseInstanceModel>> cycles = cycleFind.findSimpleCycles();
for (List<BaseInstanceModel> cycle : cycles) {
errored = true;
errorListner.accept(new ErrorModel(ErrorType.CYCLE_IN_DEFINITION_SOURCES, cycle, cycle));
}
return errored;
}
private boolean testAllMissingEntitiesAreExpected(final DefinitionModel definition, final Consumer<ErrorModel> errorListner,
final Map<String, ExpectedModel> missing, final Graph<BaseInstanceModel, DefaultEdge> entityGraph) {
//check computed expected are actually expected
boolean errored = false;
List<String> expectedMissing = definition.getExpectedDefinitions().stream().map(em -> em.getIdentity())
.collect(Collectors.toList());
for (ExpectedModel expected : missing.values()) {
if (!expectedMissing.contains(expected.getIdentity())) {
List<AbstractModel> dependsOnMissing = Stream.concat(
Stream.of(definition),
entityGraph.incomingEdgesOf(expected).stream().map(edge -> entityGraph.getEdgeSource(edge))
.filter(m -> m.getSourceElement().isPresent()))
.collect(Collectors.toList());
errored = true;
errorListner.accept(new ErrorModel(ErrorType.MISSING_BEAN_DEFINITIONS, Arrays.asList(expected), dependsOnMissing));
} else {
definition.addComputedExpected(expected);
}
}
return errored;
}
}