Package com.linkedin.restli.internal.server

Source Code of com.linkedin.restli.internal.server.RestLiRouter

/*
   Copyright (c) 2012 LinkedIn Corp.

   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.linkedin.restli.internal.server;


import com.linkedin.data.DataList;
import com.linkedin.data.DataMap;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.r2.filter.R2Constants;
import com.linkedin.r2.message.RequestContext;
import com.linkedin.r2.message.rest.RestRequest;
import com.linkedin.restli.common.ComplexKeySpec;
import com.linkedin.restli.common.ComplexResourceKey;
import com.linkedin.restli.common.CompoundKey;
import com.linkedin.restli.common.HttpStatus;
import com.linkedin.restli.common.OperationNameGenerator;
import com.linkedin.restli.common.ProtocolVersion;
import com.linkedin.restli.common.ResourceMethod;
import com.linkedin.restli.common.RestConstants;
import com.linkedin.restli.internal.common.AllProtocolVersions;
import com.linkedin.restli.internal.common.PathSegment.PathSegmentSyntaxException;
import com.linkedin.restli.internal.server.model.ResourceMethodDescriptor;
import com.linkedin.restli.internal.server.model.ResourceModel;
import com.linkedin.restli.internal.server.util.ArgumentUtils;
import com.linkedin.restli.internal.server.util.RestLiSyntaxException;
import com.linkedin.restli.server.Key;
import com.linkedin.restli.server.ResourceLevel;
import com.linkedin.restli.server.RestLiServiceException;
import com.linkedin.restli.server.RoutingException;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* Navigates the resource hierarchy to find a Resource handler for the a given URI.
*
* @author Josh Walker
*/
public class RestLiRouter
{
  private static final Logger log = LoggerFactory.getLogger(RestLiRouter.class);
  private static final Map<ResourceMethodMatchKey, ResourceMethod> _resourceMethodLookup = setupResourceMethodLookup();
  private final Map<String, ResourceModel> _pathRootResourceMap;

  /**
   * Constructor.
   *
   * @param pathRootResourceMap a map of resource root paths to corresponding
   *          {@link ResourceModel}s
   */
  public RestLiRouter(final Map<String, ResourceModel> pathRootResourceMap)
  {
    super();
    _pathRootResourceMap = pathRootResourceMap;
  }

  private static final Pattern SLASH_PATTERN = Pattern.compile(Pattern.quote("/"));

  /**
   * Processes provided {@link RestRequest}.
   *
   * @param req {@link RestRequest}
   * @return {@link RoutingResult}
   */
  public RoutingResult process(final RestRequest req, final RequestContext requestContext)
  {
    String path = req.getURI().getRawPath();
    if (path.length() < 2)
    {
      throw new RoutingException(HttpStatus.S_404_NOT_FOUND.getCode());
    }

    if (path.charAt(0) == '/')
    {
      path = path.substring(1);
    }

    Queue<String> remainingPath =
        new LinkedList<String>(Arrays.asList(SLASH_PATTERN.split(path)));

    String rootPath = "/" + remainingPath.poll();

    ResourceModel currentResource;
    try
    {
      currentResource =
          _pathRootResourceMap.get(URLDecoder.decode(rootPath,
                                                     RestConstants.DEFAULT_CHARSET_NAME));
    }
    catch (UnsupportedEncodingException e)
    {
      throw new RestLiInternalException("UnsupportedEncodingException while trying to decode the root path",
                                        e);
    }
    if (currentResource == null)
    {
      throw new RoutingException(String.format("No root resource defined for path '%s'",
                                               rootPath),
                                 HttpStatus.S_404_NOT_FOUND.getCode());
    }
    ServerResourceContext context;

    try
    {
      context = new ResourceContextImpl(new PathKeysImpl(), req, requestContext);
    }
    catch (RestLiSyntaxException e)
    {
      throw new RoutingException(e.getMessage(), HttpStatus.S_400_BAD_REQUEST.getCode());
    }

    return processResourceTree(currentResource, context, remainingPath);
  }

  private RoutingResult processResourceTree(final ResourceModel resource,
                                            final ServerResourceContext context,
                                            final Queue<String> remainingPath)
  {
    ResourceModel currentResource = resource;

    // iterate through all path segments, simultaneously descending the resource hierarchy
    // and parsing path keys where applicable;
    // the goal of this loop is to locate the leaf resource, which will be set in
    // currentResource, and to parse the necessary information into the context
    ResourceLevel currentLevel = currentResource.getResourceLevel();

    while (remainingPath.peek() != null)
    {
      String currentPathSegment = remainingPath.poll();

      if (currentLevel.equals(ResourceLevel.ENTITY))
      {
        currentResource =
            currentResource.getSubResource(parseSubresourceName(currentPathSegment));
        currentLevel = currentResource == null ?
            ResourceLevel.ANY : currentResource.getResourceLevel();
      }
      else
      {
        ResourceModel currentCollectionResource = currentResource;
        if (currentResource.getKeys().isEmpty())
        {
          throw new RoutingException(String.format("Path key not supported on resource '%s' for URI '%s'",
                                                   currentResource.getName(),
                                                   context.getRequestURI()),
                                     HttpStatus.S_400_BAD_REQUEST.getCode());
        }
        else if (currentResource.getKeyClass() == ComplexResourceKey.class)
        {
          parseComplexKey(currentResource, context, currentPathSegment);
          currentLevel = ResourceLevel.ENTITY;
        }
        else if (currentResource.getKeyClass() == CompoundKey.class)
        {
          CompoundKey compoundKey;
          try
          {
            compoundKey = parseCompoundKey(currentCollectionResource, context, currentPathSegment);
          }
          catch (IllegalArgumentException e)
          {
            throw new RoutingException(String.format("Malformed Compound Key: '%s'", currentPathSegment),
                                       HttpStatus.S_400_BAD_REQUEST.getCode(),
                                       e);
          }

          if (compoundKey != null
              && compoundKey.getPartKeys().containsAll(currentResource.getKeyNames()))
          {
            // full match on key parts means that we are targeting a unique entity
            currentLevel = ResourceLevel.ENTITY;
          }
        }
        else // Must be a simple key then
        {
          parseSimpleKey(currentResource, context, currentPathSegment);
          currentLevel = ResourceLevel.ENTITY;
        }
      }

      if (currentResource == null)
      {
        throw new RoutingException(HttpStatus.S_404_NOT_FOUND.getCode());
      }
    }

    parseBatchKeysParameter(currentResource, context); //now we know the key type, look for batch parameter

    return findMethodDescriptor(currentResource, currentLevel, context);
  }

  /** given path segment, parses subresource name out of it */
  private static String parseSubresourceName(final String pathSegment)
  {
    try
    {
      return URLDecoder.decode(pathSegment, RestConstants.DEFAULT_CHARSET_NAME);
    }
    catch (UnsupportedEncodingException e)
    {
      throw new RestLiInternalException("UnsupportedEncodingException while trying to decode the subresource name", e);
    }
  }

  private RoutingResult findMethodDescriptor(final ResourceModel resource,
                                             final ResourceLevel resourceLevel,
                                             final ServerResourceContext context)
  {
    ResourceMethod type = mapResourceMethod(context, resourceLevel);

    String methodName = context.getRequestActionName();
    if (methodName == null)
    {
      methodName = context.getRequestFinderName();
    }

    ResourceMethodDescriptor methodDescriptor = resource.matchMethod(type, methodName, resourceLevel);

    if (methodDescriptor != null)
    {
      context.getRawRequestContext().putLocalAttr(R2Constants.OPERATION,
                                                  OperationNameGenerator.generate(methodDescriptor.getMethodType(),
                                                                                  methodName));
      return new RoutingResult(context, methodDescriptor);
    }

    String httpMethod = context.getRequestMethod();
    if (methodName != null)
    {
      throw new RoutingException(
              String.format("%s operation " +
                            "named %s " +
                            "not supported on resource '%s' " +
                            "URI: '%s'",
                            httpMethod,
                            methodName,
                            resource.getResourceClass().getName(),
                            context.getRequestURI().toString()),
                            HttpStatus.S_400_BAD_REQUEST.getCode());
    }

    throw new RoutingException(
            String.format("%s operation not supported " +
                          "for URI: '%s' " +
                          "with " + RestConstants.HEADER_RESTLI_REQUEST_METHOD + ": '%s'",
                          httpMethod,
                          context.getRequestURI().toString(),
                          context.getRestLiRequestMethod()),
                          HttpStatus.S_400_BAD_REQUEST.getCode());
  }

  // We use a table match to ensure that we have no subtle ordering dependencies in conditional logic
  //
  // Currently only POST requests set RMETHOD header (HEADER_RESTLI_REQUEST_METHOD), however we include
  // a table entry for GET methods as well to make sure the routing doesn't fail if the client sets the header
  // when it's not necessary, as long as it doesn't conflict with the rest of the parameters.
  private static Map<ResourceMethodMatchKey, ResourceMethod> setupResourceMethodLookup()
  {
    HashMap<ResourceMethodMatchKey, ResourceMethod> result = new HashMap<ResourceMethodMatchKey, ResourceMethod>();
    //                                 METHOD    RMETHOD                    ACTION   QUERY   BATCH   ENTITY
    Object[] config =
    {
            new ResourceMethodMatchKey("GET",    "",                        false,   false,  false,  true),  ResourceMethod.GET,
            new ResourceMethodMatchKey("GET",    "",                        false,   true,   false,  false), ResourceMethod.FINDER,
            new ResourceMethodMatchKey("PUT",    "",                        false,   false,  false,  true),  ResourceMethod.UPDATE,
            new ResourceMethodMatchKey("POST",   "",                        false,   false,  false,  true),  ResourceMethod.PARTIAL_UPDATE,
            new ResourceMethodMatchKey("DELETE", "",                        false,   false,  false,  true),  ResourceMethod.DELETE,
            new ResourceMethodMatchKey("POST",   "",                        true,    false,  false,  true),  ResourceMethod.ACTION,
            new ResourceMethodMatchKey("POST",   "",                        true,    false,  false,  false), ResourceMethod.ACTION,
            new ResourceMethodMatchKey("POST",   "",                        false,   false,  false,  false), ResourceMethod.CREATE,
            new ResourceMethodMatchKey("GET",    "",                        false,   false,  false,  false), ResourceMethod.GET_ALL,

            new ResourceMethodMatchKey("GET",    "GET",                     false,   false,  false,  true),  ResourceMethod.GET,
            new ResourceMethodMatchKey("GET",    "FINDER",                  false,   true,   false,  false), ResourceMethod.FINDER,
            new ResourceMethodMatchKey("PUT",    "UPDATE",                  false,   false,  false,  true),  ResourceMethod.UPDATE,
            new ResourceMethodMatchKey("POST",   "PARTIAL_UPDATE",          false,   false,  false,  true),  ResourceMethod.PARTIAL_UPDATE,
            new ResourceMethodMatchKey("DELETE", "DELETE",                  false,   false,  false,  true),  ResourceMethod.DELETE,
            new ResourceMethodMatchKey("POST",   "ACTION",                  true,    false,  false,  true),  ResourceMethod.ACTION,
            new ResourceMethodMatchKey("POST",   "ACTION",                  true,    false,  false,  false), ResourceMethod.ACTION,
            new ResourceMethodMatchKey("POST",   "CREATE",                  false,   false,  false,  false), ResourceMethod.CREATE,
            new ResourceMethodMatchKey("GET",    "GET_ALL",                 false,   false,  false,  false), ResourceMethod.GET_ALL,

            new ResourceMethodMatchKey("GET",    "",                        false,   false,  true,   false), ResourceMethod.BATCH_GET,
            new ResourceMethodMatchKey("DELETE", "",                        false,   false,  true,   false), ResourceMethod.BATCH_DELETE,
            new ResourceMethodMatchKey("PUT",    "",                        false,   false,  true,   false), ResourceMethod.BATCH_UPDATE,
            new ResourceMethodMatchKey("POST",   "",                        false,   false,  true,   false), ResourceMethod.BATCH_PARTIAL_UPDATE,

            new ResourceMethodMatchKey("GET",    "BATCH_GET",               false,   false,  true,   false), ResourceMethod.BATCH_GET,
            new ResourceMethodMatchKey("DELETE", "BATCH_DELETE",            false,   false,  true,   false), ResourceMethod.BATCH_DELETE,
            new ResourceMethodMatchKey("PUT",    "BATCH_UPDATE",            false,   false,  true,   false), ResourceMethod.BATCH_UPDATE,
            new ResourceMethodMatchKey("POST",   "BATCH_PARTIAL_UPDATE",    false,   false,  true,   false), ResourceMethod.BATCH_PARTIAL_UPDATE,

            // batch create signature collides with non-batch create. requires RMETHOD header to distinguish
            new ResourceMethodMatchKey("POST",   "BATCH_CREATE",            false,   false,  false,  false), ResourceMethod.BATCH_CREATE
    };

    for (int ii = 0; ii < config.length; ii += 2)
    {
      ResourceMethodMatchKey key = (ResourceMethodMatchKey) config[ii];
      ResourceMethod method = (ResourceMethod) config[ii + 1];
      ResourceMethod prevValue = result.put(key, method);
      if (prevValue != null)
      {
        throw new RestLiInternalException("Routing Configuration conflict: "
            + prevValue.toString() + " conflicts with " + method.toString());
      }
    }

    return result;
  }

  private ResourceMethod mapResourceMethod(final ServerResourceContext context,
                                           final ResourceLevel resourceLevel)
  {
    ResourceMethodMatchKey key =
        new ResourceMethodMatchKey(context.getRequestMethod(),
                                   context.getRestLiRequestMethod(),
                                   context.getRequestActionName() != null,
                                   context.getRequestFinderName() != null,
                                   context.getPathKeys().getBatchIds() != null,
                                   resourceLevel.equals(ResourceLevel.ENTITY));

    if (_resourceMethodLookup.containsKey(key))
    {
      return _resourceMethodLookup.get(key);
    }

    if (context.hasParameter(RestConstants.ACTION_PARAM)
        && !"POST".equalsIgnoreCase(context.getRequestMethod()))
    {
      throw new RoutingException(
              String.format("All action methods (specified via '%s' in URI) must " +
                            "be submitted as a POST (was %s)",
                            RestConstants.ACTION_PARAM,
                            context.getRequestMethod()),
                            HttpStatus.S_400_BAD_REQUEST.getCode());

    }

    throw new RoutingException(String.format("Method '%s' is not supported for URI '%s'",
                                             context.getRequestMethod(),
                                             context.getRequestURI()),
                               HttpStatus.S_400_BAD_REQUEST.getCode());

  }

  private static CompoundKey parseCompoundKey(final ResourceModel resource,
                                              final ServerResourceContext context,
                                              final String pathSegment)
{
   CompoundKey compoundKey;
   try
   {
     compoundKey =
       ArgumentUtils.parseCompoundKey(pathSegment, resource.getKeys(),
                                      context.getRestliProtocolVersion());
   }
   catch (PathSegmentSyntaxException e)
   {
     throw new RoutingException(String.format("input %s is not a Compound key", pathSegment),
                                HttpStatus.S_400_BAD_REQUEST.getCode(),
                                e);
   }
   catch (IllegalArgumentException e)
   {
     throw new RoutingException(String.format("input %s is not a Compound key", pathSegment),
                                HttpStatus.S_400_BAD_REQUEST.getCode(),
                                e);
   }


    for (String simpleKeyName : compoundKey.getPartKeys())
    {
      context.getPathKeys().append(simpleKeyName, compoundKey.getPart(simpleKeyName));
    }
    context.getPathKeys().append(resource.getKeyName(), compoundKey);
    return compoundKey;
  }


  /**
   * Instantiate the complex key from the current path segment (treat is as a list of
   * query parameters) and put it into the context.
   *
   * @param currentPathSegment
   * @param context
   * @param resource
   * @return
   */
  private static void parseComplexKey(final ResourceModel resource,
                                      final ServerResourceContext context,
                                      final String currentPathSegment)
  {
    try
    {
      ComplexKeySpec<? extends RecordTemplate, ? extends RecordTemplate> complexKeyType =
          ComplexKeySpec.forClassesMaybeNull( resource.getKeyKeyClass(), resource.getKeyParamsClass());
      ComplexResourceKey<RecordTemplate, RecordTemplate> complexKey =
          ComplexResourceKey.parseString(currentPathSegment, complexKeyType, context.getRestliProtocolVersion());

      context.getPathKeys().append(resource.getKeyName(), complexKey);
    }
    catch (PathSegmentSyntaxException e)
    {
      throw new RoutingException(String.format("Complex key query parameters parsing error: '%s'",
                                               e.getMessage()),
                                 HttpStatus.S_400_BAD_REQUEST.getCode());
    }
  }

  private void parseBatchKeysParameter(final ResourceModel resource,
                                       final ServerResourceContext context)
  {
    Class<?> keyClass = resource.getKeyClass();
    ProtocolVersion version = context.getRestliProtocolVersion();
    final Set<Object> batchKeys;

    if (ComplexResourceKey.class.equals(keyClass))
    {
      // Parse all query parameters into a data map.
      DataMap allParametersDataMap = context.getParameters();

      // Get the batch request keys from the IDS list at the root of the map.
      DataList batchIds = allParametersDataMap.getDataList(RestConstants.QUERY_BATCH_IDS_PARAM);
      if (batchIds == null)
      {
        batchKeys = null;
      }
      else if (batchIds.isEmpty())
      {
        batchKeys = Collections.emptySet();
      }
      else
      {
        batchKeys = new HashSet<Object>();

        // Validate the complex keys and put them into the context batch keys
        for (Object complexKey : batchIds)
        {
          if (!(complexKey instanceof DataMap))
          {
            log.warn("Invalid structure of key '" + complexKey.toString() + "', skipping key.");
            context.getBatchKeyErrors().put(complexKey, new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST));
            continue;
          }
          batchKeys.add(ComplexResourceKey.buildFromDataMap((DataMap) complexKey, ComplexKeySpec.forClassesMaybeNull(resource.getKeyKeyClass(), resource.getKeyParamsClass())));
        }
      }
    }
    else if (CompoundKey.class.equals(keyClass)
      && version.compareTo(AllProtocolVersions.RESTLI_PROTOCOL_2_0_0.getProtocolVersion()) >= 0)
    {
      DataMap allParametersDataMap = context.getParameters();

      // Get the batch request keys from the IDS list at the root of the map.
      DataList batchIds = allParametersDataMap.getDataList(RestConstants.QUERY_BATCH_IDS_PARAM);
      if (batchIds == null)
      {
        batchKeys = null;
      }
      else if (batchIds.isEmpty())
      {
        batchKeys = Collections.emptySet();
      }
      else
      {
        batchKeys = new HashSet<Object>();

        // Validate the compound keys and put them into the contex batch keys
        for (Object compoundKey : batchIds)
        {
          if (!(compoundKey instanceof DataMap))
          {
            log.warn("Invalid structure of key '" + compoundKey.toString() + "', skipping key.");
            context.getBatchKeyErrors().put(compoundKey.toString(), new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST));
            continue;
          }
          CompoundKey finalKey;
          try
          {
            finalKey = ArgumentUtils.dataMapToCompoundKey((DataMap) compoundKey, resource.getKeys());
          }
          catch (IllegalArgumentException e)
          {
            log.warn("Invalid structure of key '" + compoundKey.toString() + "', skipping key.");
            context.getBatchKeyErrors().put(compoundKey.toString(), new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST));
            continue;
          }
          batchKeys.add(finalKey);
        }
      }
    }
    // collection batch get in v2, collection or association batch get in v1
    else if (context.hasParameter(RestConstants.QUERY_BATCH_IDS_PARAM))
    {
      batchKeys = new HashSet<Object>();

      List<String> ids = context.getParameterValues(RestConstants.QUERY_BATCH_IDS_PARAM);
      if (version.compareTo(AllProtocolVersions.RESTLI_PROTOCOL_2_0_0.getProtocolVersion()) >= 0)
      {
        for (String id: ids)
        {
          Key key = resource.getPrimaryKey();
          Object value;
          // in v2, compound keys have already been converted and dealt with, so all we need to do here is convert simple values.
          value = ArgumentUtils.convertSimpleValue(id, key.getDataSchema(), key.getType());
          batchKeys.add(value);
        }
      }
      else
      {
        for (String id: ids)
        {
          try
          {
            // in v1, compound keys have not been fully parsed or dealt with yet, so we need to take them into account.
            Object value = parseKeyFromBatchV1(id, resource);
            batchKeys.add(value);
          }
          catch (NumberFormatException e)
          {
            log.warn("Caught NumberFormatException parsing batch key '" + id + "', skipping key.");
            context.getBatchKeyErrors().put(id, new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST, null, e));
          }
          catch (IllegalArgumentException e)
          {
            log.warn("Caught IllegalArgumentException parsing batch key '" + id + "', skipping key.");
            context.getBatchKeyErrors().put(id, new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST, null, e));
          }
          catch (PathSegmentSyntaxException e)
          {
            log.warn("Caught IllegalArgumentException parsing batch key '" + id + "', skipping key.");
            context.getBatchKeyErrors().put(id,
                                            new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST,
                                                                       null, e));
          }
        }
      }
    }
    else
    {
      batchKeys = null;
    }

    context.getPathKeys().setBatchKeys(batchKeys);
  }

  private static void parseSimpleKey(final ResourceModel resource,
                                     final ServerResourceContext context,
                                     final String pathSegment)
  {
    Object parsedKey;
    try
    {
      parsedKey = ArgumentUtils.parseSimplePathKey(pathSegment, resource, context.getRestliProtocolVersion());
    }
    catch (NumberFormatException e)
    {
      // thrown from Integer.valueOf or Long.valueOf
      throw new RoutingException(String.format("Key value '%s' must be of type '%s'",
                                                pathSegment,
                                                resource.getKeyClass().getName()),
                                 HttpStatus.S_400_BAD_REQUEST.getCode(),
                                 e);
    }
    catch (IllegalArgumentException e)
    {
      // thrown from Enum.valueOf
      throw new RoutingException(String.format("Key parameter value '%s' is invalid", pathSegment),
                                 HttpStatus.S_400_BAD_REQUEST.getCode(),
                                 e);
    }
    context.getPathKeys()
      .append(resource.getKeyName(), parsedKey);
  }

  private static Object parseKeyFromBatchV1(String value, ResourceModel resource)
    throws PathSegmentSyntaxException, IllegalArgumentException
  {
    ProtocolVersion version = AllProtocolVersions.RESTLI_PROTOCOL_1_0_0.getProtocolVersion();
    if (CompoundKey.class.isAssignableFrom(resource.getKeyClass()))
    {
      return ArgumentUtils.parseCompoundKey(value, resource.getKeys(), version);
    }
    else
    {
      Key key = resource.getPrimaryKey();
      return ArgumentUtils.convertSimpleValue(value, key.getDataSchema(), key.getType());
    }
  }

}
TOP

Related Classes of com.linkedin.restli.internal.server.RestLiRouter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.