diff --git a/build.gradle b/build.gradle index d5f58e0..d3f9261 100644 --- a/build.gradle +++ b/build.gradle @@ -30,8 +30,17 @@ dependencies { compile "org.embulk:embulk-util-config:0.3.0" compile "org.embulk:embulk-util-json:0.1.1" + compile "org.embulk:embulk-util-timestamp:0.2.1" compile "javax.validation:validation-api:1.1.0.Final" - compile 'org.apache.bval:bval-jsr303:0.5' + compile "org.apache.bval:bval-jsr303:0.5" + + compile "org.embulk:embulk-base-restclient:0.10.1" + compile("org.embulk:embulk-util-retryhelper-jaxrs:0.8.1") { + exclude group: "org.slf4j", module: "slf4j-api" + } + // NOTE: Avoid 'java.lang.LinkageError: ClassCastException: attempting to castjar:file:./build/gemContents/classpath/javax.ws.rs-api-2.0.1.jar!/javax/ws/rs/client/ClientBuilder.class to jar:file:./build/gemContents/classpath/javax.ws.rs-api-2.0.1.jar!/javax/ws/rs/client/ClientBuilder.class' + // compile "org.glassfish.jersey.core:jersey-client:3.0.3" + compile "org.glassfish.jersey.core:jersey-client:2.25.1" testImplementation "junit:junit:4.+" testImplementation "org.embulk:embulk-core:${embulkVersion}" diff --git a/gradle/dependency-locks/embulkPluginRuntime.lockfile b/gradle/dependency-locks/embulkPluginRuntime.lockfile index 214fca5..b02ff10 100644 --- a/gradle/dependency-locks/embulkPluginRuntime.lockfile +++ b/gradle/dependency-locks/embulkPluginRuntime.lockfile @@ -6,9 +6,27 @@ com.fasterxml.jackson.core:jackson-core:2.6.7 com.fasterxml.jackson.core:jackson-databind:2.6.7 com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.6.7 commons-beanutils:commons-beanutils-core:1.8.3 +javax.annotation:javax.annotation-api:1.2 +javax.inject:javax.inject:1 javax.validation:validation-api:1.1.0.Final +javax.ws.rs:javax.ws.rs-api:2.0.1 org.apache.bval:bval-core:0.5 org.apache.bval:bval-jsr303:0.5 org.apache.commons:commons-lang3:3.1 +org.embulk:embulk-base-restclient:0.10.1 org.embulk:embulk-util-config:0.3.0 org.embulk:embulk-util-json:0.1.1 +org.embulk:embulk-util-retryhelper-jaxrs:0.8.1 +org.embulk:embulk-util-retryhelper:0.8.1 +org.embulk:embulk-util-rubytime:0.3.2 +org.embulk:embulk-util-timestamp:0.2.1 +org.glassfish.hk2.external:aopalliance-repackaged:2.5.0-b32 +org.glassfish.hk2.external:javax.inject:2.5.0-b32 +org.glassfish.hk2:hk2-api:2.5.0-b32 +org.glassfish.hk2:hk2-locator:2.5.0-b32 +org.glassfish.hk2:hk2-utils:2.5.0-b32 +org.glassfish.hk2:osgi-resource-locator:1.0.1 +org.glassfish.jersey.bundles.repackaged:jersey-guava:2.25.1 +org.glassfish.jersey.core:jersey-client:2.25.1 +org.glassfish.jersey.core:jersey-common:2.25.1 +org.javassist:javassist:3.20.0-GA diff --git a/src/main/java/org/embulk/output/http_json/HttpJsonOutputPlugin.java b/src/main/java/org/embulk/output/http_json/HttpJsonOutputPlugin.java index a38c088..6040c8e 100644 --- a/src/main/java/org/embulk/output/http_json/HttpJsonOutputPlugin.java +++ b/src/main/java/org/embulk/output/http_json/HttpJsonOutputPlugin.java @@ -1,25 +1,13 @@ package org.embulk.output.http_json; -import java.util.List; -import java.util.Optional; import javax.validation.Validation; import javax.validation.Validator; import org.apache.bval.jsr303.ApacheValidationProvider; -import org.embulk.config.ConfigDiff; -import org.embulk.config.ConfigSource; -import org.embulk.config.TaskReport; -import org.embulk.config.TaskSource; -import org.embulk.spi.OutputPlugin; -import org.embulk.spi.Schema; -import org.embulk.spi.TransactionalPageOutput; -import org.embulk.util.config.Config; -import org.embulk.util.config.ConfigDefault; -import org.embulk.util.config.ConfigMapper; +import org.embulk.base.restclient.RestClientOutputPluginBase; import org.embulk.util.config.ConfigMapperFactory; -import org.embulk.util.config.Task; -import org.embulk.util.config.TaskMapper; -public class HttpJsonOutputPlugin implements OutputPlugin { +public class HttpJsonOutputPlugin + extends RestClientOutputPluginBase { private static final Validator VALIDATOR = Validation.byProvider(ApacheValidationProvider.class) .configure() @@ -28,57 +16,10 @@ public class HttpJsonOutputPlugin implements OutputPlugin { private static final ConfigMapperFactory CONFIG_MAPPER_FACTORY = ConfigMapperFactory.builder().addDefaultModules().withValidator(VALIDATOR).build(); - public interface PluginTask extends Task { - // configuration option 1 (required integer) - @Config("option1") - public int getOption1(); - - // configuration option 2 (optional string, null is not allowed) - @Config("option2") - @ConfigDefault("\"myvalue\"") - public String getOption2(); - - // configuration option 3 (optional string, null is allowed) - @Config("option3") - @ConfigDefault("null") - public Optional getOption3(); - } - - @Override - public ConfigDiff transaction( - ConfigSource config, Schema schema, int taskCount, OutputPlugin.Control control) { - final ConfigMapper configMapper = CONFIG_MAPPER_FACTORY.createConfigMapper(); - final PluginTask task = configMapper.map(config, PluginTask.class); - - // retryable (idempotent) output: - // return resume(task.dump(), schema, taskCount, control); - - // non-retryable (non-idempotent) output: - control.run(task.toTaskSource()); - return CONFIG_MAPPER_FACTORY.newConfigDiff(); - } - - @Override - public ConfigDiff resume( - TaskSource taskSource, Schema schema, int taskCount, OutputPlugin.Control control) { - throw new UnsupportedOperationException( - "http_json output plugin does not support resuming"); - } - - @Override - public void cleanup( - TaskSource taskSource, - Schema schema, - int taskCount, - List successTaskReports) {} - - @Override - public TransactionalPageOutput open(TaskSource taskSource, Schema schema, int taskIndex) { - final TaskMapper taskMapper = CONFIG_MAPPER_FACTORY.createTaskMapper(); - final PluginTask task = taskMapper.map(taskSource, PluginTask.class); - - // Write your code here :) - throw new UnsupportedOperationException( - "HttpJsonOutputPlugin.run method is not implemented yet"); + public HttpJsonOutputPlugin() { + super( + CONFIG_MAPPER_FACTORY, + HttpJsonOutputPluginDelegate.PluginTask.class, + new HttpJsonOutputPluginDelegate(CONFIG_MAPPER_FACTORY)); } } diff --git a/src/main/java/org/embulk/output/http_json/HttpJsonOutputPluginDelegate.java b/src/main/java/org/embulk/output/http_json/HttpJsonOutputPluginDelegate.java new file mode 100644 index 0000000..03296b1 --- /dev/null +++ b/src/main/java/org/embulk/output/http_json/HttpJsonOutputPluginDelegate.java @@ -0,0 +1,349 @@ +package org.embulk.output.http_json; + +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import org.embulk.base.restclient.RestClientOutputPluginDelegate; +import org.embulk.base.restclient.RestClientOutputTaskBase; +import org.embulk.base.restclient.ServiceRequestMapper; +import org.embulk.base.restclient.jackson.JacksonServiceRequestMapper; +import org.embulk.base.restclient.jackson.JacksonTopLevelValueLocator; +import org.embulk.base.restclient.jackson.scope.JacksonAllInObjectScope; +import org.embulk.base.restclient.record.RecordBuffer; +import org.embulk.base.restclient.record.ValueLocator; +import org.embulk.config.ConfigDiff; +import org.embulk.config.ConfigException; +import org.embulk.config.TaskReport; +import org.embulk.output.http_json.helpers.JacksonRequestRecordBuffer; +import org.embulk.output.http_json.units.HttpMethod; +import org.embulk.output.http_json.units.HttpScheme; +import org.embulk.output.http_json.units.RequestMode; +import org.embulk.spi.DataException; +import org.embulk.spi.Schema; +import org.embulk.util.config.Config; +import org.embulk.util.config.ConfigDefault; +import org.embulk.util.config.ConfigMapperFactory; +import org.embulk.util.config.Task; +import org.embulk.util.retryhelper.jaxrs.JAXRSClientCreator; +import org.embulk.util.retryhelper.jaxrs.JAXRSRetryHelper; +import org.embulk.util.retryhelper.jaxrs.JAXRSSingleRequester; +import org.embulk.util.retryhelper.jaxrs.StringJAXRSResponseEntityReader; +import org.embulk.util.timestamp.TimestampFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpJsonOutputPluginDelegate + implements RestClientOutputPluginDelegate { + public interface PluginTask extends RestClientOutputTaskBase { + @Config("scheme") + @ConfigDefault("\"https\"") + public HttpScheme getScheme(); + + @Config("host") + public String getHost(); + + @Config("port") + @ConfigDefault("null") + public Optional getPort(); + + @Config("path") + @ConfigDefault("null") + public Optional getPath(); + + @Config("headers") + @ConfigDefault("{}") + public List> getHeaders(); + + @Config("request") + @ConfigDefault("{}") + public Request getRequest(); + + @Config("response") + @ConfigDefault("{}") + public Response getResponse(); + + @Config("maximum_retries") + @ConfigDefault("7") + public int getMaximumRetries(); + + @Config("initial_retry_interval_millis") + @ConfigDefault("1000") + public int getInitialRetryIntervalMillis(); + + @Config("maximum_retry_interval_millis") + @ConfigDefault("60000") + public int getMaximumRetryIntervalMillis(); + + @Config("default_timezone") + @ConfigDefault("\"UTC\"") + public String getDefaultTimeZoneId(); + + @Config("default_timestamp_format") + @ConfigDefault("\"%Y-%m-%d %H:%M:%S.%N %z\"") + public String getDefaultTimestampFormat(); + + @Config("default_date") + @ConfigDefault("\"1970-01-01\"") + public String getDefaultDate(); + + public String getEndpoint(); + + public void setEndpoint(String endpoint); + } + + public interface Request extends Task { + @Config("method") + @ConfigDefault("\"POST\"") + public HttpMethod getMethod(); + + @Config("mode") + @ConfigDefault("\"buffered\"") + public RequestMode getMode(); + + @Config("fill_json_null_for_embulk_null") + @ConfigDefault("false") + public Boolean getFillJsonNullForEmbulkNull(); + + @Config("buffered_body") + @ConfigDefault("{}") + public BufferedBody getBufferedBody(); + } + + public interface BufferedBody extends Task { + @Config("buffer_size") + @ConfigDefault("100") + public Integer getBufferSize(); + + @Config("root_pointer") + @ConfigDefault("null") + public Optional getRootPointer(); + } + + public interface Response extends Task { + @Config("success_condition") + @ConfigDefault("null") + public Optional getSuccessCondition(); + + @Config("retry_condition") + @ConfigDefault("null") + public Optional getRetryCondition(); + } + + public interface Condition extends Task { + @Config("status_codes") + @ConfigDefault("null") + public Optional> getStatusCodes(); + + @Config("messages") + @ConfigDefault("null") + public Optional> getMessages(); + + @Config("message_pointer") + @ConfigDefault("/message") + public String getMessagePointer(); + } + + private static final Logger logger = + LoggerFactory.getLogger(HttpJsonOutputPluginDelegate.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String BUFFER_ATTRIBUTE_KEY = "buf"; + + @SuppressWarnings("unused") + private final ConfigMapperFactory configMapperFactory; + + public HttpJsonOutputPluginDelegate(ConfigMapperFactory configMapperFactory) { + this.configMapperFactory = configMapperFactory; + } + + @Override + public void validateOutputTask(PluginTask task, Schema embulkSchema, int taskCount) { + task.setEndpoint(buildEndpoint(task)); + } + + @Override + public ServiceRequestMapper buildServiceRequestMapper(PluginTask task) { + final TimestampFormatter formatter = + TimestampFormatter.builder(task.getDefaultTimestampFormat(), true) + .setDefaultZoneFromString(task.getDefaultTimeZoneId()) + .setDefaultDateFromString(task.getDefaultDate()) + .build(); + return JacksonServiceRequestMapper.builder() + .add( + new JacksonAllInObjectScope( + formatter, task.getRequest().getFillJsonNullForEmbulkNull()), + new JacksonTopLevelValueLocator(BUFFER_ATTRIBUTE_KEY)) + .build(); + } + + @Override + public RecordBuffer buildRecordBuffer(PluginTask task, Schema schema, int taskIndex) { + return new JacksonRequestRecordBuffer( + "responses", + (records) -> { + switch (task.getRequest().getMode()) { + case BUFFERED: + return eachSlice( + records.map(r -> r.get(BUFFER_ATTRIBUTE_KEY)) + .collect(Collectors.toList()), + task.getRequest().getBufferedBody().getBufferSize(), + slicedRecords -> + requestWithRetry( + task, buildBufferedBody(task, slicedRecords))); + case DIRECT: + return records.map(r -> r.get(BUFFER_ATTRIBUTE_KEY)) + .map(json -> requestWithRetry(task, json)) + .collect(Collectors.toList()); + default: + throw new ConfigException( + "Unknown request mode: " + task.getRequest().getMode()); + } + }); + } + + @Override + public ConfigDiff egestEmbulkData( + PluginTask task, Schema schema, int taskCount, List taskReports) { + taskReports.forEach(report -> logger.info(report.toString())); + return configMapperFactory.newConfigDiff(); + } + + private List eachSlice(List list, int sliceSize, Function, R> function) { + List resultBuilder = new ArrayList<>(); + for (int i = 0; i < list.size(); i += sliceSize) { + R result = function.apply(list.subList(i, Integer.min(i + sliceSize, list.size()))); + resultBuilder.add(result); + } + return Collections.unmodifiableList(resultBuilder); + } + + private JsonNode buildBufferedBody(PluginTask task, List records) { + final ArrayNode an = OBJECT_MAPPER.createArrayNode(); + records.forEach(an::add); + if (!task.getRequest().getBufferedBody().getRootPointer().isPresent()) { + return an; + } + final ObjectNode root = OBJECT_MAPPER.createObjectNode(); + final JsonPointer jp = + JsonPointer.compile(task.getRequest().getBufferedBody().getRootPointer().get()); + createNestedMissingNodes(root, jp, an); + return root; + } + + private void createNestedMissingNodes( + JsonNode json, JsonPointer jp, JsonNode leafValue) { + final JsonNode parent = json.at(jp.head()); + if (parent.isArray()) { + throw new DataException( + String.format( + "Unsupported data type of the value specified by Json Pointer: %s", + jp.head().toString())); + } + if (parent.isMissingNode()) { + createNestedMissingNodes(json, jp.head(), OBJECT_MAPPER.createObjectNode()); + } + ((ObjectNode) parent).set(jp.last().getMatchingProperty(), leafValue); + } + + private String requestWithRetry(final PluginTask task, final JsonNode json) { + return tryWithJAXRSRetryHelper( + task, + retryHelper -> { + return retryHelper.requestWithRetry( + new StringJAXRSResponseEntityReader(), + newJAXRSSingleRequester(task, json)); + }); + } + + private JAXRSSingleRequester newJAXRSSingleRequester(PluginTask task, JsonNode json) { + return new JAXRSSingleRequester() { + + @Override + public javax.ws.rs.core.Response requestOnce(Client client) { + return buildRequest(task, client, json); + } + + @Override + protected boolean isResponseStatusToRetry(javax.ws.rs.core.Response response) { + if (task.getResponse().getRetryCondition().isPresent()) { + return isMatchedResponse( + task.getResponse().getRetryCondition().get(), response); + } + return false; + } + }; + } + + private javax.ws.rs.core.Response buildRequest( + PluginTask task, javax.ws.rs.client.Client client, final JsonNode json) { + Entity entity = Entity.entity(json.toString(), MediaType.APPLICATION_JSON); + MultivaluedMap headers = new MultivaluedHashMap<>(); + task.getHeaders().forEach(h -> h.forEach((k, v) -> headers.add(k, v))); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + return client.target(task.getEndpoint()) + .request() + .headers(headers) + .method(task.getRequest().getMethod().name(), entity); + } + + private Boolean isMatchedResponse(Condition cond, javax.ws.rs.core.Response response) { + if (cond.getStatusCodes().isPresent()) { + if (!cond.getStatusCodes().get().contains(response.getStatus())) { + return false; + } + } + if (cond.getMessages().isPresent()) { + ObjectNode oj; + try { + oj = OBJECT_MAPPER.readValue(response.readEntity(String.class), ObjectNode.class); + } catch (IOException e) { + throw new DataException(e); + } + if (!cond.getMessages().get().contains(oj.get(cond.getMessagePointer()).asText())) { + return false; + } + } + return true; + } + + private T tryWithJAXRSRetryHelper(PluginTask task, Function f) { + try (JAXRSRetryHelper retryHelper = + new JAXRSRetryHelper( + task.getMaximumRetries(), + task.getInitialRetryIntervalMillis(), + task.getMaximumRetryIntervalMillis(), + new JAXRSClientCreator() { + @Override + public javax.ws.rs.client.Client create() { + return javax.ws.rs.client.ClientBuilder.newBuilder().build(); + } + })) { + return f.apply(retryHelper); + } + } + + private String buildEndpoint(PluginTask task) { + StringBuilder endpointBuilder = new StringBuilder(); + endpointBuilder.append(task.getScheme().toString()); + endpointBuilder.append("://"); + endpointBuilder.append(task.getHost()); + task.getPort().ifPresent(port -> endpointBuilder.append(":").append(port)); + task.getPath().ifPresent(path -> endpointBuilder.append(path)); + return endpointBuilder.toString(); + } +} diff --git a/src/main/java/org/embulk/output/http_json/helpers/JacksonRequestRecordBuffer.java b/src/main/java/org/embulk/output/http_json/helpers/JacksonRequestRecordBuffer.java new file mode 100644 index 0000000..96f543a --- /dev/null +++ b/src/main/java/org/embulk/output/http_json/helpers/JacksonRequestRecordBuffer.java @@ -0,0 +1,60 @@ +package org.embulk.output.http_json.helpers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayDeque; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; +import org.embulk.base.restclient.jackson.JacksonTaskReportRecordBuffer; +import org.embulk.config.TaskReport; +import org.embulk.spi.DataException; + +public class JacksonRequestRecordBuffer extends JacksonTaskReportRecordBuffer { + + private static final ObjectMapper om = new ObjectMapper(); + private final String taskReportKeyName; + private final Function, List> requestResponseHandler; + + public JacksonRequestRecordBuffer( + String taskReportKeyName, + Function, List> requestResponseHandler) { + super(taskReportKeyName); + this.taskReportKeyName = taskReportKeyName; + this.requestResponseHandler = requestResponseHandler; + } + + @Override + public TaskReport commitWithTaskReportUpdated(final TaskReport taskReport) { + ArrayDeque records = forceToGetRecords(); + List jsonList = this.requestResponseHandler.apply(records.stream()); + ArrayNode an = om.createArrayNode(); + for (String j : jsonList) { + try { + an.add(om.readValue(j, ObjectNode.class)); + } catch (IOException e) { + throw new DataException(e); + } + } + taskReport.set(this.taskReportKeyName, an); + return taskReport; + } + + @SuppressWarnings("unchecked") + private ArrayDeque forceToGetRecords() { + try { + Class klass = this.getClass().getSuperclass(); + Field field = klass.getDeclaredField("records"); + field.setAccessible(true); + return field.get(this) == null ? null : (ArrayDeque) field.get(this); + } catch (IllegalArgumentException + | IllegalAccessException + | NoSuchFieldException + | SecurityException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/embulk/output/http_json/units/HttpMethod.java b/src/main/java/org/embulk/output/http_json/units/HttpMethod.java new file mode 100644 index 0000000..f397122 --- /dev/null +++ b/src/main/java/org/embulk/output/http_json/units/HttpMethod.java @@ -0,0 +1,38 @@ +package org.embulk.output.http_json.units; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.embulk.config.ConfigException; + +public enum HttpMethod { + GET, + POST, + PUT, + DELETE, + PATCH; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(Locale.ENGLISH); + } + + @JsonCreator + public static HttpMethod fromString(String value) { + return Stream.of(HttpMethod.values()) + .filter(mode -> mode.toString().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow( + () -> + new ConfigException( + String.format( + "Unknown method: %s. Available methods are [%s].", + value, + Stream.of(HttpMethod.values()) + .map(HttpMethod::toString) + .collect(Collectors.joining(", "))))); + } +} diff --git a/src/main/java/org/embulk/output/http_json/units/HttpScheme.java b/src/main/java/org/embulk/output/http_json/units/HttpScheme.java new file mode 100644 index 0000000..ae9e817 --- /dev/null +++ b/src/main/java/org/embulk/output/http_json/units/HttpScheme.java @@ -0,0 +1,35 @@ +package org.embulk.output.http_json.units; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.embulk.config.ConfigException; + +public enum HttpScheme { + HTTP, + HTTPS; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(Locale.ENGLISH); + } + + @JsonCreator + public static HttpScheme fromString(String value) { + return Stream.of(HttpScheme.values()) + .filter(mode -> mode.toString().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow( + () -> + new ConfigException( + String.format( + "Unknown scheme: %s. Available schemes are [%s].", + value, + Stream.of(HttpScheme.values()) + .map(HttpScheme::toString) + .collect(Collectors.joining(", "))))); + } +} diff --git a/src/main/java/org/embulk/output/http_json/units/RequestMode.java b/src/main/java/org/embulk/output/http_json/units/RequestMode.java new file mode 100644 index 0000000..9f18c94 --- /dev/null +++ b/src/main/java/org/embulk/output/http_json/units/RequestMode.java @@ -0,0 +1,35 @@ +package org.embulk.output.http_json.units; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.embulk.config.ConfigException; + +public enum RequestMode { + DIRECT, + BUFFERED; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(Locale.ENGLISH); + } + + @JsonCreator + public static RequestMode fromString(String value) { + return Stream.of(RequestMode.values()) + .filter(mode -> mode.toString().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow( + () -> + new ConfigException( + String.format( + "Unknown mode: %s. Available modes are [%s].", + value, + Stream.of(RequestMode.values()) + .map(RequestMode::toString) + .collect(Collectors.joining(", "))))); + } +}