diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java index 42129e4f5e4bb..606284f046244 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java @@ -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}. @@ -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; } @@ -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; } @@ -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)) { @@ -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) + ";") || diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index fd27d0512bb0b..3d8211b61201c 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -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; @@ -428,7 +427,7 @@ public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpr IndexScopedSettings indexScopedSettings, ClusterSettings clusterSettings, SettingsFilter settingsFilter, ThreadPool threadPool, List actionPlugins, NodeClient nodeClient, CircuitBreakerService circuitBreakerService, UsageService usageService, ClusterService clusterService, - List restCompatPlugins) { + BiFunction restCompatibleFunction) { this.settings = settings; this.indexNameExpressionResolver = indexNameExpressionResolver; this.indexScopedSettings = indexScopedSettings; @@ -460,18 +459,7 @@ public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpr indicesAliasesRequestRequestValidators = new RequestValidators<>( actionPlugins.stream().flatMap(p -> p.indicesAliasesRequestValidators().stream()).collect(Collectors.toList())); - BiFunction>, Boolean, Boolean> minimumRestCompatibilityVersion = getMinimumRestCompatibilityVersion(restCompatPlugins); - restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService, minimumRestCompatibilityVersion); - } - - private BiFunction>, Boolean, Boolean> getMinimumRestCompatibilityVersion(List 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> getActions() { diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 53771c7cd7d13..5551f047b79bc 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -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; @@ -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; @@ -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(); @@ -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 getRestCompatibleFunction(){ + List restCompatibilityPlugins = pluginsService.filterPlugins(RestCompatibility.class); + BiFunction 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 localNodeFactory, diff --git a/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java b/server/src/main/java/org/elasticsearch/plugins/RestCompatibility.java similarity index 81% rename from server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java rename to server/src/main/java/org/elasticsearch/plugins/RestCompatibility.java index 5f4266155fb2e..54af55a757c1c 100644 --- a/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/RestCompatibility.java @@ -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> headers, boolean hasContent); +@FunctionalInterface +public interface RestCompatibility { + Version getCompatibleVersion(@Nullable String acceptHeader, @Nullable String contentTypeHeader); } diff --git a/server/src/main/java/org/elasticsearch/rest/RestController.java b/server/src/main/java/org/elasticsearch/rest/RestController.java index cc106c4ae869b..8355143fe7ea4 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestController.java +++ b/server/src/main/java/org/elasticsearch/rest/RestController.java @@ -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 headersToCopy; private final UsageService usageService; - private BiFunction>,Boolean,Boolean> isRestCompatibleFunction; + private BiFunction restCompatibleFunction; + public RestController(Set headersToCopy, UnaryOperator handlerWrapper, NodeClient client, CircuitBreakerService circuitBreakerService, UsageService usageService, - BiFunction>, Boolean, Boolean> isRestCompatibleFunction) { + BiFunction restCompatibleFunction) { this.headersToCopy = headersToCopy; this.usageService = usageService; - this.isRestCompatibleFunction = isRestCompatibleFunction; + this.restCompatibleFunction = restCompatibleFunction; if (handlerWrapper == null) { handlerWrapper = h -> h; // passthrough if no wrapper set } @@ -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(); diff --git a/server/src/main/java/org/elasticsearch/rest/RestRequest.java b/server/src/main/java/org/elasticsearch/rest/RestRequest.java index deaa7074c68b7..70c99adf98d62 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestRequest.java +++ b/server/src/main/java/org/elasticsearch/rest/RestRequest.java @@ -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>,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 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 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 } @@ -597,4 +552,5 @@ public static class BadParameterException extends RuntimeException { } } + } diff --git a/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java b/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java index b7cb017f3d224..8f42cf0ed5c2a 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.rest; +import org.elasticsearch.Version; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.bytes.BytesArray; @@ -92,7 +93,7 @@ public void setup() { inFlightRequestsBreaker = circuitBreakerService.getBreaker(CircuitBreaker.IN_FLIGHT_REQUESTS); HttpServerTransport httpServerTransport = new TestHttpServerTransport(); - restController = new RestController(Collections.emptySet(), null, null, circuitBreakerService, usageService, (a, b) -> false); + restController = new RestController(Collections.emptySet(), null, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); restController.registerHandler(RestRequest.Method.GET, "/", (request, channel, client) -> channel.sendResponse( new BytesRestResponse(RestStatus.OK, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY))); @@ -107,7 +108,7 @@ public void testApplyRelevantHeaders() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); Set headers = new HashSet<>(Arrays.asList(new RestHeaderDefinition("header.1", true), new RestHeaderDefinition("header.2", true))); - final RestController restController = new RestController(headers, null, null, circuitBreakerService, usageService, (a, b) -> false); + final RestController restController = new RestController(headers, null, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); Map> restHeaders = new HashMap<>(); restHeaders.put("header.1", Collections.singletonList("true")); restHeaders.put("header.2", Collections.singletonList("true")); @@ -143,7 +144,7 @@ public void testRequestWithDisallowedMultiValuedHeader() { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); Set headers = new HashSet<>(Arrays.asList(new RestHeaderDefinition("header.1", true), new RestHeaderDefinition("header.2", false))); - final RestController restController = new RestController(headers, null, null, circuitBreakerService, usageService, (a, b) -> false); + final RestController restController = new RestController(headers, null, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); Map> restHeaders = new HashMap<>(); restHeaders.put("header.1", Collections.singletonList("boo")); restHeaders.put("header.2", List.of("foo", "bar")); @@ -157,7 +158,7 @@ public void testRequestWithDisallowedMultiValuedHeaderButSameValues() { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); Set headers = new HashSet<>(Arrays.asList(new RestHeaderDefinition("header.1", true), new RestHeaderDefinition("header.2", false))); - final RestController restController = new RestController(headers, null, null, circuitBreakerService, usageService, (a, b) -> false); + final RestController restController = new RestController(headers, null, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); Map> restHeaders = new HashMap<>(); restHeaders.put("header.1", Collections.singletonList("boo")); restHeaders.put("header.2", List.of("foo", "foo")); @@ -211,7 +212,7 @@ public void testRegisterWithDeprecatedHandler() { } public void testRegisterSecondMethodWithDifferentNamedWildcard() { - final RestController restController = new RestController(null, null, null, circuitBreakerService, usageService, (a, b) -> false); + final RestController restController = new RestController(null, null, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); RestRequest.Method firstMethod = randomFrom(RestRequest.Method.values()); RestRequest.Method secondMethod = @@ -238,7 +239,7 @@ public void testRestHandlerWrapper() throws Exception { h -> { assertSame(handler, h); return (RestRequest request, RestChannel channel, NodeClient client) -> wrapperCalled.set(true); - }, null, circuitBreakerService, usageService, (a, b) -> false); + }, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); restController.registerHandler(RestRequest.Method.GET, "/wrapped", handler); RestRequest request = testRestRequest("/wrapped", "{}", XContentType.JSON); AssertingChannel channel = new AssertingChannel(request, true, RestStatus.BAD_REQUEST); @@ -301,7 +302,7 @@ public void testDispatchRequiresContentTypeForRequestsWithContent() { String content = randomAlphaOfLength((int) Math.round(BREAKER_LIMIT.getBytes() / inFlightRequestsBreaker.getOverhead())); RestRequest request = testRestRequest("/", content, null); AssertingChannel channel = new AssertingChannel(request, true, RestStatus.NOT_ACCEPTABLE); - restController = new RestController(Collections.emptySet(), null, null, circuitBreakerService, usageService, (a, b) -> false); + restController = new RestController(Collections.emptySet(), null, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); restController.registerHandler(RestRequest.Method.GET, "/", (r, c, client) -> c.sendResponse( new BytesRestResponse(RestStatus.OK, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY))); diff --git a/server/src/test/java/org/elasticsearch/rest/RestHttpResponseHeadersTests.java b/server/src/test/java/org/elasticsearch/rest/RestHttpResponseHeadersTests.java index 4814d817359b3..3c94576a0d7a4 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestHttpResponseHeadersTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestHttpResponseHeadersTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.rest; +import org.elasticsearch.Version; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.ClusterSettings; @@ -90,7 +91,7 @@ public void testUnsupportedMethodResponseHttpHeader() throws Exception { final Settings settings = Settings.EMPTY; UsageService usageService = new UsageService(); RestController restController = new RestController(Collections.emptySet(), - null, null, circuitBreakerService, usageService, (a, b) -> false); + null, null, circuitBreakerService, usageService, (a, b) -> Version.CURRENT); // A basic RestHandler handles requests to the endpoint RestHandler restHandler = new RestHandler() { diff --git a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryActionTests.java index 33540ba2dcca8..e6fcf1327f2eb 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestValidateQueryActionTests.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.rest.action.admin.indices; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionType; @@ -58,7 +59,7 @@ public class RestValidateQueryActionTests extends AbstractSearchTestCase { private static UsageService usageService = new UsageService(); private static RestController controller = new RestController(emptySet(), null, client, - new NoneCircuitBreakerService(), usageService, (a, b) -> false); + new NoneCircuitBreakerService(), usageService, (a,b) -> Version.CURRENT); private static RestValidateQueryAction action = new RestValidateQueryAction(); /** diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java index 0abed7f2321c2..145ad803cc926 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java @@ -19,6 +19,7 @@ package org.elasticsearch.test.rest; +import org.elasticsearch.Version; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -47,7 +48,7 @@ public void setUpController() { controller = new RestController(Collections.emptySet(), null, nodeClient, new NoneCircuitBreakerService(), - new UsageService(), (a, b) -> false); + new UsageService(), (a, b) -> Version.CURRENT); } /** diff --git a/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatRestRequest.java b/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatRestRequest.java new file mode 100644 index 0000000000000..85847e741e9f2 --- /dev/null +++ b/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatRestRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.compat; + +import org.elasticsearch.Version; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.RestCompatibility; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CompatRestRequest extends Plugin implements RestCompatibility { + + private static final Pattern COMPATIBLE_API_HEADER_PATTERN = Pattern.compile( + "(application|text)/(vnd.elasticsearch\\+)?([^;]+)(\\s*;\\s*compatible-with=(\\d+))?", + Pattern.CASE_INSENSITIVE); + + @Override + public Version getCompatibleVersion(@Nullable String acceptHeader, @Nullable String contentTypeHeader) { + + Integer acceptVersion = acceptHeader == null ? null : parseVersion(acceptHeader); + Integer contentTypeVersion = contentTypeHeader == null ? null : parseVersion(contentTypeHeader); + + //request version must be current or prior + if (acceptVersion != null && acceptVersion > Version.CURRENT.major || + contentTypeVersion != null && contentTypeVersion > Version.CURRENT.major) { + throw new CompatibleApiException( + String.format(Locale.ROOT, "Compatible version must be equal or less then the current version. " + + "Accept=%s Content-Type=%s", acceptHeader, contentTypeHeader)); + } + + //request version can not be older then last major + if (acceptVersion != null && acceptVersion < Version.CURRENT.major - 1 || + contentTypeVersion != null && contentTypeVersion < Version.CURRENT.major - 1) { + throw new CompatibleApiException( + String.format(Locale.ROOT, "Compatible versioning only is only available for past major version. " + + "Accept=%s Content-Type=%s", acceptHeader, contentTypeHeader)); + } + + // if a compatible content type is sent, so must a versioned accept header. + if (contentTypeVersion != null && acceptVersion == null ) { + throw new CompatibleApiException( + String.format(Locale.ROOT, "The Accept header must have request a version if the Content-Type version is requested." + + "Accept=%s Content-Type=%s", acceptHeader, contentTypeHeader)); + } + + // if both accept and content-type are sent , the version must match + if (acceptVersion != null && contentTypeVersion != null && contentTypeVersion != acceptVersion) { + throw new CompatibleApiException( + String.format(Locale.ROOT, "Content-Type and Accept version requests have to match. " + + "Accept=%s Content-Type=%s", acceptHeader, + contentTypeHeader)); + } + return Version.fromString(Version.CURRENT.major - 1 + ".0.0"); + } + + private static Integer parseVersion(String mediaType) { + if (mediaType != null) { + Matcher matcher = COMPATIBLE_API_HEADER_PATTERN.matcher(mediaType); + if (matcher.find() && "vnd.elasticsearch+".equalsIgnoreCase(matcher.group(2))) { + return Integer.valueOf(matcher.group(5)); + } + } + return null; + } +} diff --git a/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatRestRequestPlugin.java b/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatRestRequestPlugin.java deleted file mode 100644 index cb3c55f58e52b..0000000000000 --- a/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatRestRequestPlugin.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.compat; - -import org.elasticsearch.Version; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.RestCompatibilityPlugin; - -import java.util.List; -import java.util.Locale; -import java.util.Map; - -public class CompatRestRequestPlugin extends Plugin implements RestCompatibilityPlugin { - private static final String COMPATIBLE_ACCEPT_HEADER = "Accept"; - private static final String COMPATIBLE_CONTENT_TYPE_HEADER = "Content-Type"; - - @Override - public boolean isRequestingCompatibility(Map> headers, boolean hasContent) { - String acceptHeader = header(headers, COMPATIBLE_ACCEPT_HEADER); - String aVersion = XContentType.parseVersion(acceptHeader); - byte acceptVersion = aVersion == null ? Version.CURRENT.major : Integer.valueOf(aVersion).byteValue(); - String contentTypeHeader = header(headers, 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", acceptHeader, - contentTypeHeader, hasContent)); - } - 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", acceptHeader, - contentTypeHeader, hasContent)); - } - - 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", acceptHeader, - contentTypeHeader, hasContent)); - } - // 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)); - } - - return contentTypeVersion < Version.CURRENT.major; - } - - return acceptVersion < Version.CURRENT.major; - } - - public final String header(Map> headers, String name) { - List values = headers.get(name); - if (values != null && values.isEmpty() == false) { - return values.get(0); - } - return null; - } - - - public static class CompatibleApiHeadersCombinationException extends RuntimeException { - - CompatibleApiHeadersCombinationException(String cause) { - super(cause); - } - } -} diff --git a/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatibleApiException.java b/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatibleApiException.java new file mode 100644 index 0000000000000..97616e0dfd244 --- /dev/null +++ b/x-pack/plugin/compat-rest-request/src/main/java/org/elasticsearch/compat/CompatibleApiException.java @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.compat; + +public class CompatibleApiException extends RuntimeException { + + CompatibleApiException(String cause) { + super(cause); + } +}