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;
- }
- }