io.github.microcks.web.GrpcServerCallHandler Maven / Gradle / Ivy
The newest version!
/*
* Copyright The Microcks Authors.
*
* 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 io.github.microcks.web;
import io.github.microcks.domain.Operation;
import io.github.microcks.domain.Resource;
import io.github.microcks.domain.ResourceType;
import io.github.microcks.domain.Response;
import io.github.microcks.domain.Service;
import io.github.microcks.repository.ResourceRepository;
import io.github.microcks.repository.ResponseRepository;
import io.github.microcks.repository.ServiceRepository;
import io.github.microcks.repository.ServiceStateRepository;
import io.github.microcks.util.DispatchCriteriaHelper;
import io.github.microcks.util.DispatchStyles;
import io.github.microcks.util.IdBuilder;
import io.github.microcks.util.dispatcher.FallbackSpecification;
import io.github.microcks.util.dispatcher.JsonEvaluationSpecification;
import io.github.microcks.util.dispatcher.JsonExpressionEvaluator;
import io.github.microcks.util.dispatcher.JsonMappingException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.TypeRegistry;
import com.google.protobuf.util.JsonFormat;
import io.github.microcks.util.grpc.GrpcUtil;
import io.github.microcks.util.script.ScriptEngineBinder;
import io.github.microcks.service.ServiceStateStore;
import io.grpc.ServerCallHandler;
import io.grpc.Status;
import io.grpc.stub.ServerCalls;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* A Handler for GRPC Server calls invocation that is using Microcks dispatching and mock definitions.
* @author laurent
*/
@Component
public class GrpcServerCallHandler {
/** A simple logger for diagnostic messages. */
private static final Logger log = LoggerFactory.getLogger(GrpcServerCallHandler.class);
private final ServiceRepository serviceRepository;
private final ServiceStateRepository serviceStateRepository;
private final ResourceRepository resourceRepository;
private final ResponseRepository responseRepository;
private final ApplicationContext applicationContext;
private final ObjectMapper mapper = new ObjectMapper();
@Value("${mocks.enable-invocation-stats}")
private Boolean enableInvocationStats;
/**
* Build a new GrpcServerCallHandler with all the repositories it needs and application context.
* @param serviceRepository Repository for getting service definitions
* @param serviceStateRepository Repository for getting service state
* @param resourceRepository Repository for getting service resources definitions
* @param responseRepository Repository for getting mock responses definitions
* @param applicationContext The Spring current application context
*/
public GrpcServerCallHandler(ServiceRepository serviceRepository, ServiceStateRepository serviceStateRepository,
ResourceRepository resourceRepository, ResponseRepository responseRepository,
ApplicationContext applicationContext) {
this.serviceRepository = serviceRepository;
this.serviceStateRepository = serviceStateRepository;
this.resourceRepository = resourceRepository;
this.responseRepository = responseRepository;
this.applicationContext = applicationContext;
}
/**
* Create an ServerCallHandler that uses Microcks mocks for unary calls.
* @param fullMethodName The GRPC method full name.
* @return A ServerCallHandler
*/
public ServerCallHandler getUnaryServerCallHandler(String fullMethodName) {
return ServerCalls.asyncUnaryCall(new MockedUnaryMethod(fullMethodName));
}
/**
* This internal class is handling UnaryMethod calls. It takes care of building a JSON representation from input,
* apply a dispatcher to find correct response and serialize response JSON content into binary back.
*/
protected class MockedUnaryMethod implements ServerCalls.UnaryMethod {
private String fullMethodName;
private String serviceName;
private String serviceVersion;
private String operationName;
/**
* Build a UnaryMethod for handling GRPC call.
* @param fullMethodName The GRPC method full identifier.
*/
public MockedUnaryMethod(String fullMethodName) {
this.fullMethodName = fullMethodName;
// Retrieve operation name, service name and version from fullMethodName.
operationName = fullMethodName.substring(fullMethodName.indexOf("/") + 1);
serviceName = fullMethodName.substring(0, fullMethodName.indexOf("/"));
String packageName = fullMethodName.substring(0, fullMethodName.lastIndexOf("."));
String[] parts = packageName.split("\\.");
serviceVersion = (parts.length > 2 ? parts[parts.length - 1] : packageName);
}
@Override
public void invoke(byte[] bytes, StreamObserver streamObserver) {
log.info("Servicing mock response for service [{}, {}] and method {}", serviceName, serviceVersion,
operationName);
long startTime = System.currentTimeMillis();
try {
// Get service and spotted operation.
Service service = serviceRepository.findByNameAndVersion(serviceName, serviceVersion);
if (service == null) {
// No service found.
log.debug("No GRPC Service def found for [{}, {}]", serviceName, serviceVersion);
streamObserver.onError(Status.UNIMPLEMENTED
.withDescription("No GRPC Service def found for " + fullMethodName).asException());
return;
}
Operation grpcOperation = null;
for (Operation operation : service.getOperations()) {
if (operation.getName().equals(operationName)) {
grpcOperation = operation;
break;
}
}
if (grpcOperation != null) {
log.debug("Found a valid operation {} with rules: {}", grpcOperation.getName(),
grpcOperation.getDispatcherRules());
// We must find dispatcher and its rules. Default to operation ones but
// if we have a Fallback this is the one who is holding the first pass rules.
String dispatcher = grpcOperation.getDispatcher();
String dispatcherRules = grpcOperation.getDispatcherRules();
FallbackSpecification fallback = MockControllerCommons.getFallbackIfAny(grpcOperation);
if (fallback != null) {
dispatcher = fallback.getDispatcher();
dispatcherRules = fallback.getDispatcherRules();
}
// In order to inspect incoming byte array, we need the Protobuf binary descriptor that should
// have been processed while importing the .proto schema for the service.
List resources = resourceRepository.findByServiceIdAndType(service.getId(),
ResourceType.PROTOBUF_DESCRIPTOR);
if (resources == null || resources.size() != 1) {
log.error("Did not found any pre-processed Protobuf binary descriptor...");
streamObserver.onError(Status.FAILED_PRECONDITION
.withDescription("No pre-processed Protobuf binary descriptor found").asException());
return;
}
Resource pbResource = resources.get(0);
// Get the method descriptor and type registry.
Descriptors.MethodDescriptor md = GrpcUtil.findMethodDescriptor(pbResource.getContent(), serviceName,
operationName);
TypeRegistry registry = GrpcUtil.buildTypeRegistry(pbResource.getContent());
// Now parse the incoming message.
DynamicMessage inMsg = DynamicMessage.parseFrom(md.getInputType(), bytes);
String jsonBody = JsonFormat.printer().usingTypeRegistry(registry).print(inMsg);
log.debug("Request body: {}", jsonBody);
//
DispatchContext dispatchContext = computeDispatchCriteria(service, dispatcher, dispatcherRules,
jsonBody);
log.debug("Dispatch criteria for finding response is {}", dispatchContext.dispatchCriteria());
// Trying to retrieve the responses with context elements.
List responses = findCandidateResponses(service, grpcOperation, dispatchContext, fallback);
// No filter to apply, just check that we have a response.
if (!responses.isEmpty()) {
manageResponseTransmission(streamObserver, service, grpcOperation, md, registry, dispatchContext,
jsonBody, responses.get(0), startTime);
} else {
// No response found.
log.info("No appropriate response found for this input {}, returning an error", jsonBody);
streamObserver.onError(
Status.NOT_FOUND.withDescription("No response found for the GRPC input request").asException());
}
} else {
// No operation found.
log.debug("No valid operation found for [{}, {}] and {}", serviceName, serviceVersion, operationName);
streamObserver.onError(Status.UNIMPLEMENTED
.withDescription("No valid operation found for " + fullMethodName).asException());
}
} catch (Exception t) {
log.error("Unexpected throwable during GRPC input request processing", t);
streamObserver
.onError(Status.UNKNOWN.withDescription("Unexpected throwable during GRPC input request processing")
.withCause(t).asException());
}
}
/** Compute a dispatch context with a dispatchCriteria string from type, rules and request elements. */
private DispatchContext computeDispatchCriteria(Service service, String dispatcher, String dispatcherRules,
String jsonBody) {
String dispatchCriteria = null;
Map requestContext = null;
// Depending on dispatcher, evaluate request with rules.
if (dispatcher != null) {
switch (dispatcher) {
case DispatchStyles.QUERY_ARGS:
try {
Map paramsMap = mapper.readValue(jsonBody,
TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class));
dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap);
} catch (JsonProcessingException jpe) {
log.error("Incoming body cannot be parsed as JSON", jpe);
}
break;
case DispatchStyles.JSON_BODY:
try {
JsonEvaluationSpecification specification = JsonEvaluationSpecification
.buildFromJsonString(dispatcherRules);
dispatchCriteria = JsonExpressionEvaluator.evaluate(jsonBody, specification);
} catch (JsonMappingException jme) {
log.error("Dispatching rules of operation cannot be interpreted as JsonEvaluationSpecification",
jme);
}
break;
case DispatchStyles.SCRIPT:
ScriptEngineManager sem = new ScriptEngineManager();
requestContext = new HashMap<>();
try {
// Evaluating request with script coming from operation dispatcher rules.
ScriptEngine se = sem.getEngineByExtension("groovy");
ScriptEngineBinder.bindEnvironment(se, jsonBody, requestContext,
new ServiceStateStore(serviceStateRepository, service.getId()));
dispatchCriteria = (String) se.eval(dispatcherRules);
} catch (Exception e) {
log.error("Error during Script evaluation", e);
}
break;
default:
break;
}
}
return new DispatchContext(dispatchCriteria, requestContext);
}
/** Find suitable responses regarding dispatch context and cirteria. */
private List findCandidateResponses(Service service, Operation grpcOperation,
DispatchContext dispatchContext, FallbackSpecification fallback) {
// Trying to retrieve the responses with dispatch criteria.
List responses = responseRepository.findByOperationIdAndDispatchCriteria(
IdBuilder.buildOperationId(service, grpcOperation), dispatchContext.dispatchCriteria());
if (responses.isEmpty()) {
// When using the SCRIPT or JSON_BODY dispatchers, return of evaluation may be the name of response.
responses = responseRepository.findByOperationIdAndName(IdBuilder.buildOperationId(service, grpcOperation),
dispatchContext.dispatchCriteria());
}
if (responses.isEmpty() && fallback != null) {
// If we've found nothing and got a fallback, that's the moment!
responses = responseRepository.findByOperationIdAndName(IdBuilder.buildOperationId(service, grpcOperation),
fallback.getFallback());
}
if (responses.isEmpty()) {
// In case no response found (because dispatcher is null for example), just get one for the operation.
log.debug("No responses found so far, tempting with just bare operationId...");
responses = responseRepository.findByOperationId(IdBuilder.buildOperationId(service, grpcOperation));
}
return responses;
}
/** Manage the transmission of response on observer + all the common rendering mechanisms. */
private void manageResponseTransmission(StreamObserver streamObserver, Service service,
Operation grpcOperation, Descriptors.MethodDescriptor md, TypeRegistry registry,
DispatchContext dispatchContext, String requestJsonBody, Response response, long startTime)
throws InvalidProtocolBufferException {
// Use a builder for out type with a Json parser to merge content and build outMsg.
DynamicMessage.Builder outBuilder = DynamicMessage.newBuilder(md.getOutputType());
// Render response content before.
String responseContent = MockControllerCommons.renderResponseContent(requestJsonBody,
dispatchContext.requestContext(), response);
JsonFormat.parser().usingTypeRegistry(registry).merge(responseContent, outBuilder);
DynamicMessage outMsg = outBuilder.build();
// Setting delay to default one if not set.
if (grpcOperation.getDefaultDelay() != null) {
MockControllerCommons.waitForDelay(startTime, grpcOperation.getDefaultDelay());
}
// Publish an invocation event before returning if enabled.
if (Boolean.TRUE.equals(enableInvocationStats)) {
MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime);
}
// Send the output message and complete the stream.
streamObserver.onNext(outMsg.toByteArray());
streamObserver.onCompleted();
}
}
}