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 committed 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.
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
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
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
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
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
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 {
}

}

}

0 comments on commit 72d7b13

Please sign in to comment.