Skip to content

Commit

Permalink
Merge pull request elastic#21 from jakelandis/jake-xpack-spi-request
Browse files Browse the repository at this point in the history
Some revisions to the idea of using a function
  • Loading branch information
pgomulka authored Jul 29, 2020
2 parents 88598dc + 3470081 commit 72d7b13
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@

import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* The content type of {@link org.elasticsearch.common.xcontent.XContent}.
Expand Down Expand Up @@ -116,19 +114,13 @@ public XContent xContent() {
}
};

private static final Pattern COMPATIBLE_API_HEADER_PATTERN = Pattern.compile(
"(application|text)/(vnd.elasticsearch\\+)?([^;]+)(\\s*;\\s*compatible-with=(\\d+))?",
Pattern.CASE_INSENSITIVE);

/**
* Accepts either a format string, which is equivalent to {@link XContentType#shortName()} or a media type that optionally has
* parameters and attempts to match the value to an {@link XContentType}. The comparisons are done in lower case format and this method
* also supports a wildcard accept for {@code application/*}. This method can be used to parse the {@code Accept} HTTP header or a
* format query string parameter. This method will return {@code null} if no match is found
*/
public static XContentType fromMediaTypeOrFormat(String mediaTypeHeaderValue) {
String mediaType = parseMediaType(mediaTypeHeaderValue);

public static XContentType fromMediaTypeOrFormat(String mediaType) {
if (mediaType == null) {
return null;
}
Expand All @@ -138,7 +130,7 @@ public static XContentType fromMediaTypeOrFormat(String mediaTypeHeaderValue) {
}
}
final String lowercaseMediaType = mediaType.toLowerCase(Locale.ROOT);
if (lowercaseMediaType.startsWith("application/*") || lowercaseMediaType.equals("*/*")) {
if (lowercaseMediaType.startsWith("application/*")) {
return JSON;
}

Expand All @@ -150,9 +142,7 @@ public static XContentType fromMediaTypeOrFormat(String mediaTypeHeaderValue) {
* The provided media type should not include any parameters. This method is suitable for parsing part of the {@code Content-Type}
* HTTP header. This method will return {@code null} if no match is found
*/
public static XContentType fromMediaType(String mediaTypeHeaderValue) {
String mediaType = parseMediaType(mediaTypeHeaderValue);

public static XContentType fromMediaType(String mediaType) {
final String lowercaseMediaType = Objects.requireNonNull(mediaType, "mediaType cannot be null").toLowerCase(Locale.ROOT);
for (XContentType type : values()) {
if (type.mediaTypeWithoutParameters().equals(lowercaseMediaType)) {
Expand All @@ -167,28 +157,6 @@ public static XContentType fromMediaType(String mediaTypeHeaderValue) {
return null;
}

//public scope needed for text formats hack
public static String parseMediaType(String mediaType) {
if (mediaType != null) {
Matcher matcher = COMPATIBLE_API_HEADER_PATTERN.matcher(mediaType);
if (matcher.find()) {
return (matcher.group(1) + "/" + matcher.group(3)).toLowerCase(Locale.ROOT);
}
}

return mediaType;
}

public static String parseVersion(String mediaType){
if(mediaType != null){
Matcher matcher = COMPATIBLE_API_HEADER_PATTERN.matcher(mediaType);
if (matcher.find() && "vnd.elasticsearch+".equalsIgnoreCase(matcher.group(2))) {

return matcher.group(5);
}
}
return null;
}
private static boolean isSameMediaTypeOrFormatAs(String stringType, XContentType type) {
return type.mediaTypeWithoutParameters().equalsIgnoreCase(stringType) ||
stringType.toLowerCase(Locale.ROOT).startsWith(type.mediaTypeWithoutParameters().toLowerCase(Locale.ROOT) + ";") ||
Expand Down
16 changes: 2 additions & 14 deletions server/src/main/java/org/elasticsearch/action/ActionModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@
import org.elasticsearch.persistent.UpdatePersistentTaskStatusAction;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.ActionPlugin.ActionHandler;
import org.elasticsearch.plugins.RestCompatibilityPlugin;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.rest.RestHeaderDefinition;
Expand Down Expand Up @@ -428,7 +427,7 @@ public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpr
IndexScopedSettings indexScopedSettings, ClusterSettings clusterSettings, SettingsFilter settingsFilter,
ThreadPool threadPool, List<ActionPlugin> actionPlugins, NodeClient nodeClient,
CircuitBreakerService circuitBreakerService, UsageService usageService, ClusterService clusterService,
List<RestCompatibilityPlugin> restCompatPlugins) {
BiFunction<String, String, Version> restCompatibleFunction) {
this.settings = settings;
this.indexNameExpressionResolver = indexNameExpressionResolver;
this.indexScopedSettings = indexScopedSettings;
Expand Down Expand Up @@ -460,18 +459,7 @@ public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpr
indicesAliasesRequestRequestValidators = new RequestValidators<>(
actionPlugins.stream().flatMap(p -> p.indicesAliasesRequestValidators().stream()).collect(Collectors.toList()));

BiFunction<Map<String, List<String>>, Boolean, Boolean> minimumRestCompatibilityVersion = getMinimumRestCompatibilityVersion(restCompatPlugins);
restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService, minimumRestCompatibilityVersion);
}

private BiFunction<Map<String, List<String>>, Boolean, Boolean> getMinimumRestCompatibilityVersion(List<RestCompatibilityPlugin> restCompatPlugins) {
if (restCompatPlugins.size() > 1) {
throw new IllegalStateException("Only one rest compatibility plugin is allowed");
}
return (headers, hasContent) -> restCompatPlugins.stream()
.findFirst()
.orElse((a, b) -> false)
.isRequestingCompatibility(headers, hasContent);
restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService, restCompatibleFunction);
}

public Map<String, ActionHandler<?, ?>> getActions() {
Expand Down
20 changes: 18 additions & 2 deletions server/src/main/java/org/elasticsearch/node/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.PluginsService;
import org.elasticsearch.plugins.RepositoryPlugin;
import org.elasticsearch.plugins.RestCompatibilityPlugin;
import org.elasticsearch.plugins.RestCompatibility;
import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.plugins.SystemIndexPlugin;
Expand Down Expand Up @@ -190,6 +190,7 @@
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -513,7 +514,7 @@ protected Node(final Environment initialEnvironment,
ActionModule actionModule = new ActionModule(settings, clusterModule.getIndexNameExpressionResolver(),
settingsModule.getIndexScopedSettings(), settingsModule.getClusterSettings(), settingsModule.getSettingsFilter(),
threadPool, pluginsService.filterPlugins(ActionPlugin.class), client, circuitBreakerService, usageService, clusterService,
pluginsService.filterPlugins(RestCompatibilityPlugin.class));
getRestCompatibleFunction());
modules.add(actionModule);

final RestController restController = actionModule.getRestController();
Expand Down Expand Up @@ -682,6 +683,21 @@ protected Node(final Environment initialEnvironment,
}
}

/**
* @return A function that can be used to determine the requested REST compatible version
*/
private BiFunction<String, String, Version> getRestCompatibleFunction(){
List<RestCompatibility> restCompatibilityPlugins = pluginsService.filterPlugins(RestCompatibility.class);
BiFunction<String, String, Version> restCompatibleFunction = (a, b) -> Version.CURRENT;
if (restCompatibilityPlugins.size() > 1) {
throw new IllegalStateException("Only one rest compatibility plugin is allowed");
} else if (restCompatibilityPlugins.size() == 1){
restCompatibleFunction =
(acceptHeader, contentTypeHeader) -> restCompatibilityPlugins.get(0).getCompatibleVersion(acceptHeader, contentTypeHeader);
}
return restCompatibleFunction;
}

protected TransportService newTransportService(Settings settings, Transport transport, ThreadPool threadPool,
TransportInterceptor interceptor,
Function<BoundTransportAddress, DiscoveryNode> localNodeFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
package org.elasticsearch.plugins;

import org.elasticsearch.Version;
import org.elasticsearch.common.Nullable;

import java.util.List;
import java.util.Map;

public interface RestCompatibilityPlugin {
boolean isRequestingCompatibility(Map<String, List<String>> headers, boolean hasContent);
@FunctionalInterface
public interface RestCompatibility {
Version getCompatibleVersion(@Nullable String acceptHeader, @Nullable String contentTypeHeader);
}
11 changes: 6 additions & 5 deletions server/src/main/java/org/elasticsearch/rest/RestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,15 @@ public class RestController implements HttpServerTransport.Dispatcher {
/** Rest headers that are copied to internal requests made during a rest request. */
private final Set<RestHeaderDefinition> headersToCopy;
private final UsageService usageService;
private BiFunction<Map<String,List<String>>,Boolean,Boolean> isRestCompatibleFunction;
private BiFunction<String, String, Version> restCompatibleFunction;


public RestController(Set<RestHeaderDefinition> headersToCopy, UnaryOperator<RestHandler> handlerWrapper,
NodeClient client, CircuitBreakerService circuitBreakerService, UsageService usageService,
BiFunction<Map<String, List<String>>, Boolean, Boolean> isRestCompatibleFunction) {
BiFunction<String, String, Version> restCompatibleFunction) {
this.headersToCopy = headersToCopy;
this.usageService = usageService;
this.isRestCompatibleFunction = isRestCompatibleFunction;
this.restCompatibleFunction = restCompatibleFunction;
if (handlerWrapper == null) {
handlerWrapper = h -> h; // passthrough if no wrapper set
}
Expand Down Expand Up @@ -300,8 +301,8 @@ private void tryAllHandlers(final RestRequest request, final RestChannel channel
final String rawPath = request.rawPath();
final String uri = request.uri();
final RestRequest.Method requestMethod;
//once we have a version then we can find a handler registered for path, method and version
Version version = request.getCompatibleApiVersion(isRestCompatibleFunction);
//TODO: now that we have a version we can implement a REST handler that accepts path, method AND version
//Version version = request.getRequestedCompatibility(restCompatibleFunction);
try {
// Resolves the HTTP method and fails if the method is invalid
requestMethod = request.method();
Expand Down
62 changes: 9 additions & 53 deletions server/src/main/java/org/elasticsearch/rest/RestRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,64 +175,19 @@ public static RestRequest requestWithoutParameters(NamedXContentRegistry xConten
requestIdGenerator.incrementAndGet());
}

/**
* An http request can be accompanied with a compatible version indicating with what version a client is using.
* Only a major Versions are supported. Internally we use Versions objects, but only use Version(major,0,0)
* @return a version with what a client is compatible with.
*/
public Version getCompatibleApiVersion(BiFunction<Map<String,List<String>>,Boolean,Boolean> isRequestingCompatibilityFunction) {
if (/*headersValidation &&*/ isRequestingCompatibilityFunction.apply(getHeaders(),hasContent())) {
return Version.fromString(Version.CURRENT.major-1+".0.0");
} else {
return Version.CURRENT;
}
public Version getRequestedCompatibility(BiFunction<String, String, Version> restCompatibleFunction) {
return restCompatibleFunction.apply(getSingleHeader("Accept"), getSingleHeader("Content-Type"));
}


private boolean isRequestingCompatibility() {
/* String acceptHeader = header(CompatibleConstants.COMPATIBLE_ACCEPT_HEADER);
String aVersion = XContentType.parseVersion(acceptHeader);
byte acceptVersion = aVersion == null ? Version.CURRENT.major : Integer.valueOf(aVersion).byteValue();
String contentTypeHeader = header(CompatibleConstants.COMPATIBLE_CONTENT_TYPE_HEADER);
String cVersion = XContentType.parseVersion(contentTypeHeader);
byte contentTypeVersion = cVersion == null ? Version.CURRENT.major : Integer.valueOf(cVersion).byteValue();
if(Version.CURRENT.major < acceptVersion || Version.CURRENT.major - acceptVersion > 1 ){
throw new CompatibleApiHeadersCombinationException(
String.format(Locale.ROOT, "Unsupported version provided. " +
"Accept=%s Content-Type=%s hasContent=%b path=%s params=%s method=%s", acceptHeader,
contentTypeHeader, hasContent(), path(), params.toString(), method().toString()));
}
if (hasContent()) {
if(Version.CURRENT.major < contentTypeVersion || Version.CURRENT.major - contentTypeVersion > 1 ){
throw new CompatibleApiHeadersCombinationException(
String.format(Locale.ROOT, "Unsupported version provided. " +
"Accept=%s Content-Type=%s hasContent=%b path=%s params=%s method=%s", acceptHeader,
contentTypeHeader, hasContent(), path(), params.toString(), method().toString()));
}
if (contentTypeVersion != acceptVersion) {
throw new CompatibleApiHeadersCombinationException(
String.format(Locale.ROOT, "Content-Type and Accept headers have to match when content is present. " +
"Accept=%s Content-Type=%s hasContent=%b path=%s params=%s method=%s", acceptHeader,
contentTypeHeader, hasContent(), path(), params.toString(), method().toString()));
}
// both headers should be versioned or none
if ((cVersion == null && aVersion!=null) || (aVersion ==null && cVersion!=null) ){
throw new CompatibleApiHeadersCombinationException(
String.format(Locale.ROOT, "Versioning is required on both Content-Type and Accept headers. " +
"Accept=%s Content-Type=%s hasContent=%b path=%s params=%s method=%s", acceptHeader,
contentTypeHeader, hasContent(), path(), params.toString(), method().toString()));
}
return contentTypeVersion < Version.CURRENT.major;
private final String getSingleHeader(String name) {
//TODO: is this case sensitive ?
List<String> values = headers.get(name);
if (values != null && values.isEmpty() == false) {
return values.get(0);
}
return acceptVersion < Version.CURRENT.major;*/
return true;
return null;
}


public enum Method {
GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH, TRACE, CONNECT
}
Expand Down Expand Up @@ -597,4 +552,5 @@ public static class BadParameterException extends RuntimeException {
}

}

}
Loading

0 comments on commit 72d7b13

Please sign in to comment.