All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dell.doradus.service.rest.RESTCommand Maven / Gradle / Ivy

/*
 * 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.HashMap;
import java.util.List;
import java.util.Map;

import com.dell.doradus.common.Utils;

/**
 * This class manages the mapping from a REST request to the handler callback. The request
 * consists of an HTTP method and a URI. The URI is a pattern that defines fixed and
 * variable parts. For example, consider the following command:
 * 
 *      GET /{application}/{table}/_query?{query}
 * 
* This command matches HTTP GET requests that have 3 nodes in the URI path and a query * (?) parameter. The first node can be anything and the actual value passed will be stored * in the variable "application". The second node can also be anything and its value will * be stored in the variable "table". The third node must be the literal "_query" * (case-sensitive). The value of query component will be stored in the variable "query". *

* For example, if this command matches the following request: *

 *      GET /Magellan/documents/_query?q=foo+f=body
 * 
* The command will match, and it will return the variables: *
      
 *      application = "Magellan"
 *      table = "documents"
 *      query = "q=foo+f=body"
 * 
* Note that path nodes and the query component, if any, are not decoded in case they contain * any escaped characters. *

* RESTCommand implements the {@link Comparable} interface, using a {@link #compareTo(RESTCommand)} * method that sorts commands in the proper evaluation sequence. */ public final class RESTCommand implements Comparable { // Components of a REST command: private String m_method; // Value for private List m_pathNodes; // Value for path nodes private String m_query; // Value for private Class m_callbackClass; // Callback class for this command private boolean m_bSystemCmd; // true for non-tenant commands. /** * Creates a RESTCommand from a "REST rule", which includes the HTTP method, URI, and * callback method. The REST rule must have 3 space-separated components in the * following format: *

     *      method URI callback
     * 
* where method is an HTTP method such as GET or PUT; URI is a path/query pattern, * optionally with variable components; and callback is the full package path of the * class that handles requests. For example, a typically rule string is: *
     *      GET /{application}/{table}/_query?{query} com.dell.doradus.service.foo.QueryCmd
     * 
* The command class must be derived from {@link RESTCallback}, and it must have a * zero-argument constructor. Using that constructor, an instance of the given command * class is created and used to process requests to the specified REST command. *

* This constructor creates a non-system command, which means it is executed in the * context of a specific tenant. * * @param ruleString REST request and command callback class in the form "method * URI callback". */ public RESTCommand(String ruleString) { m_bSystemCmd = false; parseRuleString(ruleString); } // constructor /** * Same as {@link #RESTCommand(String)} but optionally marks this command as a system * command, which means it is not executed in the context of a tenant. * * @param ruleString REST request and command callback class in the form * "method URI callback". * @param bSystemCommand If true, marks this object as a non-system command. */ public RESTCommand(String ruleString, boolean bSystemCommand) { m_bSystemCmd = bSystemCommand; parseRuleString(ruleString); } // constructor /** * Indicate if this is a system command, which means it executes without a specific * tenant context. * * @return True if this is a system command. */ public boolean isSystemCommand() { return m_bSystemCmd; } // isSystemCommand /** * Return true if this object matches the given URI components. If it does, any * variables defined by the URI are extracted from the URL and placed in the given * map without decoding. If no match is found, the given map is unmodified. * * @param method HTTP method representing an HTTP verb. * @param pathNodeList List of path nodes in order. For example, the path /A/B/C * should be passed as a size=3 list with the slashes removed. * @param query Query parameter from HTTP request, if any. For example, if * the query parameter is "?foo=bar", the string "foo=bar" should * be passed for this parameter. * @param variableMap Populated with *encoded* variable values, if any, if this * RESTCommand matches the given request line. * @return True if this command the given request. */ public boolean matchesURL(String method, List pathNodeList, String query, Map variableMap) { if (!m_method.equalsIgnoreCase(method) || pathNodeList.size() != m_pathNodes.size()) { return false; } Map matchedVarMap = new HashMap<>(); for (int index = 0; index < pathNodeList.size(); index++) { if (!matches(pathNodeList.get(index), m_pathNodes.get(index), matchedVarMap)) { return false; } } if (matches(query, m_query, matchedVarMap)) { variableMap.putAll(matchedVarMap); return true; } return false; } // matchesURL /** * Compare this RESTCommand to the given one. This method sorts commands by the proper * evaluation sequence. For example, consider the following two commands: *

     *      GET /{application}/_statstatus
     *      GET /_applications/{application}
     * 
     * The second command must appear before the first command because nodes with literal
     * values must appear before parameterized nodes. Similarly, a command with more nodes
     * but otherwise the same as another command must appear first.
     * 
     * @param  other Another {@link RESTCommand} to compaere to this one.
     * @return       A negative, zero, or positive value reflecting whether this object is
     *               less than, equal to, or greater than the given object.
     */
    @Override
    public int compareTo(RESTCommand other) {
        // Compare the node list for each object.
        for (int index = 0; index < Math.min(m_pathNodes.size(), other.m_pathNodes.size()); index++) {
            String node1 = m_pathNodes.get(index);
            String node2 = other.m_pathNodes.get(index);
            int diff = compareNodes(node1, node2);
            if (diff != 0) {
                return diff;    // this node decides it
            }
        }
        
        // Here, all common nodes are identical.
        if (m_pathNodes.size() < other.m_pathNodes.size()) {
            return 1;   // r2 has more nodes, so sort before r1
        }
        if (m_pathNodes.size() > other.m_pathNodes.size()) {
            return -1;  // r1 has more nodes, so sort before r2
        }
        
        // Path nodes are identical. It depends on the query parameter.
        int diff = compareNodes(this.getQuery(), other.getQuery());
        return diff;
    }   // compareTo

    /**
     * Returns a string representation of this RESTCommand in the form:
     * 
     *      {method} /{path}/[?{query}] -> {command class}
     * 
* * @return A string representation of this RESTCommand. */ @Override public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append(m_method); buffer.append(" /"); for (String node : m_pathNodes) { buffer.append(node); buffer.append("/"); } if (m_query.length() > 0) { buffer.append("?"); buffer.append(m_query); } buffer.append(" -> "); buffer.append(m_callbackClass.toString()); return buffer.toString(); } // toString /** * Return true if the given object is a RESTCommand with the same method, path nodes, * and query component as this one. * * @return True if the given object is considered the same as this one. */ @Override public boolean equals(Object other) { if (!(other instanceof RESTCommand)) { return false; } // As we compare, a variable {foo} is considered identical to a variable {bar} // if they occur in the same spot. RESTCommand otherCmd = (RESTCommand)other; if (!m_method.equalsIgnoreCase(otherCmd.m_method)) { return false; // Different method } if (m_pathNodes.size() != otherCmd.m_pathNodes.size()) { return false; // Different # of nodes } for (int index = 0; index < m_pathNodes.size(); index++) { if (!samePattern(m_pathNodes.get(index), otherCmd.m_pathNodes.get(index))) { return false; // This node is different } } return samePattern(m_query, otherCmd.m_query); } // equals /** * Provide a hash code that corresponds to the {@link #equals(Object)} method, * allowing this object to be used in hash maps, for example. * * @return A hashcode unique to this object. */ @Override public int hashCode() { int code = m_method.hashCode(); String query = m_query.length() > 0 && m_query.startsWith("{") ? "{" : m_query; code ^= query.hashCode(); for (String partNode : m_pathNodes) { if (partNode.length() > 0 && partNode.startsWith("{")) { partNode = "{"; } code ^= partNode.hashCode(); } return code; } // hashCode // Getters /** * Return this command's HTTP method (e.g., GET, PUT). It is always uppercase. * * @return This command's HTTP method. */ public String getMethod() { return m_method; } // getMethod /** * Return this command's URI path, which always begins with a '/' and consists of * path nodes separated by '/'. The query component is not included. * * @return This command's URI path. */ public List getPath() { return m_pathNodes; } // getApplication /** * Return this command's query component, if any. * * @return This command's query component. It is empty (but not null) if this command * has no query component. */ public String getQuery() { return m_query; } // getQuery /** * Create a new instance of this command's callback object, which is invoked to handle * requests to corresponding REST requests. * * @return A new instance of this command's callback object. */ public RESTCallback getNewCallback(RESTRequest request) { try { RESTCallback callback = m_callbackClass.newInstance(); callback.setRequest(request); return callback; } catch (Exception e) { throw new RuntimeException("Unable to invoke callback", e); } } // getCallback ///// Private methods // Parse the given rule string and map to member variables. @SuppressWarnings("unchecked") private void parseRuleString(String ruleString) { String[] parts = ruleString.split(" +"); Utils.require(parts.length == 3, "Invalid rule format: " + ruleString); // Method m_method = parts[0].toUpperCase(); // URI and query List pathNodeList = new ArrayList(); StringBuilder query = new StringBuilder(); StringBuilder fragment = new StringBuilder(); Utils.parseURI(parts[1], pathNodeList, query, fragment); if (pathNodeList.size() == 0) { throw new IllegalArgumentException("Invalid URI path: " + parts[1]); } m_pathNodes = new ArrayList<>(); for (String encodedNode : pathNodeList) { m_pathNodes.add(Utils.urlDecode(encodedNode)); } m_query = Utils.urlDecode(query.toString()); // Command class String cmdClassPath = parts[2]; try { m_callbackClass = (Class) Class.forName(cmdClassPath); } catch (Exception e) { throw new RuntimeException("Error instantiating callback object '" + cmdClassPath + "'", e); } } // parseRuleString // Extract the variable name from the given URI component value. For example, if the // value is {application}, the variable name "application" is returned. An error is // thrown if the trailing '}' is missing. private static String getVariableName(String value) { assert value.charAt(0) == '{'; assert value.charAt(value.length() - 1) == '}'; return value.substring(1, value.length() - 1); } // getVariableName // When comparing path nodes or query parts, two values are considered equal if either // (1) they are both empty, (2) they both start with '{' or, (3) neither start with // '{' and they have the same value (case-sensitive). private static boolean samePattern(String value1, String value2) { if (value1.length() == 0) { return value2.length() == 0; } if (value1.charAt(0) == '{') { return value2.length() > 0 && value2.charAt(0) == '{'; } return value1.equals(value2); } // samePattern // Similar to samePattern(), except that we are matching a candidate URI component // value to a component. If a match is made and the component is a variable, the // variable value is extracted and added to the given map. private static boolean matches(String value, String component, Map variableMap) { if (Utils.isEmpty(component)) { return Utils.isEmpty(value); } if (component.charAt(0) == '{') { // The component is a variable, so it always matches. if (!Utils.isEmpty(value)) { String varName = getVariableName(component); variableMap.put(varName, value); } return true; } return component.equals(value); } // matches // Compare the given nodes and return -1 if the first node should appear first, 1 if // the second should appear first, and 0 if they are identical. The nodes can be // path nodes are query parameters. Either node can be empty, but not null. private static int compareNodes(String node1, String node2) { assert node1 != null; assert node2 != null; if (node1.equals(node2)) { return 0; } if (node1.length() > 0 && node1.charAt(0) == '{') { if (node2.length() > 0 && node2.charAt(0) == '{') { return 0; // Both nodes are parameters; names are irrelevant } return 1; // r1 is a parameter but r2 is not, so r2 should come first } if (node2.length() > 0 && node2.charAt(0) == '{') { return -1; // r2 is a parameter but r1 is not, so r1 should come first } return node1.compareTo(node2); // neither node is a parameter } // compareNodes } // class RESTCommand




© 2015 - 2025 Weber Informatics LLC | Privacy Policy