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

io.scalecube.services.methods.ServiceMethodInvoker Maven / Gradle / Ivy

package io.scalecube.services.methods;

import io.scalecube.services.CommunicationMode;
import io.scalecube.services.RequestContext;
import io.scalecube.services.api.ServiceMessage;
import io.scalecube.services.auth.Principal;
import io.scalecube.services.auth.PrincipalMapper;
import io.scalecube.services.exceptions.ForbiddenException;
import io.scalecube.services.exceptions.ServiceProviderErrorMapper;
import io.scalecube.services.transport.api.ServiceMessageDataDecoder;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

public class ServiceMethodInvoker {

  private static final Logger LOGGER = LoggerFactory.getLogger(ServiceMethodInvoker.class);

  private final Method method;
  private final Object service;
  private final MethodInfo methodInfo;
  private final ServiceProviderErrorMapper errorMapper;
  private final ServiceMessageDataDecoder dataDecoder;
  private final PrincipalMapper principalMapper;
  private final Logger logger;

  public ServiceMethodInvoker(
      Method method,
      Object service,
      MethodInfo methodInfo,
      ServiceProviderErrorMapper errorMapper,
      ServiceMessageDataDecoder dataDecoder,
      PrincipalMapper principalMapper,
      Logger logger) {
    this.method = Objects.requireNonNull(method, "method");
    this.service = Objects.requireNonNull(service, "service");
    this.methodInfo = Objects.requireNonNull(methodInfo, "methodInfo");
    this.errorMapper = Objects.requireNonNull(errorMapper, "errorMapper");
    this.dataDecoder = Objects.requireNonNull(dataDecoder, "dataDecoder");
    this.principalMapper = principalMapper;
    this.logger = logger;
  }

  /**
   * Invokes service method with single response.
   *
   * @param message request service message
   * @return mono of service message
   */
  public Mono invokeOne(ServiceMessage message) {
    return RequestContext.deferContextual()
        .flatMap(
            context -> {
              final var request = toRequest(message);
              return mapPrincipal(context)
                  .flatMap(
                      principal ->
                          requestContext()
                              .then(Mono.defer(() -> Mono.from(invokeRequest(request))))
                              .contextWrite(enhanceRequestContext(context, request, principal)))
                  .doOnSuccess(
                      response -> {
                        if (logger != null && logger.isDebugEnabled()) {
                          logger.debug(
                              "[{}] request: {}, response: {}",
                              message.qualifier(),
                              request,
                              response);
                        }
                      })
                  .doOnError(
                      ex -> {
                        if (logger != null) {
                          logger.error("[{}][error] request: {}", message.qualifier(), request, ex);
                        }
                      });
            })
        .map(response -> toResponse(response, message.qualifier(), message.dataFormat()))
        .onErrorResume(ex -> Mono.just(errorMapper.toMessage(message.qualifier(), ex)))
        .subscribeOn(methodInfo.scheduler());
  }

  /**
   * Invokes service method with message stream response.
   *
   * @param message request service message
   * @return flux of service messages
   */
  public Flux invokeMany(ServiceMessage message) {
    if (methodInfo.communicationMode() == CommunicationMode.REQUEST_RESPONSE) {
      return Flux.from(invokeOne(message));
    }
    return RequestContext.deferContextual()
        .flatMapMany(
            context -> {
              final var request = toRequest(message);
              return mapPrincipal(context)
                  .flatMapMany(
                      principal ->
                          requestContext()
                              .thenMany(Flux.defer(() -> Flux.from(invokeRequest(request))))
                              .contextWrite(enhanceRequestContext(context, request, principal)))
                  .doOnSubscribe(
                      s -> {
                        if (logger != null && logger.isDebugEnabled()) {
                          logger.debug("[{}][subscribe] request: {}", message.qualifier(), request);
                        }
                      })
                  .doOnComplete(
                      () -> {
                        if (logger != null && logger.isDebugEnabled()) {
                          logger.debug("[{}][complete] request: {}", message.qualifier(), request);
                        }
                      })
                  .doOnError(
                      ex -> {
                        if (logger != null) {
                          logger.error("[{}][error] request: {}", message.qualifier(), request, ex);
                        }
                      });
            })
        .map(response -> toResponse(response, message.qualifier(), message.dataFormat()))
        .onErrorResume(ex -> Flux.just(errorMapper.toMessage(message.qualifier(), ex)))
        .subscribeOn(methodInfo.scheduler());
  }

  /**
   * Invokes service method with bidirectional communication.
   *
   * @param publisher request service message
   * @return flux of service messages
   */
  public Flux invokeBidirectional(Publisher publisher) {
    return Flux.from(publisher)
        .switchOnFirst(
            (first, messages) -> {
              final var message = first.get();
              final var qualifier = message.qualifier();
              final var dataFormat = message.dataFormat();

              return messages
                  .map(this::toRequest)
                  .transform(this::invokeRequest)
                  .map(response -> toResponse(response, qualifier, dataFormat))
                  .onErrorResume(ex -> Flux.just(errorMapper.toMessage(qualifier, ex)))
                  .subscribeOn(methodInfo.scheduler());
            });
  }

  private Mono requestContext() {
    final Mono contextMono;
    final var secured = methodInfo.secured();

    if (secured != null && secured.deferSecured()) {
      contextMono = RequestContext.deferSecured();
    } else {
      contextMono = RequestContext.deferContextual();
    }

    return contextMono;
  }

  private Publisher invokeRequest(Object request) {
    Publisher result = null;
    Throwable throwable = null;
    try {
      if (methodInfo.parameterCount() == 0) {
        result = (Publisher) method.invoke(service);
      } else {
        Object[] arguments = prepareArguments(request);
        result = (Publisher) method.invoke(service, arguments);
      }
      if (result == null) {
        result = Mono.empty();
      }
    } catch (InvocationTargetException ex) {
      throwable = ex.getCause();
    } catch (Throwable ex) {
      throwable = ex;
    }
    return throwable != null ? Mono.error(throwable) : result;
  }

  private Object[] prepareArguments(Object request) {
    Object[] arguments = new Object[methodInfo.parameterCount()];
    if (methodInfo.requestType() != Void.TYPE) {
      arguments[0] = request;
    }
    return arguments;
  }

  private Context enhanceRequestContext(
      RequestContext context, Object request, Principal principal) {
    final var dynamicQualifier = methodInfo.dynamicQualifier();

    Map pathVars = null;
    if (dynamicQualifier != null) {
      pathVars = dynamicQualifier.matchQualifier(context.requestQualifier());
    }

    return new RequestContext(context)
        .request(request)
        .principal(principal)
        .pathVars(pathVars)
        .methodInfo(methodInfo);
  }

  private Object toRequest(ServiceMessage message) {
    final var request = dataDecoder.apply(message, methodInfo.requestType());
    return methodInfo.isRequestTypeServiceMessage() ? request : request.data();
  }

  private static ServiceMessage toResponse(Object response, String qualifier, String dataFormat) {
    if (response instanceof ServiceMessage message) {
      final var builder = ServiceMessage.from(message).qualifier(qualifier);
      return dataFormat != null && !dataFormat.equals(message.dataFormat())
          ? builder.dataFormat(dataFormat).build()
          : builder.build();
    }
    return ServiceMessage.builder()
        .qualifier(qualifier)
        .data(response)
        .dataFormatIfAbsent(dataFormat)
        .build();
  }

  public Object service() {
    return service;
  }

  public MethodInfo methodInfo() {
    return methodInfo;
  }

  public PrincipalMapper principalMapper() {
    return principalMapper;
  }

  private Mono mapPrincipal(RequestContext context) {
    if (methodInfo.secured() == null) {
      return Mono.just(context.principal());
    }

    if (principalMapper == null) {
      if (context.hasPrincipal()) {
        return Mono.just(context.principal());
      } else {
        LOGGER.warn(
            "Insufficient permissions for secured method ({}) -- "
                + "request context ({}) does not have principal"
                + "and principalMapper is also not set",
            methodInfo,
            context);
        throw new ForbiddenException("Insufficient permissions");
      }
    }

    return Mono.defer(() -> principalMapper.map(context))
        .switchIfEmpty(Mono.just(context.principal()));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy