blob: e5a7e73e3e7d30505607a0e9488d0e29bc72a78a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.solr.common.util;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static java.util.Collections.emptyList;
/**
* A utility class to efficiently parse/store/lookup hierarchical paths which are templatized
* like /collections/{collection}/shards/{shard}/{replica}
*/
public class PathTrie<T> {
private final Set<String> reserved = new HashSet<>();
Node root = new Node(emptyList(), null, null);
public PathTrie() {
}
public PathTrie(Set<String> reserved) {
this.reserved.addAll(reserved);
}
public void insert(String path, Map<String, String> replacements, T o) {
List<String> parts = getPathSegments(path);
insert(parts, replacements, o);
}
public void insert(List<String> parts, Map<String, String> replacements, T o) {
if (parts.isEmpty()) {
root.obj = o;
return;
}
replaceTemplates(parts, replacements);
root.insert(parts, o);
}
public static void replaceTemplates(List<String> parts, Map<String, String> replacements) {
for (int i = 0; i < parts.size(); i++) {
String part = parts.get(i);
if (part.charAt(0) == '$') {
String replacement = replacements.get(part.substring(1));
if (replacement == null) {
throw new RuntimeException(part + " is not provided");
}
replacement = replacement.charAt(0) == '/' ? replacement.substring(1) : replacement;
parts.set(i, replacement);
}
}
}
// /a/b/c will be returned as ["a","b","c"]
public static List<String> getPathSegments(String path) {
if (path == null || path.isEmpty()) return emptyList();
List<String> parts = new ArrayList<String>() {
@Override
public boolean add(String s) {
if (s == null || s.isEmpty()) return false;
return super.add(s);
}
};
StrUtils.splitSmart(path, '/', parts);
return parts;
}
public T remove(List<String> path) {
Node node = root.lookupNode(path, 0, null, null);
T result = null;
if (node != null) {
result = node.obj;
node.obj = null;
if (node.children == null || node.children.isEmpty()) {
if (node.parent != null) {
node.parent.children.remove(node.name);
}
}
return result;
}
return result;
}
public T lookup(String path, Map<String, String> templateValues) {
return root.lookup(getPathSegments(path), 0, templateValues);
}
public T lookup(List<String> path, Map<String, String> templateValues) {
return root.lookup(path, 0, templateValues);
}
public T lookup(String path, Map<String, String> templateValues, Set<String> paths) {
return root.lookup(getPathSegments(path), 0, templateValues, paths);
}
public static String templateName(String templateStr) {
return templateStr.startsWith("{") && templateStr.endsWith("}") ?
templateStr.substring(1, templateStr.length() - 1) :
null;
}
class Node {
String name;
Map<String, Node> children;
T obj;
String templateName;
final Node parent;
Node(List<String> path, T o, Node parent) {
this.parent = parent;
if (path.isEmpty()) {
obj = o;
return;
}
String part = path.get(0);
templateName = templateName(part);
name = part;
if (path.isEmpty()) obj = o;
}
private synchronized void insert(List<String> path, T o) {
String part = path.get(0);
Node matchedChild = null;
if ("*".equals(name)) {
return;
}
if (children == null) children = new ConcurrentHashMap<>();
String varName = templateName(part);
String key = varName == null ? part : "";
matchedChild = children.get(key);
if (matchedChild == null) {
children.put(key, matchedChild = new Node(path, o, this));
}
if (varName != null) {
if (!matchedChild.templateName.equals(varName)) {
throw new RuntimeException("wildcard name must be " + matchedChild.templateName);
}
}
path.remove(0);
if (!path.isEmpty()) {
matchedChild.insert(path, o);
} else {
matchedChild.obj = o;
}
}
void findAvailableChildren(String path, Set<String> availableSubPaths) {
if (availableSubPaths == null) return;
if (children != null) {
for (Node node : children.values()) {
if (node.obj != null) {
String s = path + "/" + node.name;
availableSubPaths.add(s);
}
}
for (Node node : children.values()) {
node.findAvailableChildren(path + "/" + node.name, availableSubPaths);
}
}
}
public T lookup(List<String> pieces, int i, Map<String, String> templateValues) {
return lookup(pieces, i, templateValues, null);
}
/**
* @param pathSegments pieces in the url /a/b/c has pieces as 'a' , 'b' , 'c'
* @param index current index of the pieces that we are looking at in /a/b/c 0='a' and 1='b'
* @param templateVariables The mapping of template variable to its value
* @param availableSubPaths If not null , available sub paths will be returned in this set
*/
public T lookup(List<String> pathSegments, int index, Map<String, String> templateVariables, Set<String> availableSubPaths) {
Node node = lookupNode(pathSegments, index, templateVariables, availableSubPaths);
return node == null ? null : node.obj;
}
Node lookupNode(List<String> pathSegments, int index, Map<String, String> templateVariables, Set<String> availableSubPaths) {
if (templateName != null && templateVariables != null)
templateVariables.put(templateName, pathSegments.get(index - 1));
if (pathSegments.size() < index + 1) {
findAvailableChildren("", availableSubPaths);
if (obj == null) {//this is not a leaf node
Node n = children.get("*");
if (n != null) {
return n;
}
}
return this;
}
String piece = pathSegments.get(index);
if (children == null) {
return null;
}
Node n = children.get(piece);
if (n == null && !reserved.contains(piece)) n = children.get("");
if (n == null) {
n = children.get("*");
if (n != null) {
StringBuffer sb = new StringBuffer();
for (int i = index; i < pathSegments.size(); i++) {
sb.append("/").append(pathSegments.get(i));
}
if (templateVariables != null) templateVariables.put("*", sb.toString());
return n;
}
}
if (n == null) {
return null;
}
return n.lookupNode(pathSegments, index + 1, templateVariables, availableSubPaths);
}
}
}