package org.rakam.server.http;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.thoughtworks.paranamer.BytecodeReadingParanamer;
import io.airlift.log.Logger;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.internal.ConcurrentSet;
import io.swagger.models.Operation;
import io.swagger.models.Swagger;
import io.swagger.util.Json;
import io.swagger.util.PrimitiveType;
import org.rakam.server.http.HttpServerBuilder.IRequestParameterFactory;
import org.rakam.server.http.IRequestParameter.BodyParameter;
import org.rakam.server.http.IRequestParameter.HeaderParameter;
import org.rakam.server.http.annotations.ApiParam;
import org.rakam.server.http.annotations.BodyParam;
import org.rakam.server.http.annotations.CookieParam;
import org.rakam.server.http.annotations.HeaderParam;
import org.rakam.server.http.annotations.JsonRequest;
import org.rakam.server.http.annotations.QueryParam;
import org.rakam.server.http.util.Lambda;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import sun.reflect.generics.reflectiveObjects.TypeVariableImpl;
import javax.inject.Named;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.ws.rs.Path;
import java.lang.invoke.MethodHandle;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.SET_COOKIE;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.handler.codec.http.cookie.ServerCookieEncoder.STRICT;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.lang.String.format;
import static java.lang.invoke.MethodHandles.lookup;
import static java.util.Objects.requireNonNull;
import static org.rakam.server.http.util.Lambda.produceLambdaForFunction;
public class HttpServer
{
private final static Logger LOGGER = Logger.get(HttpServer.class);
static final ObjectMapper DEFAULT_MAPPER;
private static BytecodeReadingParanamer PARAMETER_LOOKUP = new BytecodeReadingParanamer();
public final RouteMatcher routeMatcher;
private final Swagger swagger;
private final ObjectMapper mapper;
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
private final List<PreprocessorEntry> preProcessors;
private final boolean proxyProtocol;
private final List<PostProcessorEntry> postProcessors;
final HttpServerBuilder.ExceptionHandler uncaughtExceptionHandler;
private final Map<String, IRequestParameterFactory> customParameters;
private final BiConsumer<Method, Operation> swaggerOperationConsumer;
private final boolean useEpoll;
protected final Map<RakamHttpRequest, Long> processingRequests;
protected final long maximumBodySize;
private Channel channel;
private final ImmutableMap<Class, PrimitiveType> swaggerBeanMappings = ImmutableMap.<Class, PrimitiveType>builder()
.put(LocalDate.class, PrimitiveType.DATE)
.put(Duration.class, PrimitiveType.STRING)
.put(Instant.class, PrimitiveType.DATE_TIME)
.put(ObjectNode.class, PrimitiveType.OBJECT)
.build();
protected ConcurrentSet activeChannels;
static {
DEFAULT_MAPPER = new ObjectMapper();
}
HttpServer(Set<HttpService> httpServicePlugins,
Set<WebSocketService> websocketServices,
Swagger swagger, EventLoopGroup eventLoopGroup,
List<PreprocessorEntry> preProcessors,
ImmutableList<PostProcessorEntry> postProcessors,
ObjectMapper mapper,
Map<Class, PrimitiveType> overriddenMappings,
HttpServerBuilder.ExceptionHandler exceptionHandler,
Map<String, IRequestParameterFactory> customParameters,
BiConsumer<Method, Operation> swaggerOperationConsumer,
boolean useEpoll,
boolean proxyProtocol,
long maximumBodySize)
{
this.routeMatcher = new RouteMatcher();
this.preProcessors = preProcessors;
this.workerGroup = requireNonNull(eventLoopGroup, "eventLoopGroup is null");
this.swagger = requireNonNull(swagger, "swagger is null");
this.mapper = mapper;
this.customParameters = customParameters;
this.swaggerOperationConsumer = swaggerOperationConsumer;
this.uncaughtExceptionHandler = exceptionHandler == null ? (t, e) -> {
} : exceptionHandler;
this.postProcessors = postProcessors;
this.proxyProtocol = proxyProtocol;
this.maximumBodySize = maximumBodySize;
this.bossGroup = useEpoll ? new EpollEventLoopGroup(1) : new NioEventLoopGroup(1);
registerEndPoints(requireNonNull(httpServicePlugins, "httpServices is null"), overriddenMappings);
registerWebSocketPaths(requireNonNull(websocketServices, "webSocketServices is null"));
routeMatcher.add(GET, "/api/swagger.json", this::swaggerApiHandle);
this.useEpoll = useEpoll && Epoll.isAvailable();
this.processingRequests = new ConcurrentHashMap<>();
}
public void setNotFoundHandler(HttpRequestHandler handler)
{
routeMatcher.noMatch(handler);
}
private void swaggerApiHandle(RakamHttpRequest request)
{
String content;
try {
content = Json.mapper().writeValueAsString(swagger);
}
catch (JsonProcessingException e) {
request.response("Error").end();
return;
}
request.response(content).end();
}
public void markProcessing(RakamHttpRequest request)
{
processingRequests.put(request, System.currentTimeMillis());
}
public void unmarkProcessing(RakamHttpRequest request)
{
processingRequests.remove(request);
}
private void registerWebSocketPaths(Set<WebSocketService> webSocketServices)
{
webSocketServices.forEach(service -> {
String path = service.getClass().getAnnotation(Path.class).value();
if (path == null) {
throw new IllegalStateException(format("Classes that implement WebSocketService must have %s annotation.",
Path.class.getCanonicalName()));
}
routeMatcher.add(path, service);
});
}
private void registerEndPoints(Set<HttpService> httpServicePlugins, Map<Class, PrimitiveType> overriddenMappings)
{
Map<Class, PrimitiveType> swaggerBeanMappings;
if (overriddenMappings != null) {
swaggerBeanMappings = ImmutableMap.<Class, PrimitiveType>builder().putAll(this.swaggerBeanMappings).putAll(overriddenMappings).build();
}
else {
swaggerBeanMappings = this.swaggerBeanMappings;
}
SwaggerReader reader = new SwaggerReader(swagger, mapper, swaggerOperationConsumer, swaggerBeanMappings);
httpServicePlugins.forEach(service -> {
reader.read(service.getClass());
if (!service.getClass().isAnnotationPresent(Path.class)) {
throw new IllegalStateException(format("HttpService class %s must have javax.ws.rs.Path annotation", service.getClass()));
}
String mainPath = service.getClass().getAnnotation(Path.class).value();
if (mainPath == null) {
throw new IllegalStateException(format("Classes that implement HttpService must have %s annotation.", Path.class.getCanonicalName()));
}
RouteMatcher.MicroRouteMatcher microRouteMatcher = new RouteMatcher.MicroRouteMatcher(routeMatcher, mainPath);
for (Method method : service.getClass().getMethods()) {
Path annotation = method.getAnnotation(Path.class);
if (annotation == null) {
continue;
}
method.setAccessible(true);
String lastPath = annotation.value();
Iterator<HttpMethod> methodExists = Arrays.stream(method.getAnnotations())
.filter(ann -> ann.annotationType().isAnnotationPresent(javax.ws.rs.HttpMethod.class))
.map(ann -> HttpMethod.valueOf(ann.annotationType().getAnnotation(javax.ws.rs.HttpMethod.class).value()))
.iterator();
final JsonRequest jsonRequest = method.getAnnotation(JsonRequest.class);
// if no @Path annotation exists and @JsonRequest annotation is present, bind POST httpMethod by default.
if (!methodExists.hasNext() && jsonRequest != null) {
microRouteMatcher.add(lastPath, POST, getJsonRequestHandler(method, service));
}
else {
while (methodExists.hasNext()) {
HttpMethod httpMethod = methodExists.next();
if (jsonRequest != null && httpMethod != POST) {
if (Arrays.stream(method.getParameters()).anyMatch(p -> p.isAnnotationPresent(ApiParam.class))) {
throw new IllegalStateException("@ApiParam annotation can only be used within POST requests");
}
if (method.getParameterCount() == 1 && method.getParameters()[0].isAnnotationPresent(BodyParam.class)) {
throw new IllegalStateException("@ParamBody annotation can only be used within POST requests");
}
}
HttpRequestHandler handler;
if (httpMethod == GET && !method.getReturnType().equals(void.class)) {
handler = createGetRequestHandler(service, method);
}
else if (isRawRequestMethod(method)) {
handler = generateRawRequestHandler(service, method);
}
else {
handler = getJsonRequestHandler(method, service);
}
microRouteMatcher.add(lastPath, httpMethod, handler);
}
}
}
});
swagger.getDefinitions().forEach((k, v) -> Optional.ofNullable(v)
.map(a -> a.getProperties())
.ifPresent(a -> a.values().forEach(x -> x.setReadOnly(null))));
}
private HttpRequestHandler getJsonRequestHandler(Method method, HttpService service)
{
final List<RequestPreprocessor> preprocessorRequest = getPreprocessorRequest(method);
List<ResponsePostProcessor> postProcessors = getPostPreprocessorsRequest(method);
if (method.getParameterCount() == 1 && method.getParameters()[0].isAnnotationPresent(BodyParam.class)) {
return new JsonBeanRequestHandler(this, mapper, method,
getPreprocessorRequest(method), postProcessors,
service);
}
ArrayList<IRequestParameter> bodyParams = new ArrayList<>();
for (Parameter parameter : method.getParameters()) {
bodyParams.add(getHandler(parameter, service, method));
}
boolean isAsync = CompletionStage.class.isAssignableFrom(method.getReturnType());
final List<RequestPreprocessor> preprocessorForJsonRequest = getPreprocessorRequest(method);
if (bodyParams.size() == 0) {
final ObjectNode emptyNode = mapper.createObjectNode();
final Function<HttpService, Object> lambda = Lambda.produceLambdaForFunction(method);
return request -> {
Object invoke;
try {
if (!preprocessorForJsonRequest.isEmpty()) {
for (RequestPreprocessor preprocessor : preprocessorForJsonRequest) {
preprocessor.handle(request, emptyNode);
}
}
if (!preprocessorRequest.isEmpty()) {
HttpServer.applyPreprocessors(request, preprocessorRequest);
}
invoke = lambda.apply(service);
}
catch (Throwable e) {
requestError(e.getCause(), request, postProcessors);
return;
}
handleRequest(mapper, isAsync, invoke, request, postProcessors);
};
}
else {
// TODO: we may specialize for a number of parameters to avoid generic MethodHandle invoker and use lambda generation instead.
MethodHandle methodHandle;
try {
methodHandle = lookup().unreflect(method);
}
catch (IllegalAccessException e) {
throw Throwables.propagate(e);
}
return new JsonParametrizedRequestHandler(this, mapper, bodyParams,
methodHandle, postProcessors, service,
preprocessorForJsonRequest, isAsync, !method.getReturnType().equals(void.class));
}
}
private Type getActualType(Class readClass, Type parameterizedType)
{
// if the parameter has a generic type, it will be read as Object
// so we need to find the actual implementation and return that type.
if (parameterizedType instanceof TypeVariableImpl) {
TypeVariable[] genericParameters = readClass.getSuperclass().getTypeParameters();
Type[] implementations = ((ParameterizedTypeImpl) readClass.getGenericSuperclass()).getActualTypeArguments();
for (int i = 0; i < genericParameters.length; i++) {
if (genericParameters[i].getName().equals(((TypeVariableImpl) parameterizedType).getName())) {
return implementations[i];
}
}
}
return parameterizedType;
}
private List<RequestPreprocessor> getPreprocessorRequest(Method method)
{
return preProcessors.stream()
.filter(p -> p.test(method)).map(p -> p.getPreprocessor()).collect(Collectors.toList());
}
private List<ResponsePostProcessor> getPostPreprocessorsRequest(Method method)
{
return postProcessors.stream()
.filter(p -> p.test(method)).map(PostProcessorEntry::getProcessor).collect(Collectors.toList());
}
void handleRequest(ObjectMapper mapper, boolean isAsync, Object invoke, RakamHttpRequest request, List<ResponsePostProcessor> postProcessors)
{
if (isAsync) {
handleAsyncJsonRequest(mapper, request, (CompletionStage) invoke, postProcessors);
}
else {
returnJsonResponse(mapper, request, OK, invoke, postProcessors);
}
}
private boolean isRawRequestMethod(Method method)
{
return method.getReturnType().equals(void.class) &&
method.getParameterCount() == 1 &&
method.getParameterTypes()[0].equals(RakamHttpRequest.class);
}
private HttpRequestHandler generateRawRequestHandler(HttpService service, Method method)
{
List<RequestPreprocessor> requestPreprocessors = getPreprocessorRequest(method);
List<ResponsePostProcessor> postProcessors = getPostPreprocessorsRequest(method);
// we don't need to pass service object is the method is static.
// it's also better for performance since there will be only one object to send the stack.
if (Modifier.isStatic(method.getModifiers())) {
Consumer<RakamHttpRequest> lambda;
lambda = Lambda.produceLambdaForConsumer(method);
return request -> {
try {
if (!requestPreprocessors.isEmpty()) {
HttpServer.applyPreprocessors(request, requestPreprocessors);
}
lambda.accept(request);
}
catch (Exception e) {
requestError(e, request, postProcessors);
}
};
}
else {
BiConsumer<HttpService, RakamHttpRequest> lambda;
lambda = Lambda.produceLambdaForBiConsumer(method);
return request -> {
try {
lambda.accept(service, request);
}
catch (Exception e) {
requestError(e, request, postProcessors);
}
};
}
}
private HttpRequestHandler createGetRequestHandler(HttpService service, Method method)
{
boolean isAsync = CompletionStage.class.isAssignableFrom(method.getReturnType());
final List<RequestPreprocessor> preprocessors = getPreprocessorRequest(method);
List<ResponsePostProcessor> postProcessors = getPostPreprocessorsRequest(method);
if (method.getParameterCount() == 0) {
Function<HttpService, Object> function = produceLambdaForFunction(method);
return (request) -> {
try {
if (!preprocessors.isEmpty()) {
applyPreprocessors(request, preprocessors);
}
}
catch (Throwable e) {
requestError(e, request, postProcessors);
return;
}
if (isAsync) {
CompletionStage apply;
try {
apply = (CompletionStage) function.apply(service);
}
catch (Exception e) {
requestError(e.getCause(), request, postProcessors);
return;
}
handleAsyncJsonRequest(mapper, request, apply, postProcessors);
}
else {
handleJsonRequest(mapper, service, request, function, postProcessors);
}
};
}
else {
MethodHandle methodHandle;
try {
methodHandle = lookup().unreflect(method);
}
catch (IllegalAccessException e) {
throw Throwables.propagate(e);
}
IRequestParameter[] parameters = Arrays.stream(method.getParameters())
.map(e -> getHandler(e, service, method))
.toArray(IRequestParameter[]::new);
int parameterSize = parameters.length + 1;
return request -> {
try {
if (!preprocessors.isEmpty()) {
HttpServer.applyPreprocessors(request, preprocessors);
}
}
catch (Throwable e) {
requestError(e, request, postProcessors);
return;
}
ObjectNode json = generate(request.params());
Object[] objects = new Object[parameterSize];
objects[0] = service;
try {
for (int i = 0; i < parameters.length; i++) {
objects[i + 1] = parameters[i].extract(json, request);
}
}
catch (Exception e) {
requestError(e, request, null);
return;
}
if (isAsync) {
CompletionStage apply;
try {
apply = (CompletionStage) methodHandle.invokeWithArguments(objects);
}
catch (Throwable e) {
requestError(e.getCause(), request, postProcessors);
return;
}
handleAsyncJsonRequest(mapper, request, apply, postProcessors);
}
else {
handleJsonRequest(mapper, request, methodHandle, objects, postProcessors);
}
};
}
}
private static final ImmutableMap<Class<?>, Function<String, ?>> primitiveMapper = ImmutableMap.<Class<?>, Function<String, ?>>of(
int.class, Integer::parseInt,
long.class, Long::parseLong,
double.class, Double::parseDouble,
boolean.class, Boolean::parseBoolean,
float.class, Float::parseFloat);
private IRequestParameter getHandler(Parameter parameter, HttpService service, Method method)
{
if (parameter.isAnnotationPresent(ApiParam.class)) {
ApiParam apiParam = parameter.getAnnotation(ApiParam.class);
return new BodyParameter(mapper, apiParam.value(), getActualType(service.getClass(), parameter.getParameterizedType()),
apiParam == null ? false : apiParam.required());
}
else if (parameter.isAnnotationPresent(HeaderParam.class)) {
HeaderParam param = parameter.getAnnotation(HeaderParam.class);
Type actualType = getActualType(service.getClass(), parameter.getParameterizedType());
if (actualType.equals(String.class) || (actualType instanceof Class && primitiveMapper.containsKey(actualType))) {
return new HeaderParameter(param.value(), param.required(),
actualType.equals(String.class) ? (Function) Function.identity() : primitiveMapper.get(actualType));
}
else {
if (actualType instanceof Class && ((Class) actualType).isEnum()) {
return new HeaderParameter<>(param.value(), param.required(),
o -> mapper.convertValue(o, ((Class) actualType)));
}
else {
throw new IllegalArgumentException(String.format("Invalid HeaderParameter type: %s. Header parameters can only be String, Enum or primitive types", actualType));
}
}
}
else if (parameter.isAnnotationPresent(CookieParam.class)) {
CookieParam param = parameter.getAnnotation(CookieParam.class);
return new IRequestParameter.CookieParameter(param.value(), param.required());
}
else if (parameter.isAnnotationPresent(Named.class)) {
Named param = parameter.getAnnotation(Named.class);
IRequestParameterFactory iRequestParameter = customParameters.get(param.value());
if (iRequestParameter == null) {
throw new IllegalStateException(String.format("Custom parameter %s doesn't have implementation", param.value()));
}
return iRequestParameter.create(method);
}
else if (parameter.isAnnotationPresent(Named.class)) {
Named param = parameter.getAnnotation(Named.class);
IRequestParameterFactory iRequestParameter = customParameters.get(param.value());
if (iRequestParameter == null) {
throw new IllegalStateException(String.format("Custom parameter %s doesn't have implementation", param.value()));
}
return iRequestParameter.create(method);
}
else if (parameter.getType().equals(RakamHttpRequest.class)) {
return (node, request) -> request;
}
else if (parameter.isAnnotationPresent(BodyParam.class)) {
return new IRequestParameter.FullBodyParameter(mapper, parameter.getParameterizedType());
}
else if (parameter.isAnnotationPresent(QueryParam.class)) {
QueryParam param = parameter.getAnnotation(QueryParam.class);
Type actualType = getActualType(service.getClass(), parameter.getParameterizedType());
if (actualType.equals(String.class) || (actualType instanceof Class && primitiveMapper.containsKey(actualType))) {
return new IRequestParameter.QueryParameter(param.value(), param.required(),
actualType.equals(String.class) ? (Function) Function.identity() : primitiveMapper.get(actualType));
}
else {
if (actualType instanceof Class && ((Class) actualType).isEnum()) {
return new IRequestParameter.QueryParameter(param.value(), param.required(),
o -> mapper.convertValue(o, ((Class) actualType)));
}
else {
throw new IllegalArgumentException(String.format("Invalid HeaderParameter type: %s. Header parameters can only be String, Enum or primitive types", actualType));
}
}
}
else {
String[] strings;
try {
strings = PARAMETER_LOOKUP.lookupParameterNames(method);
}
catch (Exception e) {
throw new IllegalStateException(String.format("Parameter %s in method % does not have @ApiParam annotation" +
" and class bytecode doesn't have parameter name info.", parameter.toString(), method.toString()));
}
return new BodyParameter(mapper,
strings[Arrays.asList(method.getParameters()).indexOf(parameter)],
parameter.getParameterizedType(), false);
}
}
void handleJsonRequest(ObjectMapper mapper, RakamHttpRequest request, MethodHandle methodHandle, Object[] arguments, List<ResponsePostProcessor> postProcessors)
{
try {
Object apply = methodHandle.invokeWithArguments(arguments);
returnJsonResponse(mapper, request, OK, apply, postProcessors);
}
catch (HttpRequestException e) {
uncaughtExceptionHandler.handle(request, e);
HttpResponseStatus statusCode = e.getStatusCode();
returnJsonResponse(mapper, request, statusCode, errorMessage(e.getMessage(), statusCode), postProcessors);
}
catch (Throwable e) {
uncaughtExceptionHandler.handle(request, e);
LOGGER.error(e, "An uncaught exception raised while processing request.");
returnJsonResponse(mapper, request, BAD_REQUEST,
errorMessage("Error processing request.", INTERNAL_SERVER_ERROR), postProcessors);
}
}
void handleJsonRequest(ObjectMapper mapper, HttpService serviceInstance, RakamHttpRequest request, Function<HttpService, Object> function, List<ResponsePostProcessor> postProcessors)
{
try {
Object apply = function.apply(serviceInstance);
returnJsonResponse(mapper, request, OK, apply, postProcessors);
}
catch (HttpRequestException ex) {
uncaughtExceptionHandler.handle(request, ex);
HttpResponseStatus statusCode = ex.getStatusCode();
returnJsonResponse(mapper, request, statusCode, errorMessage(ex.getMessage(), statusCode), postProcessors);
}
catch (Exception e) {
uncaughtExceptionHandler.handle(request, e);
LOGGER.error(e, "An uncaught exception raised while processing request.");
returnJsonResponse(mapper, request, BAD_REQUEST,
errorMessage("Error processing request.", INTERNAL_SERVER_ERROR), postProcessors);
}
}
private static void returnJsonResponse(ObjectMapper mapper, RakamHttpRequest request, HttpResponseStatus status, Object apply, List<ResponsePostProcessor> postProcessors)
{
FullHttpResponse response;
try {
if (apply instanceof Response) {
Response responseData = (Response) apply;
byte[] bytes = mapper.writeValueAsBytes(responseData.getData());
response = new DefaultFullHttpResponse(HTTP_1_1, responseData.getStatus(), Unpooled.wrappedBuffer(bytes));
if (responseData.getCookies() != null) {
response.headers().add(SET_COOKIE, STRICT.encode(responseData.getCookies()));
}
}
else {
final ByteBuf byteBuf = Unpooled.wrappedBuffer(mapper.writeValueAsString(apply).getBytes(UTF_8));
response = new DefaultFullHttpResponse(HTTP_1_1, status, byteBuf);
}
}
catch (JsonProcessingException e) {
LOGGER.error(e, "Couldn't serialize returned object");
throw new RuntimeException("couldn't serialize object", e);
}
response.headers().set(CONTENT_TYPE, "application/json; charset=utf-8");
if (postProcessors != null) {
applyPostProcessors(response, postProcessors);
}
request.response(response).end();
}
static void applyPostProcessors(FullHttpResponse httpResponse, List<ResponsePostProcessor> postProcessors)
{
if (!postProcessors.isEmpty()) {
for (ResponsePostProcessor postProcessor : postProcessors) {
postProcessor.handle(httpResponse);
}
}
}
void handleAsyncJsonRequest(ObjectMapper mapper, RakamHttpRequest request, CompletionStage apply, List<ResponsePostProcessor> postProcessors)
{
if (apply == null) {
NullPointerException e = new NullPointerException();
uncaughtExceptionHandler.handle(request, e);
LOGGER.error(e, "Error while processing request. The async method returned null.");
return;
}
apply.whenComplete((BiConsumer<Object, Throwable>) (result, ex) -> {
if (ex != null) {
while (ex instanceof CompletionException) {
ex = ex.getCause();
}
uncaughtExceptionHandler.handle(request, ex);
requestError(ex, request, postProcessors);
}
else {
returnJsonResponse(mapper, request, OK, result, postProcessors);
}
});
}
public void bind(String host, int port)
throws InterruptedException
{
channel = bindInternal(host, port).channel();
}
public void bindAwait(String host, int port)
throws InterruptedException
{
bind(host, port);
channel.closeFuture().sync(); // Wait until the channel is closed.
}
public static class Request
{
private final String uri;
private final List<Map.Entry<String, String>> entries;
private final String method;
private final String remoteAddress;
private final long runningTime;
public Request(String method, String uri, List<Map.Entry<String, String>> entries, String remoteAddress, long startTime)
{
this.method = method;
this.uri = uri;
this.remoteAddress = remoteAddress;
this.entries = entries;
this.runningTime = System.currentTimeMillis() - startTime;
}
public String getUri()
{
return uri;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public List<Map.Entry<String, String>> getEntries()
{
return entries;
}
public String getMethod()
{
return method;
}
public String getRemoteAddress()
{
return remoteAddress;
}
public long getRunningTime()
{
return runningTime;
}
}
private ChannelFuture bindInternal(String host, int port)
throws InterruptedException
{
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
activeChannels = new ConcurrentSet();
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
try {
ObjectName serverName = new ObjectName(SHttpServer.class.getPackage().getName() + ":name=" + SHttpServer.class.getSimpleName());
mbs.registerMBean(new SHttpServer(this), serverName);
}
catch (InstanceAlreadyExistsException | MalformedObjectNameException | NotCompliantMBeanException | MBeanRegistrationException e) {
LOGGER.warn(e, "Error while registering MBean");
}
b.group(bossGroup, workerGroup)
.channel(useEpoll ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>()
{
@Override
protected void initChannel(SocketChannel ch)
throws Exception
{
ChannelPipeline p = ch.pipeline();
HttpServerHandler handler;
if (proxyProtocol) {
p.addLast(new HAProxyMessageDecoder());
handler = new HaProxyBackendServerHandler(activeChannels, HttpServer.this);
}
else {
handler = new HttpServerHandler(activeChannels, HttpServer.this);
}
// make it configurable
p.addLast("httpCodec", new HttpServerCodec(36192 * 2, 36192 * 8, 36192 * 16));
p.addLast("serverHandler", handler);
}
});
return b.bind(host, port).sync();
}
public void stop()
{
if (channel != null) {
channel.close().syncUninterruptibly();
}
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
static void applyPreprocessors(RakamHttpRequest request, List<RequestPreprocessor> preprocessors)
{
for (RequestPreprocessor preprocessor : preprocessors) {
preprocessor.handle(request, null);
}
}
public static void returnError(RakamHttpRequest request, String message, HttpResponseStatus statusCode)
{
byte[] bytes;
try {
bytes = DEFAULT_MAPPER.writeValueAsBytes(errorMessage(message, statusCode));
}
catch (JsonProcessingException e) {
throw new RuntimeException();
}
request.response(bytes, statusCode).headers().set("Content-Type", "application/json");
request.end();
}
public static ErrorMessage errorMessage(String message, HttpResponseStatus statusCode)
{
return new ErrorMessage(message, statusCode.code());
}
public static class ErrorMessage
{
public final String error;
public final int error_code;
@JsonCreator
private ErrorMessage(@ApiParam("error") String error, @ApiParam("error_code") int error_code)
{
this.error = error;
this.error_code = error_code;
}
}
void requestError(Throwable ex, RakamHttpRequest request, List<ResponsePostProcessor> postProcessors)
{
uncaughtExceptionHandler.handle(request, ex);
if (ex instanceof HttpRequestException) {
HttpResponseStatus statusCode = ((HttpRequestException) ex).getStatusCode();
returnJsonResponse(mapper, request, statusCode, errorMessage(ex.getMessage(), statusCode), postProcessors);
}
else {
returnJsonResponse(mapper, request, INTERNAL_SERVER_ERROR,
errorMessage("Error processing request.", INTERNAL_SERVER_ERROR), postProcessors);
LOGGER.error(ex, "Error while processing request");
}
}
private ObjectNode generate(Map<String, List<String>> map)
{
ObjectNode obj = mapper.createObjectNode();
for (Map.Entry<String, List<String>> item : map.entrySet()) {
String key = item.getKey();
obj.put(key, item.getValue().get(0));
}
return obj;
}
}