package cloudone.cumulonimbus.model;
import cloudone.internal.ApplicationFullName;
import cloudone.cumulonimbus.util.PathUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Pattern;
/**
* Tree model of registered paths.
*
* @author Martin Mares (martin.mares at oracle.com)
*/
public class PathRegistry {
private static class Node {
public enum Type {
EXACT, ANY, REGEXP;
}
private final Type type;
private final String pathElement;
private final Pattern pattern;
private EnumMap<HttpMethod, Set<ApplicationFullName>> registry;
private Map<String, Node> subNodes;
/**
* Just to construct root node.
*/
public Node() {
type = Type.ANY;
pathElement = "";
pattern = null;
}
public Node(String pathElement) {
if (pathElement == null) {
pathElement = "";
}
this.pathElement = pathElement;
if ("(.*)".equals(pathElement)) {
type = Type.ANY;
pattern = null;
} else if (pathElement.indexOf('\\') > -1 || pathElement.indexOf('(') > -1) {
type = Type.REGEXP;
pattern = Pattern.compile(pathElement);
} else {
type = Type.EXACT;
pattern = null;
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Node)) return false;
Node node = (Node) o;
return pathElement.equals(node.pathElement);
}
@Override
public int hashCode() {
return pathElement.hashCode();
}
@Override
public String toString() {
return "Node{" + type + ", " + pathElement + '}';
}
public boolean matches(String element) {
switch (type) {
case EXACT:
return pathElement.equals(element);
case ANY:
return true;
case REGEXP:
return pattern.matcher(element).matches();
default:
return false;
}
}
public String getPathElement() {
return pathElement;
}
public void register(final HttpMethod method, final ApplicationFullName appName) {
if (registry == null) {
registry = new EnumMap<>(HttpMethod.class);
}
registry.computeIfAbsent(method, m -> new HashSet<>()).add(appName);
}
public Node addNode(final Node node) {
if (subNodes == null) {
subNodes = new HashMap<>();
subNodes.put(node.getPathElement(), node);
return node;
} else if (subNodes.containsKey(node.getPathElement())) {
return subNodes.get(node.getPathElement());
} else {
subNodes.put(node.getPathElement(), node);
return node;
}
}
public void registerToPath(final List<String> path,
final int index,
final HttpMethod method,
final ApplicationFullName appName) {
if (index < path.size()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("registerToPath(" + path + ", " + index + ", " + method + ", " + appName + ")");
}
Node node = new Node(path.get(index));
node = addNode(node);
node.registerToPath(path, index + 1, method, appName);
} else {
register(method, appName);
}
}
public void unregisterFromPath(final ApplicationFullName appName) {
//First unregister here
if (registry != null) {
Set<HttpMethod> toRemove = EnumSet.noneOf(HttpMethod.class);
for (Map.Entry<HttpMethod, Set<ApplicationFullName>> entry : registry.entrySet()) {
if (entry.getValue().remove(appName) && entry.getValue().isEmpty()) {
toRemove.add(entry.getKey());
}
}
for (HttpMethod method : toRemove) {
registry.remove(method);
}
if (registry.isEmpty()) {
registry = null;
}
}
//Now unrwgister subPath
if (subNodes != null) {
Set<String> toRemove = new HashSet<>(1);
for (Map.Entry<String, Node> entry : subNodes.entrySet()) {
Node subNode = entry.getValue();
subNode.unregisterFromPath(appName);
if (subNode.registry == null && subNode.subNodes == null) {
toRemove.add(entry.getKey());
}
}
for (String key : toRemove) {
subNodes.remove(key);
}
if (subNodes.isEmpty()) {
subNodes = null;
}
}
}
public void collectPossibilities(final List<String> path,
final int index,
final HttpMethod method,
final Set<ApplicationFullName> appNames) {
if (path.size() > index) {
String pathElement = path.get(index);
for (Node subNode : subNodes.values()) {
if (subNode.matches(pathElement)) {
subNode.collectPossibilities(path, index + 1, method, appNames);
}
}
} else if (registry != null) {
Set<ApplicationFullName> result = registry.get(method);
appNames.addAll(result);
}
}
}
private static final Logger LOGGER = LoggerFactory.getLogger(PathRegistry.class);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Node root = new Node();
public void register(final String path, final HttpMethod method, final ApplicationFullName appName) {
final List<String> parsedPath = PathUtil.parsePath(path, true);
lock.writeLock().lock();
try {
root.registerToPath(parsedPath, 0, method, appName);
} finally {
lock.writeLock().unlock();
}
}
public void unregister(final ApplicationFullName appName) {
lock.writeLock().lock();
try {
root.unregisterFromPath(appName);
} finally {
lock.writeLock().unlock();
}
}
public Set<ApplicationFullName> get(final String path, final HttpMethod method) {
final List<String> parsedPath = PathUtil.parsePath(path, true);
final Set<ApplicationFullName> result = new HashSet<>();
lock.readLock().lock();
try {
root.collectPossibilities(parsedPath, 0, method, result);
} finally {
lock.readLock().unlock();
}
return result;
}
}