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

com.outbrain.ob1k.server.registry.ServiceRegistry Maven / Gradle / Ivy

The newest version!
package com.outbrain.ob1k.server.registry;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.concurrent.Executor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;

import com.google.common.collect.Maps;
import com.outbrain.ob1k.HttpRequestMethodType;
import com.outbrain.ob1k.Request;
import com.outbrain.ob1k.common.concurrent.ComposableFutureHelper;
import com.outbrain.ob1k.common.filters.ServiceFilter;
import com.outbrain.ob1k.common.filters.StreamFilter;
import com.outbrain.ob1k.Service;
import com.outbrain.ob1k.common.marshalling.RequestMarshallerRegistry;
import com.outbrain.ob1k.common.marshalling.TypeHelper;
import com.outbrain.ob1k.common.filters.AsyncFilter;
import com.outbrain.ob1k.server.MethodParamNamesExtractor;
import com.outbrain.ob1k.common.filters.SyncFilter;
import com.outbrain.ob1k.server.registry.endpoints.AbstractServerEndpoint;
import com.outbrain.ob1k.server.registry.endpoints.AsyncServerEndpoint;
import com.outbrain.ob1k.server.registry.endpoints.StreamServerEndpoint;
import com.outbrain.ob1k.server.registry.endpoints.SyncServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;

/**
 * User: aronen
 * Date: 6/23/13
 * Time: 11:25 AM
 */
public class ServiceRegistry {
  private static final Logger logger = LoggerFactory.getLogger(ServiceRegistry.class);

  private final PathTrie> endpoints;
  private String contextPath;
  private final RequestMarshallerRegistry marshallerRegistry;

  public ServiceRegistry(final RequestMarshallerRegistry marshallerRegistry) {
    this.endpoints = new PathTrie<>();
    this.marshallerRegistry = marshallerRegistry;
  }

  public void setContextPath(final String contextPath) {
    this.contextPath = contextPath;
  }

  public AbstractServerEndpoint findEndpoint(final String path, final HttpRequestMethodType requestMethodType, final Map pathParams) {
    final Map serviceEndpoints = endpoints.retrieve(path, pathParams);
    if (serviceEndpoints == null) {
      return null;
    }
    if (serviceEndpoints.containsKey(HttpRequestMethodType.ANY)) {
      return serviceEndpoints.get(HttpRequestMethodType.ANY);
    }
    return serviceEndpoints.get(requestMethodType);
  }

  public void register(final String name, final Service service, final boolean bindPrefix,
                       final Executor executorService) {
    register(name, service, null, null, null, bindPrefix, executorService);
  }

  public static class EndpointDescriptor {
    public final Method method;
    public final List filters;
    public final HttpRequestMethodType requestMethodType;

    public EndpointDescriptor(final Method method, final List filters, final HttpRequestMethodType requestMethodType) {
      this.method = method;
      this.filters = filters;
      this.requestMethodType = requestMethodType;
    }
  }

  public void register(final String name, final Service service, final Map> descriptors,
                       final boolean bindPrefix, final Executor executorService) {

    if (contextPath == null) {
      throw new RuntimeException("Can't add service before context path is set.");
    }

    final Map> methodsParams;

    try {
      methodsParams = MethodParamNamesExtractor.extract(service.getClass(), getMethods(descriptors));
    } catch (final Exception e) {
      throw new RuntimeException("Service " + name + " can't be analyzed", e);
    }

    /**
     * Building full path
     */
    for (final String methodBind: descriptors.keySet()) {

      final StringBuilder path = new StringBuilder();

      path.append(contextPath);

      if (!contextPath.endsWith("/")) {
        path.append('/');
      }

      if (name.startsWith("/")) {
        path.append(name.substring(1));
      } else {
        path.append(name);
      }

      if (!name.endsWith("/")) {
        path.append('/');
      }

      if (methodBind.startsWith("/")) {
        path.append(methodBind.substring(1));
      } else {
        path.append(methodBind);
      }

      final Map endpointDescriptors = descriptors.get(methodBind);
      final Map endpointsMap = new HashMap<>();

      if (endpointDescriptors.containsKey(HttpRequestMethodType.ANY) && endpointDescriptors.size() > 1) {
        throw new RuntimeException("Cannot add more request methods for the path after defining an ANY (all) endpoint path");
      }

      for (final Map.Entry endpointDescriptorEntry : endpointDescriptors.entrySet()) {
        final EndpointDescriptor endpointDesc = endpointDescriptorEntry.getValue();
        final Method method = endpointDesc.method;
        final List methodParamNames = methodsParams.get(method);

        marshallerRegistry.registerTypes(TypeHelper.extractTypes(method));

        validateMethodParams(methodBind, endpointDesc, method, methodParamNames);

        final String[] params = methodParamNames.toArray(new String[methodParamNames.size()]);
        final AbstractServerEndpoint endpoint = isAsyncMethod(method) ?
                new AsyncServerEndpoint(service, getAsyncFilters(endpointDesc.filters, methodBind), method, endpointDesc.requestMethodType, params) :
                isStreamingMethod(method) ?
                        new StreamServerEndpoint(service, getStreamFilters(endpointDesc.filters, methodBind), method, endpointDesc.requestMethodType, params) :
                        new SyncServerEndpoint(service, getSyncFilters(endpointDesc.filters, methodBind), method, endpointDesc.requestMethodType, params, executorService);
        endpointsMap.put(endpointDescriptorEntry.getKey(), endpoint);
      }

      endpoints.insert(path.toString(), endpointsMap, bindPrefix);
    }
  }

  private void validateMethodParams(final String methodBind, final EndpointDescriptor endpointDesc, final Method method, final List methodParamNames) {
    final Class[] parameterTypes = method.getParameterTypes();
    if (parameterTypes.length == 1 && parameterTypes[0] == Request.class) {
      return;
    }
    if (Arrays.asList(parameterTypes).contains(Request.class)) {
      throw new RuntimeException("Request object must be the only param in the method signature");
    }
    if (methodBind.contains("{")) {
      int index = methodBind.indexOf('{');
      int methodParamPos = 0;

      while (index >= 0) {
        final int endIndex = methodBind.indexOf('}', index);
        final String pathParameter = methodBind.substring(index + 1, endIndex);

        if (!methodParamNames.contains(pathParameter)) {
          throw new RuntimeException("Parameter " + pathParameter + " does not exists in method signature");
        }

        if (endpointDesc.requestMethodType == HttpRequestMethodType.POST || endpointDesc.requestMethodType == HttpRequestMethodType.PUT) {
          if (methodParamNames.indexOf(pathParameter) != methodParamPos) {
            throw new RuntimeException("Path parameters should be in prefix when using method binding, i.e. methodName(id, name) => {id}/{name} or {id} with body of [name]");
          }
          methodParamPos++;
        }

        final Class paramType = parameterTypes[methodParamNames.indexOf(pathParameter)];

        if (!paramType.isPrimitive() && !String.class.isAssignableFrom(paramType)) {
          throw new RuntimeException("Path parameter " + paramType + " can be only primitive or String type");
        }

        index = methodBind.indexOf('{', endIndex);
      }
    }
  }

  private static List getMethods(final Map> descriptors) {
    final List methods = new ArrayList<>();
    for (final Map descMap : descriptors.values()) {
      for (final EndpointDescriptor desc : descMap.values()) {
        methods.add(desc.method);
      }
    }

    return methods;
  }

  private static AsyncFilter[] getAsyncFilters(final List filters, final String methodName) {
    if (filters == null)
      return null;

    final AsyncFilter[] result = new AsyncFilter[filters.size()];
    int index = 0;
    for (final ServiceFilter filter : filters) {
      if (filter instanceof AsyncFilter) {
        result[index++] = (AsyncFilter) filter;
      } else {
        throw new RuntimeException("method " + methodName + " can only receive async filters");
      }
    }

    return result;
  }

  private static SyncFilter[] getSyncFilters(final List filters, final String methodName) {
    if (filters == null)
      return null;

    final SyncFilter[] result = new SyncFilter[filters.size()];
    int index = 0;
    for (final ServiceFilter filter : filters) {
      if (filter instanceof SyncFilter) {
        result[index++] = (SyncFilter) filter;
      } else {
        throw new RuntimeException("method " + methodName + " can only receive sync filters");
      }
    }

    return result;
  }

  private static StreamFilter[] getStreamFilters(final List filters, final String methodName) {
    if (filters == null)
      return null;

    final StreamFilter[] result = new StreamFilter[filters.size()];
    int index = 0;
    for (final ServiceFilter filter : filters) {
      if (filter instanceof StreamFilter) {
        result[index++] = (StreamFilter) filter;
      } else {
        throw new RuntimeException("method " + methodName + " can only receive stream filters");
      }
    }

    return result;
  }


  public void register(final String name, final Service service, final List asyncFilters,
                       final List syncFilters, final List streamFilters,
                       final boolean bindPrefix, final Executor executorService) {

    final Map> descriptors = getEndpointsDescriptor(service,
            executorService, asyncFilters, syncFilters, streamFilters);
    register(name, service, descriptors, bindPrefix, executorService);
  }

  private Map> getEndpointsDescriptor(final Service service,
                                                                final Executor executorService,
                                                                final List asyncFilters,
                                                                final List syncFilters,
                                                                final List streamFilters) {

    final Method[] methods = service.getClass().getDeclaredMethods();
    final Map> result = new HashMap<>();

    for (final Method m : methods) {
      final int modifiers = m.getModifiers();
      if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) {
        if (isAsyncMethod(m)) {
          result.put(m.getName(), singleEndpointDescToMap(new EndpointDescriptor(m, asyncFilters, HttpRequestMethodType.ANY)));
        } else if (isStreamingMethod(m)) {
          result.put(m.getName(), singleEndpointDescToMap(new EndpointDescriptor(m, streamFilters, HttpRequestMethodType.ANY)));
        } else if (executorService != null) {
          // A sync method with a defined executor service.
          result.put(m.getName(), singleEndpointDescToMap(new EndpointDescriptor(m, syncFilters, HttpRequestMethodType.ANY)));
        } else {
          logger.info("Method " + m.getName() + " wasn't bounded. Sync method needs a configured executor service");
        }
      }
    }

    // in case of a single method service (e.g. SelfTestService) we don't want to include the method name as a path
    if (result.size() == 1) {
      result.put("", result.values().iterator().next());
    }

    return result;
  }

  private Map singleEndpointDescToMap(final EndpointDescriptor endpointDescriptor) {
    final Map endpointDescriptorMap = Maps.newHashMap();
    endpointDescriptorMap.put(HttpRequestMethodType.ANY, endpointDescriptor);
    return endpointDescriptorMap;
  }

  public SortedMap> getRegisteredEndpoints() {
    return endpoints.getPathToValueMapping();
  }

  public void logRegisteredEndpoints() {
    for (final Map.Entry> pathEndpointsMap : getRegisteredEndpoints().entrySet()) {
      for (final Map.Entry endpointEntry : pathEndpointsMap.getValue().entrySet()) {
        final AbstractServerEndpoint endpointValue = endpointEntry.getValue();
        logger.info("Registered endpoint [{} ==> {}, via method: {}]", pathEndpointsMap.getKey(), endpointValue.getTargetAsString(), endpointValue.requestMethodType);
      }
    }
  }

  private boolean isAsyncMethod(final Method m) {
    return ComposableFutureHelper.isComposableFuture(m.getReturnType());
  }

  private boolean isStreamingMethod(final Method m) {
    return m.getReturnType() == Observable.class;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy