content: package act.apidoc;
  
  /*-
   * #%L
   * ACT Framework
   * %%
   * Copyright (C) 2014 - 2017 ActFramework
   * %%
   * 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.
   * #L%
   */
  
  import static act.apidoc.SampleDataCategory.EMAIL;
  import static act.apidoc.SimpleEndpointIdProvider.className;
  import static act.apidoc.SimpleEndpointIdProvider.id;
  
  import act.Act;
  import act.app.data.StringValueResolverManager;
  import act.conf.AppConfig;
  import act.data.DataPropertyRepository;
  import act.data.Sensitive;
  import act.handler.RequestHandler;
  import act.handler.builtin.controller.RequestHandlerProxy;
  import act.handler.builtin.controller.impl.ReflectedHandlerInvoker;
  import act.inject.DefaultValue;
  import act.inject.DependencyInjector;
  import act.inject.HeaderVariable;
  import act.inject.SessionVariable;
  import act.inject.param.NoBind;
  import act.inject.param.ParamValueLoaderService;
  import act.util.*;
  import act.validation.NotBlank;
  import com.alibaba.fastjson.JSON;
  import com.alibaba.fastjson.serializer.SerializeConfig;
  import com.alibaba.fastjson.serializer.SerializeFilter;
  import com.alibaba.fastjson.serializer.SerializerFeature;
  import org.apache.bval.constraints.NotEmpty;
  import org.joda.time.*;
  import org.osgl.$;
  import org.osgl.OsglConfig;
  import org.osgl.http.H;
  import org.osgl.inject.BeanSpec;
  import org.osgl.inject.Injector;
  import org.osgl.logging.Logger;
  import org.osgl.mvc.annotation.*;
  import org.osgl.mvc.result.Result;
  import org.osgl.storage.ISObject;
  import org.osgl.storage.impl.SObject;
  import org.osgl.util.*;
  
  import java.io.File;
  import java.lang.annotation.Annotation;
  import java.lang.reflect.*;
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.util.*;
  import javax.validation.constraints.NotNull;
  
  /**
   * An `Endpoint` represents an API that provides specific service
   */
  public class Endpoint implements Comparable<Endpoint>, EndpointIdProvider {
  
      private static final Logger LOGGER = ApiManager.LOGGER;
  
      private static BeanSpecInterpreter beanSpecInterpreter = new BeanSpecInterpreter();
  
      private static boolean mapTypeQuerySampleWarned = false;
  
      public static class ParamInfo {
          public String bindName;
          public transient BeanSpec beanSpec;
          public String type;
          public String description;
          public String defaultValue;
          public H.Method httpMethod;
          public boolean required;
          public boolean sessionVariable;
          public boolean headerVariable;
          public List<String> options;
          public String fieldKey;
          public boolean body;
          public boolean pathVar;
  
          private ParamInfo() {}
  
          private ParamInfo(H.Method httpMethod, String bindName, BeanSpec beanSpec, String description, String fieldKey, boolean pathVar, boolean body) {
              this.bindName = bindName;
              this.beanSpec = beanSpec;
              this.httpMethod = httpMethod;
              this.description = description;
              this.defaultValue = checkDefaultValue(beanSpec);
              this.required = checkRequired(beanSpec);
              this.sessionVariable = checkSessionVariable(beanSpec);
              this.headerVariable = checkHeaderVariable(beanSpec);
              this.options = checkOptions(beanSpec);
              this.fieldKey = fieldKey;
              this.pathVar = pathVar;
              this.body = body;
          }
  
          public String getName() {
              if (sessionVariable) {
                  return bindName + "[S]";
              } else if (headerVariable) {
                  return bindName + "[H]";
              } else if (pathVar) {
                  return bindName + "[U]";
              } else if (httpMethod == H.Method.GET) {
                  return bindName + "[Q]";
              } else if (body) {
                  return bindName + "[B]";
              }
              return bindName;
          }
  
          public String getTooltip() {
              if (sessionVariable) {
                  return "Session variable";
              } else if (headerVariable) {
                  return "Header variable";
              } else if (pathVar) {
                  return "URL path variable";
              } else if (body) {
                  return "JSON body or post form field";
              }
              return "Query parameter";
          }
  
          public String getType() {
              return null == beanSpec ? type : beanSpecInterpreter.interpret(beanSpec);
          }
  
          public String getDescription() {
              if (S.blank(description)) {
                  if (sessionVariable) {
                      return "Session variable";
                  } else if (headerVariable) {
                      return "Header variable";
                  }
              }
              return description;
          }
  
          public void setDescription(String description) {
              this.description = description.replaceAll("\\n\\s+","\n");
          }
  
          public boolean isRequired() {
              return required;
          }
  
          public List<String> getOptions() {
              return options;
          }
  
          public String getDefaultValue() {
              return defaultValue;
          }
  
          private String checkDefaultValue(BeanSpec spec) {
              DefaultValue def = spec.getAnnotation(DefaultValue.class);
              String defStr = "";
              if (null != def) {
                  defStr = def.value();
              }
              Class<?> type = spec.rawType();
              try {
                  Object o = Act.app().resolverManager().resolve(defStr, type);
                  return $.toString2(o);
              } catch (Exception e) {
                  return "";
              }
          }
  
          private boolean checkRequired(BeanSpec spec) {
              return (spec.hasAnnotation(NotNull.class)
                      || spec.hasAnnotation(NotBlank.class)
                      || spec.hasAnnotation(NotEmpty.class));
          }
  
          private boolean checkSessionVariable(BeanSpec spec) {
              return spec.hasAnnotation(SessionVariable.class);
          }
  
          private boolean checkHeaderVariable(BeanSpec spec) {
              return spec.hasAnnotation(HeaderVariable.class);
          }
  
          private List<String> checkOptions(BeanSpec spec) {
              Class<?> type = spec.rawType();
              if (type.isEnum()) {
                  return C.listOf(type.getEnumConstants()).map($.F.asString());
              }
              return null;
          }
      }
  
      /**
       * The scheme defines the protocol used to access the endpoint
       *
       * At the moment we support HTTP only
       */
      public enum Scheme {
          HTTP
      }
  
      /**
       * unique identify an endpoint in an application.
       *
       * Generated from class and method name via {@link SimpleEndpointIdProvider#id(Class, Method)}
       */
      public String id;
  
      public transient EndpointIdProvider parent;
  
      /**
       * The scheme used to access the endpoint
       */
      public Scheme scheme = Scheme.HTTP;
  
      public int port;
  
      /**
       * The HTTP method
       */
      public H.Method httpMethod;
  
      /**
       * The URL path
       */
      public String path;
  
      /**
       * The handler.
       *
       * In most case should be `pkg.Class.method`
       */
      public String handler;
  
      FastJsonPropertyPreFilter fastJsonPropertyPreFilter;
  
      /**
       * The description
       */
      public String description;
  
      /**
       * The return info description
       */
      public String returnDescription;
  
      public String module;
  
      private transient BeanSpec returnType;
  
      private Map<String, Class> typeLookups;
  
      private Set<String> urlPathVarNames;
  
      public String returnSample;
  
      public transient Object returnSampleObject;
  
      /**
       * Param list.
       *
       * Only available when handler is driven by
       * {@link act.handler.builtin.controller.impl.ReflectedHandlerInvoker}
       */
      public List<ParamInfo> params = new ArrayList<>();
  
      public String sampleJsonPost;
      public String sampleQuery;
      public Class<?> controllerClass;
      public transient SampleDataProviderManager sampleDataProviderManager;
  
      private Endpoint() {}
  
      Endpoint(int port, H.Method httpMethod, String path, Set<String> urlPathVarNames, RequestHandler handler) {
          AppConfig conf = Act.appConfig();
          this.httpMethod = $.requireNotNull(httpMethod);
          String urlContext = conf.urlContext();
          this.path = null == urlContext || path.startsWith("/~/") ? $.requireNotNull(path) : S.concat(urlContext, $.requireNotNull(path));
          this.handler = handler.toString();
          this.port = port;
          this.urlPathVarNames = urlPathVarNames;
          this.sampleDataProviderManager = Act.app().sampleDataProviderManager();
          explore(handler);
      }
  
      @Override
      public int compareTo(Endpoint o) {
          int n = path.compareTo(o.path);
          if (0 != n) {
              return n;
          }
          return httpMethod.ordinal() - o.httpMethod.ordinal();
      }
  
      @Override
      public String toString() {
          return id;
      }
  
      public String getId() {
          return id;
      }
  
      @Override
      public String getParentId() {
          return null == parent ? null : parent.getId();
      }
  
      /**
       * Returns extends id. This is the concatenation of
       * {@link #httpMethod} and {@link #id}. This will
       * be used by the frontend UI.
       *
       * @return the extended id
       */
      public String getXid() {
          return S.concat(httpMethod, id.replace('.', '_'));
      }
  
      public Scheme getScheme() {
          return scheme;
      }
  
      public int getPort() {
          return port;
      }
  
      public H.Method getHttpMethod() {
          return httpMethod;
      }
  
      public String getPath() {
          return path;
      }
  
      public String getPathHtml() {
          String s = path.replace("{", "<span class='var'>{");
          return s.replace("}", "}</span>");
      }
  
      public String getHandler() {
          return handler;
      }
  
      public String getDescription() {
          return description;
      }
  
      public void setDescription(String description) {
          this.description = processTypeImplSubstitution(description);
      }
  
      public String getModule() {
          return module;
      }
  
      public void setModule(String module) {
          this.module = module;
      }
  
      public List<ParamInfo> getParams() {
          return params;
      }
  
      public BeanSpec returnType() {
          return returnType;
      }
  
      public String getReturnSample() {
          return returnSample;
      }
  
      public String getReturnType() {
          return null == returnType ? null : returnType.toString();
      }
  
      public String getSampleJsonPost() {
          return sampleJsonPost;
      }
  
      public String getSampleQuery() {
          return sampleQuery;
      }
  
      public Class<?> controllerClass() {
          return controllerClass;
      }
  
      public String processTypeImplSubstitution(String s) {
          if (null == s) {
              return null;
          }
          int n = s.indexOf("${");
          if (n < 0) {
              return s;
          }
          int a = 0;
          int z = n;
          S.Buffer buf = S.buffer();
          while (true) {
              buf.append(s.substring(a, z));
              n = s.indexOf("}", z);
              a = n;
              E.illegalArgumentIf(n < -1, "Invalid string: " + s);
              String key = s.substring(z + 2, a);
              Class<?> impl = typeLookups.get(key);
              if (null != impl) {
                  buf.append(Keyword.of(className(impl)).readable().toLowerCase());
              } else {
                  buf.append("${").append(key).append("}");
              }
              n = s.indexOf("${", a);
              if (n < 0) {
                  buf.append(s.substring(a + 1));
                  return buf.toString();
              }
              z = n;
          }
      }
  
      private void explore(RequestHandler handler) {
          RequestHandlerProxy proxy = $.cast(handler);
          ReflectedHandlerInvoker invoker = $.cast(proxy.actionHandler().invoker());
          Class<?> controllerClass = invoker.controllerClass();
          typeLookups = Generics.buildTypeParamImplLookup(controllerClass);
          Method method = invoker.method();
          returnType = BeanSpec.of(method.getGenericReturnType(), Act.injector(), typeLookups);
          PropertySpec pspec = method.getAnnotation(PropertySpec.class);
          if (null != pspec) {
              PropertySpec.MetaInfo propSpec = new PropertySpec.MetaInfo();
              for (String v : pspec.value()) {
                  propSpec.onValue(v);
              }
              for (String v : pspec.http()) {
                  propSpec.onValue(v);
              }
              List<String> outputs = propSpec.outputFieldsForHttp();
              Set<String> excluded = propSpec.excludeFieldsForHttp();
              if (!(outputs.isEmpty() && excluded.isEmpty())) {
                  fastJsonPropertyPreFilter = new FastJsonPropertyPreFilter(returnType.rawType(), outputs, excluded, Act.app().service(DataPropertyRepository.class));
              }
              // just ignore cli value here
          }
          Module classModule = controllerClass.getAnnotation(Module.class);
          String classModuleText = null == classModule ? inferModule(controllerClass) : classModule.value();
          this.id = id(controllerClass, method);
          Class<?> superClass = controllerClass.getSuperclass();
          try {
              Method overwrittenMethod = superClass.getMethod(method.getName(), method.getParameterTypes());
              parent = new SimpleEndpointIdProvider(overwrittenMethod.getDeclaringClass(), method);
          } catch (Exception e) {
              // so we don't have an overwritten method, that's fine, just ignore the exception
          }
          Map<String, Class> typeParamLookup = C.newMap();
          if (controllerClass.getGenericSuperclass() instanceof ParameterizedType) {
              typeParamLookup = Generics.buildTypeParamImplLookup(controllerClass);
          }
          Description descAnno = method.getAnnotation(Description.class);
          this.description = null == descAnno ? id(controllerClass, method) : descAnno.value();
          Module methodModule = method.getAnnotation(Module.class);
          this.module = null == methodModule ? classModuleText : methodModule.value();
          boolean payloadMethod = H.Method.POST == httpMethod || H.Method.PUT == httpMethod || H.Method.PATCH == httpMethod;
          boolean body = payloadMethod && (null != invoker.singleJsonFieldName() || invoker.fieldsAndParamsCount() - urlPathVarNames.size() == 1);
          exploreParamInfo(method, typeParamLookup, body);
          if (!Modifier.isStatic(method.getModifiers())) {
              exploreParamInfo(controllerClass, typeParamLookup, body);
          }
          this.controllerClass = controllerClass;
          try {
              this.returnSample = null == returnType ? null : generateSampleJson(returnType, typeParamLookup, true);
          } catch (Exception e) {
              LOGGER.warn(e, "Error creating returnSample of endpoint for request handler [%s] for [%s %s]", handler, httpMethod, path);
          }
      }
  
      private String inferModule(Class<?> controllerClass) {
          Class<?> enclosingClass = controllerClass.getEnclosingClass();
          if (null != enclosingClass) {
              return inferModule(enclosingClass);
          }
          return controllerClass.getSimpleName();
      }
  
      private void exploreParamInfo(Method method, Map<String, Class> typeParamLookup, boolean body) {
          Type[] paramTypes = method.getGenericParameterTypes();
          int paramCount = paramTypes.length;
          if (0 == paramCount) {
              return;
          }
          DependencyInjector injector = Act.injector();
          Method declaredMethod = overridenRequestHandlerMethod(method);
          if (null == declaredMethod) {
              return;
          }
          Annotation[][] allAnnos = declaredMethod.getParameterAnnotations();
          Map<String, Object> sampleData = new HashMap<>();
          StringValueResolverManager resolver = Act.app().resolverManager();
          List<String> sampleQuery = new ArrayList<>();
          for (int i = 0; i < paramCount; ++i) {
              Type type = paramTypes[i];
              Annotation[] annos = allAnnos[i];
              ParamInfo info = paramInfo(type, typeParamLookup, annos, injector, null, null, body);
              if (null != info) {
                  params.add(info);
                  if (info.pathVar) {
                      // no sample data for URL path variable
                      continue;
                  }
                  Object sample;
                  sample = generateSampleData(info.beanSpec, typeParamLookup, new HashSet<Type>(), new ArrayList<String>(), false);
                  if (null == sample && null != info.defaultValue) {
                      sample = resolver.resolve(info.defaultValue, info.beanSpec.rawType());
                  }
                  if (H.Method.GET == this.httpMethod) {
                      try {
                          String query = generateSampleQuery(info.beanSpec.withoutName(), typeParamLookup, info.bindName, new HashSet<Type>(), C.<String>newList());
                          if (S.notBlank(query)) {
                              sampleQuery.add(query);
                          }
                      } catch (Exception e) {
                          LOGGER.warn("error generating sample query for method: %s", info.beanSpec);
                      }
                  } else {
                      sampleData.put(info.bindName, sample);
                  }
              }
          }
          if (!sampleData.isEmpty()) {
              Object payload = sampleData;
              if (sampleData.size() == 1) {
                  payload = sampleData.values().iterator().next();
              }
              if (null != payload && $.isSimpleType(payload.getClass())) {
                  payload = sampleData;
              }
              if (null != payload) {
                  sampleJsonPost = JSON.toJSONString(payload, true);
              }
          }
          if (!sampleQuery.isEmpty()) {
              this.sampleQuery = S.join("&", sampleQuery);
          }
      }
  
      // we don't need fields declared in `@NoBind` or `@Stateless` classes
      private static final $.Predicate<Field> FIELD_PREDICATE = new $.Predicate<Field>() {
          @Override
          public boolean test(Field field) {
              return !ParamValueLoaderService.shouldWaive(field);
          }
      };
  
      private void exploreParamInfo(Class<?> controller, Map<String, Class> typeParamLookup, boolean body) {
          DependencyInjector injector = Act.injector();
          List<Field> fields = $.fieldsOf(controller, FIELD_PREDICATE);
          for (Field field : fields) {
              Type type = field.getGenericType();
              Annotation[] annos = field.getAnnotations();
              String fieldKey = field.getDeclaringClass().getName().replace('$', '.') + "." + field.getName();
              ParamInfo info = paramInfo(type, typeParamLookup, annos, injector, field.getName(), fieldKey, body);
              if (null != info) {
                  params.add(info);
              }
          }
      }
  
      private ParamInfo paramInfo(
              Type type, Map<String, Class> typeParamLookup, Annotation[] annos,
              DependencyInjector injector, String name, String fieldKey, boolean body
      ) {
          if (isLoginUser(annos)) {
              return null;
          }
          BeanSpec spec = BeanSpec.of(type, annos, name, injector, typeParamLookup);
          if (ParamValueLoaderService.providedButNotDbBind(spec, injector)) {
              return null;
          }
          if (org.osgl.util.S.blank(name)) {
              name = spec.name();
          }
          boolean pathVar = urlPathVarNames.contains(name);
          if (ParamValueLoaderService.hasDbBind(spec.allAnnotations())) {
              return new ParamInfo(httpMethod, name, BeanSpec.of(String.class, injector, typeParamLookup), name + " id", fieldKey, pathVar, body);
          }
          String description = "";
          Description descAnno = spec.getAnnotation(Description.class);
          if (null != descAnno) {
              description = descAnno.value();
          }
          return new ParamInfo(httpMethod, name, spec, description, fieldKey, pathVar, body);
      }
  
      private boolean isLoginUser(Annotation[] annos) {
          for (Annotation a : annos) {
              if ("LoginUser".equals(a.annotationType().getSimpleName())) {
                  return true;
              }
          }
          return false;
      }
  
      private boolean isValidJson(String s) {
          return S.is(s).wrappedWith(S.BRACES) || S.is(s).wrappedWith(S.BRACKETS);
      }
  
      private String generateSampleJson(BeanSpec spec, Map<String, Class> typeParamLookup, boolean isReturn) {
          Class<?> type = spec.rawType();
          if (Result.class.isAssignableFrom(type) || void.class == type) {
              return null;
          }
          returnSampleObject = generateSampleData(spec, typeParamLookup, new HashSet<Type>(), new ArrayList<String>(), isReturn);
          if (null == returnSampleObject) {
              return null;
          }
          if (returnSampleObject instanceof Map && ((Map) returnSampleObject).isEmpty()) {
              return null;
          }
          if (!type.isArray() && $.isSimpleType(type) && (String.class != type || !isValidJson(S.string(returnSampleObject)))) {
              returnSampleObject = C.Map("result", returnSampleObject);
          }
          SerializeFilter[] filters = new SerializeFilter[null == fastJsonPropertyPreFilter ? 0 : 1];
          if (null != fastJsonPropertyPreFilter) {
              filters[0] = fastJsonPropertyPreFilter;
          }
          SerializerFeature[] features = new SerializerFeature[] {
                  SerializerFeature.PrettyFormat
          };
          return JSON.toJSONString(returnSampleObject, SerializeConfig.globalInstance, filters, features);
      }
  
      private String generateSampleQuery(BeanSpec spec, Map<String, Class> typeParamLookup, String bindName, Set<Type> typeChain, List<String> nameChain) {
          Class<?> type = spec.rawType();
          typeParamLookup = new HashMap<>(typeParamLookup);
          Generics.buildTypeParamImplLookup(type, typeParamLookup);
          String specName = spec.name();
          if (S.notBlank(specName)) {
              nameChain.add(specName);
          }
          if (type.isArray()) {
              // TODO handle datetime component type
              Class<?> elementType = type.getComponentType();
              BeanSpec elementSpec = BeanSpec.of(elementType, Act.injector(), typeParamLookup);
              if ($.isSimpleType(elementType)) {
                  Object o = generateSampleData(elementSpec, typeParamLookup, typeChain, nameChain, false);
                  if (null == o) {
                      return "";
                  }
                  return bindName + "=" + o
                          + "&" + bindName + "=" + generateSampleData(elementSpec, typeParamLookup, typeChain, nameChain, false);
              }
          } else if (Collection.class.isAssignableFrom(type)) {
              // TODO handle datetime component type
              List<Type> typeParams = spec.typeParams();
              Type elementType = typeParams.isEmpty() ? Object.class : typeParams.get(0);
              BeanSpec elementSpec = BeanSpec.of(elementType, null, Act.injector(), typeParamLookup);
              if ($.isSimpleType(elementSpec.rawType())) {
                  Object o = generateSampleData(elementSpec, typeParamLookup, typeChain, nameChain, false);
                  if (null == o) {
                      return "";
                  }
                  return bindName + "=" + o
                          + "&" + bindName + "=" + generateSampleData(elementSpec, typeParamLookup, typeChain, nameChain, false);
              }
          } else if (Map.class.isAssignableFrom(type)) {
              if (!mapTypeQuerySampleWarned) {
                  mapTypeQuerySampleWarned = true;
                  LOGGER.warn("Query sample does not support Map type! Make sure your @GetAction handler params/fields be simple types");
              }
              return "";
          } else if (ReadableInstant.class.isAssignableFrom(type)) {
              return bindName + "=<datetime>";
          }
          if ($.isSimpleType(type)) {
              Object o = generateSampleData(spec, typeParamLookup, typeChain, nameChain, false);
              if (null == o) {
                  return "";
              }
              return bindName + "=" + o;
          }
          if (null != stringValueResolver(type)) {
              return bindName + "=" + sampleDataFromAnnotation(spec, bindName);
          }
          List<String> queryPairs = new ArrayList<>();
          List<Field> fields = $.fieldsOf(type);
          for (Field field : fields) {
              if (ParamValueLoaderService.shouldWaive(field)) {
                  continue;
              }
              String fieldBindName = bindName + "." + field.getName();
              String pair = generateSampleQuery(BeanSpec.of(field, Act.injector(), typeParamLookup), typeParamLookup, fieldBindName, C.newSet(typeChain), C.newList(nameChain));
              if (S.notBlank(pair)) {
                  queryPairs.add(pair);
              }
          }
          return S.join(queryPairs).by("&").get();
      }
  
      private static Object sampleDataFromAnnotation(BeanSpec spec, String bindName) {
          SampleData.ProvidedBy annoProvided = spec.getAnnotation(SampleData.ProvidedBy.class);
          if (null != annoProvided) {
              return Act.getInstance(annoProvided.value()).get();
          }
          SampleData.StringList annoStrList = spec.getAnnotation(SampleData.StringList.class);
          if (null != annoStrList) {
              return $.random(annoStrList.value());
          }
          SampleData.IntList annoIntList = spec.getAnnotation(SampleData.IntList.class);
          if (null != annoIntList) {
              int[] ia = annoIntList.value();
              return $.random(ia);
          }
          SampleData.DoubleList annoDblList = spec.getAnnotation(SampleData.DoubleList.class);
          if (null != annoDblList) {
              return $.random(annoDblList.value());
          }
          SampleData.Category anno = spec.getAnnotation(SampleData.Category.class);
          SampleDataCategory category = null != anno ? anno.value() : null;
          return Act.getInstance(SampleDataProviderManager.class).getSampleData(category, bindName, spec.rawType());
      }
  
      private static boolean isCollection(Type type) {
          if (type instanceof Class) {
              Class clazz = $.cast(type);
              if (Iterable.class.isAssignableFrom(clazz)) {
                  return true;
              }
              return false;
          }
          if (type instanceof ParameterizedType) {
              ParameterizedType ptype = $.cast(type);
              return isCollection(ptype.getRawType());
          }
          return false;
      }
  
      private Object generateSampleData(BeanSpec spec, Map<String, Class> typeParamLookup, Set<Type> typeChain, List<String> nameChain, boolean isReturn) {
          return generateSampleData(spec, typeParamLookup, typeChain, nameChain, isReturn ? fastJsonPropertyPreFilter : null, isReturn);
      }
  
      public static Object generateSampleData(
              BeanSpec spec, Map<String, Class> typeParamLookup,
              Set<Type> typeChain, List<String> nameChain,
              FastJsonPropertyPreFilter fastJsonPropertyPreFilter,
              boolean isReturn
      ) {
          Type type = spec.type();
          if (void.class == type) {
              return null;
          }
          if (typeChain.contains(type) && !isCollection(type) && !$.isSimpleType(spec.rawType())) {
              return S.concat(spec.name(), ":", type); // circular reference detected
          }
          typeChain.add(type);
          String name = spec.name();
          if (S.notBlank(name)) {
              nameChain.add(name);
          }
          if (null != fastJsonPropertyPreFilter) {
              String path = S.join(nameChain).by(".").get();
              if (S.notBlank(path) && !fastJsonPropertyPreFilter.matches(path)) {
                  return null;
              }
          }
          Object o = sampleDataFromAnnotation(spec, name);
          if (null != o) {
              return o;
          }
          Class<?> classType = spec.rawType();
          if (type instanceof ParameterizedType) {
              ParameterizedType ptype = (ParameterizedType) type;
              Type[] actualTypeArguments = ptype.getActualTypeArguments();
              TypeVariable[] typeVariables = classType.getTypeParameters();
              Generics.buildTypeParamImplLookup("", actualTypeArguments, typeVariables, typeParamLookup);
          }
          SampleDataProviderManager sampleDataProviderManager = Act.getInstance(SampleDataProviderManager.class);
          SampleData.Category anno = spec.getAnnotation(SampleData.Category.class);
          SampleDataCategory category = null != anno ? anno.value() : null;
          try {
              if (void.class == classType || Void.class == classType || Result.class.isAssignableFrom(classType)) {
                  return null;
              }
              if (Object.class == classType) {
                  return "<Any>";
              }
              try {
                  if (classType.isEnum()) {
                      Class<? extends Enum<?>> ec = $.cast(classType);
                      return $.random(ec);
                  } else if (Locale.class == classType) {
                      return (Act.appConfig().locale());
                  } else if (String.class == classType || Keyword.class == classType) {
                      String mockValue = sampleDataProviderManager.getSampleData(category, name, String.class);
                      if (spec.hasAnnotation(Sensitive.class)) {
                          return Act.crypto().encrypt(mockValue);
                      }
                      return Keyword.class == classType ? Keyword.of(mockValue) : mockValue;
                  } else if (classType.isArray()) {
                      int len = 2 + N.randInt(5);
                      Object sample = Array.newInstance(classType.getComponentType(), len);
                      for (int i = 0; i < len; ++i) {
                          Array.set(sample, i, generateSampleData(BeanSpec.of(classType.getComponentType(), Act.injector(), typeParamLookup), typeParamLookup, C.newSet(typeChain), C.newList(nameChain), fastJsonPropertyPreFilter, isReturn));
                      }
                      return sample;
                  } else if ($.isSimpleType(classType)) {
                      if (!classType.isPrimitive()) {
                          classType = $.primitiveTypeOf(classType);
                      }
                      if (String.class == classType) {
                          return S.random();
                      } else if (int.class == classType) {
                          return N.randInt(10000);
                      } else if (char.class == classType) {
                          return 48 + N.randInt(41);
                      } else if (short.class == classType) {
                          return N.randInt(256);
                      } else if (byte.class == classType) {
                          return N.randInt(128);
                      } else if (long.class == classType) {
                          return N.randLong();
                      } else if (float.class == classType) {
                          return N.randFloat();
                      } else if (double.class == classType) {
                          return N.randDouble();
                      } else if (boolean.class == classType) {
                          return $.random(true, false);
                      }
                      return StringValueResolver.predefined().get(classType).resolve(null);
                  } else if (LocalDateTime.class.isAssignableFrom(classType)) {
                      return sampleDataProviderManager.getSampleData(category, name, LocalDateTime.class);
                  } else if (DateTime.class.isAssignableFrom(classType)) {
                      return sampleDataProviderManager.getSampleData(category, name, DateTime.class);
                  } else if (LocalDate.class.isAssignableFrom(classType)) {
                      return sampleDataProviderManager.getSampleData(category, name, LocalDate.class);
                  } else if (LocalTime.class.isAssignableFrom(classType)) {
                      return LocalTime.now();
                  } else if (Date.class.isAssignableFrom(classType)) {
                      return sampleDataProviderManager.getSampleData(category, name, Date.class);
                  } else if (BigDecimal.class == classType) {
                      return BigDecimal.valueOf(1.1);
                  } else if (BigInteger.class == classType) {
                      return BigInteger.valueOf(1);
                  } else if (File.class == classType) {
                      return new File("/path/to/upload/file");
                  } else if (ISObject.class.isAssignableFrom(classType) || SObject.class.isAssignableFrom(classType)) {
                      return SObject.of("/path/to/upload/file", "");
                  } else if (Map.class.isAssignableFrom(classType)) {
                      Map map = $.cast(Act.getInstance(classType));
                      List<Type> typeParams = spec.typeParams();
                      if (typeParams.isEmpty()) {
                          typeParams = Generics.tryGetTypeParamImplementations(classType, Map.class);
                      }
                      if (typeParams.size() < 2) {
                          return null;
                      } else {
                          Type keyType = typeParams.get(0);
                          Type valType = typeParams.get(1);
                          if (Object.class == valType) {
                              return null;
                          }
                          int len = 2 + N.randInt(5);
                          for (int i = 0; i < len; ++i) {
                              Object key = String.class == keyType ? S.random() : generateSampleData(BeanSpec.of(keyType, null, Act.injector(), typeParamLookup), typeParamLookup, C.newSet(typeChain), C.newList(nameChain), fastJsonPropertyPreFilter, isReturn);
                              Object val = generateSampleData(BeanSpec.of(valType, null, Act.injector(), typeParamLookup), typeParamLookup, C.newSet(typeChain), C.newList(nameChain), fastJsonPropertyPreFilter, isReturn);;
                              map.put(key, val);
                          }
                      }
                      return map;
                  } else if (Iterable.class.isAssignableFrom(classType)) {
                      Collection col = $.cast(Act.getInstance(classType));
                      List<Type> typeParams = spec.typeParams();
                      if (typeParams.isEmpty()) {
                          typeParams = Generics.tryGetTypeParamImplementations(classType, Map.class);
                      }
                      if (typeParams.isEmpty()) {
                          return null;
                      } else {
                          Type componentType = typeParams.get(0);
                          int len = 2 + N.randInt(5);
                          for (int i = 0; i < len; ++i) {
                              col.add(generateSampleData(BeanSpec.of(componentType, null, Act.injector(), typeParamLookup), typeParamLookup, C.newSet(typeChain), C.newList(nameChain), fastJsonPropertyPreFilter, isReturn));
                          }
                      }
                      return col;
                  }
  
                  Object obj = sampleDataProviderManager.getSampleData(category, name, classType);
                  if (null != obj) {
                      return obj;
                  }
  
                  Injector injector = Act.injector();
                  try {
                      obj = Act.getInstance(classType);
                  } catch (Exception e) {
                      Method emailGetter = null;
                      Map<String, Object> map = new HashMap<>();
                      Method[] ma = classType.getMethods();
                      for (Method m : ma) {
                          if (!Modifier.isStatic(m.getModifiers()) && m.getName().startsWith("get") && m.getReturnType() != void.class) {
                              if (shouldWaive(m, classType)) {
                                  continue;
                              }
                              Class<?> propertyClass = m.getReturnType();
                              Object val;
                              try {
                                  String propertyName = m.getName().substring(3);
                                  if ("name".equalsIgnoreCase(propertyName)) {
                                      propertyName = m.getDeclaringClass().getSimpleName();
                                  }
                                  Annotation[] annotations = m.getDeclaredAnnotations();
                                  Type propertyType = m.getGenericReturnType();
                                  if (propertyType instanceof TypeVariable) {
                                      propertyType = propertyClass;
                                  }
                                  BeanSpec propertySpec = BeanSpec.of(propertyType, annotations, propertyName, injector, m.getModifiers(), typeParamLookup);
                                  if (null == emailGetter && isEmail(propertySpec)) {
                                      emailGetter = m;
                                  } else {
                                      val = generateSampleData(propertySpec, typeParamLookup, C.newSet(typeChain), C.newList(nameChain), fastJsonPropertyPreFilter, isReturn);
                                      if (null != val) {
                                          Class<?> valType = val.getClass();
                                          if (!propertyClass.isAssignableFrom(valType)) {
                                              val = $.convert(val).to(propertyClass);
                                          }
                                          if (null != val) {
                                              map.put(propertyName, val);
                                          }
                                      }
                                  }
                              } catch (Exception e1) {
  
                              }
                          }
                      }
                      if (null != emailGetter) {
                          String mockEmail = sampleDataProviderManager.getSampleData(SampleDataCategory.EMAIL, name, String.class);
                          map.put(emailGetter.getName().substring(3), mockEmail);
                      }
                      return map;
                  }
                  List<Field> fields = $.fieldsOf(classType);
                  Field emailField = null;
                  for (Field field : fields) {
                      if (Modifier.isStatic(field.getModifiers())) {
                          continue;
                      }
                      if (!isReturn) {
                          if (ParamValueLoaderService.shouldWaive(field)) {
                              continue;
                          }
                      } else {
                          // for return type we shouldn't waive NoBind fields
                          int modifiers = field.getModifiers();
                          if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers)) {
                              continue;
                          }
                          String fieldName = field.getName();
                          Class<?> entityType = field.getDeclaringClass();
                          boolean shouldWaive = Object.class.equals(entityType)
                                  || Class.class.equals(entityType)
                                  || OsglConfig.globalMappingFilter_shouldIgnore(fieldName);
                          if (shouldWaive) {
                              continue;
                          }
                      }
                      Class<?> fieldClass = field.getType();
                      Object val = null;
                      try {
                          field.setAccessible(true);
                          String fieldName = field.getName();
                          if ("name".equalsIgnoreCase(fieldName)) {
                              fieldName = field.getDeclaringClass().getSimpleName();
                          }
                          Annotation[] annotations = field.getDeclaredAnnotations();
                          Type fieldType = field.getGenericType();
                          if (fieldType instanceof TypeVariable) {
                              fieldType = fieldClass;
                          }
                          BeanSpec fieldSpec = BeanSpec.of(fieldType, annotations, fieldName, injector, field.getModifiers(), typeParamLookup);
                          if (null == emailField && isEmail(fieldSpec)) {
                              if (null != fastJsonPropertyPreFilter) {
                                  List<String> newNameChain = C.newList(nameChain).append(fieldSpec.name());
                                  String path = S.join(newNameChain).by(".").get();
                                  if (fastJsonPropertyPreFilter.matches(path)) {
                                      emailField = field;
                                  }
                              } else {
                                  emailField = field;
                              }
                          } else {
                              val = generateSampleData(fieldSpec, typeParamLookup, C.newSet(typeChain), C.newList(nameChain), fastJsonPropertyPreFilter, isReturn);
                              if (null == val) {
                                  continue;
                              }
                              Class<?> valType = val.getClass();
                              if (!fieldClass.isAssignableFrom(valType)) {
                                  val = $.convert(val).to(fieldClass);
                              }
                              if (null != val) {
                                  if (valType == String.class && fieldSpec.hasAnnotation(Sensitive.class)) {
                                      val = Act.app().crypto().encrypt((String) val);
                                  }
                                  field.set(obj, val);
                              }
                          }
                      } catch (Exception e2) {
                          LOGGER.warn("Error setting value[%s] to field[%s.%s]", val, classType.getSimpleName(), field.getName());
                      }
                  }
                  if (null != emailField) {
                      String mockEmail = sampleDataProviderManager.getSampleData(SampleDataCategory.EMAIL, name, String.class);
                      $.setFieldValue(obj, emailField, mockEmail);
                  }
                  return obj;
              } catch (Exception e) {
                  LOGGER.warn("error generating sample data for type: %s", classType);
                  return null;
              }
          } finally {
              //typeChain.remove(classType);
          }
      }
  
      private static boolean isEmail(BeanSpec spec) {
          SampleData.Category anno = spec.getAnnotation(SampleData.Category.class);
          SampleDataCategory category = null != anno ? anno.value() : null;
          if (null != category && category != EMAIL) {
              return false;
          }
          category = SampleDataCategory.of(spec.name());
          return category == EMAIL;
      }
  
      private static <T> StringValueResolver stringValueResolver(Class<? extends T> type) {
          return Act.app().resolverManager().resolver(type);
      }
  
      // see ParamValueLoaderService.shouldWaive(Field)
      private static boolean shouldWaive(Method getter, Class<?> implementClass) {
          int modifiers = getter.getModifiers();
          if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers)) {
              return true;
          }
          String fieldName = getter.getName().substring(3);
          Class<?> entityType = Generics.getReturnType(getter, implementClass);
          return ParamValueLoaderService.noBind(entityType)
                  || getter.isAnnotationPresent(NoBind.class)
                  || getter.isAnnotationPresent(Stateless.class)
                  || getter.isAnnotationPresent(Global.class)
                  || ParamValueLoaderService.isInBlackList(fieldName)
                  || Object.class.equals(entityType)
                  || Class.class.equals(entityType)
                  || OsglConfig.globalMappingFilter_shouldIgnore(fieldName);
      }
  
      private static Method overridenRequestHandlerMethod(Method method) {
          if (!Modifier.isPublic(method.getModifiers())) {
              return null;
          }
          if (isRequestHandler(method)) {
              return method;
          }
          Method overridenMethod = overridenMethod(method);
          return null == overridenMethod ? null : overridenRequestHandlerMethod(overridenMethod);
      }
  
      private static Method overridenMethod(Method method) {
          Class<?> host = method.getDeclaringClass();
          Class<?> superHost = host.getSuperclass();
          if (null == superHost || Object.class == superHost) {
              return null;
          }
          Method[] ma = superHost.getMethods();
          for (Method m : ma) {
              if (m.getName().equals(method.getName()) && $.eq2(method.getParameterTypes(), method.getParameterTypes())) {
                  return m;
              }
          }
          return null;
      }
  
      private static boolean isRequestHandler(Method method) {
          return method.isAnnotationPresent(Action.class)
                  || method.isAnnotationPresent(GetAction.class)
                  || method.isAnnotationPresent(PutAction.class)
                  || method.isAnnotationPresent(PostAction.class)
                  || method.isAnnotationPresent(PatchAction.class)
                  || method.isAnnotationPresent(DeleteAction.class);
      }
  
  
  }
  