/**
* Copyright 2016-2017 Sixt GmbH & Co. Autovermietung KG
* 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.sixt.service.framework.jetty;
import com.codahale.metrics.MetricRegistry;
import com.google.common.io.CharStreams;
import com.google.gson.*;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.protobuf.Message;
import com.sixt.service.framework.MethodHandlerDictionary;
import com.sixt.service.framework.OrangeContext;
import com.sixt.service.framework.ServiceMethodHandler;
import com.sixt.service.framework.ServiceProperties;
import com.sixt.service.framework.json.JsonRpcRequest;
import com.sixt.service.framework.json.JsonRpcResponse;
import com.sixt.service.framework.metrics.GoTimer;
import com.sixt.service.framework.protobuf.ProtobufUtil;
import com.sixt.service.framework.rpc.RpcCallException;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.tag.Tags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import static com.sixt.service.framework.OrangeContext.CORRELATION_ID;
import static com.sixt.service.framework.jetty.RpcServlet.TYPE_JSON;
import static com.sixt.service.framework.json.JsonRpcRequest.METHOD_FIELD;
import static com.sixt.service.framework.json.JsonRpcRequest.PARAMS_FIELD;
import static com.sixt.service.framework.json.JsonRpcResponse.ERROR_FIELD;
import static com.sixt.service.framework.util.ReflectionUtil.findSubClassParameterType;
@Singleton
public class JsonHandler extends RpcHandler {
private static final Logger logger = LoggerFactory.getLogger(JsonHandler.class);
@Inject
public JsonHandler(MethodHandlerDictionary handlers, MetricRegistry registry,
RpcHandlerMetrics handlerMetrics, ServiceProperties serviceProperties, Tracer tracer) {
super(handlers, registry, handlerMetrics, serviceProperties, tracer);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) {
logger.debug("Handling json request");
GoTimer methodTimer = null;
String methodName = null;
Span span = null;
long startTime = System.nanoTime();
Map<String, String> headers = gatherHttpHeaders(req);
OrangeContext context = new OrangeContext(headers);
try {
MDC.put(CORRELATION_ID, context.getCorrelationId());
String postedContent = CharStreams.toString(req.getReader());
logger.debug("Request JSON: {}", postedContent);
JsonRpcRequest rpcRequest;
try {
rpcRequest = parseRpcRequest(postedContent);
} catch (IllegalArgumentException iaex) {
logger.warn("Error parsing request: " + postedContent, iaex);
@SuppressWarnings("ThrowableNotThrown")
RpcCallException callException = new RpcCallException(RpcCallException.Category.BadRequest,
iaex.getMessage());
JsonObject jsonResponse = new JsonObject();
jsonResponse.add(ERROR_FIELD, callException.toJson());
writeResponse(resp, HttpServletResponse.SC_BAD_REQUEST, jsonResponse.toString());
incrementFailureCounter("unknown", context.getRpcOriginService(),
context.getRpcOriginMethod());
return;
}
methodName = rpcRequest.getMethod();
span = getSpan(methodName, headers, context);
methodTimer = getMethodTimer(methodName, context.getRpcOriginService(),
context.getRpcOriginMethod());
startTime = methodTimer.start();
context.setCorrelationId(rpcRequest.getIdAsString());
JsonRpcResponse finalResponse = dispatchJsonRpcRequest(rpcRequest, context);
resp.setContentType(TYPE_JSON);
writeResponse(resp, finalResponse.getStatusCode(), finalResponse.toJson().toString());
//TODO: should we check the response for errors (for metrics)?
methodTimer.recordSuccess(startTime);
incrementSuccessCounter(methodName, context.getRpcOriginService(),
context.getRpcOriginMethod());
} catch (IOException e) {
if (span != null) {
Tags.ERROR.set(span, true);
}
//TODO: this case doesn't return a response. should it?
methodTimer.recordFailure(startTime);
logger.error("Error handling request", e);
incrementFailureCounter(methodName, context.getRpcOriginService(),
context.getRpcOriginMethod());
} finally {
if (span != null) {
span.finish();
}
MDC.remove(CORRELATION_ID);
}
}
protected JsonRpcRequest parseRpcRequest(String jsonRequestString)
throws IllegalArgumentException {
JsonObject jsonRpcRequest = null;
try {
jsonRpcRequest = new JsonParser().parse(jsonRequestString).getAsJsonObject();
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
JsonElement methodElement = jsonRpcRequest.get(METHOD_FIELD);
if (methodElement == null || methodElement.getAsString().isEmpty()) {
throw new IllegalArgumentException("Missing method name");
}
if (! handlers.hasMethodHandler(methodElement.getAsString())) {
throw new IllegalArgumentException("No handler registered for method '" +
methodElement.getAsString() + "'");
}
JsonArray paramsArray = jsonRpcRequest.getAsJsonArray(PARAMS_FIELD);
JsonElement idElement = jsonRpcRequest.get(JsonRpcRequest.ID_FIELD);
return new JsonRpcRequest(idElement, methodElement.getAsString(), paramsArray);
}
@SuppressWarnings("unchecked")
private JsonRpcResponse dispatchJsonRpcRequest(JsonRpcRequest rpcRequest, OrangeContext cxt) {
ServiceMethodHandler handler = handlers.getMethodHandler(rpcRequest.getMethod());
Message innerRequest = convertJsonToProtobuf(handler, rpcRequest);
JsonRpcResponse jsonResponse = new JsonRpcResponse(rpcRequest.getId(), JsonNull.INSTANCE,
JsonNull.INSTANCE, HttpServletResponse.SC_OK);
JsonElement idElement = rpcRequest.getId();
if (idElement == null) {
jsonResponse.setId(new JsonPrimitive(-1));
}
try {
Message innerResponse = invokeHandlerChain(rpcRequest.getMethod(), handler, innerRequest, cxt);
jsonResponse.setResult(ProtobufUtil.protobufToJson(innerResponse));
} catch (RpcCallException rpcEx) {
logger.debug("Error processing request", rpcEx);
jsonResponse.setError(rpcEx.toJson());
jsonResponse.setStatusCode(rpcEx.getCategory().getHttpStatus());
} catch (Exception ex) {
logger.warn("Error processing request", ex);
if (ex.getMessage() != null) {
jsonResponse.setError(new JsonPrimitive(ex.getMessage()));
}
jsonResponse.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
return jsonResponse;
}
@SuppressWarnings("unchecked")
private Message convertJsonToProtobuf(ServiceMethodHandler handler,
JsonRpcRequest rpcRequest) {
try {
Class<?> requestKlass = findSubClassParameterType(handler, 0);
return ProtobufUtil.jsonToProtobuf(rpcRequest.getParams(),
(Class<? extends Message>) requestKlass);
} catch (ClassNotFoundException ex) {
throw new IllegalStateException("Reflection for handler " +
handler.getClass() + " failed");
}
}
}