package io.divolte.server;
import static io.divolte.server.HttpSource.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.ParametersAreNonnullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import io.divolte.server.processing.Item;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes;
@ParametersAreNonnullByDefault
public class JsonEventHandler implements HttpHandler {
private static final Logger logger = LoggerFactory.getLogger(JsonEventHandler.class);
private final IncomingRequestProcessingPool processingPool;
private final int sourceIndex;
private final String partyIdParameter;
private final AsyncRequestBodyReceiver receiver;
public JsonEventHandler(final IncomingRequestProcessingPool processingPool,
final int sourceIndex,
final String partyIdParameter,
final int maximumBodySize) {
this.processingPool = Objects.requireNonNull(processingPool);
this.sourceIndex = sourceIndex;
this.partyIdParameter = Objects.requireNonNull(partyIdParameter);
receiver = new AsyncRequestBodyReceiver(maximumBodySize);
}
@Override
public void handleRequest(final HttpServerExchange exchange) {
captureAndPersistSourceAddress(exchange);
receiver.receive((body,length) -> {
try {
if (0 < length) {
logEvent(exchange, body);
exchange.setStatusCode(StatusCodes.NO_CONTENT);
} else {
// Empty body; bad by definition.
exchange.setStatusCode(StatusCodes.BAD_REQUEST);
}
} catch (final IncompleteRequestException e) {
// Improper request, could be anything.
exchange.setStatusCode(StatusCodes.BAD_REQUEST);
logger.warn("Improper request received from {}.",
Optional.ofNullable(exchange.getSourceAddress())
.map(InetSocketAddress::getHostString)
.orElse("<UNKNOWN HOST>"));
} finally {
exchange.endExchange();
}
}, exchange);
}
private void logEvent(final HttpServerExchange exchange, final InputStream body) throws IncompleteRequestException {
final DivolteIdentifier partyId = queryParamFromExchange(exchange, partyIdParameter).flatMap(DivolteIdentifier::tryParse)
.orElseThrow(IncompleteRequestException::new);
final UndertowEvent event = new JsonUndertowEvent(Instant.now(), exchange, partyId, body);
processingPool.enqueue(Item.of(sourceIndex, partyId.value, event));
}
@ParametersAreNonnullByDefault
private static final class JsonUndertowEvent extends UndertowEvent {
private static final ObjectMapper OBJECT_MAPPER;
static {
final ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
// Support JDK8 parameter name discovery
mapper.registerModules(new ParameterNamesModule());
mapper.registerModule(new Jdk8Module());
OBJECT_MAPPER = mapper;
}
private final InputStream requestBody;
private JsonUndertowEvent(
final Instant requestTime,
final HttpServerExchange exchange,
final DivolteIdentifier partyId,
final InputStream requestBody) throws IncompleteRequestException {
super(requestTime, exchange, partyId);
this.requestBody = Objects.requireNonNull(requestBody);
}
@Override
public DivolteEvent parseRequest() throws IncompleteRequestException {
final EventContainer container;
try {
container = OBJECT_MAPPER.readValue(requestBody, EventContainer.class);
} catch(final JsonMappingException me) {
logger.info("JSON mapping failed for request: {}", me.getMessage());
throw new IncompleteRequestException();
} catch (final IOException e) {
// This indicates we couldn't parse the data. Corrupt or incomplete,
// we can't proceed because mapping is all-or-nothing and we don't
// even have a partial object.
logger.warn("Parsing failed for request.", e);
throw new IncompleteRequestException();
}
/*
* Parse the client provided timestamp as ISO offsetted date/time. We use the ofEpochSecond creator to
* obtain an Instant, as the Instant#from(TemporalAccessor) performs some additional checks unnecessary
* in our case.
*/
final TemporalAccessor parsed = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(container.clientTimestampIso);
final Instant clientTime = Instant.from(parsed);
final DivolteEvent event = DivolteEvent.createJsonEvent(
exchange, partyId,
DivolteIdentifier.tryParse(container.sessionId).orElseThrow(IncompleteRequestException::new),
container.eventId, JsonSource.EVENT_SOURCE_NAME, requestTime, clientTime,
container.isNewParty, container.isNewSession, container.eventType,
() -> container.parameters, // Note that it's possible to send a JSON event without parameters
DivolteEvent.JsonEventData.EMPTY);
return event;
}
@ParametersAreNonnullByDefault
private final static class EventContainer {
public final Optional<String> eventType;
@JsonProperty(required=true) public final String sessionId;
@JsonProperty(required=true) public final String eventId;
@JsonProperty(required=true) public final boolean isNewParty;
@JsonProperty(required=true) public final boolean isNewSession;
@JsonProperty(required=true) public final String clientTimestampIso;
public final Optional<JsonNode> parameters;
@JsonCreator
public EventContainer(
final Optional<String> eventType, final String sessionId, final String eventId, final boolean isNewParty,
final boolean isNewSession, final String clientTimestampIso, final Optional<JsonNode> parameters) {
this.eventType = Objects.requireNonNull(eventType);
this.sessionId = Objects.requireNonNull(sessionId);
this.eventId = Objects.requireNonNull(eventId);
this.isNewParty = Objects.requireNonNull(isNewParty);
this.isNewSession = Objects.requireNonNull(isNewSession);
this.clientTimestampIso = Objects.requireNonNull(clientTimestampIso);
this.parameters = Objects.requireNonNull(parameters);
}
}
}
}