From 81e6ff51623792ebe9cb7c277fccd044bdd432b9 Mon Sep 17 00:00:00 2001 From: Shay Banon Date: Thu, 2 Feb 2012 20:19:15 +0200 Subject: [PATCH] Allow for plugins to register REST filter (better support with async execution and some renaming), closes #1658. --- .../org/elasticsearch/http/HttpServer.java | 38 +++--- .../elasticsearch/rest/RestController.java | 126 ++++++++++++++---- ...{RestPreProcessor.java => RestFilter.java} | 36 ++--- .../elasticsearch/rest/RestFilterChain.java | 32 +++++ 4 files changed, 171 insertions(+), 61 deletions(-) rename src/main/java/org/elasticsearch/rest/{RestPreProcessor.java => RestFilter.java} (53%) create mode 100644 src/main/java/org/elasticsearch/rest/RestFilterChain.java diff --git a/src/main/java/org/elasticsearch/http/HttpServer.java b/src/main/java/org/elasticsearch/http/HttpServer.java index 6ad5b876404..0e5fb51f0ff 100644 --- a/src/main/java/org/elasticsearch/http/HttpServer.java +++ b/src/main/java/org/elasticsearch/http/HttpServer.java @@ -51,6 +51,8 @@ public class HttpServer extends AbstractLifecycleComponent { private final boolean disableSites; + private final PluginSiteFilter pluginSiteFilter = new PluginSiteFilter(); + @Inject public HttpServer(Settings settings, Environment environment, HttpServerTransport transport, RestController restController, @@ -111,33 +113,33 @@ public class HttpServer extends AbstractLifecycleComponent { public void internalDispatchRequest(final HttpRequest request, final HttpChannel channel) { if (request.rawPath().startsWith("/_plugin/")) { - for (RestPreProcessor preProcessor : restController.preProcessors()) { - if (!preProcessor.handleExternal()) { - continue; - } - if (!preProcessor.process(request, channel)) { - return; - } - } - handlePluginSite(request, channel); + RestFilterChain filterChain = restController.filterChain(pluginSiteFilter); + filterChain.continueProcessing(request, channel); return; } - if (!restController.dispatchRequest(request, channel)) { - if (request.method() == RestRequest.Method.OPTIONS) { - // when we have OPTIONS request, simply send OK by default (with the Access Control Origin header which gets automatically added) - StringRestResponse response = new StringRestResponse(OK); - channel.sendResponse(response); - } else { - channel.sendResponse(new StringRestResponse(BAD_REQUEST, "No handler found for uri [" + request.uri() + "] and method [" + request.method() + "]")); - } + restController.dispatchRequest(request, channel); + } + + + class PluginSiteFilter extends RestFilter { + + @Override + public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) { + handlePluginSite((HttpRequest) request, (HttpChannel) channel); } } - private void handlePluginSite(HttpRequest request, HttpChannel channel) { + void handlePluginSite(HttpRequest request, HttpChannel channel) { if (disableSites) { channel.sendResponse(new StringRestResponse(FORBIDDEN)); return; } + if (request.method() == RestRequest.Method.OPTIONS) { + // when we have OPTIONS request, simply send OK by default (with the Access Control Origin header which gets automatically added) + StringRestResponse response = new StringRestResponse(OK); + channel.sendResponse(response); + return; + } if (request.method() != RestRequest.Method.GET) { channel.sendResponse(new StringRestResponse(FORBIDDEN)); return; diff --git a/src/main/java/org/elasticsearch/rest/RestController.java b/src/main/java/org/elasticsearch/rest/RestController.java index 30926d0fdba..e852e76c2f5 100644 --- a/src/main/java/org/elasticsearch/rest/RestController.java +++ b/src/main/java/org/elasticsearch/rest/RestController.java @@ -21,6 +21,8 @@ package org.elasticsearch.rest; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.ElasticSearchIllegalArgumentException; +import org.elasticsearch.ElasticSearchIllegalStateException; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.path.PathTrie; @@ -31,6 +33,9 @@ import java.io.IOException; import java.util.Arrays; import java.util.Comparator; +import static org.elasticsearch.rest.RestStatus.BAD_REQUEST; +import static org.elasticsearch.rest.RestStatus.OK; + /** * */ @@ -43,8 +48,10 @@ public class RestController extends AbstractLifecycleComponent { private final PathTrie headHandlers = new PathTrie(RestUtils.REST_DECODER); private final PathTrie optionsHandlers = new PathTrie(RestUtils.REST_DECODER); + private final RestHandlerFilter handlerFilter = new RestHandlerFilter(); + // non volatile since the assumption is that pre processors are registered on startup - private RestPreProcessor[] preProcessors = new RestPreProcessor[0]; + private RestFilter[] filters = new RestFilter[0]; @Inject public RestController(Settings settings) { @@ -61,22 +68,25 @@ public class RestController extends AbstractLifecycleComponent { @Override protected void doClose() throws ElasticSearchException { + for (RestFilter filter : filters) { + filter.close(); + } } /** * Registers a pre processor to be executed before the rest request is actually handled. */ - public synchronized void registerPreProcessor(RestPreProcessor preProcessor) { - RestPreProcessor[] copy = new RestPreProcessor[preProcessors.length + 1]; - System.arraycopy(preProcessors, 0, copy, 0, preProcessors.length); - copy[preProcessors.length] = preProcessor; - Arrays.sort(copy, new Comparator() { + public synchronized void registerFilter(RestFilter preProcessor) { + RestFilter[] copy = new RestFilter[filters.length + 1]; + System.arraycopy(filters, 0, copy, 0, filters.length); + copy[filters.length] = preProcessor; + Arrays.sort(copy, new Comparator() { @Override - public int compare(RestPreProcessor o1, RestPreProcessor o2) { + public int compare(RestFilter o1, RestFilter o2) { return o2.order() - o1.order(); } }); - preProcessors = copy; + filters = copy; } /** @@ -107,30 +117,55 @@ public class RestController extends AbstractLifecycleComponent { } } - public RestPreProcessor[] preProcessors() { - return preProcessors; + /** + * Returns a filter chain (if needed) to execute. If this method returns null, simply execute + * as usual. + */ + @Nullable + public RestFilterChain filterChainOrNull(RestFilter executionFilter) { + if (filters.length == 0) { + return null; + } + return new ControllerFilterChain(executionFilter); } - public boolean dispatchRequest(final RestRequest request, final RestChannel channel) { - try { - for (RestPreProcessor preProcessor : preProcessors) { - if (!preProcessor.process(request, channel)) { - return true; + /** + * Returns a filter chain with the final filter being the provided filter. + */ + public RestFilterChain filterChain(RestFilter executionFilter) { + return new ControllerFilterChain(executionFilter); + } + + public void dispatchRequest(final RestRequest request, final RestChannel channel) { + if (filters.length == 0) { + try { + executeHandler(request, channel); + } catch (Exception e) { + try { + channel.sendResponse(new XContentThrowableRestResponse(request, e)); + } catch (IOException e1) { + logger.error("Failed to send failure response for uri [" + request.uri() + "]", e1); } } - final RestHandler handler = getHandler(request); - if (handler == null) { - return false; - } + } else { + ControllerFilterChain filterChain = new ControllerFilterChain(handlerFilter); + filterChain.continueProcessing(request, channel); + } + } + + void executeHandler(RestRequest request, RestChannel channel) { + final RestHandler handler = getHandler(request); + if (handler != null) { handler.handleRequest(request, channel); - } catch (Exception e) { - try { - channel.sendResponse(new XContentThrowableRestResponse(request, e)); - } catch (IOException e1) { - logger.error("Failed to send failure response for uri [" + request.uri() + "]", e1); + } else { + if (request.method() == RestRequest.Method.OPTIONS) { + // when we have OPTIONS request, simply send OK by default (with the Access Control Origin header which gets automatically added) + StringRestResponse response = new StringRestResponse(OK); + channel.sendResponse(response); + } else { + channel.sendResponse(new StringRestResponse(BAD_REQUEST, "No handler found for uri [" + request.uri() + "] and method [" + request.method() + "]")); } } - return true; } private RestHandler getHandler(RestRequest request) { @@ -159,4 +194,45 @@ public class RestController extends AbstractLifecycleComponent { // my_index/my_type/http%3A%2F%2Fwww.google.com return request.rawPath(); } + + class ControllerFilterChain implements RestFilterChain { + + private final RestFilter executionFilter; + + private volatile int index; + + ControllerFilterChain(RestFilter executionFilter) { + this.executionFilter = executionFilter; + } + + @Override + public void continueProcessing(RestRequest request, RestChannel channel) { + try { + int loc = index; + if (loc > filters.length) { + throw new ElasticSearchIllegalStateException("filter continueProcessing was called more than expected"); + } else if (loc == filters.length) { + executionFilter.process(request, channel, this); + } else { + RestFilter preProcessor = filters[loc]; + preProcessor.process(request, channel, this); + } + index++; + } catch (Exception e) { + try { + channel.sendResponse(new XContentThrowableRestResponse(request, e)); + } catch (IOException e1) { + logger.error("Failed to send failure response for uri [" + request.uri() + "]", e1); + } + } + } + } + + class RestHandlerFilter extends RestFilter { + + @Override + public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) { + executeHandler(request, channel); + } + } } diff --git a/src/main/java/org/elasticsearch/rest/RestPreProcessor.java b/src/main/java/org/elasticsearch/rest/RestFilter.java similarity index 53% rename from src/main/java/org/elasticsearch/rest/RestPreProcessor.java rename to src/main/java/org/elasticsearch/rest/RestFilter.java index 273fbd5ca19..cf71e9f9734 100644 --- a/src/main/java/org/elasticsearch/rest/RestPreProcessor.java +++ b/src/main/java/org/elasticsearch/rest/RestFilter.java @@ -19,30 +19,30 @@ package org.elasticsearch.rest; +import org.elasticsearch.ElasticSearchException; +import org.elasticsearch.common.component.CloseableComponent; + /** - * Rest pre processor allowing to pre process REST requests. - *

- * Experimental interface. + * A filter allowing to filter rest operations. */ -public interface RestPreProcessor { +public abstract class RestFilter implements CloseableComponent { /** - * Optionally, the order the processor will work on. Execution is done from lowest value to highest. - * It is a good practice to allow to configure this for the relevant processor. + * Optionally, the order of the filter. Execution is done from lowest value to highest. + * It is a good practice to allow to configure this for the relevant filter. */ - int order(); + public int order() { + return 0; + } + + @Override + public void close() throws ElasticSearchException { + // a no op + } /** - * Should this processor also process external (non REST) requests, like plugin site requests. + * Process the rest request. Using the channel to send a response, or the filter chain to continue + * processing the request. */ - boolean handleExternal(); - - /** - * Process the request, returning false if no further processing should be done. Note, - * make sure to send a response if returning false, otherwise, no response will be sent. - *

- * It is recommended that the process method will not do blocking calls, or heavily cache data - * if a blocking call is done. - */ - boolean process(RestRequest request, RestChannel channel); + public abstract void process(RestRequest request, RestChannel channel, RestFilterChain filterChain); } diff --git a/src/main/java/org/elasticsearch/rest/RestFilterChain.java b/src/main/java/org/elasticsearch/rest/RestFilterChain.java new file mode 100644 index 00000000000..564e43af6f4 --- /dev/null +++ b/src/main/java/org/elasticsearch/rest/RestFilterChain.java @@ -0,0 +1,32 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch 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 org.elasticsearch.rest; + +/** + * A filter chain allowing to continue and process the rest request. + */ +public interface RestFilterChain { + + /** + * Continue processing the request. Should only be called if a response has not been sent + * through the channel. + */ + void continueProcessing(RestRequest request, RestChannel channel); +}