/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * 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 uk.q3c.krail.core.services;
import com.google.common.collect.ImmutableList;
import edu.uci.ics.jung.graph.DelegateForest;
import edu.uci.ics.jung.graph.DirectedOrderedSparseMultigraph;
import edu.uci.ics.jung.graph.Forest;
import org.slf4j.Logger;
import uk.q3c.util.CycleDetectedException;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;
import java.util.*;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Default implementation of {@link ServicesGraph}
* <p>
* Created by David Sowerby on 03 Dec 2015
*/
@ThreadSafe
public abstract class AbstractServicesGraph<T> implements ServicesGraph<T> {
private static final EnumMap<Selection, EnumSet<Dependency.Type>> selectionCriteria;
private static Logger log = getLogger(AbstractServicesGraph.class);
static {
selectionCriteria = new EnumMap<>(Selection.class);
selectionCriteria.put(Selection.ALL, EnumSet.allOf(Dependency.Type.class));
selectionCriteria.put(Selection.ALWAYS_REQUIRED, EnumSet.of(Dependency.Type.ALWAYS_REQUIRED));
selectionCriteria.put(Selection.REQUIRED_AT_START, EnumSet.of(Dependency.Type.ALWAYS_REQUIRED, Dependency.Type.REQUIRED_ONLY_AT_START));
selectionCriteria.put(Selection.ONLY_REQUIRED_AT_START, EnumSet.of(Dependency.Type.REQUIRED_ONLY_AT_START));
selectionCriteria.put(Selection.OPTIONAL, EnumSet.of(Dependency.Type.OPTIONAL));
}
private Forest<T, ServiceEdge> graph = new DelegateForest<>(new DirectedOrderedSparseMultigraph<>());
/**
* Finds all the vertices at the opposite end of the edges from {@code referenceVertex}, filtered to include only those identified in {@code selection}
*
* @param referenceVertex the vertex at the centre of the 'query'
* @param wantDependencies if true, select dependency edges, otherwise select dependant edges
* @param selection the Dependency.Type(s) to include
* @return list of vertices at the opposite end of the edges from {@code referenceVertex}, or an empty list if there are no edges
*/
private List<T> findRelations(T referenceVertex, boolean wantDependencies, Selection selection) {
Collection<ServiceEdge> edges = wantDependencies ? getDependenciesEdges(referenceVertex) : getDependantsEdges(referenceVertex);
EnumSet<Dependency.Type> selectionSet = selectionCriteria.get(selection);
return edges.stream()
.filter(edge -> selectionSet.contains(edge.getType()))
.map(edge -> graph.getOpposite(referenceVertex, edge))
.collect(Collectors.toList());
}
@Override
public synchronized List<T> findDependencies(@Nonnull T dependant, @Nonnull Selection selection) {
checkNotNull(dependant);
checkNotNull(selection);
return findRelations(
dependant, true, selection);
}
@Override
public synchronized List<T> findDependants(@Nonnull T dependency, @Nonnull Selection selection) {
checkNotNull(dependency);
checkNotNull(selection);
return findRelations(dependency, false, selection);
}
private Collection<ServiceEdge> getDependenciesEdges(@Nonnull T dependant) {
Collection<ServiceEdge> edges = graph.getOutEdges(dependant);
return (edges == null) ? new ArrayList<ServiceEdge>() : edges;
}
private Collection<ServiceEdge> getDependantsEdges(@Nonnull T dependency) {
Collection<ServiceEdge> edges = graph.getInEdges(dependency);
return (edges == null) ? new ArrayList<ServiceEdge>() : edges;
}
@Override
public synchronized boolean addService(@Nonnull T service) {
checkNotNull(service);
log.debug("adding service");
if (graph.containsVertex(service)) {
return false;
}
return graph.addVertex(service);
}
@Override
public synchronized boolean removeService(@Nonnull T service) {
checkNotNull(service);
log.debug("removing service");
return graph.removeVertex(service);
}
@Override
public synchronized boolean contains(@Nonnull T service) {
checkNotNull(service);
return graph
.getVertices()
.contains(service);
}
@Override
public synchronized Optional<ServiceEdge> getEdge(@Nonnull T dependant, @Nonnull T dependency) {
checkNotNull(dependant);
checkNotNull(dependency);
ServiceEdge result = graph.findEdge(dependant, dependency);
return result == null ? Optional.empty() : Optional.of(result);
}
@Override
public synchronized void createDependency(@Nonnull T dependant, @Nonnull T dependency, @Nonnull Dependency.Type type) {
checkNotNull(dependant);
checkNotNull(dependency);
checkNotNull(type);
if (hasDependency(dependant, dependency)) {
throw new DuplicateDependencyException("There can only be one dependency between the same two services");
}
if (!graph.containsVertex(dependant)) {
graph.addVertex(dependant);
}
if (!graph.containsVertex(dependency)) {
graph.addVertex(dependency);
}
ServiceEdge edge = new ServiceEdge(type);
graph.addEdge(edge, dependant, dependency);
if (detectCycle(dependant, dependency)) {
throw new CycleDetectedException("Creating dependency from " + dependant + " to " + dependency + " has caused a loop");
}
}
@Override
public synchronized ImmutableList<T> getServices() {
return ImmutableList.copyOf(graph.getVertices());
}
@Override
public synchronized boolean hasDependency(@Nonnull T dependant, @Nonnull T dependency) {
checkNotNull(dependant);
checkNotNull(dependency);
return findDependencies(dependant, Selection.ALL).contains(dependency);
}
@Override
public synchronized boolean hasDependant(@Nonnull T dependency, @Nonnull T dependant) {
checkNotNull(dependant);
checkNotNull(dependency);
return findDependants(dependency, Selection.ALL).contains(dependant);
}
/**
* Checks the proposed connection between parent and child nodes, and returns true if a cycle would be created by
* adding the child to the parent, or false if not
*
* @param parentNode dependant
* @param childNode dependency
* @return true if a cycle detected
*/
protected boolean detectCycle(T parentNode, T childNode) {
if (parentNode.equals(childNode)) {
return true;
}
Stack<T> stack = new Stack<>();
stack.push(parentNode);
while (!stack.isEmpty()) {
T node = stack.pop();
Collection<T> predecessors = graph.getPredecessors(node);
if (predecessors != null) {
for (T pred : predecessors) {
if (pred == childNode) {
return true;
}
}
stack.addAll(predecessors);
}
}
return false;
}
@Override
public synchronized int size() {
return graph.getVertexCount();
}
@Override
public synchronized boolean isOptionalDependency(@Nonnull T dependency, @Nonnull T dependant) {
checkNotNull(dependant);
checkNotNull(dependency);
List<T> optionalDependencies = findRelations(dependant, true, Selection.OPTIONAL);
return optionalDependencies.contains(dependency);
}
}