diff --git a/pom.xml b/pom.xml
index 18756daa..78a6e61e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
io.revolver
dropwizard-callback-resilience4j
- 1.0.5
+ 1.0.5-SNAPSHOT
jar
dropwizard-revolver
diff --git a/src/main/java/io/dropwizard/revolver/RevolverBundle.java b/src/main/java/io/dropwizard/revolver/RevolverBundle.java
index ac74e468..77b7cf86 100644
--- a/src/main/java/io/dropwizard/revolver/RevolverBundle.java
+++ b/src/main/java/io/dropwizard/revolver/RevolverBundle.java
@@ -31,11 +31,7 @@
import io.dropwizard.revolver.aeroapike.AerospikeConnectionManager;
import io.dropwizard.revolver.callback.InlineCallbackHandler;
import io.dropwizard.revolver.core.RevolverExecutionException;
-import io.dropwizard.revolver.core.config.AerospikeMailBoxConfig;
-import io.dropwizard.revolver.core.config.InMemoryMailBoxConfig;
-import io.dropwizard.revolver.core.config.RevolverConfig;
-import io.dropwizard.revolver.core.config.RevolverServiceConfig;
-import io.dropwizard.revolver.core.config.ServiceDiscoveryConfig;
+import io.dropwizard.revolver.core.config.*;
import io.dropwizard.revolver.core.config.hystrix.HystrixUtil;
import io.dropwizard.revolver.core.config.hystrix.ThreadPoolConfig;
import io.dropwizard.revolver.core.model.RevolverExecutorType;
@@ -66,33 +62,28 @@
import io.dropwizard.revolver.persistence.AeroSpikePersistenceProvider;
import io.dropwizard.revolver.persistence.InMemoryPersistenceProvider;
import io.dropwizard.revolver.persistence.PersistenceProvider;
-import io.dropwizard.revolver.resource.RevolverApiManageResource;
-import io.dropwizard.revolver.resource.RevolverConfigResource;
-import io.dropwizard.revolver.resource.RevolverMailboxResource;
-import io.dropwizard.revolver.resource.RevolverMailboxResourceV2;
-import io.dropwizard.revolver.resource.RevolverMetadataResource;
+import io.dropwizard.revolver.provider.BlacklistMethodData;
+import io.dropwizard.revolver.provider.BlacklistProcessor;
+import io.dropwizard.revolver.resource.*;
import io.dropwizard.revolver.splitting.PathExpressionSplitConfig;
import io.dropwizard.revolver.splitting.SplitConfig;
import io.dropwizard.riemann.RiemannBundle;
import io.dropwizard.riemann.RiemannConfig;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.curator.framework.CuratorFramework;
+import org.msgpack.jackson.dataformat.MessagePackFactory;
+
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.MultivaluedMap;
-import lombok.extern.slf4j.Slf4j;
-import lombok.val;
-import org.apache.curator.framework.CuratorFramework;
-import org.msgpack.jackson.dataformat.MessagePackFactory;
/**
* @author phaneesh
@@ -167,6 +158,8 @@ public void run(T configuration, Environment environment) {
public abstract ConfigSource getConfigSource();
+ public abstract Set getBlacklistData();
+
public void onConfigChange(String configData) {
log.info("Config changed! Override to propagate config changes to other bundles");
}
@@ -295,7 +288,7 @@ private static void registerHttpsCommand(RevolverServiceConfig config) {
}
private static void registerCommand(RevolverServiceConfig config,
- RevolverHttpServiceConfig revolverHttpServiceConfig) {
+ RevolverHttpServiceConfig revolverHttpServiceConfig) {
if (config instanceof RevolverHttpServiceConfig) {
@@ -424,9 +417,9 @@ private void initializeRevolver(T configuration, Environment environment) {
.resolverConfig(revolverConfig.getServiceResolverConfig())
.serviceDiscoveryConfig(serviceDiscoveryConfig).build()
: RevolverServiceResolver.builder()
- .resolverConfig(revolverConfig.getServiceResolverConfig())
- .objectMapper(environment.getObjectMapper()).
- serviceDiscoveryConfig(serviceDiscoveryConfig).build();
+ .resolverConfig(revolverConfig.getServiceResolverConfig())
+ .objectMapper(environment.getObjectMapper()).
+ serviceDiscoveryConfig(serviceDiscoveryConfig).build();
} else {
serviceNameResolver = RevolverServiceResolver.builder()
.objectMapper(environment.getObjectMapper())
@@ -518,7 +511,7 @@ private void registerTypes(Bootstrap> bootstrap) {
private void registerResources(Environment environment, MetricRegistry metrics,
- PersistenceProvider persistenceProvider, InlineCallbackHandler callbackHandler) {
+ PersistenceProvider persistenceProvider, InlineCallbackHandler callbackHandler) {
environment.jersey().register(new RevolverMetadataResource(revolverConfig));
environment.jersey().register(
new RevolverMailboxResource(persistenceProvider, environment.getObjectMapper(),
@@ -535,6 +528,13 @@ private void registerResources(Environment environment, MetricRegistry metrics,
}
environment.jersey().register(new RevolverConfigResource(dynamicConfigHandler));
environment.jersey().register(new RevolverApiManageResource());
+ environment.jersey().register(new RevolverCallbackResource(persistenceProvider, callbackHandler));
+ environment.jersey().register(
+ new RevolverRequestResource(environment.getObjectMapper(), MSG_PACK_OBJECT_MAPPER,
+ persistenceProvider, callbackHandler, metrics, revolverConfig));
+
+ // Register blacklist processor
+ environment.jersey().register(new BlacklistProcessor(getBlacklistData()));
}
private void registerFilters(Environment environment) {
diff --git a/src/main/java/io/dropwizard/revolver/provider/BlacklistMethodData.java b/src/main/java/io/dropwizard/revolver/provider/BlacklistMethodData.java
new file mode 100644
index 00000000..0bdbfd82
--- /dev/null
+++ b/src/main/java/io/dropwizard/revolver/provider/BlacklistMethodData.java
@@ -0,0 +1,14 @@
+package io.dropwizard.revolver.provider;
+
+import lombok.*;
+
+@Getter
+@Builder
+@ToString
+@AllArgsConstructor
+@EqualsAndHashCode
+public class BlacklistMethodData {
+ private final String httpMethod;
+ private final String relativePath; // path of the method excluding parent resource path
+ private final String resourceClassName;
+}
diff --git a/src/main/java/io/dropwizard/revolver/provider/BlacklistProcessor.java b/src/main/java/io/dropwizard/revolver/provider/BlacklistProcessor.java
new file mode 100644
index 00000000..4f33102c
--- /dev/null
+++ b/src/main/java/io/dropwizard/revolver/provider/BlacklistProcessor.java
@@ -0,0 +1,79 @@
+package io.dropwizard.revolver.provider;
+
+import lombok.extern.slf4j.Slf4j;
+import org.glassfish.jersey.server.model.ModelProcessor;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.server.model.Resource.Builder;
+import org.glassfish.jersey.server.model.ResourceMethod;
+import org.glassfish.jersey.server.model.ResourceModel;
+
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.ext.Provider;
+import java.util.List;
+import java.util.Set;
+
+@Provider
+@Slf4j
+public class BlacklistProcessor implements ModelProcessor {
+
+ private final Set blacklistMethods;
+
+ public BlacklistProcessor(final Set blacklistMethods) {
+ this.blacklistMethods = blacklistMethods;
+ }
+
+ @Override
+ public ResourceModel processResourceModel(final ResourceModel resourceModel, final Configuration configuration) {
+ if (blacklistMethods == null || blacklistMethods.isEmpty()) {
+ log.info("No API end-point to blacklist");
+ return resourceModel;
+ }
+
+ ResourceModel.Builder newResourceModelBuilder = new ResourceModel.Builder(false);
+ for (final Resource resource : resourceModel.getResources()) {
+ final Resource.Builder resourceBuilder;
+
+ final List childResources = resource.getChildResources();
+
+ resourceBuilder = childResources.isEmpty()
+ ? Resource.builder(resource)
+ : Resource.builder(resource.getPath());
+
+ // Add child resources with non-blacklisted child resource methods only
+ childResources.forEach(childResource -> {
+ final Builder childResourceBuilder = Resource.builder(childResource.getPath());
+ childResource.getResourceMethods().stream()
+ .filter(this::shouldAddMethod)
+ .forEach(childResourceBuilder::addMethod);
+ resourceBuilder.addChildResource(childResourceBuilder.build());
+ });
+
+ newResourceModelBuilder.addResource(resourceBuilder.build());
+ }
+
+ return newResourceModelBuilder.build();
+ }
+
+ @Override
+ public ResourceModel processSubResource(final ResourceModel subResourceModel, final Configuration configuration) {
+ return subResourceModel;
+ }
+
+ private boolean shouldAddMethod(final ResourceMethod method) {
+ // This is slightly sub-optimal,
+ // but given that blacklist count will be small, not creating a complex Map to match Jersey resource structure
+ return blacklistMethods.stream()
+ .noneMatch(blacklistData -> {
+ if (method.getInvocable().getHandler().getHandlerClass().getName().equals(blacklistData.getResourceClassName()) &&
+ method.getHttpMethod().equalsIgnoreCase(blacklistData.getHttpMethod()) &&
+ method.getParent().getPath().equals(blacklistData.getRelativePath())) {
+ log.info("Blacklisting method with path: {}, http method: {}, resource: {}",
+ blacklistData.getRelativePath(),
+ blacklistData.getHttpMethod(),
+ blacklistData.getResourceClassName());
+ return true;
+ }
+ return false;
+ });
+ }
+}
diff --git a/src/main/java/io/dropwizard/revolver/resource/RevolverCallbackResource.java b/src/main/java/io/dropwizard/revolver/resource/RevolverCallbackResource.java
new file mode 100644
index 00000000..e1b238e0
--- /dev/null
+++ b/src/main/java/io/dropwizard/revolver/resource/RevolverCallbackResource.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2016 Phaneesh Nagaraja .
+ *
+ * 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.google.common.base.Strings;
+import io.dropwizard.msgpack.MsgPackMediaType;
+import io.dropwizard.revolver.base.core.RevolverCallbackResponse;
+import io.dropwizard.revolver.callback.InlineCallbackHandler;
+import io.dropwizard.revolver.http.RevolverHttpCommand;
+import io.dropwizard.revolver.persistence.PersistenceProvider;
+import io.dropwizard.revolver.util.HeaderUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import javax.inject.Singleton;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+/**
+ * @author phaneesh
+ */
+@Path("/revolver")
+@Slf4j
+@Singleton
+@Api(value = "RequestCallback", description = "Revolver gateway api for callbacks on mailbox requests")
+public class RevolverCallbackResource {
+
+ private static final String RESPONSE_CODE_HEADER = "X-RESPONSE-CODE";
+
+ private final PersistenceProvider persistenceProvider;
+
+ private final InlineCallbackHandler callbackHandler;
+
+ public RevolverCallbackResource(PersistenceProvider persistenceProvider,
+ InlineCallbackHandler callbackHandler) {
+ this.persistenceProvider = persistenceProvider;
+ this.callbackHandler = callbackHandler;
+ }
+
+ @Path("/v1/callback/{requestId}")
+ @POST
+ @Metered
+ @ApiOperation(value = "Callback for updating responses for a given mailbox request")
+ @Produces({MediaType.APPLICATION_JSON, MsgPackMediaType.APPLICATION_MSGPACK,
+ MediaType.APPLICATION_XML})
+ @Consumes({MediaType.APPLICATION_JSON, MsgPackMediaType.APPLICATION_MSGPACK,
+ MediaType.APPLICATION_XML})
+ public Response handleCallback(@PathParam("requestId") String requestId,
+ @HeaderParam(RESPONSE_CODE_HEADER) String responseCode,
+ @Context HttpHeaders headers, byte[] responseBody) {
+ long start = System.currentTimeMillis();
+ try {
+ val callbackRequest = persistenceProvider.request(requestId);
+ log.debug("Callback request in handleCallback : " + callbackRequest);
+ if (callbackRequest == null) {
+ return Response.status(Response.Status.BAD_REQUEST).build();
+ }
+ val response = RevolverCallbackResponse.builder().body(responseBody)
+ .headers(headers.getRequestHeaders()).statusCode(
+ responseCode != null ? Integer.parseInt(responseCode)
+ : Response.Status.OK.getStatusCode()).build();
+ val mailboxTtl = HeaderUtil.getTTL(callbackRequest);
+ persistenceProvider.saveResponse(requestId, response, mailboxTtl);
+ if (callbackRequest.getMode() != null && (
+ callbackRequest.getMode().equals(RevolverHttpCommand.CALL_MODE_CALLBACK)
+ || callbackRequest.getMode()
+ .equals(RevolverHttpCommand.CALL_MODE_CALLBACK_SYNC)) && !Strings
+ .isNullOrEmpty(callbackRequest.getCallbackUri())) {
+ callbackHandler.handle(requestId, response);
+ }
+ log.info(
+ "Callback processing for request id: {} with response size: {} bytes completed in {} ms",
+ requestId, responseBody.length, (System.currentTimeMillis() - start));
+ return Response.accepted().build();
+ } catch (Exception e) {
+ log.error("Callback error", e);
+ return Response.serverError().build();
+ }
+ }
+}
diff --git a/src/main/java/io/dropwizard/revolver/resource/RevolverRequestResource.java b/src/main/java/io/dropwizard/revolver/resource/RevolverRequestResource.java
new file mode 100644
index 00000000..1893b89b
--- /dev/null
+++ b/src/main/java/io/dropwizard/revolver/resource/RevolverRequestResource.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright 2016 Phaneesh Nagaraja .
+ *
+ * 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.MetricRegistry;
+import com.codahale.metrics.annotation.Metered;
+import com.collections.CollectionUtils;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.InlineCallbackHandler;
+import io.dropwizard.revolver.core.config.ApiLatencyConfig;
+import io.dropwizard.revolver.core.config.RevolverConfig;
+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.ApiPathMap;
+import io.dropwizard.revolver.http.model.RevolverHttpRequest;
+import io.dropwizard.revolver.http.model.RevolverHttpResponse;
+import io.dropwizard.revolver.optimizer.config.OptimizerConfig;
+import io.dropwizard.revolver.optimizer.config.OptimizerTimeConfig;
+import io.dropwizard.revolver.persistence.PersistenceProvider;
+import io.dropwizard.revolver.splitting.HeaderExpressionSplitConfig;
+import io.dropwizard.revolver.splitting.PathExpressionSplitConfig;
+import io.dropwizard.revolver.splitting.RevolverHttpApiSplitConfig;
+import io.dropwizard.revolver.splitting.SplitConfig;
+import io.dropwizard.revolver.splitting.SplitStrategy;
+import io.dropwizard.revolver.util.ResponseTransformationUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+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;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.inject.Singleton;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+/**
+ * @author phaneesh
+ */
+@Path("/apis")
+@Slf4j
+@Singleton
+@Api(value = "Revolver Gateway", description = "Revolver api gateway endpoints")
+public class RevolverRequestResource {
+
+ private static final Map BAD_REQUEST_RESPONSE = Collections
+ .singletonMap("message", "Bad Request");
+ private static final Map DUPLICATE_REQUEST_RESPONSE = Collections
+ .singletonMap("message", "Duplicate");
+ private static Map SERVICE_UNAVAILABLE_RESPONSE = Collections
+ .singletonMap("message", "Service Unavailable");
+ private final ObjectMapper jsonObjectMapper;
+ private final ObjectMapper msgPackObjectMapper;
+ private final PersistenceProvider persistenceProvider;
+ private final InlineCallbackHandler callbackHandler;
+ private final MetricRegistry metrics;
+ private final RevolverConfig revolverConfig;
+
+ public RevolverRequestResource(ObjectMapper jsonObjectMapper, ObjectMapper msgPackObjectMapper,
+ PersistenceProvider persistenceProvider, InlineCallbackHandler callbackHandler,
+ MetricRegistry metrics, RevolverConfig revolverConfig) {
+ this.jsonObjectMapper = jsonObjectMapper;
+ this.msgPackObjectMapper = msgPackObjectMapper;
+ this.persistenceProvider = persistenceProvider;
+ this.callbackHandler = callbackHandler;
+ this.metrics = metrics;
+ this.revolverConfig = revolverConfig;
+ }
+
+ @GET
+ @Path(value = "/{service}/{path: .*}")
+ @Metered
+ @ApiOperation(value = "Revolver GET api endpoint")
+ public Response get(@PathParam("service") String service, @PathParam("path") String path,
+ @Context HttpHeaders headers, @Context UriInfo uriInfo) throws Exception {
+ Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.GET, path,
+ headers, uriInfo, null);
+ pushMetrics(response, service, path);
+ return response;
+ }
+
+ @HEAD
+ @Path(value = "/{service}/{path: .*}")
+ @Metered
+ @ApiOperation(value = "Revolver HEAD api endpoint")
+ public Response head(@PathParam("service") String service, @PathParam("path") String path,
+ @Context HttpHeaders headers, @Context UriInfo uriInfo) throws Exception {
+ Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.HEAD, path,
+ headers, uriInfo, null);
+ pushMetrics(response, service, path);
+ return response;
+ }
+
+ @POST
+ @Path(value = "/{service}/{path: .*}")
+ @Metered
+ @ApiOperation(value = "Revolver POST api endpoint")
+ public Response post(@PathParam("service") String service, @PathParam("path") String path,
+ @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception {
+ Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.POST, path,
+ headers, uriInfo, body);
+ pushMetrics(response, service, path);
+ return response;
+ }
+
+ @PUT
+ @Path(value = "/{service}/{path: .*}")
+ @Metered
+ @ApiOperation(value = "Revolver PUT api endpoint")
+ public Response put(@PathParam("service") String service, @PathParam("path") String path,
+ @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception {
+ Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.PUT, path,
+ headers, uriInfo, body);
+ pushMetrics(response, service, path);
+ return response;
+ }
+
+ @DELETE
+ @Path(value = "/{service}/{path: .*}")
+ @Metered
+ @ApiOperation(value = "Revolver DELETE api endpoint")
+ public Response delete(@PathParam("service") String service, @PathParam("path") String path,
+ @Context HttpHeaders headers, @Context UriInfo uriInfo) throws Exception {
+ Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.DELETE,
+ path, headers, uriInfo, null);
+ pushMetrics(response, service, path);
+ return response;
+ }
+
+ @PATCH
+ @Path(value = "/{service}/{path: .*}")
+ @Metered
+ @ApiOperation(value = "Revolver PATCH api endpoint")
+ public Response patch(@PathParam("service") String service, @PathParam("path") String path,
+ @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception {
+ Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.PATCH, path,
+ headers, uriInfo, body);
+ pushMetrics(response, service, path);
+ return response;
+ }
+
+ @OPTIONS
+ @Path(value = "/{service}/{path: .*}")
+ @Metered
+ @ApiOperation(value = "Revolver OPTIONS api endpoint")
+ public Response options(@PathParam("service") String service, @PathParam("path") String path,
+ @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception {
+ Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.OPTIONS,
+ path, headers, uriInfo, body);
+ pushMetrics(response, service, path);
+ return response;
+ }
+
+
+ private Response processRequest(String service, RevolverHttpApiConfig.RequestMethod method,
+ String path, HttpHeaders headers, UriInfo uriInfo, byte[] body) throws Exception {
+ val apiMap = resolvePath(service, path, headers);
+ if (apiMap == null) {
+ return Response.status(Response.Status.BAD_REQUEST).entity(ResponseTransformationUtil
+ .transform(BAD_REQUEST_RESPONSE,
+ headers.getMediaType() != null ? headers.getMediaType().toString()
+ : MediaType.APPLICATION_JSON, jsonObjectMapper,
+ msgPackObjectMapper)).build();
+ }
+ String serviceKey = service + "." + apiMap.getApi().getApi();
+ if (RevolverBundle.apiStatus.containsKey(serviceKey) && !RevolverBundle.apiStatus
+ .get(serviceKey)) {
+ return Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity(ResponseTransformationUtil.transform(SERVICE_UNAVAILABLE_RESPONSE,
+ headers.getMediaType() != null ? headers.getMediaType().toString()
+ : MediaType.APPLICATION_JSON, jsonObjectMapper,
+ msgPackObjectMapper)).build();
+ }
+ val callMode = getCallMode(apiMap, headers);
+
+ 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)
+ .entity(ResponseTransformationUtil.transform(BAD_REQUEST_RESPONSE,
+ headers.getMediaType() != null ? headers.getMediaType()
+ .toString() : MediaType.APPLICATION_JSON,
+ jsonObjectMapper, msgPackObjectMapper)).build();
+ }
+ return executeCommandAsync(service, apiMap.getApi(), method, path, headers, uriInfo,
+ body, apiMap.getApi().isAsync(), callMode);
+ case RevolverHttpCommand.CALL_MODE_CALLBACK_SYNC:
+ if (Strings.isNullOrEmpty(
+ headers.getHeaderString(RevolversHttpHeaders.CALLBACK_URI_HEADER))) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity(ResponseTransformationUtil.transform(BAD_REQUEST_RESPONSE,
+ headers.getMediaType() != null ? headers.getMediaType()
+ .toString() : MediaType.APPLICATION_JSON,
+ jsonObjectMapper, msgPackObjectMapper)).build();
+ }
+ return executeCallbackSync(service, apiMap.getApi(), method, path, headers, uriInfo,
+ body);
+ }
+ return Response.status(Response.Status.BAD_REQUEST).entity(ResponseTransformationUtil
+ .transform(BAD_REQUEST_RESPONSE,
+ headers.getMediaType() != null ? headers.getMediaType().toString()
+ : MediaType.APPLICATION_JSON, jsonObjectMapper,
+ msgPackObjectMapper)).build();
+ }
+
+ private ApiPathMap resolvePath(String service, String path, HttpHeaders headers) {
+ val apiMap = RevolverBundle.matchPath(service, path);
+ if (apiMap == null) {
+ return null;
+ }
+
+ String newPath = null;
+
+ RevolverHttpApiConfig httpApiConfiguration = apiMap.getApi();
+ RevolverHttpApiSplitConfig revolverHttpApiSplitConfig = httpApiConfiguration
+ .getSplitConfig();
+ if (null != revolverHttpApiSplitConfig && revolverHttpApiSplitConfig.isEnabled()
+ && revolverHttpApiSplitConfig.getSplitStrategy() != null) {
+ SplitStrategy splitStrategy = revolverHttpApiSplitConfig.getSplitStrategy();
+ switch (splitStrategy) {
+ case PATH:
+ newPath = getPathFromSplitConfig(httpApiConfiguration);
+ break;
+ case PATH_EXPRESSION:
+ newPath = getPathFromPathExpression(revolverHttpApiSplitConfig, path);
+ break;
+ case HEADER_EXPRESSION:
+ newPath = getPathFromHeaderExpression(revolverHttpApiSplitConfig, path,
+ headers);
+ break;
+ }
+ }
+ if (Strings.isNullOrEmpty(newPath)) {
+ return apiMap;
+ }
+ return RevolverBundle.matchPath(service, newPath);
+ }
+
+ private String getPathFromSplitConfig(RevolverHttpApiConfig httpApiConfiguration) {
+ double random = Math.random();
+ for (SplitConfig splitConfig : httpApiConfiguration.getSplitConfig().getSplits()) {
+ if (splitConfig.getFrom() <= random && splitConfig.getTo() > random) {
+ return splitConfig.getPath();
+ }
+ }
+ return null;
+ }
+
+ private String getPathFromPathExpression(RevolverHttpApiSplitConfig revolverHttpApiSplitConfig,
+ String path) {
+
+ List pathExpressionSplitConfigs = revolverHttpApiSplitConfig
+ .getPathExpressionSplitConfigs();
+
+ for (PathExpressionSplitConfig pathExpressionSplitConfig : CollectionUtils
+ .nullSafeList(pathExpressionSplitConfigs)) {
+ if (matches(pathExpressionSplitConfig.getExpression(), path)) {
+ return pathExpressionSplitConfig.getPath();
+ }
+ }
+ return null;
+ }
+
+ private String getPathFromHeaderExpression(
+ RevolverHttpApiSplitConfig revolverHttpApiSplitConfig, String path,
+ HttpHeaders headers) {
+
+ List headerExpressionSplitConfigs = revolverHttpApiSplitConfig
+ .getHeaderExpressionSplitConfigs();
+
+ for (HeaderExpressionSplitConfig headerExpressionSplitConfig : CollectionUtils
+ .nullSafeList(headerExpressionSplitConfigs)) {
+ if (!headers.getRequestHeaders().containsKey(headerExpressionSplitConfig.getHeader())) {
+ continue;
+ }
+ if (matches(headerExpressionSplitConfig.getExpression(),
+ headers.getHeaderString(headerExpressionSplitConfig.getHeader()))) {
+ return headerExpressionSplitConfig.getPath();
+ }
+ }
+ return null;
+ }
+
+ private boolean matches(String expression, String path) {
+ Pattern pattern = Pattern.compile(expression);
+ Matcher matcher = pattern.matcher(path);
+ return matcher.matches();
+ }
+
+ private String getCallMode(ApiPathMap apiMap, HttpHeaders headers) {
+
+ val callMode = headers.getRequestHeaders().getFirst(RevolversHttpHeaders.CALL_MODE_HEADER);
+ OptimizerConfig optimizerConfig = revolverConfig.getOptimizerConfig();
+ if (optimizerConfig == null || !optimizerConfig.isEnabled()
+ || optimizerConfig.getTimeConfig() == null || !Strings.isNullOrEmpty(callMode)
+ || !(headers.getRequestHeaders()
+ .containsKey(RevolversHttpHeaders.DYAMIC_MAILBOX))) {
+ return callMode;
+ }
+ ApiLatencyConfig apiLatencyConfig = apiMap.getApi().getApiLatencyConfig();
+ if (apiLatencyConfig == null || apiLatencyConfig.isDowngradeDisable()) {
+ return callMode;
+ }
+ OptimizerTimeConfig timeoutConfig = revolverConfig.getOptimizerConfig().getTimeConfig();
+ if (apiLatencyConfig.getLatency() > timeoutConfig.getAppLatencyThresholdValue()) {
+ return RevolverHttpCommand.CALL_MODE_POLLING;
+ }
+ return callMode;
+ }
+
+ private Response executeInline(String service, RevolverHttpApiConfig api,
+ RevolverHttpApiConfig.RequestMethod method, String path, HttpHeaders headers,
+ UriInfo uriInfo, byte[] body) throws IOException, TimeoutException {
+ val sanatizedHeaders = new MultivaluedHashMap();
+ headers.getRequestHeaders().forEach(sanatizedHeaders::put);
+ cleanHeaders(sanatizedHeaders, api);
+ val httpCommand = RevolverBundle.getHttpCommand(service, api.getApi());
+ RevolverHttpResponse revolverHttpResponse = execute(httpCommand, service, api, method, path,
+ headers, uriInfo, body, sanatizedHeaders);
+ return transform(headers, revolverHttpResponse, api.getApi(), path, method);
+ }
+
+ private RevolverHttpResponse execute(RevolverHttpCommand httpCommand, String service,
+ RevolverHttpApiConfig api, RevolverHttpApiConfig.RequestMethod method, String path,
+ HttpHeaders headers, UriInfo uriInfo, byte[] body,
+ MultivaluedHashMap sanatizedHeaders) throws TimeoutException {
+ return 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());
+ }
+
+ 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);
+ String responseMediaType = response.getHeaders() != null && Strings
+ .isNullOrEmpty(response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE))
+ ? MediaType.TEXT_HTML : response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
+ String requestMediaType = headers != null && Strings
+ .isNullOrEmpty(headers.getHeaderString(HttpHeaders.ACCEPT)) ? null
+ : headers.getHeaderString(HttpHeaders.ACCEPT);
+ //If no accept was specified in request or accept was wildcard; 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.startsWith(MediaType.MEDIA_TYPE_WILDCARD)
+ || 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)) {
+ 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(MsgPackMediaType.APPLICATION_MSGPACK)) {
+ 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(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(MultivaluedMap 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(String service, RevolverHttpApiConfig api,
+ RevolverHttpApiConfig.RequestMethod method, String path, HttpHeaders headers,
+ UriInfo uriInfo, byte[] body, boolean isDownstreamAsync, String callMode)
+ throws Exception {
+ val sanatizedHeaders = new MultivaluedHashMap();
+ headers.getRequestHeaders().forEach(sanatizedHeaders::put);
+ cleanHeaders(sanatizedHeaders, api);
+ val httpCommand = RevolverBundle.getHttpCommand(service, api.getApi());
+ val requestId = headers.getHeaderString(RevolversHttpHeaders.REQUEST_ID_HEADER);
+ val transactionId = headers.getHeaderString(RevolversHttpHeaders.TXN_ID_HEADER);
+ val mailBoxId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_ID_HEADER);
+ val mailBoxAuthId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_AUTH_ID_HEADER);
+
+ val mailBoxTtl =
+ headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER) != null ? Integer
+ .parseInt(headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER))
+ : -1;
+ //Short circuit if it is a duplicate request
+ if (persistenceProvider.exists(requestId)) {
+ return Response.status(Response.Status.NOT_ACCEPTABLE).entity(ResponseTransformationUtil
+ .transform(DUPLICATE_REQUEST_RESPONSE,
+ headers.getMediaType() == null ? MediaType.APPLICATION_JSON
+ : headers.getMediaType().toString(), jsonObjectMapper,
+ msgPackObjectMapper)).build();
+ }
+ persistenceProvider.saveRequest(requestId, mailBoxId, mailBoxAuthId,
+ 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(), mailBoxTtl);
+ CompletableFuture 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, mailBoxTtl);
+ } else {
+ persistenceProvider
+ .setRequestState(requestId, RevolverRequestState.RESPONDED, mailBoxTtl);
+ saveResponse(requestId, result, callMode, mailBoxTtl);
+ }
+ Response httpResponse = transform(headers, result, api.getApi(), path, method);
+ if (api.getApiLatencyConfig() != null) {
+ httpResponse.getHeaders().putSingle(RevolversHttpHeaders.RETRY_AFTER,
+ api.getApiLatencyConfig().getLatency());
+ }
+ return httpResponse;
+ } else {
+ response.thenAcceptAsync(result -> {
+ try {
+ if (result.getStatusCode() == Response.Status.ACCEPTED.getStatusCode()) {
+ persistenceProvider
+ .setRequestState(requestId, RevolverRequestState.REQUESTED,
+ mailBoxTtl);
+ } else if (result.getStatusCode() == Response.Status.OK.getStatusCode()) {
+ persistenceProvider
+ .setRequestState(requestId, RevolverRequestState.RESPONDED,
+ mailBoxTtl);
+ saveResponse(requestId, result, callMode, mailBoxTtl);
+ } else {
+ persistenceProvider
+ .setRequestState(requestId, RevolverRequestState.ERROR, mailBoxTtl);
+ saveResponse(requestId, result, callMode, mailBoxTtl);
+ }
+ } catch (Exception e) {
+ log.error("Error setting request state for request id: {}", requestId, e);
+ }
+ });
+ RevolverAckMessage revolverAckMessage = RevolverAckMessage.builder()
+ .requestId(requestId).acceptedAt(Instant.now().toEpochMilli()).build();
+ return Response.accepted().entity(ResponseTransformationUtil
+ .transform(revolverAckMessage,
+ headers.getMediaType() == null ? MediaType.APPLICATION_JSON
+ : headers.getMediaType().toString(), jsonObjectMapper,
+ msgPackObjectMapper)).header(RevolversHttpHeaders.RETRY_AFTER,
+ api.getApiLatencyConfig() == null ? 0 : api.getApiLatencyConfig().getLatency())
+ .build();
+ }
+ }
+
+ private Response executeCallbackSync(String service, RevolverHttpApiConfig api,
+ RevolverHttpApiConfig.RequestMethod method, String path, HttpHeaders headers,
+ UriInfo uriInfo, byte[] body) throws Exception {
+ val sanatizedHeaders = new MultivaluedHashMap();
+ headers.getRequestHeaders().forEach(sanatizedHeaders::put);
+ cleanHeaders(sanatizedHeaders, api);
+ val httpCommand = RevolverBundle.getHttpCommand(service, api.getApi());
+ val requestId = headers.getHeaderString(RevolversHttpHeaders.REQUEST_ID_HEADER);
+ val transactionId = headers.getHeaderString(RevolversHttpHeaders.TXN_ID_HEADER);
+ val mailBoxId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_ID_HEADER);
+ val mailBoxAuthId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_AUTH_ID_HEADER);
+
+ val mailBoxTtl =
+ headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER) != null ? Integer
+ .parseInt(headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER))
+ : -1;
+ //Short circuit if it is a duplicate request
+ if (persistenceProvider.exists(requestId)) {
+ return Response.status(Response.Status.NOT_ACCEPTABLE).entity(ResponseTransformationUtil
+ .transform(DUPLICATE_REQUEST_RESPONSE,
+ headers.getMediaType() == null ? MediaType.APPLICATION_JSON
+ : headers.getMediaType().toString(), jsonObjectMapper,
+ msgPackObjectMapper)).build();
+ }
+ persistenceProvider.saveRequest(requestId, mailBoxId, mailBoxAuthId,
+ 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(), mailBoxTtl);
+ CompletableFuture 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());
+ persistenceProvider.setRequestState(requestId, RevolverRequestState.REQUESTED, mailBoxTtl);
+ val result = response.get();
+ return transform(headers, result, api.getApi(), path, method);
+ }
+
+ private void saveResponse(String requestId, RevolverHttpResponse result, String callMode,
+ int ttl) {
+ try {
+ val response = RevolverCallbackResponse.builder().body(result.getBody())
+ .headers(result.getHeaders()).statusCode(result.getStatusCode()).build();
+ persistenceProvider.saveResponse(requestId, response, ttl);
+ if (callMode != null && callMode.equals(RevolverHttpCommand.CALL_MODE_CALLBACK)) {
+ callbackHandler.handle(requestId, response);
+ }
+ } catch (Exception e) {
+ log.error("Error saving response!", e);
+ }
+ }
+
+ private void pushMetrics(Response response, String service, String path) {
+ val apiMap = RevolverBundle.matchPath(service, path);
+ if (apiMap == null) {
+ return;
+ }
+ String api = apiMap.getApi().getApi();
+ metrics.meter(String.format("%s.%s.%s", service, api, response.getStatus()));
+ }
+}
diff --git a/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java b/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java
index 8486ab25..6f6bf16b 100644
--- a/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java
+++ b/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java
@@ -58,6 +58,7 @@
import io.dropwizard.revolver.optimizer.config.OptimizerConfig;
import io.dropwizard.revolver.optimizer.utils.OptimizerUtils;
import io.dropwizard.revolver.persistence.InMemoryPersistenceProvider;
+import io.dropwizard.revolver.provider.BlacklistMethodData;
import io.dropwizard.revolver.retry.RevolverApiRetryConfig;
import io.dropwizard.revolver.splitting.PathExpressionSplitConfig;
import io.dropwizard.revolver.splitting.RevolverHttpApiSplitConfig;
@@ -74,6 +75,8 @@
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.List;
+import java.util.Set;
+
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.curator.framework.CuratorFramework;
@@ -386,6 +389,11 @@ public CuratorFramework getCurator() {
public ConfigSource getConfigSource() {
return null;
}
+
+ @Override
+ public Set getBlacklistData() {
+ return null;
+ }
};
@Rule
public WireMockRule wireMockRule = new WireMockRule(9999, 9933);