ProvidenceHttpServlet.java
/*
* Copyright 2016-2017 Providence Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 net.morimekta.providence.server;
import net.morimekta.providence.PApplicationException;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.serializer.DefaultSerializerProvider;
import net.morimekta.providence.serializer.Serializer;
import net.morimekta.providence.serializer.SerializerException;
import net.morimekta.providence.serializer.SerializerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static net.morimekta.providence.PApplicationExceptionType.INVALID_PROTOCOL;
/**
* A simple HTTP POST servlet that simply deserializes the POST body
* as a providence message, and serializes the response message using
* the requested content type or accept type.
* <p>
* Note that the {@link ProvidenceHttpServlet} is <b>NOT</b> usable for
* thrift services, but is meant to be used to make simple POST based
* HTTP servlets.
*
* <pre>{@code
* public class MyServlet extends ProvidenceHttpServlet<
* MyRequest, MyRequest._Field,
* MyResponse, MyResponse._Field> {
* {@literal@}Override
* protected MyResponse handle(HttpServletRequest httpRequest,
* MyRequest request)
* throws MyException, InternalFailureException {
* // ... do stuff with request, or throw MyException or IFE.
* return MyResponse.builder()
* .setFieldOut("yes")
* .build();
* }
*
* {@literal@}Override
* protected int statusCodeForException({@literal@}Nonnull Throwable exception) {
* if (exception instanceof MyException) {
* return HttpStatus.BAD_REQUEST_400;
* }
* return super.statusCodeForException(ex);
* }
* }
* }</pre>
*
* This will result in a simple HTTP servlet that can be queries with CURL
* e.g. like this:
*
* <pre>{@code
* # Simple success
* $ curl -sS -X POST -d "{\"field_in\": \"value\"}" \
* > -H "Content-Type: application/json" \
* > localhost:8080/my/servlet
* {\"field_out\":\"yes\"}
*
* # Simple Error
* $ curl -sSv -X POST -d "{\"field_in\": \"not valid\"}" \
* > -H "Content-Type: application/json" \
* > localhost:8080/my/servlet
* ...
* > Content-Type: application/json
* > Accept: *{@literal/}*
* ...
* < HTTP/1.1 400 Bad Request
* < Content-Type: application/json
* ...
* {\"text\":\"not valid value\"}
* }</pre>
*
* Alternatively you can hijack the whole exception / error response handling, which
* might be needed where custom headers etc are needed, e.g. with <code>Unauthorized (401)</code>:
*
* <pre>{@code
* public class MyServlet extends ProvidenceHttpServlet<
* MyRequest, MyRequest._Field,
* MyResponse, MyResponse._Field> {
* public MyServlet() {
* super(MyRequest.kDescriptor, new MyExceptionHandler(), DefaultSerializerProvider.INSTANCE);
* }
*
* {@literal@}Override
* protected MyResponse handle(HttpServletRequest httpRequest,
* MyRequest request)
* throws MyException, InternalFailureException {
* // ... do stuff with request, or throw MyException or IFE.
* return MyResponse.builder()
* .setFieldOut("yes")
* .build();
* }
* }
* }</pre>
*
* <pre>{@code
* public class MyExceptionHandler extends ExceptionHandler {
* {@literal@}Override
* protected void writeErrorResponse(
* {@literal@}Nonnull Throwable exception,
* {@literal@}Nonnull Serializer responseSerializer,
* {@literal@}Nonnull HttpServletRequest httpRequest,
* {@literal@}Nonnull HttpServletResponse httpResponse)
* throws IOException {
* if (exception instanceof MyException) {
* httpResponse.setStatus(HttpStatus.UNAUTHORIZED_401);
* httpResponse.setHeader(HttpHeaders.WWW_AUTHENTICATE, "www.my-domain.com");
* responseSerializer.serialize(httpResponse.getOutputStream(), (MyException) exception);
* return;
* }
* super.writeErrorResponse(exception);
* }
* }
* }</pre>
*
* <h2>Overridable Methods</h2>
*
* <ul>
* <li>
* <code>{@link #handle(HttpServletRequest, PMessage)}</code>: The main handle method. This <b>must</b> be
* implemented.
* </li>
* </ul>
*
* @param <RequestType> The request type.
* @param <ResponseType> The response type.
* @since 1.6.0
*/
public abstract class ProvidenceHttpServlet<
RequestType extends PMessage<RequestType>,
ResponseType extends PMessage<ResponseType>> extends HttpServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(ProvidenceHttpServlet.class);
private final PMessageDescriptor<RequestType> requestDescriptor;
private final ExceptionHandler exceptionHandler;
private final SerializerProvider serializerProvider;
public ProvidenceHttpServlet(@Nonnull PMessageDescriptor<RequestType> requestDescriptor,
@Nullable ExceptionHandler exceptionHandler,
@Nullable SerializerProvider serializerProvider) {
this.requestDescriptor = requestDescriptor;
this.exceptionHandler = exceptionHandler == null ? ExceptionHandler.INSTANCE : exceptionHandler;
this.serializerProvider = serializerProvider == null ? DefaultSerializerProvider.INSTANCE : serializerProvider;
}
/**
* Handle the request itself as a simple called method.
*
* @param httpRequest The HTTP request.
* @param request The parsed providence request.
* @return The response object.
* @throws Exception On any internal exception.
*/
@Nonnull
protected abstract
ResponseType handle(@Nonnull HttpServletRequest httpRequest,
@Nonnull RequestType request) throws Exception;
@Override
protected final void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException {
ResponseType response;
Serializer requestSerializer = serializerProvider.getDefault();
if (httpRequest.getContentType() != null) {
try {
requestSerializer = serializerProvider.getSerializer(httpRequest.getContentType());
} catch (IllegalArgumentException e) {
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown content-type: " + httpRequest.getContentType());
LOGGER.warn("Unknown content type in request", e);
return;
}
} else {
LOGGER.debug("Request is missing content type.");
}
Serializer responseSerializer = requestSerializer;
String acceptHeader = httpRequest.getHeader("Accept");
if (acceptHeader != null) {
String[] entries = acceptHeader.split(",");
for (String entry : entries) {
entry = entry.trim();
if (entry.isEmpty()) {
continue;
}
if ("*/*".equals(entry)) {
// Then responding same as request is good.
break;
}
try {
MimeType mediaType = new MimeType(entry);
responseSerializer = serializerProvider.getSerializer(mediaType.getBaseType());
break;
} catch (MimeTypeParseException ignore) {
// Ignore. Bad header input is pretty common.
}
}
}
RequestType request;
try {
request = requestSerializer.deserialize(httpRequest.getInputStream(), requestDescriptor);
requestSerializer.verifyEndOfContent(httpRequest.getInputStream());
} catch (SerializerException e) {
LOGGER.info("Failed to deserialize request to {}: {}", httpRequest.getServletPath(), e.displayString(), e);
PApplicationException ex = new PApplicationException(e.getMessage(), INVALID_PROTOCOL).initCause(e);
exceptionHandler.handleException(ex, responseSerializer, httpRequest, httpResponse);
return;
}
try {
response = handle(httpRequest, request);
} catch (Exception e) {
try {
exceptionHandler.handleException(e, responseSerializer, httpRequest, httpResponse);
if (!httpResponse.isCommitted()) {
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
} catch (Exception e1) {
LOGGER.error("Exception sending error", e1);
if (!httpResponse.isCommitted()) {
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e1.getMessage());
}
}
return;
}
httpResponse.setStatus(HttpServletResponse.SC_OK);
httpResponse.setContentType(responseSerializer.mediaType());
responseSerializer.serialize(httpResponse.getOutputStream(), response);
httpResponse.flushBuffer();
}
}