com.dell.doradus.service.rest.RESTRegistry Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2014 Dell, Inc.
*
* 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 com.dell.doradus.service.rest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import com.dell.doradus.common.HttpMethod;
import com.dell.doradus.common.Utils;
import com.dell.doradus.common.rest.RESTCommand;
import com.dell.doradus.common.rest.RESTCatalog;
import com.dell.doradus.service.StorageService;
import com.dell.doradus.service.rest.annotation.Description;
/**
* Holds a collection of REST Commands registered by Doradus components and organizes them
* in the correct order for evaluation. This class extends {@link RESTCatalog}, adding an
* "evaluation map" and an "owner hierarchy". The evaluation map groups commands by owner
* and method and sorts them by evaluation order. For example, suppose the following
* commands are registered to the same owner:
*
* GET /{application}/{shard}
* GET /{application}/_shards
*
* The second command must be matched before the first command so that "_shards" isn't
* evaluated as a {shard} name.
*
* The owner hierarchy allows an owner to "extend" the commands of another owner. Commands
* are first registered by calling {@link #registerCallbacks(String, Iterable)}. An owner can
* register its commands as extending those of another owner by calling
* {@link #setParent(String, String)}. A REST request is matched to a REST command by the
* following process:
*
* - {@link #findCommand()} is called passing the starting owner name plus other
* parameters about the REST request.
*
- If a matching REST command is found belonging to the given owner, it is returned.
*
- If no match is found but the specified owner has a parent, the search continues in
* the parent's command set.
*
- If the hierarchy is searched and no match is found, {@link #findCommand()} returns
* null, indicating that no matching REST command was found.
*
*/
public class RESTRegistry extends RESTCatalog {
// Commands by owner and HttpMethod, sorted by evaluation order:
private final Map>> m_cmdEvalMap = new HashMap<>();
// Map of command owners to parent command owners.
private final Map m_parentMap = new HashMap();
// When this is true, new commands are not allowed. This allows us to search the
// command set without a lock after it's built.
private boolean m_bCmdSetIsFrozen;
/**
* Create a new RESTRegistry set with no commands.
*/
public RESTRegistry() { }
/**
* Add a set of REST commands by introspecting a set of callback classes. The given
* callback classes must use the {@link Description} annotation to provide command
* metadata, which is used to create a RegisteredCommand object, which is added to the
* command registry. If the given storage service is null, the commands are considered
* global. Otherwise, they belong to the given storage service and used in the context
* of applications that it manages.
*
* @param storageService {@link StorageService} that owns the commands. Null if the
* commands are global.
* @param callbackClasses Iterable {@link RESTCallback} classes that define the
* commands to be added.
*/
public void registerCallbacks(StorageService storageService,
Iterable> callbackClasses) {
if (m_bCmdSetIsFrozen) {
throw new RuntimeException("New commands cannot be added: command set is frozen");
}
String cmdOwner = storageService == null ? SYSTEM_OWNER : storageService.getClass().getSimpleName();
synchronized (m_cmdEvalMap) {
for (Class extends RESTCallback> cmdClass : callbackClasses) {
RegisteredCommand cmd = new RegisteredCommand(cmdClass);
registerCommand(cmdOwner, cmd);
}
}
}
/**
* Describe the current set of visible, registered commands in a {@link RESTCatalog}
* object.
*
* @return A copy of visible, registered commands as a {@link RESTCatalog}.
*/
public RESTCatalog describeCommands() {
Map> ownerMap = new HashMap<>(m_cmdsByOwnerMap);
for (String cmdOwner : ownerMap.keySet()) {
SortedMap cmdMap = ownerMap.get(cmdOwner);
Iterator iter = cmdMap.values().iterator();
while (iter.hasNext()) {
if (!iter.next().isVisible()) {
iter.remove();
}
}
}
return new RESTCatalog(ownerMap);
}
/**
* Set the parent of the given command owner to the given parent owner. This enables
* command owners to be arranged hierarchically so that one owner can inherit and
* optionally override its parent's commands. {@link #findCommand()} searches the
* hierarchy automatically.
*
* @param cmdOwnerName Name of a command owner.
* @param parentCmdOwnerName Name of parent command owner.
*/
public void setParent(String cmdOwnerName, String parentCmdOwnerName) {
m_parentMap.put(cmdOwnerName, parentCmdOwnerName);
}
/**
* Freeze or unfreeze the command set. This should be called after the command set has
* been frozen so that findMatch() does not need to acquire a lock, thus single-
* threading command searching. If this is called with "true", subsequent attempts to
* add a new command to the set will throw an exception.
*
* @param bFreeze True to freeze the command set.
*/
public void freezeCommandSet(boolean bFreeze) {
m_bCmdSetIsFrozen = bFreeze;
} // freezeCommandSet
/**
* Attempt to find a command that matches the properties of a REST request. If a match
* is found, the corresponding {@link RegisteredCommand} is returned and the given
* variable map is populated with extracted parameters. For example, if the command's
* URI template is:
*
* /{application}/_query?{params}
*
* And the actual URI and query components provided are:
*
* /MyApp/_query?q=Smith&s=100
*
* The variable map will be populated with to reflect the following variable settings:
*
* application = MyApp
* params = q=Smith&s=100
*
* If any parameter values are URI-encoded, they remain encoded when returned in the
* updated variable map.
*
* @param owner The command owner that defines the command set to search. If
* null or empty, only system commands are searched. Otherwise,
* the given owner's commands are searched followed by its parent
* commands, if any.
* @param method The {@link HttpMethod} of the REST request. To match, a
* command must be registerd with this as one of its methods.
* @param uri The URI provided in the REST request. The URI must remain
* encoded so that path nodes can be properly recognized.
* @param query The query parameter of the REST request, if any. May be null
* or empty if the request has no query component.
* @param variableMap Updated with variables extracted from URI and query parameters.
* Not touched if the command does not match.
* @return The {@link RegisteredCommand} that matched the given request
* or null if a match was not found.
*/
public RegisteredCommand findCommand(String owner, HttpMethod method, String uri, String query,
Map variableMap) {
String cmdOwner = Utils.isEmpty(owner) ? SYSTEM_OWNER : owner;
RegisteredCommand cmd = null;
while (cmd == null && !Utils.isEmpty(cmdOwner)) {
cmd = searchCommands(cmdOwner, method, uri, query, variableMap);
cmdOwner = m_parentMap.get(cmdOwner);
}
return cmd;
}
/**
* Get all REST commands as a list of strings for debugging purposes.
*
* @return List of commands for debugging.
*/
public Collection getCommands() {
List commands = new ArrayList();
for (String cmdOwner : m_cmdsByOwnerMap.keySet()) {
SortedMap ownerCmdMap = m_cmdsByOwnerMap.get(cmdOwner);
for (String name : ownerCmdMap.keySet()) {
RESTCommand cmd = ownerCmdMap.get(name);
commands.add(String.format("%s: %s = %s", cmdOwner, name, cmd.toString()));
}
}
return commands;
} // getCommands
//----- Private methods
// Register the given command and throw if is a duplicate name or command.
private void registerCommand(String cmdOwner, RegisteredCommand cmd) {
// Add to owner map in RESTCatalog.
Map nameMap = getCmdNameMap(cmdOwner);
String cmdName = cmd.getName();
RESTCommand oldCmd = nameMap.put(cmdName, cmd);
if (oldCmd != null) {
throw new RuntimeException("Duplicate REST command with same owner/name: " +
"owner=" + cmdOwner + ", name=" + cmdName +
", [1]=" + cmd + ", [2]=" + oldCmd);
}
// Add to local evaluation map.
Map> evalMap = getCmdEvalMap(cmdOwner);
for (HttpMethod method : cmd.getMethods()) {
SortedSet methodSet = evalMap.get(method);
if (methodSet == null) {
methodSet = new TreeSet<>();
evalMap.put(method, methodSet);
}
if (!methodSet.add(cmd)) {
throw new RuntimeException("Duplicate REST command: owner=" + cmdOwner +
", name=" + cmdName + ", commmand=" + cmd);
}
}
}
// Get the method -> command map for the given owner.
private Map getCmdNameMap(String cmdOwner) {
SortedMap cmdNameMap = m_cmdsByOwnerMap.get(cmdOwner);
if (cmdNameMap == null) {
cmdNameMap = new TreeMap<>();
m_cmdsByOwnerMap.put(cmdOwner, cmdNameMap);
}
return cmdNameMap;
}
// Get the method -> sorted command map for the given owner.
private Map> getCmdEvalMap(String cmdOwner) {
Map> evalMap = m_cmdEvalMap.get(cmdOwner);
if (evalMap == null) {
evalMap = new HashMap<>();
m_cmdEvalMap.put(cmdOwner, evalMap);
}
return evalMap;
}
// Search the given command owner for a matching command.
private RegisteredCommand searchCommands(String cmdOwner, HttpMethod method, String uri,
String query, Map variableMap) {
Map> evalMap = getCmdEvalMap(cmdOwner);
if (evalMap == null) {
return null;
}
// Find the sorted command set for the given HTTP method.
SortedSet cmdSet = evalMap.get(method);
if (cmdSet == null) {
return null;
}
// Split uri into a list of non-empty nodes.
List pathNodeList = new ArrayList<>();
String[] pathNodes = uri.split("/");
for (String pathNode : pathNodes) {
if (pathNode.length() > 0) {
pathNodeList.add(pathNode);
}
}
// Attempt to match commands in this set in order.
for (RegisteredCommand cmd : cmdSet) {
if (cmd.matches(pathNodeList, query, variableMap)) {
return cmd;
}
}
return null;
} // searchOwnerCommands
} // class RESTCommandSet
© 2015 - 2025 Weber Informatics LLC | Privacy Policy