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

com.linkedin.restli.internal.server.model.RestLiAnnotationReader Maven / Gradle / Ivy

Go to download

Pegasus is a framework for building robust, scalable service architectures using dynamic discovery and simple asychronous type-checked REST + JSON APIs.

There is a newer version: 27.7.18
Show newest version
/*
   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.model;


import com.linkedin.common.callback.Callback;
import com.linkedin.data.DataMap;
import com.linkedin.data.schema.ArrayDataSchema;
import com.linkedin.data.schema.DataSchema;
import com.linkedin.data.schema.RecordDataSchema;
import com.linkedin.data.schema.TyperefDataSchema;
import com.linkedin.data.template.Custom;
import com.linkedin.data.template.DataTemplateUtil;
import com.linkedin.data.template.DynamicRecordMetadata;
import com.linkedin.data.template.FieldDef;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.data.template.TemplateRuntimeException;
import com.linkedin.data.template.TyperefInfo;
import com.linkedin.data.transform.filter.request.MaskTree;
import com.linkedin.parseq.Task;
import com.linkedin.parseq.promise.Promise;
import com.linkedin.restli.common.ActionResponse;
import com.linkedin.restli.common.ComplexResourceKey;
import com.linkedin.restli.common.PatchRequest;
import com.linkedin.restli.common.ResourceMethod;
import com.linkedin.restli.common.RestConstants;
import com.linkedin.restli.internal.common.TyperefUtils;
import com.linkedin.restli.internal.server.PathKeysImpl;
import com.linkedin.restli.internal.server.RestLiInternalException;
import com.linkedin.restli.internal.server.model.ResourceMethodDescriptor.InterfaceType;
import com.linkedin.restli.internal.common.ReflectionUtils;
import com.linkedin.restli.server.ActionResult;
import com.linkedin.restli.server.BatchCreateRequest;
import com.linkedin.restli.server.BatchDeleteRequest;
import com.linkedin.restli.server.BatchPatchRequest;
import com.linkedin.restli.server.BatchUpdateRequest;
import com.linkedin.restli.server.CollectionResult;
import com.linkedin.restli.server.Key;
import com.linkedin.restli.server.NoMetadata;
import com.linkedin.restli.server.PagingContext;
import com.linkedin.restli.server.PathKeys;
import com.linkedin.restli.server.ResourceConfigException;
import com.linkedin.restli.server.ResourceLevel;
import com.linkedin.restli.server.annotations.Action;
import com.linkedin.restli.server.annotations.ActionParam;
import com.linkedin.restli.server.annotations.AssocKey;
import com.linkedin.restli.server.annotations.CallbackParam;
import com.linkedin.restli.server.annotations.Context;
import com.linkedin.restli.server.annotations.Finder;
import com.linkedin.restli.server.annotations.HeaderParam;
import com.linkedin.restli.server.annotations.Keys;
import com.linkedin.restli.server.annotations.Optional;
import com.linkedin.restli.server.annotations.ParSeqContext;
import com.linkedin.restli.server.annotations.Projection;
import com.linkedin.restli.server.annotations.QueryParam;
import com.linkedin.restli.server.annotations.RestAnnotations;
import com.linkedin.restli.server.annotations.RestLiActions;
import com.linkedin.restli.server.annotations.RestLiAssociation;
import com.linkedin.restli.server.annotations.RestLiCollection;
import com.linkedin.restli.server.annotations.RestLiCollectionCompoundKey;
import com.linkedin.restli.server.annotations.RestLiSimpleResource;
import com.linkedin.restli.server.annotations.RestMethod;
import com.linkedin.restli.server.resources.ComplexKeyResource;
import com.linkedin.restli.server.resources.ComplexKeyResourceAsync;
import com.linkedin.restli.server.resources.KeyValueResource;
import com.linkedin.restli.server.resources.SingleObjectResource;
import java.lang.reflect.AccessibleObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 *
 * @author dellamag
 */
public final class RestLiAnnotationReader
{
  private static final Logger log = LoggerFactory.getLogger(RestLiAnnotationReader.class);
  private static final Pattern INVALID_CHAR_PATTERN = Pattern.compile("\\W");
  /**
   * This is a utility class.
   */
  private RestLiAnnotationReader()
  {
  }

  /**
   * Processes an annotated resource class, producing a ResourceModel.
   *
   * @param resourceClass annotated resource class
   * @return {@link ResourceModel} for the provided resource class
   */
  public static ResourceModel processResource(final Class resourceClass)
  {
    final ResourceModel model;

    if ((resourceClass.isAnnotationPresent(RestLiCollection.class) ||
         resourceClass.isAnnotationPresent(RestLiAssociation.class) ||
         resourceClass.isAnnotationPresent(RestLiCollectionCompoundKey.class)))
    {
      // If any of these annotations, a subclass of KeyValueResource is expected
      if (!KeyValueResource.class.isAssignableFrom(resourceClass))
      {
        throw new RestLiInternalException("Resource class '" + resourceClass.getName()
            + "' declares RestLi annotation but does not implement "
            + KeyValueResource.class.getName() + " interface.");
      }

      @SuppressWarnings("unchecked")
      Class> clazz =
          (Class>) resourceClass;
      model = processCollection(clazz);
    }
    else if (resourceClass.isAnnotationPresent(RestLiActions.class))
    {
      model = processActions(resourceClass);
    }
    else if (resourceClass.isAnnotationPresent(RestLiSimpleResource.class))
    {
      @SuppressWarnings("unchecked")
      Class> clazz =
          (Class>) resourceClass;
      model = processSingleObjectResource(clazz);
    }
    else
    {
      throw new ResourceConfigException("Class '" + resourceClass.getName()
          + "' must be annotated with a valid @RestLi... annotation");
    }

    DataMap annotationsMap = ResourceModelAnnotation.getAnnotationsMap(resourceClass.getAnnotations());
    addDeprecatedAnnotation(annotationsMap, resourceClass);

    model.setCustomAnnotation(annotationsMap);
    return model;
  }

  private static DataMap addDeprecatedAnnotation(DataMap annotationsMap, Class clazz)
  {
    if(clazz.isAnnotationPresent(Deprecated.class))
    {
      annotationsMap.put("deprecated", new DataMap());
    }
    return annotationsMap;
  }

  private static DataMap addDeprecatedAnnotation(DataMap annotationsMap, AccessibleObject accessibleObject)
  {
    if(accessibleObject.isAnnotationPresent(Deprecated.class))
    {
      annotationsMap.put("deprecated", new DataMap());
    }
    return annotationsMap;
  }

  private static ResourceModel processCollection(final Class> collectionResourceClass)
  {
    Class keyClass;
    Class keyKeyClass = null;
    Class keyParamsClass = null;
    Class valueClass;
    Class complexKeyResourceBase = null;
    // If ComplexKeyResource or ComplexKeyResourceAsync, the parameters are Key type K, Params type P and Resource
    // type V and the resource key type is ComplexResourceKey
    if (ComplexKeyResource.class.isAssignableFrom(collectionResourceClass))
    {
      complexKeyResourceBase = ComplexKeyResource.class;
    }
    else if (ComplexKeyResourceAsync.class.isAssignableFrom(collectionResourceClass))
    {
      complexKeyResourceBase = ComplexKeyResourceAsync.class;
    }

    if (complexKeyResourceBase != null)
    {
      @SuppressWarnings("unchecked")
      List> kvParams = complexKeyResourceBase.equals(ComplexKeyResource.class) ?
          ReflectionUtils.getTypeArguments(ComplexKeyResource.class,
                                           (Class>) collectionResourceClass) :
          ReflectionUtils.getTypeArguments(ComplexKeyResourceAsync.class,
                                           (Class>) collectionResourceClass);

      keyClass = ComplexResourceKey.class;
      keyKeyClass = kvParams.get(0).asSubclass(RecordTemplate.class);
      keyParamsClass = kvParams.get(1).asSubclass(RecordTemplate.class);
      valueClass = kvParams.get(2).asSubclass(RecordTemplate.class);
    }
    // Otherwise, it's a KeyValueResource, whose parameters are resource key and resource
    // value
    else
    {
      List actualTypeArguments =
          ReflectionUtils.getTypeArgumentsParametrized(KeyValueResource.class,
                                                       collectionResourceClass);
      keyClass = ReflectionUtils.getClass(actualTypeArguments.get(0));

      if (RecordTemplate.class.isAssignableFrom(keyClass))
      {
        // a complex key is being used and thus ComplexKeyResource should be implemented so that we can wrap it in a
        // ComplexResourceKey
        throw new ResourceConfigException("Class '" + collectionResourceClass.getName() +
                                              "' should implement 'ComplexKeyResource' as a complex key '" +
                                              keyClass.getName() + "' is being used.");
      }
      else if (TyperefInfo.class.isAssignableFrom(keyClass))
      {
        throw new ResourceConfigException("Typeref '" + keyClass.getName() + "' cannot be key type for class '" +
                                              collectionResourceClass.getName() + "'.");
      }

      if (keyClass.equals(ComplexResourceKey.class))
      {
        @SuppressWarnings("unchecked")
        Type[] typeArguments = ((ParameterizedType)actualTypeArguments.get(0)).getActualTypeArguments();
        keyKeyClass = ReflectionUtils.getClass(typeArguments[0]).asSubclass(RecordTemplate.class);
        keyParamsClass = ReflectionUtils.getClass(typeArguments[1]).asSubclass(RecordTemplate.class);
      }

      valueClass = ReflectionUtils.getClass(actualTypeArguments.get(1)).asSubclass(RecordTemplate.class);
    }

    ResourceType resourceType = getResourceType(collectionResourceClass);

    RestLiAnnotationData annotationData;
    if (collectionResourceClass.isAnnotationPresent(RestLiCollection.class))
    {
      annotationData =
          new RestLiAnnotationData(collectionResourceClass.getAnnotation(RestLiCollection.class));
    }
    else if (collectionResourceClass.isAnnotationPresent(RestLiCollectionCompoundKey.class))
    {
      annotationData =
          new RestLiAnnotationData(collectionResourceClass.getAnnotation(RestLiCollectionCompoundKey.class));
    }
    else if (collectionResourceClass.isAnnotationPresent(RestLiAssociation.class))
    {
      annotationData =
          new RestLiAnnotationData(collectionResourceClass.getAnnotation(RestLiAssociation.class));
    }
    else
    {
      throw new ResourceConfigException("No valid annotation on resource class '"
          + collectionResourceClass.getName() + "'");
    }

    String name = annotationData.name();
    String namespace = annotationData.namespace();

    String keyName;
    if (annotationData.keyName() == null)
    {
      keyName = name + "Id";
    }
    else
    {
      keyName = annotationData.keyName();
    }

    Key primaryKey = buildKey(name, keyName, keyClass, annotationData.typerefInfoClass());
    Set keys = new HashSet();
    if (annotationData.keys() == null)
    {
      keys.add(primaryKey);
    }
    else
    {
      keys.addAll(buildKeys(name, annotationData.keys()));
    }

    Class parentResourceClass =
        annotationData.parent().equals(RestAnnotations.ROOT.class) ? null
            : annotationData.parent();

    ResourceModel collectionModel =
        new ResourceModel(primaryKey,
                          keyKeyClass,
                          keyParamsClass,
                          keys,
                          valueClass,
                          collectionResourceClass,
                          parentResourceClass,
                          name,
                          resourceType,
                          namespace);
    addResourceMethods(collectionResourceClass, collectionModel);

    log.info("Processed collection resource '" + collectionResourceClass.getName() + "'");

    return collectionModel;
  }

  private static ResourceModel processSingleObjectResource(
      final Class> singleObjectResourceClass)
  {
    Class valueClass;

    List> kvParams =
        ReflectionUtils.getTypeArguments(SingleObjectResource.class,
                                         singleObjectResourceClass);

    valueClass = kvParams.get(0).asSubclass(RecordTemplate.class);

    ResourceType resourceType = getResourceType(singleObjectResourceClass);

    RestLiAnnotationData annotationData;
    if (singleObjectResourceClass.isAnnotationPresent(RestLiSimpleResource.class))
    {
      annotationData =
          new RestLiAnnotationData(singleObjectResourceClass.getAnnotation(RestLiSimpleResource.class));
    }
    else
    {
      throw new ResourceConfigException("No valid annotation on resource class '"
                                            + singleObjectResourceClass.getName() + "'");
    }

    String name = annotationData.name();
    String namespace = annotationData.namespace();

    Class parentResourceClass =
        annotationData.parent().equals(RestAnnotations.ROOT.class) ? null
            : annotationData.parent();

    ResourceModel singleObjectResourceModel =
        new ResourceModel(valueClass,
                          singleObjectResourceClass,
                          parentResourceClass,
                          name,
                          resourceType,
                          namespace);

    addResourceMethods(singleObjectResourceClass, singleObjectResourceModel);

    log.info("Processed single object resource '" + singleObjectResourceClass.getName() + "'");

    return singleObjectResourceModel;
  }

  private static ResourceType getResourceType(final Class resourceClass)
  {
    RestLiCollection collAnno = resourceClass.getAnnotation(RestLiCollection.class);
    RestLiCollectionCompoundKey collCKAnno =
        resourceClass.getAnnotation(RestLiCollectionCompoundKey.class);
    RestLiAssociation assocAnno = resourceClass.getAnnotation(RestLiAssociation.class);
    RestLiSimpleResource simpleResourceAnno = resourceClass.getAnnotation(RestLiSimpleResource.class);

    if (resourceClass.isAnnotationPresent(RestLiActions.class))
    {
      throw new ResourceConfigException("Resource class '" + resourceClass.getName()
                                            + "' cannot have both @RestLiCollection and @RestLiActions annotations.");
    }

    int annoCount = 0;
    annoCount += collAnno != null ? 1 : 0;
    annoCount += collCKAnno != null ? 1 : 0;
    annoCount += assocAnno != null ? 1 : 0;
    annoCount += simpleResourceAnno != null ? 1 : 0;

    if (annoCount > 1)
    {
      throw new ResourceConfigException("Class '" + resourceClass.getName()
          + "' is annotated " + "with too many RestLi annotations");
    }
    else if (collAnno != null || collCKAnno != null)
    {
      return ResourceType.COLLECTION;
    }
    else if (assocAnno != null)
    {
      return ResourceType.ASSOCIATION;
    }
    else if (simpleResourceAnno != null)
    {
      return ResourceType.SIMPLE;
    }
    else
    {
      throw new ResourceConfigException("Class '" + resourceClass.getName()
          + "' should be annotated " + "with '" + RestLiAssociation.class.getName() + "'"
          + " or '" + RestLiCollection.class.getName() + "'" + " or '"
          + RestLiCollectionCompoundKey.class.getName() + "'"+ " or '"
          + RestLiSimpleResource.class.getName() + "'");
    }
  }

  /**
   * Keys, values, patch requests, and batch requests have fixed positions.
   */
  private static Parameter getPositionalParameter(final ResourceModel model,
                                                  final ResourceMethod methodType,
                                                  final int idx,
                                                  final AnnotationSet annotations)
  {
    boolean isSingleObjectResource = model.getResourceType() == ResourceType.SIMPLE;

    Parameter parameter = null;

    if (isSingleObjectResource)
    {
      parameter = getPositionalParameterForSingleObject(model, methodType, idx, annotations);
    }
    else
    {
      parameter = getPositionalParameterForCollection(model, methodType, idx, annotations);
    }

    return parameter;
  }

  private static Parameter getPositionalParameterForCollection(final ResourceModel model,
                                                               final ResourceMethod methodType,
                                                               final int idx,
                                                               final AnnotationSet annotations)
  {
    switch (methodType)
    {
      case GET:
        if (idx == 0)
        {
          return makeKeyParam(model);
        }
        break;
      case CREATE:
        if (idx == 0)
        {
          return makeValueParam(model);
        }
        break;
      case UPDATE:
        if (idx == 0)
        {
          return makeKeyParam(model);
        }
        else if (idx == 1)
        {
          return makeValueParam(model);
        }
        break;
      case DELETE:
        if (idx == 0)
        {
          return makeKeyParam(model);
        }
        break;
      case PARTIAL_UPDATE:
        if (idx == 0)
        {
          return makeKeyParam(model);
        }
        else if (idx == 1)
        {
          return makePatchParam(annotations);
        }
        break;
      case BATCH_GET:
        if (idx == 0)
        {
          @SuppressWarnings({"unchecked", "rawtypes"})
          Parameter p =
              new Parameter("",
                            Set.class,
                            null,
                            false,
                            null,
                            Parameter.ParamType.BATCH,
                            false,
                            annotations);
          return p;
        }
        break;
      case BATCH_CREATE:
        if (idx == 0)
        {
          @SuppressWarnings({"unchecked", "rawtypes"})
          Parameter p =
              new Parameter("",
                            BatchCreateRequest.class,
                            null,
                            false,
                            null,
                            Parameter.ParamType.BATCH,
                            false,
                            annotations);
          return p;
        }
        break;
      case BATCH_UPDATE:
        if (idx == 0)
        {
          @SuppressWarnings({"unchecked", "rawtypes"})
          Parameter p =
              new Parameter("",
                            BatchUpdateRequest.class,
                            null,
                            false,
                            null,
                            Parameter.ParamType.BATCH,
                            false,
                            annotations);
          return p;
        }
        break;
      case BATCH_DELETE:
        if (idx == 0)
        {
          @SuppressWarnings({"unchecked", "rawtypes"})
          Parameter p =
              new Parameter("",
                            BatchDeleteRequest.class,
                            null,
                            false,
                            null,
                            Parameter.ParamType.BATCH,
                            false,
                            annotations);
          return p;
        }
        break;
      case BATCH_PARTIAL_UPDATE:
        if (idx == 0)
        {
          @SuppressWarnings({"unchecked", "rawtypes"})
          Parameter p =
              new Parameter("",
                            BatchPatchRequest.class,
                            null,
                            false,
                            null,
                            Parameter.ParamType.BATCH,
                            false,
                            annotations);
          return p;
        }
        break;
      default:
        break;
    }

    return null;
  }

  private static Parameter getPositionalParameterForSingleObject(final ResourceModel model,
                                                               final ResourceMethod methodType,
                                                               final int idx,
                                                               final AnnotationSet annotations)
  {
    Parameter parameter = null;

    switch(methodType)
    {
      case UPDATE:
        if (idx == 0)
        {
          return makeValueParam(model);
        }

        break;
      case PARTIAL_UPDATE:
        if (idx == 0)
        {
          return makePatchParam(annotations);
        }

        break;
    }

    return parameter;
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private static Parameter makeValueParam(final ResourceModel model)
  {
    return new Parameter("",
                         model.getValueClass(),
                         getDataSchema(model.getValueClass(), null),
                         false,
                         null,
                         Parameter.ParamType.POST,
                         false,
                         AnnotationSet.EMPTY);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private static Parameter makeKeyParam(final ResourceModel model)
  {
    return new Parameter(model.getKeyName(),
                         model.getKeyClass(),
                         model.getPrimaryKey().getDataSchema(),
                         false,
                         null,
                         Parameter.ParamType.KEY,
                         false,
                         AnnotationSet.EMPTY);
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  private static Parameter makePatchParam(AnnotationSet annotations)
  {
    return new Parameter("",
                         PatchRequest.class,
                         null,
                         false,
                         null,
                         Parameter.ParamType.POST,
                         false,
                         annotations);
  }

  private static String getDefaultValueData(final Optional optional)
  {
    if (optional == null || optional.value() == null || optional.value().equals(RestAnnotations.DEFAULT))
    {
      return null;
    }

    return optional.value();
  }

  private static List> getParameters(final ResourceModel model,
                                                  final Method method,
                                                  final ResourceMethod methodType)
  {
    Set paramNames = new HashSet();

    List> queryParameters = new ArrayList>();
    Annotation[][] paramsAnnos = method.getParameterAnnotations();

    // Iterate over the method parameters.
    for (int idx = 0; idx < paramsAnnos.length; idx++)
    {
      AnnotationSet paramAnnotations = new AnnotationSet(paramsAnnos[idx]);
      Class paramType = method.getParameterTypes()[idx];

      Parameter param = getPositionalParameter(model, methodType, idx, paramAnnotations);

      // if no positional definition, look for custom annotated parameters
      if (param == null)
      {
        if (paramAnnotations.contains(QueryParam.class))
        {
          param = buildQueryParam(method, paramAnnotations, paramType);
        }
        else if (paramAnnotations.contains(ActionParam.class))
        {
          param = buildActionParam(method, paramAnnotations, paramType);
        }
        else if (paramAnnotations.contains(AssocKey.class))
        {
          param = buildAssocKeyParam(method, paramAnnotations, paramType);
        }
        else if (paramAnnotations.contains(Context.class))
        {
          param = buildContextParam(paramAnnotations, paramType);
        }
        else if (paramAnnotations.contains(CallbackParam.class))
        {
          param = buildCallbackParam(method, methodType, idx, paramType, paramAnnotations);
        }
        else if (paramAnnotations.contains(ParSeqContext.class))
        {
          param = buildParSeqContextParam(method, methodType, idx, paramType, paramAnnotations);
        }
        else if (paramAnnotations.contains(Projection.class))
        {
          param = buildProjectionParam(paramAnnotations, paramType);
        }
        else if (paramAnnotations.contains(Keys.class))
        {
          param = buildKeysParam(paramAnnotations, paramType);
        }
        else if (paramAnnotations.contains(HeaderParam.class))
        {
          param = buildHeaderParam(paramAnnotations, paramType);
        }
        else
        {
          throw new ResourceConfigException(buildMethodMessage(method)
              + " must annotate each parameter with @QueryParam, @ActionParam, @AssocKey, @Context, @Projection, @Keys, @HeaderParam, @CallbackParam or @ParSeqContext");
        }
      }

      if (param != null)
      {
        validateParameter(method,
                          methodType,
                          paramNames,
                          paramAnnotations,
                          param,
                          paramType);

        queryParameters.add(param);
      }
    }

    return queryParameters;
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private static Parameter buildCallbackParam(final Method method,
                                              final ResourceMethod methodType,
                                              final int idx,
                                              final Class paramType,
                                              final AnnotationSet annotations)
  {
    if (!Callback.class.equals(paramType))
    {
      throw new ResourceConfigException(String.format("%s '%s' of class '%s' does not have a proper callback",
                                                      methodType,
                                                      method.getName(),
                                                      method.getDeclaringClass()
                                                            .getName()));
    }
    Parameter param =
        new Parameter("",
                      paramType,
                      null,
                      false,
                      null,
                      Parameter.ParamType.CALLBACK,
                      false,
                      annotations);
    return param;
  }

  private static Parameter buildParSeqContextParam(final Method method,
                                                                                final ResourceMethod methodType,
                                                                                final int idx,
                                                                                final Class paramType,
                                                                                final AnnotationSet annotations)
  {
    if (!com.linkedin.parseq.Context.class.equals(paramType))
    {
      throw new ResourceConfigException("@ParSeqContext must be com.linkedin.parseq.Context");
    }
    if (getInterfaceType(method) != InterfaceType.PROMISE)
    {
      throw new ResourceConfigException("Cannot have ParSeq context on non-promise method");
    }
    return new Parameter("",
                                                      com.linkedin.parseq.Context.class,
                                                      null,
                                                      false,
                                                      null,
                                                      Parameter.ParamType.PARSEQ_CONTEXT,
                                                      false,
                                                      annotations);
  }

  // bug in javac 7 that doesn't obey the unchecked suppression, had to abstract to method to workaround.
  @SuppressWarnings({"unchecked"})
  private static Integer annotationCount(final AnnotationSet annotations)
  {
    return annotations.count(QueryParam.class,
                             ActionParam.class,
                             AssocKey.class,
                             Context.class,
                             CallbackParam.class,
                             ParSeqContext.class);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private static void validateParameter(final Method method,
                                        final ResourceMethod methodType,
                                        final Set paramNames,
                                        final AnnotationSet annotations,
                                        final Parameter param,
                                        final Class actualParamType)
  {
    String paramName = param.getName();
    if (!paramName.isEmpty() && paramNames.contains(paramName))
    {
      throw new ResourceConfigException("Parameter '" + paramName + "' on "
          + buildMethodMessage(method) + " is specified more than once");
    }
    paramNames.add(paramName);

    if (!actualParamType.isAssignableFrom(param.getType()))
    {
      throw new ResourceConfigException("Parameter '" + paramName + "' on "
          + buildMethodMessage(method) + " is not a valid type '" + actualParamType
          + "'.  Must be assignable from '" + param.getType() + "'.");
    }

    if (methodType == ResourceMethod.ACTION)
    {
      if (annotations.contains(QueryParam.class))
      {
        throw new ResourceConfigException("Parameter '" + paramName + "' on "
                                              + buildMethodMessage(method) + " is a @QueryParam but action method cannot have @QueryParam");
      }

      if (param.getParamType() == Parameter.ParamType.POST
          && !(checkParameterType(param.getType(), RestModelConstants.VALID_ACTION_PARAMETER_TYPES) ||
               checkParameterHasTyperefSchema(param)))
      {
        throw new ResourceConfigException("Parameter '" + paramName + "' on "
                                              + buildMethodMessage(method) + " is not a valid type (" + param.getType() + ')');
      }
    }
    else if (param.getParamType() == Parameter.ParamType.QUERY
          && !(checkParameterType(param.getType(), RestModelConstants.VALID_QUERY_PARAMETER_TYPES) ||
               checkParameterHasTyperefSchema(param)))
    {
        throw new ResourceConfigException("Parameter '" + paramName + "' on "
                                              + buildMethodMessage(method) + " is not a valid type (" + param.getType() + ')');
    }

    if (param.getType().isPrimitive() && param.isOptional() && !param.hasDefaultValue())
    {
      throw new ResourceConfigException("Parameter '"
          + paramName
          + "' on "
          + buildMethodMessage(method)
          + " is a primitive type, but does not specify a default value in the @Optional annotation");
    }

    final String checkTyperefMessage = checkTyperefSchema(param.getType(), param.getDataSchema());
    if (checkTyperefMessage != null)
    {
      throw new ResourceConfigException("Parameter '" + paramName + "' on "
          + buildMethodMessage(method) + ", " + checkTyperefMessage);
    }

    if (annotationCount(annotations) > 1)
    {
      throw new ResourceConfigException(buildMethodMessage(method)
          + "' must declare only one of @QueryParam, @ActionParam, @AssocKey, @Context, or @CallbackParam");
    }
  }

  private static boolean checkParameterHasTyperefSchema(Parameter parameter)
  {
    boolean result = false;
    DataSchema dataSchema = parameter.getDataSchema();
    Class dataType = parameter.getType();

    if (dataType.isArray())
    {
      if (dataSchema instanceof ArrayDataSchema)
      {
        dataSchema = ((ArrayDataSchema) dataSchema).getItems();
      }
      else
      {
        throw new ResourceConfigException(
            "Array typed parameter " + parameter.getName() + " must have an array schema.");
      }
    }

    if (dataSchema instanceof TyperefDataSchema)
    {
      result = true;
    }

    return result;
  }

  private static Set buildKeys(String resourceName,
                                    com.linkedin.restli.server.annotations.Key[] annoKeys)
  {
    Set keys = new HashSet();
    for(com.linkedin.restli.server.annotations.Key key : annoKeys)
    {
      keys.add(buildKey(resourceName, key.name(), key.type(), key.typeref()));
    }
    return keys;
  }

  private static Key buildKey(String resourceName,
                              String keyName, Class keyType, Class typerefInfoClass)
  {
    try
    {
      return new Key(keyName, keyType, getDataSchema(keyType, getSchemaFromTyperefInfo(typerefInfoClass)));
    }
    catch (TemplateRuntimeException e)
    {
      throw new ResourceConfigException("DataSchema for key '" + keyName + "' of type " + keyType + " on resource "
                                                + resourceName + "cannot be found; type is invalid or requires typeref", e);
    }
    catch (Exception e)
    {
      throw new ResourceConfigException("Typeref for parameter '" + keyName + "' on resource "
                                                + resourceName + " cannot be instantiated, " + e.getMessage(), e);
    }

  }

  private static Parameter buildProjectionParam(final AnnotationSet annotations,
                                                   final Class paramType)
  {
    if (!paramType.equals(MaskTree.class))
    {
      throw new ResourceConfigException("Projection must be MaskTree");
    }
    Optional optional = annotations.get(Optional.class);

    @SuppressWarnings({"unchecked", "rawtypes"})
    Parameter param = new Parameter("",
                                       paramType,
                                       null,
                                       optional != null,
                                       null, // default mask is null.
                                       Parameter.ParamType.PROJECTION,
                                       false,
                                       annotations);
    return param;
  }

  private static Parameter buildKeysParam(final AnnotationSet annotations,
                                             final Class paramType)
  {
    if (!paramType.equals(PathKeys.class))
    {
      throw new ResourceConfigException("Keys must be PathKeys");
    }

    Optional optional = annotations.get(Optional.class);

    @SuppressWarnings({"unchecked", "rawtypes"})
    Parameter param = new Parameter("",
                                       paramType,
                                       null,
                                       optional != null,
                                       new PathKeysImpl(),
                                       Parameter.ParamType.PATH_KEYS,
                                       false,
                                       annotations);
    return param;
  }

  private static Parameter buildHeaderParam(final AnnotationSet annotations,
                                               final Class paramType)
  {
    if (!paramType.equals(String.class))
    {
      throw new ResourceConfigException("Header must be a String");
    }
    Optional optional = annotations.get(Optional.class);

    @SuppressWarnings({"unchecked", "rawtypes"})
    Parameter param = new Parameter("",
                                       paramType,
                                       null,
                                       optional != null,
                                       "",
                                       Parameter.ParamType.HEADER,
                                       false,
                                       annotations);
    return param;
  }

  private static Parameter buildContextParam(final AnnotationSet annotations,
                                                final Class paramType)
  {
    if (!paramType.equals(PagingContext.class))
    {
      throw new ResourceConfigException("Context must be PagingContext");
    }

    Context context = annotations.get(Context.class);
    Optional optional = annotations.get(Optional.class);
    PagingContext defaultContext =
        new PagingContext(context.defaultStart(), context.defaultCount(), false, false);
    @SuppressWarnings({"unchecked", "rawtypes"})
    Parameter param =
        new Parameter("",
                      paramType,
                      null,
                      optional != null,
                      defaultContext,
                      Parameter.ParamType.CONTEXT,
                      false,
                      annotations);
    return param;
  }

  private static Parameter buildAssocKeyParam(final Method method,
                                                 final AnnotationSet annotations,
                                                 final Class paramType)
  {
    AssocKey assocKey = annotations.get(AssocKey.class);
    Optional optional = annotations.get(Optional.class);
    Class typerefInfoClass = assocKey.typeref();
    try
    {
      @SuppressWarnings({"unchecked", "rawtypes"})
      Parameter param =
          new Parameter(assocKey.value(),
                        paramType,
                        getDataSchema(paramType, getSchemaFromTyperefInfo(typerefInfoClass)),
                        optional != null,
                        getDefaultValueData(optional),
                        Parameter.ParamType.KEY,
                        true,
                        annotations);
      return param;
    }
    catch (TemplateRuntimeException e)
    {
      throw new ResourceConfigException("DataSchema for assocKey '" + assocKey.value() + "' of type " + paramType.getSimpleName() + " on "
                                                + buildMethodMessage(method) + "cannot be found; type is invalid or requires typeref", e);
    }
    catch (Exception e)
    {
      throw new ResourceConfigException("Typeref for assocKey '" + assocKey.value() + "' on "
                                                + buildMethodMessage(method) + " cannot be instantiated, " + e.getMessage(), e);
    }
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private static Parameter buildActionParam(final Method method,
                                            final AnnotationSet annotations,
                                            final Class paramType)
  {
    ActionParam actionParam = annotations.get(ActionParam.class);
    Optional optional = annotations.get(Optional.class);
    String paramName = actionParam.value();
    Class typerefInfoClass = actionParam.typeref();
    try
    {
      Parameter param =
          new Parameter(paramName,
                        paramType,
                        getDataSchema(paramType, getSchemaFromTyperefInfo(typerefInfoClass)),
                        optional != null,
                        getDefaultValueData(optional),
                        Parameter.ParamType.POST,
                        true,
                        annotations);
      return param;
    }
    catch (TemplateRuntimeException e)
    {
      throw new ResourceConfigException("DataSchema for parameter '" + paramName + "' of type " + paramType.getSimpleName() + " on "
          + buildMethodMessage(method) + "cannot be found; type is invalid or requires typeref", e);
    }
    catch (Exception e)
    {
      throw new ResourceConfigException("Typeref for parameter '" + paramName + "' on "
          + buildMethodMessage(method) + " cannot be instantiated, " + e.getMessage(), e);
    }
  }

  private static TyperefDataSchema getSchemaFromTyperefInfo(Class typerefInfoClass)
          throws IllegalAccessException, InstantiationException
  {
    if (typerefInfoClass == null)
    {
      return null;
    }

    TyperefInfo typerefInfo = typerefInfoClass.newInstance();
    return typerefInfo.getSchema();

  }

  private static DataSchema getDataSchema(Class type, TyperefDataSchema typerefDataSchema)
  {
    if (type == Void.TYPE)
    {
      return null;
    }
    if (typerefDataSchema != null)
    {
      if (type.isArray())
      {
        return new ArrayDataSchema(typerefDataSchema);
      }
      else
      {
        return typerefDataSchema;
      }
    }
    else if (RestModelConstants.CLASSES_WITHOUT_SCHEMAS.contains(type))
    {
      return null;
    }
    else if (type.isArray())
    {
      DataSchema itemSchema = DataTemplateUtil.getSchema(type.getComponentType());
      return new ArrayDataSchema(itemSchema);
    }
    return DataTemplateUtil.getSchema(type);
  }


  private static Parameter buildQueryParam(final Method method,
                                              final AnnotationSet annotations,
                                              final Class paramType)
  {
    QueryParam queryParam = annotations.get(QueryParam.class);
    Optional optional = annotations.get(Optional.class);
    String paramName = queryParam.value();
    if (INVALID_CHAR_PATTERN.matcher(paramName).find())
    {
      throw new ResourceConfigException("Unsupported character in the parameter name :"
          + paramName);
    }
    Class typerefInfoClass = queryParam.typeref();
    try
    {
      @SuppressWarnings({"unchecked", "rawtypes"})
      Parameter param =
          new Parameter(queryParam.value(),
                        paramType,
                        getDataSchema(paramType, getSchemaFromTyperefInfo(typerefInfoClass)),
                        optional != null,
                        getDefaultValueData(optional),
                        Parameter.ParamType.QUERY,
                        true,
                        annotations);
      return param;
    }
    catch (TemplateRuntimeException e)
    {
      throw new ResourceConfigException("DataSchema for parameter '" + paramName + "' of type " + paramType.getSimpleName() + " on "
            + buildMethodMessage(method) + "cannot be found; type is invalid or requires typeref", e);
    }
    catch (Exception e)
    {
      throw new ResourceConfigException("Typeref for parameter '" + paramName + "' on "
          + buildMethodMessage(method) + " cannot be instantiated, " + e.getMessage(), e);
    }
  }

  private static String buildMethodMessage(final Method method)
  {
    return "Method '" + method.getName() + "' method on class '"
        + method.getDeclaringClass().getName() + '\'';
  }

  private static String checkTyperefSchema(final Class type, final DataSchema dataSchema)
  {
    if (!(dataSchema instanceof TyperefDataSchema))
    {
      return null;
    }

    TyperefDataSchema typerefSchema = (TyperefDataSchema)dataSchema;
    boolean ok;
    DataSchema.Type schemaType = typerefSchema.getDereferencedType();
    Class[] validTypes =
        RestModelConstants.PRIMITIVE_DATA_SCHEMA_TYPE_ALLOWED_TYPES.get(schemaType);
    if (validTypes != null)
    {
      String javaClassNameFromSchema =
          TyperefUtils.getJavaClassNameFromSchema(typerefSchema);

      if (javaClassNameFromSchema != null)
      {
        registerCoercer(typerefSchema);
        ok =
            type.getName().equals(javaClassNameFromSchema)
                || (type.isArray() && (type.getComponentType().getName()).equals(javaClassNameFromSchema));
      }
      else
      {
        ok = checkParameterType(type, validTypes);
      }
    }
    else
    {
      try
      {
        DataSchema inferredSchema = DataTemplateUtil.getSchema(type);
        DataSchema derefSchema = typerefSchema.getDereferencedDataSchema();
        if (inferredSchema.equals(derefSchema))
        {
          return null;
        }
        return "typeref " + typerefSchema + " is not compatible with (" + type
            + ") with schema " + derefSchema;
      }
      catch (TemplateRuntimeException e)
      {
      }
      ok = false;
    }
    if (!ok)
    {
      return "typeref " + typerefSchema + " is not compatible with (" + type + ")";
    }
    return null;
  }

  private static void registerCoercer(final TyperefDataSchema schema)
  {
    String coercerClassName = TyperefUtils.getCoercerClassFromSchema(schema);
    String javaClassNameFromSchema = TyperefUtils.getJavaClassNameFromSchema(schema);

    // initialize the custom class
    try
    {
      Custom.initializeCustomClass(Class.forName(javaClassNameFromSchema, true, Thread.currentThread().getContextClassLoader()));
    }
    catch (ClassNotFoundException e)
    {
      throw new ResourceConfigException("Could not find class for type "
          + javaClassNameFromSchema, e);
    }
    if (coercerClassName != null)
    {
      try
      {
        Custom.initializeCoercerClass(Class.forName(coercerClassName, true, Thread.currentThread().getContextClassLoader()));
      }
      catch (ClassNotFoundException e)
      {
        throw new ResourceConfigException("Could not find coercer " + coercerClassName
            + " for type " + javaClassNameFromSchema, e);
      }
    }
  }

  private static boolean checkParameterType(final Class type,
                                            final Class[] validTypes)
  {
    for (Class validType : validTypes)
    {
      if (validType.isAssignableFrom(type))
      {
        return true;
      }
    }

    return false;
  }

  private static void addResourceMethods(final Class resourceClass,
                                                   final ResourceModel model)
  {
    // this ignores methods declared in superclasses (e.g. template methods)
    for (Method method : resourceClass.getDeclaredMethods())
    {
      // ignore synthetic, type-erased versions of methods
      if (method.isSynthetic())
      {
        continue;
      }

      addActionResourceMethod(model, method);
      addFinderResourceMethod(model, method);
      addTemplateResourceMethod(resourceClass, model, method);
      addCrudResourceMethod(resourceClass, model, method);
    }

    validateResourceModel(model);
  }

  private static void validateResourceModel(final ResourceModel model)
  {
    validateAssociation(model);
    validateCrudMethods(model);
    validateSimpleResource(model);
  }

  private static void validateSimpleResource(ResourceModel model)
  {
    if (model.getResourceType() != ResourceType.SIMPLE)
    {
      return;
    }

    for (ResourceMethodDescriptor descriptor : model.getResourceMethodDescriptors())
    {
      ResourceMethod type = descriptor.getType();

      if (!RestConstants.SIMPLE_RESOURCE_METHODS.contains(type))
      {
        throw new ResourceConfigException(
            String.format(
                "Resource '%s' is a simple resource but it contains a method of type %s" +
                " which is not supported on simple resources.",
                                                        model.getName(),
                                                        type.toString()));
      }
    }
  }

  private static void validateAssociation(final ResourceModel model)
  {
    if (model.getResourceType() != ResourceType.ASSOCIATION)
    {
      return;
    }

    if (model.getKeys().size() <= 1)
    {
      throw new ResourceConfigException(String.format("Association '%s' requires more than 1 key.",
                                                      model.getName()));
    }
  }

  private static void validateCrudMethods(final ResourceModel model)
  {
    Map crudMethods =
        new HashMap();
    for (ResourceMethodDescriptor descriptor : model.getResourceMethodDescriptors())
    {
      ResourceMethod type = descriptor.getType();
      switch (type)
      {
        case ACTION:
          continue;
        case FINDER:
          continue;
        default:
          if (crudMethods.containsKey(type))
          {
            ResourceMethodDescriptor oldDescriptor = crudMethods.get(type);
            throw new ResourceConfigException(String.format("Resource '%s' contains duplicate methods of type '%s'.  Methods are '%s' and '%s'.",
                                                            model.getName(),
                                                            type.toString(),
                                                            oldDescriptor.getMethod()
                                                                         .getName(),
                                                            descriptor.getMethod()
                                                                      .getName()));
          }
          crudMethods.put(type, descriptor);
      }
    }
  }

  private static void addFinderResourceMethod(final ResourceModel model, final Method method)
  {
    Finder finderAnno = method.getAnnotation(Finder.class);
    if (finderAnno == null)
    {
      return;
    }

    String queryType = finderAnno.value();

    List> queryParameters =
        getParameters(model, method, ResourceMethod.FINDER);

    if (queryType != null)
    {
      Class metadataType = null;
      final Class returnClass = getLogicalReturnClass(method);
      if (CollectionResult.class.isAssignableFrom(returnClass))
      {
        final List> typeArguments = ReflectionUtils.getTypeArguments(CollectionResult.class, returnClass.asSubclass(CollectionResult.class));
        final Class metadataClass;
        if (typeArguments == null || typeArguments.get(1) == null)
        {
          // the return type may leave metadata type as parameterized and specify in runtime
          metadataClass = ((Class) ((ParameterizedType) getLogicalReturnType(method)).getActualTypeArguments()[1]);
        }
        else
        {
          metadataClass = typeArguments.get(1);
        }

        if (!metadataClass.equals(NoMetadata.class))
        {
          metadataType = metadataClass.asSubclass(RecordTemplate.class);
        }
      }

      DataMap annotationsMap = ResourceModelAnnotation.getAnnotationsMap(method.getAnnotations());
      addDeprecatedAnnotation(annotationsMap, method);

      ResourceMethodDescriptor finderMethodDescriptor =
          ResourceMethodDescriptor.createForFinder(method,
                                                   queryParameters,
                                                   queryType,
                                                   metadataType,
                                                   getInterfaceType(method),
                                                   annotationsMap);
      validateFinderMethod(finderMethodDescriptor, model);

      if (!Modifier.isPublic(method.getModifiers()))
      {
        throw new ResourceConfigException(String.format("Resource '%s' contains non-public finder method '%s'.",
                                                        model.getName(),
                                                        method.getName()));
      }

      model.addResourceMethodDescriptor(finderMethodDescriptor);
    }
  }

  /**
   * Handle method that overrides resource template method. Only meaningful for classes
   * that extend a resource template class and only for methods that are NOT annotated
   * with RestMethod. Those are handled in addCrudResourceMethod()
   */
  private static void addTemplateResourceMethod(final Class resourceClass,
                                                final ResourceModel model,
                                                final Method method)
  {
    // Check if the resource class is derived from one of the resource template classes.
    if (!isResourceTemplateClass(resourceClass))
    {
      return;
    }

    // If the method is annotated with RestMethod - ignore - will be handled in
    // addCrudResourceMethod
    if (isRestMethodAnnotated(method))
    {
      return;
    }

    List> parameterTypes = Arrays.asList(method.getParameterTypes());
    boolean partial =
        parameterTypes.contains(PatchRequest.class)
            || parameterTypes.contains(BatchPatchRequest.class);
    ResourceMethod resourceMethod =
        ResourceMethodLookup.fromResourceMethodName(method.getName(), partial);

    if (resourceMethod != null)
    {
      if (!Modifier.isPublic(method.getModifiers()))
      {
        throw new ResourceConfigException(String.format("Resource '%s' contains non-public CRUD method '%s'.",
                                                        model.getName(),
                                                        method.getName()));
      }

      DataMap annotationsMap = ResourceModelAnnotation.getAnnotationsMap(method.getAnnotations());
      addDeprecatedAnnotation(annotationsMap, method);

      List> parameters = getParameters(model, method, resourceMethod);
      model.addResourceMethodDescriptor(ResourceMethodDescriptor.createForRestful(resourceMethod,
                                                                                  method,
                                                                                  parameters,
                                                                                  getInterfaceType(method),
                                                                                  annotationsMap));
    }
  }

  /**
   * Check whether the method is annotated with one of {@link RestMethod} annotations.
   */
  private static boolean isRestMethodAnnotated(final Method method)
  {
    Annotation[] methodAnnotations = method.getAnnotations();
    for (Annotation annotation : methodAnnotations)
    {
      if (RestMethod.getResourceMethod(annotation.annotationType()) != null)
      {
        return true;
      }
    }
    return false;
  }

  /**
   * Check whether the class is one of the resource templated-derived classes.
   */
  private static boolean isResourceTemplateClass(final Class resourceClass)
  {
    for (Class c : RestModelConstants.FIXED_RESOURCE_CLASSES)
    {
      if (c.isAssignableFrom(resourceClass))
      {
        return true;
      }
    }
    return false;
  }

  /**
   * Find which rest method annotation is present in the method of the resource, if any,
   * and add MethodDescriptor to ResourceModel.
   *
   * @param resourceClass
   * @param model
   * @param method
   */
  private static void addCrudResourceMethod(final Class resourceClass,
                                            final ResourceModel model,
                                            final Method method)
  {
    boolean restMethodAnnotationFound = false;
    for (Annotation methodAnnotation : method.getAnnotations())
    {
      ResourceMethod resourceMethod =
          RestMethod.getResourceMethod(methodAnnotation.annotationType());
      if (resourceMethod != null)
      {
        if (restMethodAnnotationFound)
        {
          throw new ResourceConfigException("Multiple rest method annotations in method "
              + method.getName());
        }
        restMethodAnnotationFound = true;

        if (!Modifier.isPublic(method.getModifiers()))
        {
          throw new ResourceConfigException(String.format("Resource '%s' contains non-public CRUD method '%s'.",
                                                          model.getName(),
                                                          method.getName()));
        }

        DataMap annotationsMap = ResourceModelAnnotation.getAnnotationsMap(method.getAnnotations());
        addDeprecatedAnnotation(annotationsMap, method);

        List> parameters = getParameters(model, method, resourceMethod);
        model.addResourceMethodDescriptor(ResourceMethodDescriptor.createForRestful(resourceMethod,
                                                                                    method,
                                                                                    parameters,
                                                                                    getInterfaceType(method),
                                                                                    annotationsMap));
      }
    }
  }

  /**
   * Add the given action method to the given resource model,  validating the method is a action before adding.
   * @param model provides the model to add the method to.
   * @param method provides the method to add to the model.
   * @throws ResourceConfigException on validation errors.
   */
  private static void addActionResourceMethod(final ResourceModel model, final Method method)
  {
    Action actionAnno = method.getAnnotation(Action.class);
    if (actionAnno == null)
    {
      return;
    }

    String actionName = actionAnno.name();
    List> parameters = getParameters(model, method, ResourceMethod.ACTION);

    Class returnClass = getActionReturnClass(model, method, actionAnno, actionName);
    TyperefDataSchema returnTyperefSchema = getActionTyperefDataSchema(model, actionAnno, actionName);
    validateActionReturnType(model, method, returnClass, returnTyperefSchema);

    if (!Modifier.isPublic(method.getModifiers()))
    {
      throw new ResourceConfigException(String.format("Resource '%s' contains non-public action method '%s'.",
                                                      model.getName(),
                                                      method.getName()));
    }

    RecordDataSchema recordDataSchema = DynamicRecordMetadata.buildSchema(method.getName(),
                                                                          parameters);

    RecordDataSchema actionReturnRecordDataSchema;
    FieldDef returnFieldDef;
    if(returnClass != Void.TYPE)
    {
      @SuppressWarnings({"unchecked", "rawtypes"})
      FieldDef nonVoidFieldDef = new FieldDef(ActionResponse.VALUE_NAME, returnClass, getDataSchema(returnClass, returnTyperefSchema));
      returnFieldDef = nonVoidFieldDef;
      actionReturnRecordDataSchema = DynamicRecordMetadata.buildSchema(ActionResponse.class.getName(), Collections.singleton((returnFieldDef)));
    }
    else
    {
      returnFieldDef = null;
      actionReturnRecordDataSchema = DynamicRecordMetadata.buildSchema(ActionResponse.class.getName(), Collections.>emptyList());
    }

    if (model.getResourceLevel() == ResourceLevel.ENTITY && actionAnno.resourceLevel() == ResourceLevel.COLLECTION)
    {
      throw new ResourceConfigException(
          String.format("Resource '%s' is a simple resource, it cannot contain actions at resource level \"COLLECTION\".",
            model.getName()));
    }

    DataMap annotationsMap = ResourceModelAnnotation.getAnnotationsMap(method.getAnnotations());
    addDeprecatedAnnotation(annotationsMap, method);

    model.addResourceMethodDescriptor(ResourceMethodDescriptor.createForAction(method,
                                                                               parameters,
                                                                               actionName,
                                                                               getActionResourceLevel(actionAnno, model),
                                                                               returnFieldDef,
                                                                               actionReturnRecordDataSchema,
                                                                               recordDataSchema,
                                                                               getInterfaceType(method),
                                                                               annotationsMap));

  }

  private static TyperefDataSchema getActionTyperefDataSchema(ResourceModel model, Action actionAnno, String actionName)
  {
    TyperefDataSchema returnTyperefSchema = null;
    Class typerefInfoClass = actionAnno.returnTyperef();
    try
    {
      returnTyperefSchema = getSchemaFromTyperefInfo(typerefInfoClass);
    }
    catch (Exception e)
    {
      throw new ResourceConfigException("Typeref @Action method named '" + actionName
          + "' on class '" + model.getResourceClass().getName()
          + "' cannot be instantiated, " + e.getMessage());
    }
    return returnTyperefSchema;
  }

  private static Class getActionReturnClass(ResourceModel model, Method method, Action actionAnno, String actionName)
  {
    final Type returnType = getLogicalReturnType(method);
    ResourceMethodDescriptor existingMethodDescriptor =
        model.findActionMethod(actionName, getActionResourceLevel(actionAnno, model));
    if (existingMethodDescriptor != null)
    {
      throw new ResourceConfigException("Found duplicate @Action method named '"
          + actionName + "' on class '" + model.getResourceClass().getName() + '\'');

    }

    Class returnClass = getBoxedTypeFromPrimitive(getLogicalReturnClass(method));
    if (ActionResult.class.isAssignableFrom(returnClass))
    {
      assert(returnType instanceof ParameterizedType);
      final ParameterizedType paramReturnType = (ParameterizedType) returnType;
      final Type[] actualReturnTypes = paramReturnType.getActualTypeArguments();
      assert(actualReturnTypes.length == 1);
      if (!(actualReturnTypes[0] instanceof Class))
      {
        throw new ResourceConfigException("Unsupported type parameter for ActionResult.");
      }
      returnClass = (Class) actualReturnTypes[0];

      if (returnClass == Void.class)
      {
        returnClass = Void.TYPE;
      }
    }
    return returnClass;
  }

  private static ResourceLevel getActionResourceLevel(Action annotation, ResourceModel definingModel)
  {
    return annotation.resourceLevel() == ResourceLevel.ANY ?
        definingModel.getResourceLevel() : annotation.resourceLevel();
  }

  private static Class getBoxedTypeFromPrimitive(Class type)
  {
    if (type.isPrimitive())
    {
      if(type == boolean.class)
        return Boolean.class;
      if(type == byte.class)
        return Byte.class;
      if(type == char.class)
        return Character.class;
      if(type == double.class)
        return Double.class;
      if(type == float.class)
        return Float.class;
      if(type == int.class)
        return Integer.class;
      if(type == long.class)
        return Long.class;
      if(type == short.class)
        return Short.class;
    }
    return type;
  }

  /**
   * Checks that a action method's return type is allowed.
   * @param model provides the resource model the method is being validated for, used for context.
   * @param method provides the action method to validate.
   * @param returnClass provides the declared return type of the method.
   * @param returnTyperefSchema provides the schema of the returnTyperef declared on the method's @Action annotation, or null if returnTyperef is absent.
   * @throws ResourceConfigException on validation errors.
   */
  private static void validateActionReturnType(ResourceModel model,
                                               Method method,
                                               Class returnClass,
                                               TyperefDataSchema returnTyperefSchema)
  {
    if (returnTyperefSchema == null)
    {
      Class returnType = getLogicalReturnClass(method);

      if (!checkParameterType(returnType, RestModelConstants.VALID_ACTION_RETURN_TYPES))
      {
        throw new ResourceConfigException("@Action method '" + method.getName()
            + "' on class '" + method.getDeclaringClass().getName()
            + "' has an invalid return type '" + returnType.getName()
            + "'. Expected a DataTemplate or a primitive");
      }
    }

    final String checkTyperefMessage = checkTyperefSchema(returnClass, returnTyperefSchema);
    if (checkTyperefMessage != null)
    {
      throw new ResourceConfigException("Typeref @Action method named '" + method.getAnnotation(Action.class).name()
                                            + "' on class '" + model.getResourceClass().getName() + "', "
                                            + checkTyperefMessage);
    }
  }

  private static void validateFinderMethod(final ResourceMethodDescriptor finderMethodDescriptor,
                                           final ResourceModel resourceModel)
  {
    Method method = finderMethodDescriptor.getMethod();
    Class valueClass = resourceModel.getValueClass();

    Class returnType, elementType;
    try
    {
      returnType = getLogicalReturnClass(method);
      final List> typeArguments;
      if (List.class.isAssignableFrom(returnType))
      {
        typeArguments = ReflectionUtils.getTypeArguments(List.class, returnType.asSubclass(List.class));
      }
      else if (CollectionResult.class.isAssignableFrom(returnType))
      {
        typeArguments = ReflectionUtils.getTypeArguments(CollectionResult.class, returnType.asSubclass(CollectionResult.class));
      }
      else
      {
        throw new ResourceConfigException("@Finder method '" + method.getName()
            + "' on class '" + resourceModel.getResourceClass().getName()
            + "' has an unsupported return type");
      }

      if (typeArguments == null || typeArguments.get(0) == null)
      {
        // the return type may leave value type as parameterized and specify in runtime
        final ParameterizedType collectionType = (ParameterizedType) getLogicalReturnType(method);
        elementType = (Class) collectionType.getActualTypeArguments()[0];
      }
      else
      {
        elementType = typeArguments.get(0);
      }
    }
    catch (ClassCastException e)
    {
      throw new ResourceConfigException("@Finder method '" + method.getName()
          + "' on class '" + resourceModel.getResourceClass().getName()
          + "' has an invalid return or a data template type", e);
    }

    if (!List.class.isAssignableFrom(returnType)
        && !CollectionResult.class.isAssignableFrom(returnType))
    {
      throw new ResourceConfigException("@Finder method '" + method.getName()
          + "' on class '" + resourceModel.getResourceClass().getName()
          + "' has an invalid return type '" + returnType.getName() + "'. Expected "
          + "List<" + valueClass.getName() + "> or CollectionResult<"
          + valueClass.getName() + ">");
    }

    String collectionClassName = returnType.getSimpleName();
    if (!RecordTemplate.class.isAssignableFrom(elementType)
        || !resourceModel.getValueClass().equals(elementType))
    {
      throw new ResourceConfigException("@Finder method '" + method.getName()
          + "' on class '" + resourceModel.getResourceClass().getName()
          + "' has an invalid return type. Expected " + collectionClassName + "<"
          + valueClass.getName() + ">, but found " + collectionClassName + "<"
          + elementType + '>');
    }

    ResourceMethodDescriptor existingFinder =
        resourceModel.findNamedMethod(finderMethodDescriptor.getFinderName());
    if (existingFinder != null)
    {
      throw new ResourceConfigException("Found duplicate @Finder method named '"
          + finderMethodDescriptor.getFinderName() + "' on class '"
          + resourceModel.getResourceClass().getName() + '\'');
    }

    // query parameters are checked in getQueryParameters method
  }

  private static ResourceModel processActions(final Class actionResourceClass)
  {
    RestLiActions actionsAnno = actionResourceClass.getAnnotation(RestLiActions.class);

    String name = actionsAnno.name();

    String namespace = actionsAnno.namespace();

    ResourceModel actionResourceModel = new ResourceModel(null, // primary key
                                                          null, // key key class
                                                          null, // key params class
                                                          Collections. emptySet(), // keys
                                                          null, // value class
                                                          actionResourceClass, // resource class
                                                          null, // parent resource class
                                                          name, // name
                                                          ResourceType.ACTIONS, // resource type
                                                          namespace); // namespace
    for (Method method : actionResourceClass.getDeclaredMethods())
    {
      // ignore synthetic, type-erased versions of methods
      if (method.isSynthetic())
      {
        continue;
      }

      addActionResourceMethod(actionResourceModel, method);
    }

    log.info("Processed actions resource '" + actionResourceClass.getName() + '\'');

    return actionResourceModel;
  }

  /**
   * @return the type of interface that was implemented: synchronous, callback-based, or
   *         promise-based
   */
  private static InterfaceType getInterfaceType(final Method method)
  {
    boolean promise = Promise.class.isAssignableFrom(method.getReturnType());
    boolean task = Task.class.isAssignableFrom(method.getReturnType());
    boolean callback = getParamIndex(method, Callback.class) != -1;
    boolean isVoid = method.getReturnType().equals(Void.TYPE);

    if (callback && !isVoid)
    {
      throw new ResourceConfigException(String.format("%s has both callback and return value",
                                                      method));
      // note that !callback && !isVoid is a legal synchronous action method
    }

    if (callback)
    {
      return InterfaceType.CALLBACK;
    }
    else if (task)
    {
      return InterfaceType.TASK;
    }
    else if (promise)
    {
      return InterfaceType.PROMISE;
    }
    else
    {
      return InterfaceType.SYNC;
    }
  }

  /**
   * @return the type of the callback parameter on the given method
   */
  private static Type getCallbackParamType(final Method method)
  {
    int i = getParamIndex(method, Callback.class);
    if (i == -1)
    {
      return null;
    }
    else
    {
      return method.getGenericParameterTypes()[i];
    }
  }

  /**
   * @return the index of the parameter of the given type, or -1 if there is no such
   *         parameter
   * @throws ResourceConfigException if the method has multiple parameters of the given
   *           type
   */
  private static int getParamIndex(final Method method, final Class type)
  {
    Type[] types = method.getGenericParameterTypes();
    int where = -1;
    for (int i = 0; i < types.length; i++)
    {
      Class c = ReflectionUtils.getClass(types[i]);
      if (c != null && type.equals(c))
      {
        if (where == -1)
        {
          where = i;
        }
        else
        {
          throw new ResourceConfigException(String.format("method '%s' has too many '%s' parameters",
                                                          method,
                                                          type));
        }
      }
    }
    return where;
  }

  /**
   * Analogous to {@link #getLogicalReturnType}. For callback and promise,
   * java.lang.Void.class is converted to Void.TYPE.
   */
  private static Class getLogicalReturnClass(final Method method)
  {
    Class c = ReflectionUtils.getClass(getLogicalReturnType(method));
    return Void.class.equals(c) ? Void.TYPE : c;
  }

  /**
   * Get the logical return type of a RestLi method. For callback-based resources, this is
   * the type argument of the callback parameter. For promise-based resources, this is the
   * type argument of the returned promise.
   */
  private static Type getLogicalReturnType(final Method method)
  {
    switch (getInterfaceType(method))
    {
      case CALLBACK:
        // Callback
        ParameterizedType callbackType = (ParameterizedType) getCallbackParamType(method);
        return callbackType.getActualTypeArguments()[0];
      case PROMISE:
      case TASK:
        // Promise or Task
        ParameterizedType promiseType = (ParameterizedType) method.getGenericReturnType();
        return promiseType.getActualTypeArguments()[0];
      case SYNC:
        return method.getGenericReturnType();
      default:
        throw new AssertionError();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy