io.dropwizard.revolver.resource.RevolverRequestResource.java Source code

Java tutorial

Introduction

Here is the source code for io.dropwizard.revolver.resource.RevolverRequestResource.java

Source

/*
 * Copyright 2016 Phaneesh Nagaraja <phaneesh.n@gmail.com>.
 *
 *  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.dropwizard.revolver.resource;

import com.codahale.metrics.annotation.Metered;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.google.common.base.Strings;
import io.dropwizard.jersey.PATCH;
import io.dropwizard.msgpack.MsgPackMediaType;
import io.dropwizard.revolver.RevolverBundle;
import io.dropwizard.revolver.base.core.RevolverAckMessage;
import io.dropwizard.revolver.base.core.RevolverCallbackRequest;
import io.dropwizard.revolver.base.core.RevolverCallbackResponse;
import io.dropwizard.revolver.base.core.RevolverRequestState;
import io.dropwizard.revolver.callback.CallbackHandler;
import io.dropwizard.revolver.core.tracing.TraceInfo;
import io.dropwizard.revolver.http.RevolverHttpCommand;
import io.dropwizard.revolver.http.RevolversHttpHeaders;
import io.dropwizard.revolver.http.config.RevolverHttpApiConfig;
import io.dropwizard.revolver.http.model.RevolverHttpRequest;
import io.dropwizard.revolver.http.model.RevolverHttpResponse;
import io.dropwizard.revolver.persistence.PersistenceProvider;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import lombok.val;

import javax.inject.Singleton;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.io.IOException;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;

/**
 * @author phaneesh
 */
@Path("/apis")
@Slf4j
@Singleton
@Api(value = "Revolver Gateway", description = "Revolver api gateway endpoints")
public class RevolverRequestResource {

    private final ObjectMapper jsonObjectMapper;

    private final ObjectMapper msgPackObjectMapper;

    private final XmlMapper xmlObjectMapper;

    private final PersistenceProvider persistenceProvider;

    private final CallbackHandler callbackHandler;

    public RevolverRequestResource(final ObjectMapper jsonObjectMapper, final ObjectMapper msgPackObjectMapper,
            final XmlMapper xmlObjectMapper, final PersistenceProvider persistenceProvider,
            final CallbackHandler callbackHandler) {
        this.jsonObjectMapper = jsonObjectMapper;
        this.msgPackObjectMapper = msgPackObjectMapper;
        this.xmlObjectMapper = xmlObjectMapper;
        this.persistenceProvider = persistenceProvider;
        this.callbackHandler = callbackHandler;
    }

    @GET
    @Path(value = "/{service}/{path: .*}")
    @Metered
    @ApiOperation(value = "Revolver GET api endpoint")
    @Produces({ MediaType.WILDCARD })
    public Response get(@PathParam("service") final String service, @PathParam("path") final String path,
            @Context final HttpHeaders headers, @Context final UriInfo uriInfo) throws Exception {
        return processRequest(service, RevolverHttpApiConfig.RequestMethod.GET, path, headers, uriInfo, null);
    }

    @HEAD
    @Path(value = "/{service}/{path: .*}")
    @Metered
    @ApiOperation(value = "Revolver HEAD api endpoint")
    @Produces({ MediaType.WILDCARD })
    public Response head(@PathParam("service") final String service, @PathParam("path") final String path,
            @Context final HttpHeaders headers, @Context final UriInfo uriInfo) throws Exception {
        return processRequest(service, RevolverHttpApiConfig.RequestMethod.HEAD, path, headers, uriInfo, null);
    }

    @POST
    @Path(value = "/{service}/{path: .*}")
    @Metered
    @ApiOperation(value = "Revolver POST api endpoint")
    @Produces({ MediaType.WILDCARD })
    @Consumes({ MediaType.WILDCARD })
    public Response post(@PathParam("service") final String service, @PathParam("path") final String path,
            @Context final HttpHeaders headers, @Context final UriInfo uriInfo, final byte[] body)
            throws Exception {
        return processRequest(service, RevolverHttpApiConfig.RequestMethod.POST, path, headers, uriInfo, body);
    }

    @PUT
    @Path(value = "/{service}/{path: .*}")
    @Metered
    @ApiOperation(value = "Revolver PUT api endpoint")
    @Produces({ MediaType.WILDCARD })
    @Consumes({ MediaType.WILDCARD })
    public Response put(@PathParam("service") final String service, @PathParam("path") final String path,
            @Context final HttpHeaders headers, @Context final UriInfo uriInfo, final byte[] body)
            throws Exception {
        return processRequest(service, RevolverHttpApiConfig.RequestMethod.PUT, path, headers, uriInfo, body);
    }

    @DELETE
    @Path(value = "/{service}/{path: .*}")
    @Metered
    @ApiOperation(value = "Revolver DELETE api endpoint")
    @Produces({ MediaType.WILDCARD })
    public Response delete(@PathParam("service") final String service, @PathParam("path") final String path,
            @Context final HttpHeaders headers, @Context final UriInfo uriInfo) throws Exception {
        return processRequest(service, RevolverHttpApiConfig.RequestMethod.DELETE, path, headers, uriInfo, null);
    }

    @PATCH
    @Path(value = "/{service}/{path: .*}")
    @Metered
    @ApiOperation(value = "Revolver PATCH api endpoint")
    @Produces({ MediaType.WILDCARD })
    @Consumes({ MediaType.WILDCARD })
    public Response patch(@PathParam("service") final String service, @PathParam("path") final String path,
            @Context final HttpHeaders headers, @Context final UriInfo uriInfo, final byte[] body)
            throws Exception {
        return processRequest(service, RevolverHttpApiConfig.RequestMethod.PATCH, path, headers, uriInfo, body);
    }

    @OPTIONS
    @Path(value = "/{service}/{path: .*}")
    @Metered
    @ApiOperation(value = "Revolver OPTIONS api endpoint")
    @Produces({ MediaType.WILDCARD })
    @Consumes({ MediaType.WILDCARD })
    public Response options(@PathParam("service") final String service, @PathParam("path") final String path,
            @Context final HttpHeaders headers, @Context final UriInfo uriInfo, final byte[] body)
            throws Exception {
        return processRequest(service, RevolverHttpApiConfig.RequestMethod.OPTIONS, path, headers, uriInfo, body);
    }

    private Response processRequest(final String service, final RevolverHttpApiConfig.RequestMethod method,
            final String path, final HttpHeaders headers, final UriInfo uriInfo, final byte[] body)
            throws Exception {
        val apiMap = RevolverBundle.matchPath(service, path);
        if (apiMap == null) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(Collections.singletonMap("message", "Bad Request")).build();
        }
        val callMode = headers.getRequestHeaders().getFirst(RevolversHttpHeaders.CALL_MODE_HEADER);
        if (Strings.isNullOrEmpty(callMode)) {
            return executeInline(service, apiMap.getApi(), method, path, headers, uriInfo, body);
        }
        switch (callMode.toUpperCase()) {
        case RevolverHttpCommand.CALL_MODE_POLLING:
            return executeCommandAsync(service, apiMap.getApi(), method, path, headers, uriInfo, body,
                    apiMap.getApi().isAsync(), callMode);
        case RevolverHttpCommand.CALL_MODE_CALLBACK:
            if (Strings.isNullOrEmpty(headers.getHeaderString(RevolversHttpHeaders.CALLBACK_URI_HEADER))) {
                return Response.status(Response.Status.BAD_REQUEST).build();
            }
            return executeCommandAsync(service, apiMap.getApi(), method, path, headers, uriInfo, body,
                    apiMap.getApi().isAsync(), callMode);
        }
        return Response.status(Response.Status.BAD_REQUEST).build();
    }

    private Response executeInline(final String service, final RevolverHttpApiConfig api,
            final RevolverHttpApiConfig.RequestMethod method, final String path, final HttpHeaders headers,
            final UriInfo uriInfo, final byte[] body) throws IOException, TimeoutException {
        val sanatizedHeaders = new MultivaluedHashMap<String, String>();
        headers.getRequestHeaders().forEach(sanatizedHeaders::put);
        cleanHeaders(sanatizedHeaders, api);
        val httpCommand = RevolverBundle.getHttpCommand(service);
        val response = httpCommand.execute(RevolverHttpRequest.builder()
                .traceInfo(TraceInfo.builder()
                        .requestId(headers.getHeaderString(RevolversHttpHeaders.REQUEST_ID_HEADER))
                        .transactionId(headers.getHeaderString(RevolversHttpHeaders.TXN_ID_HEADER))
                        .timestamp(System.currentTimeMillis()).build())
                .api(api.getApi()).service(service).path(path).method(method).headers(sanatizedHeaders)
                .queryParams(uriInfo.getQueryParameters()).body(body).build());
        return transform(headers, response, api.getApi(), path, method);
    }

    private Response transform(HttpHeaders headers, RevolverHttpResponse response, String api, String path,
            RevolverHttpApiConfig.RequestMethod method) throws IOException {
        val httpResponse = Response.status(response.getStatusCode());
        //Add all the headers except content type header
        if (response.getHeaders() != null) {
            response.getHeaders().keySet().stream().filter(h -> !h.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE))
                    .filter(h -> !h.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH))
                    .forEach(h -> httpResponse.header(h, response.getHeaders().getFirst(h)));
        }
        httpResponse.header("X-REQUESTED-PATH", path);
        httpResponse.header("X-REQUESTED-METHOD", method);
        httpResponse.header("X-REQUESTED-API", api);
        final String responseMediaType = response.getHeaders() != null
                && Strings.isNullOrEmpty(response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE))
                        ? MediaType.TEXT_HTML
                        : response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        final String requestMediaType = headers != null
                && Strings.isNullOrEmpty(headers.getHeaderString(HttpHeaders.ACCEPT)) ? null
                        : headers.getHeaderString(HttpHeaders.ACCEPT);
        //If no no accept was specified in request; just send it as the same content type as response
        //Also send it as the content type as response content type if there requested content type is the same;
        if (Strings.isNullOrEmpty(requestMediaType) || requestMediaType.equals(responseMediaType)) {
            httpResponse.header(HttpHeaders.CONTENT_TYPE, responseMediaType);
            httpResponse.entity(response.getBody());
            return httpResponse.build();
        }
        Object responseData = null;
        if (responseMediaType.startsWith(MediaType.APPLICATION_JSON)) {
            final JsonNode jsonNode = jsonObjectMapper.readTree(response.getBody());
            if (jsonNode.isArray()) {
                responseData = jsonObjectMapper.convertValue(jsonNode, List.class);
            } else {
                responseData = jsonObjectMapper.convertValue(jsonNode, Map.class);
            }
        } else if (responseMediaType.startsWith(MediaType.APPLICATION_XML)) {
            final JsonNode jsonNode = xmlObjectMapper.readTree(response.getBody());
            if (jsonNode.isArray()) {
                responseData = xmlObjectMapper.convertValue(jsonNode, List.class);
            } else {
                responseData = xmlObjectMapper.convertValue(jsonNode, Map.class);
            }
        } else if (responseMediaType.startsWith(MsgPackMediaType.APPLICATION_MSGPACK)) {
            final JsonNode jsonNode = msgPackObjectMapper.readTree(response.getBody());
            if (jsonNode.isArray()) {
                responseData = msgPackObjectMapper.convertValue(jsonNode, List.class);
            } else {
                responseData = msgPackObjectMapper.convertValue(jsonNode, Map.class);
            }
        }
        if (responseData == null) {
            httpResponse.entity(response.getBody());
        } else {
            if (requestMediaType.startsWith(MediaType.APPLICATION_JSON)) {
                httpResponse.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
                httpResponse.entity(jsonObjectMapper.writeValueAsBytes(responseData));
            } else if (requestMediaType.startsWith(MediaType.APPLICATION_XML)) {
                httpResponse.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML);
                httpResponse
                        .entity(xmlObjectMapper.writer().withRootName("Response").writeValueAsBytes(responseData));
            } else if (requestMediaType.startsWith(MsgPackMediaType.APPLICATION_MSGPACK)) {
                httpResponse.header(HttpHeaders.CONTENT_TYPE, MsgPackMediaType.APPLICATION_MSGPACK);
                httpResponse.entity(msgPackObjectMapper.writeValueAsBytes(responseData));
            } else {
                httpResponse.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
                httpResponse.entity(jsonObjectMapper.writeValueAsBytes(responseData));
            }
        }
        return httpResponse.build();
    }

    private void cleanHeaders(final MultivaluedMap<String, String> headers, RevolverHttpApiConfig apiConfig) {
        headers.remove(HttpHeaders.HOST);
        headers.remove(HttpHeaders.ACCEPT);
        headers.remove(HttpHeaders.ACCEPT_ENCODING);
        headers.putSingle(HttpHeaders.ACCEPT, apiConfig.getAcceptType());
        headers.putSingle(HttpHeaders.ACCEPT_ENCODING, apiConfig.getAcceptEncoding());
    }

    private Response executeCommandAsync(final String service, final RevolverHttpApiConfig api,
            final RevolverHttpApiConfig.RequestMethod method, final String path, final HttpHeaders headers,
            final UriInfo uriInfo, final byte[] body, final boolean isDownstreamAsync, final String callMode)
            throws Exception {
        val sanatizedHeaders = new MultivaluedHashMap<String, String>();
        headers.getRequestHeaders().forEach(sanatizedHeaders::put);
        cleanHeaders(sanatizedHeaders, api);
        val httpCommand = RevolverBundle.getHttpCommand(service);
        val requestId = headers.getHeaderString(RevolversHttpHeaders.REQUEST_ID_HEADER);
        val transactionId = headers.getHeaderString(RevolversHttpHeaders.TXN_ID_HEADER);
        val mailBoxId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_ID_HEADER);
        //Short circuit if it is a duplicate request
        if (persistenceProvider.exists(requestId)) {
            return Response.status(Response.Status.NOT_ACCEPTABLE)
                    .entity(Collections.singletonMap("message", "Duplicate")).build();
        }
        persistenceProvider.saveRequest(requestId, mailBoxId,
                RevolverCallbackRequest.builder().api(api.getApi())
                        .mode(headers.getRequestHeaders().getFirst(RevolversHttpHeaders.CALL_MODE_HEADER))
                        .callbackUri(headers.getRequestHeaders().getFirst(RevolversHttpHeaders.CALLBACK_URI_HEADER))
                        .method(headers.getRequestHeaders().getFirst(RevolversHttpHeaders.CALLBACK_METHOD_HEADER))
                        .service(service).path(path).headers(headers.getRequestHeaders())
                        .queryParams(uriInfo.getQueryParameters()).body(body).build());
        CompletableFuture<RevolverHttpResponse> response = httpCommand.executeAsync(RevolverHttpRequest.builder()
                .traceInfo(TraceInfo.builder().requestId(requestId).transactionId(transactionId)
                        .timestamp(System.currentTimeMillis()).build())
                .api(api.getApi()).service(service).path(path).method(method).headers(sanatizedHeaders)
                .queryParams(uriInfo.getQueryParameters()).body(body).build());
        //Async Downstream send accept on request path (Still circuit breaker will kick in. Keep circuit breaker aggressive)
        if (isDownstreamAsync) {
            val result = response.get();
            if (result.getStatusCode() == Response.Status.ACCEPTED.getStatusCode()) {
                persistenceProvider.setRequestState(requestId, RevolverRequestState.REQUESTED);
            } else {
                persistenceProvider.setRequestState(requestId, RevolverRequestState.RESPONDED);
                saveResponse(requestId, result);
            }
            return transform(headers, result, api.getApi(), path, method);
        } else {
            response.thenAcceptAsync(result -> {
                if (result.getStatusCode() == Response.Status.ACCEPTED.getStatusCode()) {
                    persistenceProvider.setRequestState(requestId, RevolverRequestState.REQUESTED);
                } else if (result.getStatusCode() == Response.Status.OK.getStatusCode()) {
                    persistenceProvider.setRequestState(requestId, RevolverRequestState.RESPONDED);
                    saveResponse(requestId, result);
                } else {
                    persistenceProvider.setRequestState(requestId, RevolverRequestState.ERROR);
                    saveResponse(requestId, result);
                }
                if (callMode != null && callMode.equals(RevolverHttpCommand.CALL_MODE_CALLBACK)) {
                    callbackHandler.handle(requestId);
                }
            });
            return Response.accepted().entity(RevolverAckMessage.builder().requestId(requestId)
                    .acceptedAt(Instant.now().toEpochMilli()).build()).build();
        }
    }

    private void saveResponse(String requestId, RevolverHttpResponse result) {
        try {
            persistenceProvider.saveResponse(requestId, RevolverCallbackResponse.builder().body(result.getBody())
                    .headers(result.getHeaders()).statusCode(result.getStatusCode()).build());
        } catch (Exception e) {
            log.error("Error saving response!", e);
        }

    }
}