Skip to content

Commit

Permalink
Adds a path prefix routing object. (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkokar authored and a-dlatorre committed Jul 3, 2019
1 parent a3b2345 commit cf0c326
Show file tree
Hide file tree
Showing 18 changed files with 866 additions and 269 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.hotels.styx.routing.config;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.hotels.styx.Environment;
import com.hotels.styx.api.Eventual;
Expand All @@ -26,7 +27,9 @@
import com.hotels.styx.routing.db.StyxObjectStore;
import com.hotels.styx.routing.handlers.ConditionRouter;
import com.hotels.styx.routing.handlers.HttpInterceptorPipeline;
import com.hotels.styx.routing.handlers.PathPrefixRouter;
import com.hotels.styx.routing.handlers.ProxyToBackend;
import com.hotels.styx.routing.handlers.RouteRefLookup;
import com.hotels.styx.routing.handlers.StaticResponseHandler;

import java.util.List;
Expand All @@ -44,28 +47,38 @@
*/
public class RoutingObjectFactory {
public static final ImmutableMap<String, Schema.FieldType> BUILTIN_HANDLER_SCHEMAS;
private static final ImmutableMap<String, HttpHandlerFactory> BUILTIN_HANDLER_FACTORIES;
public static final ImmutableMap<String, HttpHandlerFactory> BUILTIN_HANDLER_FACTORIES;
public static final RouteRefLookup DEFAULT_REFERENCE_LOOKUP = reference -> (request, ctx) ->
Eventual.of(response(NOT_FOUND)
.body(format("Handler not found for '%s'.", reference), UTF_8)
.build()
.stream());
private static final String STATIC_RESPONSE = "StaticResponseHandler";
private static final String CONDITION_ROUTER = "ConditionRouter";
private static final String INTERCEPTOR_PIPELINE = "InterceptorPipeline";
private static final String PROXY_TO_BACKEND = "ProxyToBackend";
private static final String PATH_PREFIX_ROUTER = "PathPrefixRouter";


static {
BUILTIN_HANDLER_FACTORIES = ImmutableMap.<String, HttpHandlerFactory>builder()
.put(STATIC_RESPONSE, new StaticResponseHandler.Factory())
.put(CONDITION_ROUTER, new ConditionRouter.Factory())
.put(INTERCEPTOR_PIPELINE, new HttpInterceptorPipeline.Factory())
.put(PROXY_TO_BACKEND, new ProxyToBackend.Factory())
.build();
.put(STATIC_RESPONSE, new StaticResponseHandler.Factory())
.put(CONDITION_ROUTER, new ConditionRouter.Factory())
.put(INTERCEPTOR_PIPELINE, new HttpInterceptorPipeline.Factory())
.put(PROXY_TO_BACKEND, new ProxyToBackend.Factory())
.put(PATH_PREFIX_ROUTER, new PathPrefixRouter.Factory())
.build();

BUILTIN_HANDLER_SCHEMAS = ImmutableMap.<String, Schema.FieldType>builder()
.put(STATIC_RESPONSE, StaticResponseHandler.SCHEMA)
.put(CONDITION_ROUTER, ConditionRouter.SCHEMA)
.put(INTERCEPTOR_PIPELINE, HttpInterceptorPipeline.SCHEMA)
.put(PROXY_TO_BACKEND, ProxyToBackend.SCHEMA)
.build();
.put(STATIC_RESPONSE, StaticResponseHandler.SCHEMA)
.put(CONDITION_ROUTER, ConditionRouter.SCHEMA)
.put(INTERCEPTOR_PIPELINE, HttpInterceptorPipeline.SCHEMA)
.put(PROXY_TO_BACKEND, ProxyToBackend.SCHEMA)
.put(PATH_PREFIX_ROUTER, PathPrefixRouter.SCHEMA)
.build();
}

private final RouteRefLookup refLookup;
private final Environment environment;
private final StyxObjectStore<RoutingObjectRecord> routeObjectStore;
private final Iterable<NamedPlugin> plugins;
Expand All @@ -75,11 +88,14 @@ public class RoutingObjectFactory {

@VisibleForTesting
public RoutingObjectFactory(
RouteRefLookup refLookup,
Map<String, HttpHandlerFactory> builtInObjectTypes,
Environment environment,
StyxObjectStore<RoutingObjectRecord> routeObjectStore, Iterable<NamedPlugin> plugins,
StyxObjectStore<RoutingObjectRecord> routeObjectStore,
Iterable<NamedPlugin> plugins,
BuiltinInterceptorsFactory interceptorFactory,
boolean requestTracking) {
this.refLookup = requireNonNull(refLookup);
this.builtInObjectTypes = requireNonNull(builtInObjectTypes);
this.environment = requireNonNull(environment);
this.routeObjectStore = requireNonNull(routeObjectStore);
Expand All @@ -88,13 +104,30 @@ public RoutingObjectFactory(
this.requestTracking = requestTracking;
}

public RoutingObjectFactory(
Environment environment,
StyxObjectStore<RoutingObjectRecord> routeObjectStore,
List<NamedPlugin> plugins,
BuiltinInterceptorsFactory interceptorFactory,
boolean requestTracking) {
this(BUILTIN_HANDLER_FACTORIES, environment, routeObjectStore, plugins, interceptorFactory, requestTracking);
public RoutingObjectFactory(StyxObjectStore<RoutingObjectRecord> routeObjectStore) {
this(DEFAULT_REFERENCE_LOOKUP, BUILTIN_HANDLER_FACTORIES, new Environment.Builder().build(), routeObjectStore, ImmutableList.of(), new BuiltinInterceptorsFactory(), false);
}

public RoutingObjectFactory(RouteRefLookup refLookup, Map<String, HttpHandlerFactory> handlerFactories) {
this(refLookup, handlerFactories, new Environment.Builder().build(), new StyxObjectStore<>(), ImmutableList.of(), new BuiltinInterceptorsFactory(), false);
}

public RoutingObjectFactory(RouteRefLookup refLookup) {
this(refLookup, BUILTIN_HANDLER_FACTORIES, new Environment.Builder().build(), new StyxObjectStore<>(), ImmutableList.of(), new BuiltinInterceptorsFactory(), false);
}

public RoutingObjectFactory() {
this(DEFAULT_REFERENCE_LOOKUP,
BUILTIN_HANDLER_FACTORIES,
new Environment.Builder().build(),
new StyxObjectStore<>(),
ImmutableList.of(),
new BuiltinInterceptorsFactory(),
false);
}

public HttpHandler build(RoutingObjectConfiguration configNode) {
return build(ImmutableList.of(), configNode);
}

public HttpHandler build(List<String> parents, RoutingObjectConfiguration configNode) {
Expand All @@ -115,15 +148,9 @@ public HttpHandler build(List<String> parents, RoutingObjectConfiguration config

return factory.build(parents, context, configBlock);
} else if (configNode instanceof RoutingObjectReference) {
RoutingObjectReference reference = (RoutingObjectReference) configNode;

return (request, context) -> routeObjectStore.get(reference.name())
.map(handler -> handler.getHandler().handle(request, context))
.orElse(Eventual.of(
response(NOT_FOUND)
.body("Not found: " + String.join(".", parents) + "." + reference.name(), UTF_8)
.build()
.stream()));
return (request, context) -> refLookup
.apply((RoutingObjectReference) configNode)
.handle(request, context);
} else {
throw new UnsupportedOperationException(format("Unsupported configuration node type: '%s'", configNode.getClass().getName()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ private ConditionRouterConfig(@JsonProperty("routes") List<ConditionRouterRouteC
this.routes = routes;
this.fallback = fallback.isNull() ? null : toRoutingConfigNode(fallback);
}

}

private static class ConditionRouterRouteConfig {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
Copyright (C) 2013-2019 Expedia Inc.
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 com.hotels.styx.routing.handlers;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.HttpHandler;
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.common.Pair;
import com.hotels.styx.config.schema.Schema;
import com.hotels.styx.infrastructure.configuration.yaml.JsonNodeConfig;
import com.hotels.styx.routing.config.HttpHandlerFactory;
import com.hotels.styx.routing.config.RoutingObjectDefinition;
import com.hotels.styx.server.NoServiceConfiguredException;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentSkipListMap;

import static com.hotels.styx.common.Pair.pair;
import static com.hotels.styx.config.schema.SchemaDsl.field;
import static com.hotels.styx.config.schema.SchemaDsl.list;
import static com.hotels.styx.config.schema.SchemaDsl.object;
import static com.hotels.styx.config.schema.SchemaDsl.routingObject;
import static com.hotels.styx.config.schema.SchemaDsl.string;
import static com.hotels.styx.routing.config.RoutingConfigParser.toRoutingConfigNode;
import static com.hotels.styx.routing.config.RoutingSupport.missingAttributeError;
import static java.lang.String.join;
import static java.util.Comparator.comparingInt;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.toList;

/**
* Makes a routing decision based on a request path prefix.
*
* Chooses a destination according to longest matching path prefix.
* The destination can be a routing object reference or an inline definition.
*/
public class PathPrefixRouter {
public static final Schema.FieldType SCHEMA = object(
field("routes", list(object(
field("prefix", string()),
field("destination", routingObject()))
))
);

private final ConcurrentSkipListMap<String, HttpHandler> routes = new ConcurrentSkipListMap<>(
comparingInt(String::length)
.reversed()
.thenComparing(naturalOrder())
);

PathPrefixRouter(List<Pair<String, HttpHandler>> routes) {
routes.forEach(entry -> this.routes.put(entry.key(), entry.value()));
}

public Optional<HttpHandler> route(LiveHttpRequest request) {
String path = request.path();

return routes.entrySet().stream()
.filter(entry -> path.startsWith(entry.getKey()))
.findFirst()
.map(Map.Entry::getValue);
}

public static class Factory implements HttpHandlerFactory {

@Override
public HttpHandler build(List<String> parents, Context context, RoutingObjectDefinition configBlock) {
PathPrefixRouterConfig config = new JsonNodeConfig(configBlock.config()).as(PathPrefixRouterConfig.class);
if (config.routes == null) {
throw missingAttributeError(configBlock, join(".", parents), "routes");
}

PathPrefixRouter pathPrefixRouter = new PathPrefixRouter(
config.routes.stream()
.map(route -> pair(route.prefix, context.factory().build(toRoutingConfigNode(route.destination))))
.collect(toList())
);

return (request, ctx) -> pathPrefixRouter.route(request)
.orElse((x, y) -> Eventual.error(new NoServiceConfiguredException(request.path())))
.handle(request, ctx);
}

private static class PathPrefixConfig {
private final String prefix;
private final JsonNode destination;

public PathPrefixConfig(@JsonProperty("prefix") String prefix,
@JsonProperty("destination") JsonNode destination) {
this.prefix = prefix;
this.destination = destination;
}
}

private static class PathPrefixRouterConfig {
private final List<PathPrefixConfig> routes;

public PathPrefixRouterConfig(@JsonProperty("routes") List<PathPrefixConfig> routes) {
this.routes = routes;
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright (C) 2013-2019 Expedia Inc.
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 com.hotels.styx.routing.handlers;

import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.HttpHandler;
import com.hotels.styx.routing.RoutingObjectRecord;
import com.hotels.styx.routing.config.RoutingObjectReference;
import com.hotels.styx.routing.db.StyxObjectStore;

import static com.hotels.styx.api.HttpResponse.response;
import static com.hotels.styx.api.HttpResponseStatus.NOT_FOUND;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;

/**
* Resolves a routing object reference from route database.
*/
public interface RouteRefLookup {
HttpHandler apply(RoutingObjectReference route);

/**
* A StyxObjectStore based route reference lookup function.
*/
class RouteDbRefLookup implements RouteRefLookup {
private final StyxObjectStore<RoutingObjectRecord> routeDatabase;

public RouteDbRefLookup(StyxObjectStore<RoutingObjectRecord> routeDatabase) {
this.routeDatabase = requireNonNull(routeDatabase);
}

@Override
public HttpHandler apply(RoutingObjectReference route) {
return this.routeDatabase.get(route.name())
.map(RoutingObjectRecord::getHandler)
.orElse((liveRequest, na) -> {
liveRequest.consume();

return Eventual.of(response(NOT_FOUND)
.body("Not found: " + route.name(), UTF_8)
.build()
.stream()
);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.hotels.styx.routing.config.RoutingObjectDefinition;
import com.hotels.styx.routing.config.RoutingObjectFactory;
import com.hotels.styx.routing.db.StyxObjectStore;
import com.hotels.styx.routing.handlers.RouteRefLookup.RouteDbRefLookup;
import com.hotels.styx.startup.extensions.ConfiguredPluginFactory;

import java.util.HashMap;
Expand All @@ -44,6 +45,7 @@

import static com.hotels.styx.Version.readVersionFrom;
import static com.hotels.styx.infrastructure.logging.LOGBackConfigurer.initLogging;
import static com.hotels.styx.routing.config.RoutingObjectFactory.BUILTIN_HANDLER_FACTORIES;
import static com.hotels.styx.startup.ServicesLoader.SERVICES_FROM_CONFIG;
import static com.hotels.styx.startup.StyxServerComponents.LoggingSetUp.DO_NOT_MODIFY;
import static com.hotels.styx.startup.extensions.PluginLoadingForStartup.loadPlugins;
Expand Down Expand Up @@ -74,6 +76,8 @@ private StyxServerComponents(Builder builder) {
: loadPlugins(environment, builder.configuredPluginFactories);

this.routingObjectFactory = new RoutingObjectFactory(
new RouteDbRefLookup(this.routeObjectStore),
BUILTIN_HANDLER_FACTORIES,
this.environment,
this.routeObjectStore,
this.plugins,
Expand Down
Loading

0 comments on commit cf0c326

Please sign in to comment.